04 | 语法分析(二):解决二元表达式中的难点
04 | 语法分析(二):解决二元表达式中的难点
讲述:宫文学
时长13:42大小12.54M
书写语法规则,并进行推导
确保正确的优先级
确保正确的结合性
消除左递归
课程小结
一课一思
赞 28
提建议
精选留言(57)
- blacknhole2020-02-19产生式add -> add + mul | mul是如何改写成产生式add -> mul add'和add' -> + mul add' | ε的,文中并未交代,评论中有人提出来了这个问题,老师依然没有回答。 我查阅了“龙书”,找到了答案。我的理解是这样的: 1,add有两个产生式:①add —> add + mul,②add —> mul。如果只使用①来推导add,那么推导过程无法终结,会一直持续下去,形成add + mul + mul … + mul 这样的序列。因为不会有无限长的表达式,所以,推导过程必然会使用到②,且是在最后一步使用。也即,使用①②推导add后得到的序列最左边为mul。因而,add的产生式可以改写为add —> mul add'。 2,add'的产生式需要满足+ mul + mul … + mul这样的序列,所以可以写为 add' —> + mul add'。因为序列长度必然有限,所以,需要再加一个产生式以终结add'的推导过程:add' —> ε。展开
作者回复: 对。总结得很好!描述得很直观。 改写左递归的算法,实际上是有公式的。限于篇幅,我没有去陷入这个公式的细节。 在编译原理里面还有很多这样的细节。我希望能在后续出的书里都包含到,并且仍然保持容易理解。
共 8 条评论69 - 毕达哥瓦斯2020-10-25希望老师多讲讲背后的原理和为什么会想到这么做 比如为什么想到改文法来消除左递归,为什么会想到ebnf
作者回复: 你往深里又想了一层,探究背后的why,这很好,值得肯定!所以,我也拿出比较多的篇幅来回复你的问题。 1.为什么可以改写文法。 这其实有个前提,就是存在多个文法能生成相同语句。你可能有这样的经验,当需要写一个正则表达式来匹配字符串的时候,你能写出多个等价的正则表达式。对于上下文无关文法也一样,存在多个文法,能生成相同模式的语句。 既然存在等价的文法,那么自然可以去选择一个文法,能够更好的与某个算法去适配。LL算法是不能处理左递归的,那么就找到一个等价的文法,并且避免左递归就好了。 需要注意的是,虽然多个文法可以生成相同的语句,但是生成过程是不一样的。这也就导致解析树是不一样的。所以,有时候需要把解析树重新变换,来生成AST。 2.为什么想到EBNF EBNF实际上等价于产生式,只不过写法不一样而已。实际上有很多跟EBNF等价的文法书写方式。它们都是用来描述一种语言的结构,或者是一种文档(如XML文档)的结构的,所以它们也被叫做元语言(Metalanguage)。我在后面的元编程一章对Meta的级别有阐述,你也可以看一下。 所以,你的问题实际就变成了,为什么一个语言的文法会想到用产生式或者EBNF来描述。 实际上,一门语言不是必须用产生式或EBNF来描述的。有些类型的语言用其他方式描述更简单和方便,比如Indexed Language(https://en.wikipedia.org/wiki/Indexed_language)。不过,对于大多数计算机语言来说,用上下文无关文法描述是比较合适的,而上下文无关文法采用的是一种字符串重写规则(String Rewiting System, SRS),也就是把一个字符串中的一部分不断地替换成另外的字符串。采用这种工具没有别的原因,就是因为它在描述语言的语法方面是很有效的。如果你追求它的数学根基,你可以去看半图厄理论,在数理逻辑里有。 SRS这种工具出现的历史比较早,最早是用来研究自然语言的。后来,在逻辑学(作为哲学的一部分)、数学中也得到了广泛的使用。比如现代数学的公理化运动,也就是把数学(比如欧几里得几何)看做一个形式系统;把数学定理的推导,看做是一个纯粹的形式化的变换过程。所以,它首先需要一门形式化的语言来描述数学中的命题,然后再基于一套推导逻辑去变换它们。然后再来看是否在有限的时间内一定能够推导出来,这也就是图灵的停机问题。 总结起来,我们在编译原理里面用到了一些形式语言方面的工具,它是被数学、语言学、逻辑学等多个学科共享的。它们都认为,该学科被研究的对象某种意义上是一些纯形式的变换。这种严谨的形式变换的过程,构筑了西方现代科学的严密推理体系,是那么多科学发现的底层根基。从这个角度,你其实可以体会到编译原理搞的是很基础的东西,是这个世界的一些底层的思维逻辑。 再次非常肯定你的思考精神。通过这种思考,你可以越挖越深,这个过程非常有趣。而且,你挖到一定程度,会发现很多知识体系都是通着的。比如,通过今天的探讨,你已经知道现代数学和编译原理是通着的。顺着这条线,你还会发现更多通着的知识。比如,逻辑学和集成电路的底层是通着的;计算机的底层逻辑跟数学的底层逻辑是一回事;计算过程又跟物理学的某些原理是一回事。很有意思。
共 2 条评论31 - Enzo2019-09-06老师 看不懂以下的公式 add -> mul | add + mul mul -> pri | mul * pri pri -> Id | Num | (add) 是需要找本书看看吗?
作者回复: 我给你解释一下吧: 以 add -> mul | add + mul 为例, -> 意思是推导出; | 意思是“或者” 这个加号,我回头修改一下吧,可以用引号引起来,'+'只是匹配一个+号字符的意思,没有别的意思。 所以,这个产生式的意思是: 加法表达式,要么是一个乘法表达式,要么是一个加法表达式,后面跟个+号,然后再跟一个乘法表达式。 参考书的话,看看这个链接:https://time.geekbang.org/column/article/125948
共 3 条评论23 - Lafite2019-09-03请问宫老师 add -> mul add' add' -> + mul add' | ε 这两个产生式的推导过程应该是怎么样的,为什么可以转化为EBNF的写法呢。
作者回复: 转化成EBNF:add ::= mul ( '+' mul)* 一个表达式就解决了,更简洁。不需要add'了。 推导过程,要看算法。每种算法采用的推导过程是不一样的。如果用递归下降算法,推导:2+3*5 我们按照调用过程分成几层: 第1层:采用 mul add',因为mul能完整的匹配2,不能再往后匹配了,所以第一个子节点建立完毕。接着用add'去建立第二个子节点。 第2层:运用add'的第一个产生式,先匹配上了+号,之后去匹配mul,也就是3*5,也是成功的。然后再去匹配add'。 第3层:这次用add'的时候,还是先尝试第一个产生式,失败。为什么呢?因为没有+号。回溯。尝试第二个产生式,即epsilon。也就是返回空。那么第3层就完成了。 第3层成功后第2层,第2层也就成功完成了。 同理,返回第1层,第1层也成功。 这个过程是否听得清楚? 可以换着不同的例子多推导几遍,就会变得很熟练了!
共 3 条评论17 - 谱写未来2019-08-21只有第一步用add,接下来都用add',后面不是都是add'了,还是左边那张图不是吗?
作者回复: 是的。 我们通过改写规则的方法,能够避免左递归,但无法同时照顾结合性。这是很多教科书都没有提到的一件事情。 好在,这个事情比较简单,因为改写后的规则,是多了一个标准的“尾巴”。对,很多人都称呼它为尾巴。这个尾巴可以特别处理。 也就是说,结合性的信息已经不是单纯通过上下文无关文法提供了,要辅助额外的信息。 无独有偶,还有的作者用别的方法来解决算法优先级问题,比如LLVM的一个初学者教程,用的也是标注算符优先级的方法,也要在文法的基础上提供额外的信息给算法。 http://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html 本课程讲究实践。在实践中才会看到这些教科书上讲不到的点,但在面对实际问题时必须要解决。
15 - knull2019-08-28老师,我简单研究了下bnf,我觉得你写法最好修正下,不然不好看。比如: 原来的写法:add -> mul (+ mul)* 现在的写法:add -> mul ('+' mul)* '+'表示关键字; + 直接用,表示1个活多个; 加单引号以示区分,看起来方便一点展开
作者回复: 有道理!用EBNF的话,+号有特殊含义。 我们修改一下文稿。 谢谢你的建议,你很细心,并且自己去做研究了!
11 - xxx2021-02-15左递归这块确实蛮烧脑,总结一下吧: 1. 左递归会造成无限递归,从而造成递归下降法无法结束。 2. 可以将左递归改成右递归,这样便能够结束。但结合性会出现问题。 3. 改成右递归之后,就成了尾递归,那么可以用循环代替递归。而这里不是为了优化性能,而是为了修改行为!(本来直接代替后应该是后面的token产生的树会成为前面的子树,但我们就是硬改为后面产生的树变成根)展开
作者回复: 嗯,你总结出了其中的要点! 后面的课程里,还有别的方法来破除这些障碍,比如LR算法不怕左递归。 在另一门课,《编译原理实战课》中,你会看到常用语言其实用一个很优雅的运算符优先级算法就能解决常见的二元运算的表达式的解析问题。
共 2 条评论9 - 阿崔cxr2019-08-21老师 上一讲看懂了 这一讲在推导公式的时候迷糊了。可以加点推导过程的详细讲解嘛 而不是直接给一个推导的结果图
作者回复: 好的,我对于公式推导过程再加个图。加完了在回复中告诉你。 你指的是用: add -> mul add' add' -> + mul add' | ε 来推导2+3+4的过程不清楚吗?
5 - 贾献华2019-08-20https://github.com/iOSDevLog/Logo Swift 版《编译原理之美》代码,可以在 iOS 上运行。
作者回复: 厉害!点赞!
共 2 条评论5 - 侯不住2020-04-17老师,我需要帮助。。。 add ::= mul | add + mul mul ::= pri | mul * pri pri ::= Id | Num | (add) 前面一讲2+3这个表达式使用add::=mul | add + mul这个产生式会有左递归的问题,为何到了这一讲上面的产生式就能分析2+3*4这样的表达式,这应该跟前面的一样左递归就会有问题啊。不明白不明白,求指导展开共 2 条评论4
- pwlazy2019-08-212+3+4+5生产的AST 是否是这样的? Programm Calculator AdditiveExp + AdditiveExp + AdditiveExp + IntLiteral 2 IntLiteral 3 IntLiteral 4 IntLiteral 5展开
作者回复: 是,没错。
共 2 条评论3 - 许童童2019-08-21老师可以说一下生成出来的AST怎么使用吗? https://github.com/jamiebuilds/the-super-tiny-compiler 这个编译器写得怎么样,老师可以说一下吗?
作者回复: AST是对计算机语言的结构化表示,它是一切后续工作的基础,比如做语义分析,翻译成目标代码。 看了一下你发的那个链接。是从类似lisp语言的函数调用翻译到C语言的格式。这属于语言翻译的范畴。 我有两点点评: 1.lisp语言很容易翻译,一个递归下降算法肯定搞定。因为它的语法结构很简单,所有的语法结构都是一层层括号的嵌套。 2.翻译后得到AST,再生成C的格式,这就很简单了。基本上就是把括号位置改一下而已。 感谢你经常参与讨论!
3 - 草戊2019-12-14antlr4能处理直接左递归了,表达式文法写起来直观很多
作者回复: 是的。
2 - nil2019-09-11老师你好,问个问题。最终通过循环来消除递归带来的二元预算符的结合性问题?能否直接在递归中消除结合性问题?
作者回复: 理论上是可以的,但需要给算法提供额外的信息。 采用递归下降算法的时候,我们在函数中标准的处理放肆,都是创建一个AST节点,并返回给调用者。调用者都是把返回的AST作为自己的子节点。 如果要改变结合性,相当于要知道什么时候把返回的节点作为自己的父节点。 Antlr里用属性标注的方法,来提供这个额外的信息。这种信息在标准的上下文无关文法中是无法提供的。
2 - Enzo2019-09-06exp -> or | or = exp or -> and | or || and and -> equal | and && equal equal -> rel | equal == rel | equal != rel rel -> add | rel > add | rel < add | rel >= add | rel <= add add -> mul | add + mul | add - mul mul -> pri | mul * pri | mul / pri 老师不懂这里的 + - >= 等符号的意思 能推荐本书 吗展开
作者回复: 所有的操作符,你可以加上引号,也就是去匹配这样一个字符而已。没有太复杂的意思。 add -> mul | add '+' mul | add '-' mul 参考书籍的话,参见这篇攻略:https://time.geekbang.org/column/article/125948
2 - 半桶水2019-08-21是否可以给一些扩展资料的链接,有些概念,推导还是需要更多资料和练习才能掌握
作者回复: 如果想练习语法规则的推导,那么随便买哪本教材都可以。一般也都会带些练习。 其他的扩展资料,我后面有想到的,会提供链接。
2 - 不会魔法2020-07-27不明白为啥上一节的 add -> Int | add + Int 不能匹配 2+3,会出现左递归,到了这一节 add -> mul | add + mul 又能匹配 2+ 3 * 5 中的 2 + 3 了共 1 条评论1
- 杨涛2020-06-01老师,请问exp -> or | or = exp这一句中or = exp对应出来是一个如何的表达式呢?
作者回复: 是赋值表达式。 你可以注意一个细节:这里递归项(exp)是在右边的,而后面的其他产生式,递归项是在左边的。你知不知道为什么?
共 3 条评论1 - 简玉2019-12-23学习这门课的时候 结合前后和留言问答 会好理解一些
作者回复: 嗯。留言中别人遇到的问题,对自己也会有用。你如果有问题的话,也多提问!
1 - 阿尔伯特2019-09-11https://github.com/albertabc/compiler 继续攒代码。 有了上节课的基础,这节相对比较容易理解。用文法的形式推导,最终消除了左递归的思路我觉得很有意思,用左右递归代表结合性,用文法上下级实现了优先级,这些可以作为解决问题的一个思路和方法,用比较平常的普通方法解决了这些问题。感觉比中缀后缀表达式容易掌握。展开
作者回复: 嗯。编译且运行了一下。
1