提示
本文内容仅是一次新的尝试,文中的案例也仅仅是一个最简单的示例demo,文中代码仅做为学习交流,类似于伪代码,描述思路,删除了容错逻辑,逻辑不够严谨
接上一章
【编译原理创新尝试(一)】parser代码解析篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换,200行js示例入门编译原理
编译相关介绍
编译原理一般用来实现编程语言的语法扩展、和高低级或不同编程语言之间的语法转换
编译相关技术框架
- 前端,rolldown、oxc,vue之父尤雨溪融资3000w要做的事,用rust整合前端工具链
- 前端,babel,实现js代码低版本浏览器兼容、新特性支持语法扩展
- 前端,typescript,一种强类型语法,通过编译将其转为js,使其可以在浏览器中运行
- 前端,esLint,js代码格式化工具
- 前端,chevrotain,通过自定义类似于ebnf语法js类代码,可以解析任何语法
- 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转换为目标代码,为上一章,文章链接:【编译原理创新尝试(一)】parser代码解析篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换,200行js示例入门编译原理
-
第二章,介绍如何根据ebnf文件自动生成目标代码,为本章内容如下
正文二章、根据自定义类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生成器实现思路步骤概述
- 语法执行逻辑与上一章parser执行逻辑一致,只不过新增了一个标识,区分generator模式和parser模式
- SubhutiMappingParser继承自Es6Parser,仅仅修改了letKeywords语法,保证这两个语法的语法名称一致,使用上一章得到的语法树cst,递归调用
SubhutiMappingParser.program()
及其子节点语法,与parser逻辑一致,首次执行,则初始化语法栈,执行语法,将语法入栈,执行语法,语法执行完毕,语法出栈,加入父语法子节点 - 核心区别在于消费token的形式,parser是按照顺序从tokens中匹配消耗语法对应的token,而generator是从当前规则对应的语法树的子节点中寻找token,且不一定是按顺序匹配,因为不同编程语法顺序不一致
项目结构说明
与上一章对比图中代码结构可知,本章仅新增了
- SubhutiMappingParser,定义SubhutiMapping语法
- SubhutiMappingGenerator, 定义SubhutiMapping代码生成方式
- SubhutiGenerator是基础的generator代码
- 其他文件均未变化
具体代码
-
设置执行模式为生成模式
subhutiMappingParser.setGeneratorMode(true)
-
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(); } }
-
语法执行逻辑与上一章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);
}
}
项目地址
- 注意选择
generator
分支,为本文对应的代码 github/subhuti/generator
体验方式
git clone https://github.com/alamhubb/subhuti/tree/generator
npm i
npm run test
执行得到语法树结果
result: success
input: let a = 1
output: const a = 1
专栏
相关推荐
告别 HTML、Template、JSX,像 Flutter 和 Swift 一样用 OOP 语法开发 Web 前端的新尝试
结尾
本人菜鸡,文中的不足之处,请谅解
如果本文的思路存在问题,或者有更便捷的实现方式,希望您能告知,不足之处也请您指出,非常感谢
本文只是尝试一种新思路,仅为一个简单demo,细节处理中存在漏洞,请谅解
特别鸣谢 cursor,chatGPT,chevrotain
-
如果在没有cursor,chatGPT之前,完全无法想象这是我这个菜鸡可以做的事情,chatGPT扩展了我的能力边界,使我可以去尝试那些原本能力之外的事
-
chevrotain,可以通过js的语法扩展js的语法,让自定义语法变的很简单,部分初始灵感来自于chevrotain