上一节我们实现了编译原理中语法解析入门,能够解析简单的由let关键字开头的变量定义语句,现在我们再接再厉,实现解析由return 开头的返回值语句。由于return 后面可以跟着一个变量,一个数值,一个函数调用,以及一个带有操作符的计算式,这几种情况,我们统一用算术表达式来归纳。因此对应于return 语句的语法解析表达式是:

ReturnStatement := return Expression

为了简单起见,我们代码实现时,任然假设return 后面跟着一个数字字符串,后面我们会深入探讨如何解析异常复杂的算术表达式。在MonkeyCompilerParser.js中添加如下代码:

parseStatement() {switch (this.curToken.getType()) {case this.lexer.LET:return this.parseLetStatement()//change herecase this.lexer.RETURN:return this.parseReturnStatement()...}
}

上面代码表明,如果解析器读取token 序列时,如果遇到关键字return,那么就调用parseReturnStatement来解析接下来的代码,我们看看它的实现:

parseReturnStatement() {var props = {}props.token = this.curToken//change laterif (!this.expectPeek(this.lexer.INTEGER)) {return null}var exprProps = {}exprProps.token = this.curToken;props.expression = new Expression(exprProps)if (!this.expectPeek(this.lexer.SEMICOLON)) {return null}return new ReturnStatement(props) }

在上面代码的实现中,它检测跟着return关键字后面的是否是数字字符串,如果不是,那表示语法出错,如果是,那么再判断数字后面是否以分号结尾,如果这些条件都满足的话,那表明当前解析到的代码语句确实符合return语句的语法规定,于是就构建一个ReturnStatement对象返回,该对象与我们上一节实现的LetStatement类几乎一致,代码如下:

class ReturnStatement extends Statement{constructor(props) {super(props)this.token = props.tokenthis.expression = props.expressionvar s = "return with " + this.expression.getLiteral()this.tokenLiteral = s}
}

上面代码完成后,我们在编辑框输入return 语句,点击下面红色的解析按钮后,结果如下:

可以看到,点击按钮后,在控制台上显示了“return with 1234”的语句,这表明我们的语法解析器能够识别return语句。接下来我们进入到复杂算术表达式的解析阶段,这里是编译原理算法的一大难点所在。

算术表达式的解析之所以困难,主要在于表达式类型多样,并且需要考虑运算符的优先级,例如 5 * 5 + 10 , 语法解析器就得明白,需要先做乘法,然后再做加法,因为乘法的优先级要高于加法。对于算术表达式:(5+5)*10,则要先做加法,再做乘法,因为括号的优先级要高于乘号。

解析器还得考虑不同操作符产生不同含义的表达式,例如 -5 表示的是一个数值也就是负五,而–5 表示的是一次算术操作,意思是计算5-1所得的值,也就是4.

同时,解析器还得考虑符号的次序,操作符在操作数的前面则称为前序操作符,例如-5, –5, !true 等,这些运算符都叫前序操作符;5-1, 2*3 这些表达式的符号夹在两个操作数中间,所以叫中序操作符;而5–, 5++ 这些表达式中,符号在操作数的后面,因此叫后续操作符。

此外,表达式还可以是异常复杂的形式表现,例如:5 * add(5,6) + 3, add(add(5,3), add(6,7)), 前面表达式在运算中包含函数调用,后面表达式是函数调用中又包含着函数调用,由于算术表达式展现形式多种多样,要通过它光怪陆离的表象识别它的本质是一件很困难的事情,因此,语法解析器对算术表达式解析算法的发明和实现是计算机科学发展史上光辉的一页。

计算机科学家,斯坦福大学教授,梵高.普拉特(Vaughan Pratt)发明了一种非常聪明且优雅的解析算法,但一直不被学界所认识,后来由大牛Douglas Crockford,也就是写了“JavaScript: The Good Parts” 这本书的作者大力举荐,并通过展示该算法能快速有效的解析javascript语法。同时依靠该算法开发出了JS语言的静态检测器JSLint后,该算法才被业界所熟知。我们这里就采用Pratt发明的算法,名为“自顶向下的操作符优先级解析法”来解析Monkey语言的算术表达式,以下是该算法的描述链接,大家可以点击阅读:

http://crockford.com/javascript/tdop/tdop.html

道可道,非常道。有些原理是很难通过语言描述出来的,对他的理解,你只能去感知,而不能简单的去阅读,编译原理就属于这类型理论,在大学里,编译原理之所以被大家视若危途,就是因为理论讲起来晦涩难懂,其实只要有代码让学生亲手尝试一下,要掌握理论其实根本不难,我们采用的就是第二种办法,通过代码的编写和调试来讲解理论,而不是用嘴巴说话来讲解理论,抽象的原理就像“爱”,说没用,做才有用!

我们现在代码中增加一个类,用来表示算术表达式:

class ExpressionStatement extends Statement {constructor(props) {super(props)this.token = props.tokenthis.expression = props.expressionvar s = "expression: " + this.expression.getLiteral()this.tokenLiteral = s}
}

代码原来跟以前的LetStatement, ReturnStatement一样,没有独特之处。接下来我们设计一个解析函数表,当解析器遇到某种类型的token时,它就根据token在表里拿出一个解析函数,执行这个函数就能实现对当前token的解析,因此代码如下:

class MonkeyCompilerParser {constructor(lexer) {....//change herethis.LOWEST = 0this.EQUALS = 1  // ==this.LESSGREATER = 2 // < or >this.SUM = 3this.PRODUCT = 4this.PREFIX = 5 //-X or !Xthis.CALL = 6  //myFunction(X)this.prefixParseFns = {}this.prefixParseFns[this.lexer.IDENTIFIER] = this.parseIdentifierthis.prefixParseFns[this.lexer.INTEGER] = this.parseIntegerLiteral}
}

我们提到的函数表就是prefixParseFns, 从代码可以看成,如果解析器当前遇到的token类型是变量字符串,也就是lexer.IDENTIFIER,时,解析器就从该表中拿出parseIdentifier这个函数来执行,如果解析器当前遇到的token类型是数组字符串,那么它便从该表中拿出函数parseIntegerLiteral来执行。于是我们的解析主函数及上面函数表中对应的两个函数实现如下:

parseStatement() {switch (this.curToken.getType()) {case this.lexer.LET:return this.parseLetStatement()//change herecase this.lexer.RETURN:return this.parseReturnStatement()default://change herereturn this.parseExpressionStatement()}}....//change hereparseExpressionStatement() {var props = {}props.token = this.curTokenprops.expression = this.parseExpression(this.LOWEST)var stmt = new ExpressionStatement(props)if (this.peekTokenIs(this.lexer.SEMICOLON)) {this.nextToken()}return stmt}createIdentifier() {var identProps = {}identProps.token = this.curTokenidentProps.value = this.curToken.getLiteral()return new Identifier(identProps)}//change hereparseExpression(precedence) {var prefix = this.prefixParseFns[this.curToken.getType()]if (prefix === null) {return null}return prefix(this)}//change hereparseIdentifier(caller) {return caller.createIdentifier()}//change hereparseIntegerLiteral(caller) {var intProps = {}intProps.token = caller.curTokenintProps.value = parseInt(caller.curToken.getLiteral())if (intProps.value === NaN) {console.log("could not parse token as integer")return null}return new IntegerLiteral(intProps)}

在函数parseStatement中,如果解析器当前遇到的token不是关键字let 或者return,那么他就调用parseExpressionStatement来进行算术表达式的解析。在该函数里,它调用parseExpression来做解析,在后者的实现中,它就是非常简单的拿着当前token到函数表里去查询,拿到对应的解析函数后直接执行就可以了,调用这个函数时传入了一个参数叫precedence,它是用来表示解析优先级的,这点我们在后面再进行探讨。

如果当前对应的token是变量字符串,也就是IDENTIFIER,那么函数parseIdentifier就会被调用,它直接调用createIdentifier,后者则是用当前token构建一个Identifier类的实例即可。如果当前解析器读取到的是数字字符串,那么它会从表中找到函数parseIntegerLiteral来执行,该函数根据当前token,把它的内容解析成整形数值后,创建一个IntegerLiteral的类实例。IntegerLiteral的定义如下:

class IntegerLiteral extends Expression {constructor(props) {super(props)this.token = props.tokenthis.value = props.valuevar s = "Integer value is: " + this.token.getLiteral()this.tokenLiteral = s}
}

上面的代码完成后,解析器就可以解析算术表达式的两种特殊情况,也就是变量和数字。从这里我们可以看到,Pratt解析法的精髓就是通过建立一张表,把不同类型token的解析对应到不同的函数,解析器只需机械的根据当前token对象查表并执行就可以了,于是解析器的设计逻辑得以大大简化。

上面代码完成后,我们在编辑框中输入变量和数字字符串,点击解析按钮后,解析结果如下:

从上图所示结果来看,我们的解析器已经能轻松的处理算术表达式中的两种简单情况,也就是变量和数字,当然算术表达式最复杂的还是带有运算符和函数调用的情况,接下来我们会就这些复杂情况的处理做深入探讨。

普拉特解析法的特点是通过查表来获得对当前token的解析函数,程序事先配置好各种情况下的解析方式,运行时就可以根据具体遇到的token迅速从表中获得解析函数去执行即可。从这一节看来,普拉特解析法似乎只处理了两种非常简单的算术表达式情况,在后面的章节中,我们会看到该方法在解析非常复杂的表达式,例如含有多层括号,函数间套调用,运算符的优先级和前缀中序变化等棘手情况时,普拉特分析法将产生巨大的解析威力。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:

设计自制编程语言Monkey编译器:使用普拉特解析法解析复杂的算术表达式相关推荐

  1. Reactjs+BootStrap开发自制编程语言Monkey的编译器:发刊词

    编译原理几乎是计算机专业中最晦涩难懂的课程.很多学生学这门课只不过是为了通过考试,学完后对编译原理之精妙仍然是摸不着头脑.而很多教这门课的老师,也只不过是混口饭吃,他自己未必对编译原理有多少深入的了解 ...

  2. 不求甚解之自制编程语言

    开始写不求甚解系列,为了让自己开始再次学习一些it方面的知识,主要是发现自己从毕业这几年开始, 一直没有再次写写博客,让自己安静下来,让自己重新思考,这也让我一直都是在重复一些很肤浅的东西 ,但同时忘 ...

  3. 《自制编程语言--基于C语言 郑钢》学习笔记

    <自制编程语言>学习笔记 本仓库内容 <自制编程语言>源码 src/sparrow.tgz <自制编程语言>读书笔记 docs/* <自制编程语言>样章 ...

  4. CNCC技术论坛 | 面向人工智能芯片的编程语言和编译器

    本论坛将于CNCC期间,10月24日13:30-15:30,在北京新世纪日航饭店2层四川厅举行.本论坛邀请到了国内外知名学者和工业界领军人物一起,讨论在人工智能领域设计领域定制芯片的挑战和机遇.欢迎您 ...

  5. 基于c语言 自制编程语言,自制编程语言:基于C语言

    前百度高 级工程师.专业书<操作系统真相还原>的作者的又一力作业界专家联名推荐滴滴系统部技术高 级总监于晓声阿里巴巴蚂蚁金服技术专家肖金亮百度资 深运维工程师陈晓聪 360企业安全集团政企 ...

  6. 【一天一门编程语言】怎样设计一门编程语言?

    怎样设计一门编程语言? 确定目标 确定语言的用途: 是一门通用编程语言,还是一门专门面向某个特定目标的语言? 是一门面向对象的语言,还是一门过程化的语言? 将语言的最终用户定义为谁? 确定语言的特性: ...

  7. 自制编程语言 基于c语言,GitHub - yifengyou/sparrow: 郑钢《自制编程语言》随书源码及读书笔记...

    <自制编程语言>学习笔记 本仓库内容 <自制编程语言>源码 src/sparrow.tgz <自制编程语言>读书笔记 docs/* <自制编程语言>样章 ...

  8. 编译《自制编程语言 基于c语言》 郑钢 书中代码 idea

    编译<自制编程语言 基于c语言> 郑钢 书中代码 文章目录 编译<自制编程语言 基于c语言> 郑钢 书中代码 编译器 代码获取 正规途径 其他途径 运行 hello world ...

  9. 自制编程语言,六个令你迷惑的问题

    自制编程语言和虚拟机,这是一个看似很深奥的课题,也涉及当今互联网流行的主题,许多技术人员对其心驰神往,但要领悟其精髓步履维艰. <自制编程语言>循序渐进.由浅到深地讲解了丰富的基础知识,覆 ...

最新文章

  1. Linux常用安全设置
  2. python write非法字符报错_Python爬虫实现的微信公众号文章下载器
  3. 判断点是否在多边形内——射线法
  4. @transaction使自定义注解失效_【完美】SpringBoot中使用注解来实现 Redis 分布式锁...
  5. 前台html调用函数 格式化输出
  6. 代码里-3gt;gt;1是-2但3gt;gt;1是1,-3/2却又是-1,为什么?
  7. ASP.NET企业开发框架IsLine FrameWork系列之五--DataProvider 数据访问(中)
  8. c#报错不实现接口成员_《C#程序设计》 习 题 集
  9. STM8单片机低功耗---等待(Wait)模式实现
  10. 解决centos6.4 启动dell omsa 失败
  11. 蓝桥杯2020年第十一届C/C++国赛B组第二题-扩散
  12. 从Chrome源码看JS Array的实现
  13. 关于might_sleep的一点说明
  14. Hadoop完全分布安装详细过程--------****--------(ubuntu版本)
  15. 测试用例和缺陷报告模板
  16. vivo x6plus支持html,vivo x6plus手机USB驱动
  17. 电气工程及自动化 (独立本科) 自考
  18. 斐讯w2换表盘_【斐讯W2智能手表使用感受】表盘|屏幕|GPS|电量_摘要频道_什么值得买...
  19. 校长 – Roy's Blog
  20. java基础程序设计

热门文章

  1. LOOP AT SCREEN ABAP
  2. 若依RuoYi-Vue前后端项目启动流程
  3. ssm实验室设备管理系统java,项目模板、毕业设计
  4. 哈工大计算机科学与捄术学院,[哈尔滨工业大学]管理科学与工程
  5. WordPress免认证微信关注登陆插件
  6. 电脑如何同时安装JDK11和JDK1.8(适用于多个JDK)
  7. 橘子学Flink03之Flink的流处理与批处理
  8. 电脑会员管理系统怎么弄,电脑会员卡管理系统怎么弄
  9. Day739.GEO经纬度数据结构自定义数据结构 -Redis 核心技术与实战
  10. 520,用Python定制你的《本草纲目女孩》