【编译原理创新尝试(一)】parser代码解析篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换,200行js示例入门编译原理

292 阅读9分钟

提示

本文内容仅是一次新的尝试,文中的案例也仅仅是一个最简单的示例demo,文中代码仅做为学习交流,类似于伪代码,描述思路,删除了容错逻辑,逻辑不够严谨

下一章

【编译原理创新尝试(二)】generator代码生成篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换

编译相关介绍

编译原理一般用来实现编程语言的语法扩展、和高低级或不同编程语言之间的语法转换

编译相关技术框架

  1. 前端,rolldown、oxc,vue之父尤雨溪融资3000w要做的事,用rust整合前端工具链
  2. 前端,babel,实现js代码低版本浏览器兼容、新特性支持语法扩展
  3. 前端,typescript,一种强类型语法,通过编译将其转为js,使其可以在浏览器中运行
  4. 前端,esLint,js代码格式化工具
  5. 前端,chevrotain,通过自定义类似于ebnf语法js类代码,可以解析任何语法
  6. java,antlr4, 通过自定义ebnf文件可以解析任何语法

llvm、acron、tree-sitter等等,编译原理在我们的开发过程中无处不在

一般编译执行过程

读取代码 -> 使用lexer将代码处理成tokens -> 使用解析器parser将tokens处理为语法树(cst、ast) -> 使用手写代码生成器generator将ast转为对应的目标代码

编译原理创新,创新了什么?

以往的知名项目都是怎么做的

  • 以往的知名编译技术,一般都是通过手写编译原理,lexer,parser和generator(如acorn、babel、typescript),这种一般用于指定场景,性能好,但是自定义不是那么简单,需要手写代码
  • 扩展容易一点的(如antlr4,chevrotain)都是一般都是通过自定义类ebnf语法文件、实现自动化的代码解析parser,而generator还都是需要手写代码实现的

创新后怎么做

编译是将一种编程语言转换为另一种编程语言,解析器parser可以通过ebnf实现,那么生成器generator能不能也通过ebnf实现呢?

灵感来源

一切要从我想使用类似于flutter、swift那种非html类语法开发前端界面、如 div(attrs){children} 这种语法,于是我开启了编译原理学习之路,完成了一个简单demo后,我写了篇文章

告别 HTML、Template、JSX,像 Flutter 和 Swift 一样用 OOP 语法开发 Web 前端的新尝试

入坑编译原理后,我的洪荒之力算是打开了,以前没能力实现的各种想法,现在都开始跃跃欲试,js为什么不支持对象继承关键字?为什么对象不支持装饰器?我自己来实现个吧

于是我开始了写generator,写generator,不停的写写写,好累啊,于是我就想,解析器parser可以通过ebnf实现,那么生成器generator能不能也通过ebnf实现呢?

如果可以通过两个不同编程语言的语法定义文件,实现编程语言的自动转换该多好呀,于是就开启了我的编译原理进阶之路

正文

项目名称,subhuti(须菩提),寓意:使编程语言之间的转换如齐天大圣孙悟空的七十二变一样灵活,悟空的七十二变是须菩提教授的

正文分两章

  • 第一章,介绍根据类ebnf文件,解析代码的parser实现原理,已有很多相关实现(antlr4,chevrotain),第二章,创新,根据类ebnf文件,自动将语法树cst转换为目标代码

  • 第二章,介绍如何根据ebnf文件自动生成目标代码,文章内容放在下一篇文章链接:

【编译原理创新尝试(二)】generator代码生成篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换

正文一章、根据自定义类ebnf文件,解析代码的parser实现原理

parser思路概述

  1. 通过正则定义token格式
  2. lexer解析token,使用正则读字符串,遍历token读到一个最长匹配的就从字符串中删除token,加入tokens列表中
  3. 通过编码形式定义类似于ebnf的syntax语法(致敬chevrotain并创新,创新为采用装饰器方法直接定义静态可提示的具体方法,而非chevrotain通过方法动态创建匿名方法),这种方式易于调试,可读性比ebnf高
  4. parser读取tokens,执行顶级Es6Parser.program()语法(顶级语法包含子语法,嵌套层级),将顶级语法推入语法栈,匹配token,匹配到就从tokens列表删除token,加入到语法树,进入子语法,将子语法推入语法栈,执行相同匹配token,子语法执行完毕,将子语法推出语法栈,并将子语法加入父语法的子节点,执行完毕,得到完整的语法树
项目结构说明
  • es6token,定义token格式
  • es6Parser, 继承subhutiprser,定义具体语法
  • struct包内是数据结构
  • SubhutiLexer是代码解析tokens的代码
  • SubhutiLexer是parser的基础代码
  • test1LetJson为测试文件,直接执行文件,可以得到结果

image.png

具体代码
  1. 通过正则定义token格式
import {SubhutiCreateTokenGroupType, createKeywordToken, createToken} from "../subhuti/struct/SubhutiCreateToken";

export enum Es6TokenName {
    let = 'let',
    const = 'const',
    whitespace = 'whitespace',
    identifier = 'identifier',
    equal = 'equal',
    integer = 'integer',
    string = 'string'
}
export const es6Tokens = [
    createKeywordToken({ name: Es6TokenName.equal, pattern: /=/ }),
    createKeywordToken({ name: Es6TokenName.let, pattern: /let/ }),
    createKeywordToken({ name: Es6TokenName.const, pattern: /const/ }),
    createKeywordToken({ name: Es6TokenName.whitespace, pattern: /\s+/, group: SubhutiCreateTokenGroupType.skip }),
    createToken({ name: Es6TokenName.identifier, pattern: /[a-zA-Z$_]\w*/ }),
    createToken({ name: Es6TokenName.integer, pattern: /0|[1-9]\d*/ }),
    //匹配非',和转义字符
    createToken({ name: Es6TokenName.string, pattern: /'([^'\]|\.)*'/ }),
];

2. lexer解析token,使用正则读字符串,遍历token读到一个最长匹配的就从字符串中删除token,加入tokens列表中 (删减非核心容错逻辑后的代码)

lexer(input: string): SubhutiMatchToken[] {
    const resTokens: SubhutiMatchToken[] = []; // 初始化结果token数组
    while (input) { // 当输入字符串不为空时循环
        const matchTokens: SubhutiMatchToken[] = []; // 初始化匹配的token数组
        // 匹配的token数量
        for (const token of this.tokens) { // 遍历所有token
            // 处理正则
            const newPattern = new RegExp('^(' + token.pattern.source + ')'); // 创建新的正则表达式
            // token正则匹配
            const matchRes = input.match(newPattern); // 尝试匹配输入字符串
            // 存在匹配结果,
            if (matchRes) {
                // 则加入到匹配的token列表中
                matchTokens.push(createMatchToken({tokenName: token.name, tokenValue: matchRes[0]})); // 创建匹配token并加入列表
            }
        }
        if (!matchTokens.length) { // 如果没有匹配到任何token
            throw new Error('无法匹配token:' + input); // 抛出错误
        }
        let resToken = matchTokens[0]; // 选择唯一的token
        input = input.substring(resToken.tokenValue.length); // 从输入字符串中移除已匹配的部分
        const createToken = this.tokenMap.get(resToken.tokenName); // 获取创建token的配置信息
        if (createToken.group === SubhutiCreateTokenGroupType.skip) { // 如果token属于跳过组
            continue; // 跳过此token
        }
        resTokens.push(resToken); // 将token加入结果数组
    }
    return resTokens; // 返回结果token数组
}

3. 通过编码形式定义类似于ebnf的syntax语法(致敬chevrotain并创新,创新为采用装饰器方法直接定义静态可提示的具体方法,而非chevrotain通过方法动态创建匿名方法),这种方式易于调试,可读性比ebnf高

规则定义有些冗余,啰嗦,是为了多种定义语法使用方式

export default class Es6Parser extends SubhutiParser { // 定义一个ES6解析器类,继承自SubhutiParser
    constructor(tokens?: SubhutiMatchToken[]) { // 构造函数,接收可选的token数组
        super(tokens); // 调用父类构造函数
    }

    @SubhutiRule // 定义一个解析规则
    program() { // 定义program规则
        this.or([ // 定义一个选择规则
            {
                alt: () => { // 选择分支1
                    this.letKeywords(); // 引用letKeywords规则
                }
            },
            {
                alt: () => { // 选择分支2
                    this.constKeywords(); // 引用constKeywords规则
                }
            }
        ]);
        this.identifierEqual(); // 引用identifierEqual规则
        this.assignmentExpression(); // 引用assignmentExpression规则
        return this.getCurCst(); // 返回当前CST(语法树)
    }

    @SubhutiRule // 定义一个解析规则
    letKeywords() { // 定义letKeywords规则
        this.consume(Es6TokenName.let); // 消耗let关键字token
        return this.getCurCst(); // 返回当前CST
    }

    @SubhutiRule // 定义一个解析规则
    constKeywords() { // 定义constKeywords规则
        this.consume(Es6TokenName.const); // 消耗const关键字token
        return this.getCurCst(); // 返回当前CST
    }

    @SubhutiRule // 定义一个解析规则
    assignmentExpression() { // 定义assignmentExpression规则
        this.or([ // 定义一个选择规则
            {
                alt: () => { // 选择分支1
                    this.consume(Es6TokenName.integer); // 消耗整数token
                }
            },
            {
                alt: () => { // 选择分支2
                    this.consume(Es6TokenName.string); // 消耗字符串token
                }
            }
        ]);
        return this.getCurCst(); // 返回当前CST
    }

    @SubhutiRule // 定义一个解析规则
    identifierEqual() { // 定义identifierEqual规则
        this.consume(Es6TokenName.identifier); // 消耗标识符token
        this.consume(Es6TokenName.equal); // 消耗等号token
        return this.getCurCst(); // 返回当前CST
    }
}

4. parser读取tokens,执行顶级语法Es6Parser.program()(顶级语法包含子语法,嵌套层级),将顶级语法推入语法栈,匹配token,匹配到就从tokens列表删除token,加入到语法树,进入子语法,将子语法推入语法栈,执行相同匹配token,子语法执行完毕,将子语法推出语法栈,并将子语法加入父语法的子节点,执行完毕,得到完整的语法树

核心逻辑

//首次执行,则初始化语法栈,执行语法,将语法入栈,执行语法,语法执行完毕,语法出栈,加入父语法子节点
subhutiRule(targetFun: any, ruleName: string) {
    const initFlag = this.initFlag;
    if (initFlag) {
        this.initFlag = false;
        this.setMatchSuccess(false);
        this.cstStack = [];
    }
    let cst = this.processCst(ruleName, targetFun);
    if (initFlag) {
        //执行完毕,改为true
        this.initFlag = true;
    }
    else {
        if (cst) {
            const parentCst = this.cstStack[this.cstStack.length - 1];
            parentCst.children.push(cst);
            this.setCurCst(parentCst);
        }
    }
}
//执行语法,将语法入栈,执行语法,语法执行完毕,语法出栈
processCst(ruleName: string, targetFun: Function) {
    let cst = new SubhutiCst();
    cst.name = ruleName;
    cst.children = [];
    this.setCurCst(cst);
    this.cstStack.push(cst);
    // 规则解析
    targetFun.apply(this);
    this.cstStack.pop();
    if (cst.children.length) {
        return cst;
    }
    return null;
}
consume(tokenName: string) {
    return this.consumeToken(tokenName);
}
//消耗token,将token加入父语法
consumeToken(tokenName: string) {
    let popToken = this.tokens[0];
    if (popToken.tokenName !== tokenName) {
        return;
    }
    popToken = this.tokens.shift();
    const cst = new SubhutiCst();
    cst.name = popToken.tokenName;
    cst.value = popToken.tokenValue;
    this.curCst.children.push(cst);
    this.curCst.tokens.push(popToken);
    this.setMatchSuccess(true);
    return this.generateCst(cst);
}
//or语法,遍历匹配语法,语法匹配成功,则跳出匹配,执行下一规则
or(subhutiParserOrs: SubhutiParserOr[]) {
    if (!this.tokens?.length) {
        throw new Error('token is empty, please set tokens');
    }
    const tokensBackup = JsonUtil.cloneDeep(this.tokens);
    for (const subhutiParserOr of subhutiParserOrs) {
        const tokens = JsonUtil.cloneDeep(tokensBackup);
        this.setTokens(tokens);
        this.setMatchSuccess(false);
        subhutiParserOr.alt();
        // 如果处理成功则跳出
        if (this.matchSuccess) {
            break;
        }
    }
    return this.getCurCst();
}

项目地址

体验方式

git clone https://github.com/alamhubb/subhuti/tree/parser

npm i

npm run test

1728594111869.jpg

执行得到语法树结果

{
  "name": "program",
  "children": [
    {
      "name": "letKeywords",
      "children": [
        {
          "name": "let",
          "children": [],
          "tokens": [],
          "value": "let"
        }
      ],
      "tokens": [
        {
          "tokenName": "let",
          "tokenValue": "let"
        }
      ]
    },
    {
      "name": "identifierEqual",
      "children": [
        {
          "name": "identifier",
          "children": [],
          "tokens": [],
          "value": "a"
        },
        {
          "name": "equal",
          "children": [],
          "tokens": [],
          "value": "="
        }
      ],
      "tokens": [
        {
          "tokenName": "identifier",
          "tokenValue": "a"
        },
        {
          "tokenName": "equal",
          "tokenValue": "="
        }
      ]
    },
    {
      "name": "assignmentExpression",
      "children": [
        {
          "name": "integer",
          "children": [],
          "tokens": [],
          "value": "1"
        }
      ],
      "tokens": [
        {
          "tokenName": "integer",
          "tokenValue": "1"
        }
      ]
    }
  ],
  "tokens": []
}

专栏

前端编译原理开发日记:subhuti(语法文件自动转换编程语言),ovs(纯js开发前端界面语法

相关推荐

告别 HTML、Template、JSX,像 Flutter 和 Swift 一样用 OOP 语法开发 Web 前端的新尝试

结尾

本人菜鸡,文中的不足之处,请谅解

如果本文的思路存在问题,或者有更便捷的实现方式,希望您能告知,不足之处也请您指出,非常感谢

本文只是尝试一种新思路,仅为一个简单demo,细节处理中存在漏洞,请谅解

特别鸣谢 cursor,chatGPT,chevrotain

  1. 如果在没有cursor,chatGPT之前,完全无法想象这是我这个菜鸡可以做的事情,chatGPT扩展了我的能力边界,使我可以去尝试那些原本能力之外的事

  2. chevrotain,可以通过js的语法扩展js的语法,让自定义语法变的很简单,部分初始灵感来自于chevrotain

求职

30岁,成人本科,10年前端(3年java,7年前端)20年代码经验求职,坐标(北京、天津、唐山)