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

157 阅读9分钟

提示

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

接上一章

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

编译相关介绍

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

编译相关技术框架

  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文件,自动将cst转为目标代码的实现原理

generator生成器实现思路总体概述

  • 由于通过上章,我们已经得到了 let a = 1 对应的语法树cst,接下来我们继续处理通过另一编程语言的声明文件,将两个语法文件映射起来,然后实现代码的自动转换,自动转换的逻辑需要两个语法文件的,语法名称一一对应, 如下:
@SubhutiRule // 定义一个解析规则
letKeywords() { // 定义letKeywords规则
    this.consume(Es6TokenName.let); // 消耗let关键字token
    return this.getCurCst(); // 返回当前CST
}
  • Es6Parser的letKeywords语法名字叫这个,如果另一语法SubhutiMappingParser想要将let改为const,则需要有一个同名letKeywords的语法,然后将this.consume(Es6TokenName.let)改为this.consume(Es6TokenName.const)消耗不同的token即可,然后就可以自动将 let a = 1 转为 const a = 1

  • 一个最简单的generator,就是递归遍历语法树,将每一个节点的token追加到代码串上就实现了代码生成

  • 语法映射generator的区别是,在递归遍历语法树的时候,需要找到当前语法树的映射语法,将语法树的子节点,作为token,让映射语法执行,得到映射后的语法树

generator生成器实现思路步骤概述

  1. 语法执行逻辑与上一章parser执行逻辑一致,只不过新增了一个标识,区分generator模式和parser模式
  2. SubhutiMappingParser继承自Es6Parser,仅仅修改了letKeywords语法,保证这两个语法的语法名称一致,使用上一章得到的语法树cst,递归调用SubhutiMappingParser.program()及其子节点语法,与parser逻辑一致,首次执行,则初始化语法栈,执行语法,将语法入栈,执行语法,语法执行完毕,语法出栈,加入父语法子节点
  3. 核心区别在于消费token的形式,parser是按照顺序从tokens中匹配消耗语法对应的token,而generator是从当前规则对应的语法树的子节点中寻找token,且不一定是按顺序匹配,因为不同编程语法顺序不一致
项目结构说明

image.png

与上一章对比图中代码结构可知,本章仅新增了

  • SubhutiMappingParser,定义SubhutiMapping语法
  • SubhutiMappingGenerator, 定义SubhutiMapping代码生成方式
  • SubhutiGenerator是基础的generator代码
  • 其他文件均未变化
具体代码
  1. 设置执行模式为生成模式 1728600973049.jpg

    subhutiMappingParser.setGeneratorMode(true)
    
  2. SubhutiMappingParser继承自Es6Parser,仅仅修改了letKeywords语法,保证这两个语法的语法名称一致,使用上一章得到的语法树cst,递归调用SubhutiMappingParser.program()及其子节点语法,与parser逻辑一致,首次执行,则初始化语法栈,执行语法,将语法入栈,执行语法,语法执行完毕,语法出栈,加入父语法子节点

    因为默认两个语法名称一致,所以执行传入的cst的同名语法规则

    import SubhutiGenerator from "../subhuti/SubhutiGenerator";
    import SubhutiCst from "../subhuti/struct/SubhutiCst";
    import alienMappingParser from "./SubhutiMappingParser";
    
    export default class SubhutiMappingGenerator extends SubhutiGenerator {
        generator(cst: SubhutiCst, code = '') {
            if (alienMappingParser && alienMappingParser.generatorMode) {
                const newCst = alienMappingParser[cst.name]();
                if (!newCst) {
                    throw new Error('语法错误');
                }
                alienMappingParser.setGeneratorMode(false)
                return super.generator(newCst, code);
            }
            return super.generator(cst, code);
        }
    }
    export const subhutiMappingGenerator = new SubhutiMappingGenerator();
    

    定义基础的generator生成方式,递归遍历语法树,追加代码串

    import SubhutiCst from "./struct/SubhutiCst";
    
    export default class SubhutiGenerator {
        //默认就是遍历生成
        generator(cst: SubhutiCst, code = '') {
            cst.children.forEach(item => {
                if (item.value) {
                    code += ' ' + item.value;
                }
                else {
                    code = this.generator(item, code);
                }
            });
            return code.trim();
        }
    }
    
  3. 语法执行逻辑与上一章parser执行逻辑一致,只不过新增了一个标识,区分generator模式和parser模式,核心区别,token处理方式不同,

    • 判断是否有当前token的映射名称,找到映射规则,比如const映射let
    • 找到当前语法对应的语法树,获取语法树子节点
    • 在子节点中匹配token
    • 找到token后逻辑与parser一致,加入到父节点的子节点中
    • 最终生成一个映射后的语法树,然后调用普通的代码生成器,递归遍历语法树,追加代码串生成代码
generateToken(tokenName: string) { // 定义生成token的方法,接收token名称作为参数
    // let
    const genTokenName = tokenName; // 保存原始token名称
    let childTokenName = mappingTokenMap[tokenName]; // 从映射表中获取子token名称
    if (childTokenName) { // 如果存在子token名称
        tokenName = childTokenName; // 更新token名称为子token名称
    }
    const mappingCst = this.mappingCstMap.get(this.curCst.name); // 获取当前CST的映射CST
    if (!mappingCst) { // 如果没有映射CST
        return; // 直接返回
    }
    // 在子节点中找到并删除
    const mappingChildren = mappingCst.children; // 获取映射CST的子节点
    if (!mappingCst.children.length) { // 如果没有子节点
        return; // 直接返回
    }
    const findChildIndex = mappingChildren.findIndex(item => item.name === tokenName); // 查找子节点中匹配的token名称的索引
    if (findChildIndex < 0) { // 如果没有找到匹配的子节点
        return; // 直接返回
    }
    // 在父元素中删除
    const childCst = mappingChildren.splice(findChildIndex, 1)[0]; // 从子节点中删除匹配的节点并获取
    if (!childCst) { // 如果删除的节点不存在
        throw new Error('语法错误'); // 抛出语法错误
    }
    // 需要有一个标识,标志这个节点已经处理完毕了
    const cst = new SubhutiCst(); // 创建一个新的CST节点
    if (childTokenName) { // 如果存在子token名称
        cst.name = genTokenName; // 设置CST节点名称为原始token名称
        cst.value = genTokenName; // 设置CST节点值为原始token名称
    } else {
        cst.name = childCst.name; // 设置CST节点名称为子节点名称
        cst.value = childCst.value; // 设置CST节点值为子节点值
    }
    const token = new SubhutiMatchToken({ // 创建一个新的匹配token
        tokenName: cst.name, // 设置token名称为CST节点名称
        tokenValue: cst.value // 设置token值为CST节点值
    });
    this.curCst.children.push(cst); // 将CST节点添加到当前CST的子节点中
    this.curCst.tokens.push(token); // 将token添加到当前CST的tokens中
    this.setMatchSuccess(true); // 设置匹配成功标志为true
    return this.generateCst(cst); // 生成并返回CST
}

consume(tokenName: string): SubhutiCst {
    if (this.generatorMode) {
        return this.generateToken(tokenName);
    } else {
        return super.consumeToken(tokenName);
    }
}

项目地址

体验方式

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

npm i

npm run test

image.png

执行得到语法树结果

result: success
input: let a = 1
output: const a = 1

专栏

前端编译原理开发日记: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年代码经验求职,坐标(北京、天津、唐山)