说明
本文中的这个编程语言设计只支持最简单的功能,仅是为了简化理解,学习交流目的,可以支持任何复杂的语法解析,文中示例这么简单是为了简化理解,学习交流,不是只能支持这么简单的语法
语法功能:读取输入,执行代码中的特定函数并输出token和具体语法树(cst)
特定函数逻辑为翻转输入字符串
不标题党,直接上代码
代码如下
import {createToken} from "../subhuti/struct/SubhutiCreateToken";
import SubhutiParser, {SubhutiRule} from "../subhuti/SubhutiParser";
import SubhutiLexer from "../subhuti/SubhutiLexer";
//第一行代码,1.定义token,print函数token
export const hwPrint = createToken({name: 'print', pattern: /print/, isKeyword: true})
//2,定义code,token
export const hwCodeToken = createToken({name: 'code', pattern: /[\s\S]*/})
//3.定义一个编程语法,名为helloWorld语法
export default class HelloWorldGrammarParser extends SubhutiParser {
//4.定义根语法,消耗一个print token,
@SubhutiRule
program() {
//5.消耗一个print token
this.consume(hwPrint)
//6.调用另一个规则hwCodeStatement
this.hwCodeStatement()
//7.返回当前语法树
return this.getCurCst()
}
//8.定义一个hwCode规则
@SubhutiRule
hwCodeStatement() {
//9.消耗一个hwCodeToken
this.consume(hwCodeToken)
return this.getCurCst()
}
//10.执行语法
exec(cst = this.getCurCst(), code = '') {
//11.如果为根语法
if (cst.name === this.program.name) {
//12.如果包含 print token,这部分逻辑没用,lexer中已经校验过了必须包含 print token,仅为了再次说明一下
const print = cst.children.find(item => item.name === hwPrint.name)
if (!print) {
throw new Error('error')
}
//13.获取跟语法的结果code
code = super.exec(cst, code)
//14.翻转code
code = code.split('').reverse().join('')
//15.输出code
console.log(code)
return code
} else if (cst.name === hwPrint.name) {
//16.不打印 print,print作为语法规则,不属于hwCode
} else {
//17. 执行默认方法
code = super.exec(cst, code)
}
return code.trim();
}
}
//18. 获取hw语法代码
const code = 'print hello world'
//19. 创建lexer
const hwLexer = new SubhutiLexer([hwPrint, hwCodeToken])
//20. 解析代码,得到tokens
const tokens = hwLexer.lexer(code)
//21. 创建parser
const parser = new HelloWorldGrammarParser(tokens)
//解析语法树
const cst = parser.program()
console.log(cst)
//执行程序
parser.exec()
得到执行结果dlrow olleh
得到具体语法树(cst)
{
"name": "program",
"children": [
{
"name": "print",
"children": [],
"tokens": [],
"value": "print"
},
{
"name": "hwCodeStatement",
"children": [
{
"name": "code",
"children": [],
"tokens": [],
"value": " hello world"
}
],
"tokens": [
{
"tokenName": "code",
"tokenValue": " hello world"
}
]
}
],
"tokens": [
{
"tokenName": "print",
"tokenValue": "print"
}
]
}
文章总结
在这篇文章中,我们设计了一个自己定义的编程语言的语法,并且可以执行我们输入的代码
我们了解到了编译原理的
- token,读取代码,将代码解析成一个个最小的token单元
- lexer(scanner、tokenizer),读取代码,将代码解析成tokens数组的工具
- parser,读取tokens,执行特定语法规则,根据匹配的语法规则生成cst
- cst,具体语法树(Concrete Syntax Tree)
- visitor,访问者,递归遍历语法树,可以在重写visitor,在每一个语法规则中实现自己想要的逻辑,文中的exec函数就是visitor,我们就是在exec中根据当前语法判断,执行不同的逻辑
subhuti(须菩提)编译框架相关
项目地址
- 注意选择
helloworldgrammar分支,为本文对应的代码 github/subhuti/helloworldgrammar
体验方式
git clone https://github.com/alamhubb/subhuti/tree/helloworldgrammar
npm i
npm run test
核心parser灵感来源 chevrotain
本项目的核心parser部分灵感来源自 chevrotain ,chevrotain是一个使用js语法自定义语法规则的高性能编译器
与chevrotain差异
其实chevrotain已经足够简单了,但如果你想要更简单一点,可以通过subhuti来了解学习chevrotain,当你了解了subhuti,也就大致会使用chevrotain了
- chevrotain使用 ll() 实现 , ll()简述,就是在读取当前token的遇到一个 标识符,你不知道这是一个变量还是一个方法名,需要通过后面的token才能知道当前这个token的语义,这种情况就需要获取后面的token
- subhuti还未实现ll(*),而是采用穷举的方式,先匹配第一个规则,不匹配再匹配第二个的这种方式,性能距离chevrotain会差很远
优点
- 语法更简单,省略了chevrotain 大部分规则配置
- 更贴近原生js的使用习惯,使用定义方法的方式定义语法规则,调用方法的方式调用规则,基于js装饰器stage3实现
- 代码简单,目前项目仅作为交流学习使用,未投入生产,所以源码非常简单,不超500行,非常容易学习理解
缺点
- subhuti目前仅作为一个学习交流项目,不具备生产能力,当前仅是0.0.2版本,而chevrotain已经是一个非常成熟的github上2.5kstars的项目了
- subhuti测试不完备,目前仅测试了部分简单es5语法,还在继续完善支持其他各种语法中,chevrotain也缺少相关语法文件的定义模板,但是他的测试完备,因为很多人使用了
其他
个人观点,subhuti当前仅适合学习交流,可以帮助想要了解编译原理,轻松的了解和入门编译原理
结尾附言
我自己从2021年就一直想了解编译原理,一直缺少相关的时间和机会,直到最近才真正的入门了一点点,经历过入门编译原理的苦,才知道到底有多难,个人感觉主要是难在没有好的教程,
个人感觉,自信满满的觉得自己的文章,可以和大家一起管中窥豹,了解编译原理大致是怎么回事,解决入门恐惧和入门难问题
随便写个页面也得几百行代码,而入门编译原理,20多行代码就可以做到了,应该算简单的了吧
关于下一篇,实现个多俩token的语法
点赞超过5个的话吧,我就再写一篇,实现将 var a = 1 转成 let a = 10 , 或者大家有什么想看的比较有特点的,语法可以说一下,主要不能太复杂是因为文章篇幅,写的太复杂,代码就多了,感觉很难有人看
专栏
相关推荐
【编译原理创新尝试(一)】parser代码解析篇:不再写代码生成器generator,用类ebnf语法实现不同编程语言的自动转换,200行js示例入门编译原理