编译原理之美
宫文学
北京原点代码 CEO
46197 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 45 讲
开篇词 (1讲)
编译原理 · 期中考试周 (1讲)
编译原理之美
15
15
1.0x
00:00/00:00
登录|注册

04 | 语法分析(二):解决二元表达式中的难点

课程小结
书写语法规则,并进行推导
解决二元表达式中的难点
语法分析

该思维导图由 AI 生成,仅供参考

在“03 | 语法分析(一):纯手工打造公式计算器”中,我们已经初步实现了一个公式计算器。而且你还在这个过程中,直观地获得了写语法分析程序的体验,在一定程度上破除了对语法分析算法的神秘感。
当然了,你也遇到了一些问题,比如怎么消除左递归,怎么确保正确的优先级和结合性。所以本节课的主要目的就是解决这几个问题,让你掌握像算术运算这样的二元表达式(Binary Expression)。
不过在课程开始之前,我想先带你简单地温习一下什么是左递归(Left Recursive)、优先级(Priority)和结合性(Associativity)。
在二元表达式的语法规则中,如果产生式的第一个元素是它自身,那么程序就会无限地递归下去,这种情况就叫做左递归。比如加法表达式的产生式“加法表达式 + 乘法表达式”,就是左递归的。而优先级和结合性则是计算机语言中与表达式有关的核心概念。它们都涉及了语法规则的设计问题。
我们要想深入探讨语法规则设计,需要像在词法分析环节一样,先了解如何用形式化的方法表达语法规则。“工欲善其事必先利其器”。熟练地阅读和书写语法规则,是我们在语法分析环节需要掌握的一项基本功。
所以本节课我会先带你了解如何写语法规则,然后在此基础上,带你解决上面提到的三个问题。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了解决二元表达式中的难点,包括消除左递归、确保正确的优先级和结合性。通过介绍左递归、优先级和结合性的概念,以及形式化的语法规则表达,文章展示了如何生成抽象语法树(AST),并介绍了巴科斯范式(BNF)和扩展巴科斯范式(EBNF)的写法。此外,文章讨论了如何用语法规则保证表达式的优先级,以及如何确保正确的结合性,并提出了解决左递归问题的方法。通过深入浅出的方式,本文对语法分析中的关键概念和解决方法进行了全面介绍,对于想深入了解语法分析算法的读者具有很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《编译原理之美》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(57)

  • 最新
  • 精选
  • blacknhole
    产生式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' —> ε。

    作者回复: 对。总结得很好!描述得很直观。 改写左递归的算法,实际上是有公式的。限于篇幅,我没有去陷入这个公式的细节。 在编译原理里面还有很多这样的细节。我希望能在后续出的书里都包含到,并且仍然保持容易理解。

    2020-02-19
    8
    77
  • 毕达哥瓦斯
    希望老师多讲讲背后的原理和为什么会想到这么做 比如为什么想到改文法来消除左递归,为什么会想到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这种工具出现的历史比较早,最早是用来研究自然语言的。后来,在逻辑学(作为哲学的一部分)、数学中也得到了广泛的使用。比如现代数学的公理化运动,也就是把数学(比如欧几里得几何)看做一个形式系统;把数学定理的推导,看做是一个纯粹的形式化的变换过程。所以,它首先需要一门形式化的语言来描述数学中的命题,然后再基于一套推导逻辑去变换它们。然后再来看是否在有限的时间内一定能够推导出来,这也就是图灵的停机问题。 总结起来,我们在编译原理里面用到了一些形式语言方面的工具,它是被数学、语言学、逻辑学等多个学科共享的。它们都认为,该学科被研究的对象某种意义上是一些纯形式的变换。这种严谨的形式变换的过程,构筑了西方现代科学的严密推理体系,是那么多科学发现的底层根基。从这个角度,你其实可以体会到编译原理搞的是很基础的东西,是这个世界的一些底层的思维逻辑。 再次非常肯定你的思考精神。通过这种思考,你可以越挖越深,这个过程非常有趣。而且,你挖到一定程度,会发现很多知识体系都是通着的。比如,通过今天的探讨,你已经知道现代数学和编译原理是通着的。顺着这条线,你还会发现更多通着的知识。比如,逻辑学和集成电路的底层是通着的;计算机的底层逻辑跟数学的底层逻辑是一回事;计算过程又跟物理学的某些原理是一回事。很有意思。

    2020-10-25
    2
    34
  • Enzo
    老师 看不懂以下的公式 add -> mul | add + mul mul -> pri | mul * pri pri -> Id | Num | (add) 是需要找本书看看吗?

    作者回复: 我给你解释一下吧: 以 add -> mul | add + mul 为例, -> 意思是推导出; | 意思是“或者” 这个加号,我回头修改一下吧,可以用引号引起来,'+'只是匹配一个+号字符的意思,没有别的意思。 所以,这个产生式的意思是: 加法表达式,要么是一个乘法表达式,要么是一个加法表达式,后面跟个+号,然后再跟一个乘法表达式。 参考书的话,看看这个链接:https://time.geekbang.org/column/article/125948

    2019-09-06
    3
    23
  • Lafite
    请问宫老师 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层也成功。 这个过程是否听得清楚? 可以换着不同的例子多推导几遍,就会变得很熟练了!

    2019-09-03
    3
    19
  • 谱写未来
    只有第一步用add,接下来都用add',后面不是都是add'了,还是左边那张图不是吗?

    作者回复: 是的。 我们通过改写规则的方法,能够避免左递归,但无法同时照顾结合性。这是很多教科书都没有提到的一件事情。 好在,这个事情比较简单,因为改写后的规则,是多了一个标准的“尾巴”。对,很多人都称呼它为尾巴。这个尾巴可以特别处理。 也就是说,结合性的信息已经不是单纯通过上下文无关文法提供了,要辅助额外的信息。 无独有偶,还有的作者用别的方法来解决算法优先级问题,比如LLVM的一个初学者教程,用的也是标注算符优先级的方法,也要在文法的基础上提供额外的信息给算法。 http://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html 本课程讲究实践。在实践中才会看到这些教科书上讲不到的点,但在面对实际问题时必须要解决。

    2019-08-21
    15
  • knull
    老师,我简单研究了下bnf,我觉得你写法最好修正下,不然不好看。比如: 原来的写法:add -> mul (+ mul)* 现在的写法:add -> mul ('+' mul)* '+'表示关键字; + 直接用,表示1个活多个; 加单引号以示区分,看起来方便一点

    作者回复: 有道理!用EBNF的话,+号有特殊含义。 我们修改一下文稿。 谢谢你的建议,你很细心,并且自己去做研究了!

    2019-08-28
    11
  • xxx
    左递归这块确实蛮烧脑,总结一下吧: 1. 左递归会造成无限递归,从而造成递归下降法无法结束。 2. 可以将左递归改成右递归,这样便能够结束。但结合性会出现问题。 3. 改成右递归之后,就成了尾递归,那么可以用循环代替递归。而这里不是为了优化性能,而是为了修改行为!(本来直接代替后应该是后面的token产生的树会成为前面的子树,但我们就是硬改为后面产生的树变成根)

    作者回复: 嗯,你总结出了其中的要点! 后面的课程里,还有别的方法来破除这些障碍,比如LR算法不怕左递归。 在另一门课,《编译原理实战课》中,你会看到常用语言其实用一个很优雅的运算符优先级算法就能解决常见的二元运算的表达式的解析问题。

    2021-02-15
    2
    10
  • 阿崔cxr
    老师 上一讲看懂了 这一讲在推导公式的时候迷糊了。可以加点推导过程的详细讲解嘛 而不是直接给一个推导的结果图

    作者回复: 好的,我对于公式推导过程再加个图。加完了在回复中告诉你。 你指的是用: add -> mul add' add' -> + mul add' | ε 来推导2+3+4的过程不清楚吗?

    2019-08-21
    5
  • 贾献华
    https://github.com/iOSDevLog/Logo Swift 版《编译原理之美》代码,可以在 iOS 上运行。

    作者回复: 厉害!点赞!

    2019-08-20
    2
    5
  • pwlazy
    2+3+4+5生产的AST 是否是这样的? Programm Calculator AdditiveExp + AdditiveExp + AdditiveExp + IntLiteral 2 IntLiteral 3 IntLiteral 4 IntLiteral 5

    作者回复: 是,没错。

    2019-08-21
    2
    3
收起评论
显示
设置
留言
57
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部