【非标题党】用二十几行代码实现一个属于自己的编程语言编译器,我开源了一个js实现的语法编译器框架subhuti,轻松入门编译原理

351 阅读5分钟

说明

本文中的这个编程语言设计只支持最简单的功能,仅是为了简化理解,学习交流目的,可以支持任何复杂的语法解析,文中示例这么简单是为了简化理解,学习交流,不是只能支持这么简单的语法

语法功能:读取输入,执行代码中的特定函数并输出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(须菩提)编译框架相关

项目地址

体验方式

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 , 或者大家有什么想看的比较有特点的,语法可以说一下,主要不能太复杂是因为文章篇幅,写的太复杂,代码就多了,感觉很难有人看

专栏

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

相关推荐

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

求职

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