提示
本文内容仅是一次新的尝试,文中的案例也仅仅是一个最简单的示例demo,文中代码仅做为学习交流,类似于伪代码,描述思路,删除了容错逻辑,逻辑不够严谨
下一章
【编译原理创新尝试(二)】generator代码生成篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换
编译相关介绍
编译原理一般用来实现编程语言的语法扩展、和高低级或不同编程语言之间的语法转换
编译相关技术框架
- 前端,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转换为目标代码
-
第二章,介绍如何根据ebnf文件自动生成目标代码,文章内容放在下一篇文章链接:
【编译原理创新尝试(二)】generator代码生成篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换
正文一章、根据自定义类ebnf文件,解析代码的parser实现原理
parser思路概述
- 通过正则定义token格式
- lexer解析token,使用正则读字符串,遍历token读到一个最长匹配的就从字符串中删除token,加入tokens列表中
- 通过编码形式定义类似于ebnf的syntax语法(致敬chevrotain并创新,创新为采用装饰器方法直接定义静态可提示的具体方法,而非chevrotain通过方法动态创建匿名方法),这种方式易于调试,可读性比ebnf高
- parser读取tokens,执行顶级
Es6Parser.program()
语法(顶级语法包含子语法,嵌套层级),将顶级语法推入语法栈,匹配token,匹配到就从tokens列表删除token,加入到语法树,进入子语法,将子语法推入语法栈,执行相同匹配token,子语法执行完毕,将子语法推出语法栈,并将子语法加入父语法的子节点,执行完毕,得到完整的语法树
项目结构说明
- es6token,定义token格式
- es6Parser, 继承subhutiprser,定义具体语法
- struct包内是数据结构
- SubhutiLexer是代码解析tokens的代码
- SubhutiLexer是parser的基础代码
- test1LetJson为测试文件,直接执行文件,可以得到结果
具体代码
- 通过正则定义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();
}
项目地址
- 注意选择
parser
分支,为本文对应的代码 github/subhuti/parser
体验方式
git clone https://github.com/alamhubb/subhuti/tree/parser
npm i
npm run test
执行得到语法树结果
{
"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": []
}
专栏
相关推荐
告别 HTML、Template、JSX,像 Flutter 和 Swift 一样用 OOP 语法开发 Web 前端的新尝试
结尾
本人菜鸡,文中的不足之处,请谅解
如果本文的思路存在问题,或者有更便捷的实现方式,希望您能告知,不足之处也请您指出,非常感谢
本文只是尝试一种新思路,仅为一个简单demo,细节处理中存在漏洞,请谅解
特别鸣谢 cursor,chatGPT,chevrotain
-
如果在没有cursor,chatGPT之前,完全无法想象这是我这个菜鸡可以做的事情,chatGPT扩展了我的能力边界,使我可以去尝试那些原本能力之外的事
-
chevrotain,可以通过js的语法扩展js的语法,让自定义语法变的很简单,部分初始灵感来自于chevrotain