作者回复: 点赞!
作者回复: 这个实际上就是语法规则,是用BNF表达的。以addtive为例,它有两个产生式。
产生式1:一个乘法表达式
产生式2:一个加法表达式 + 乘法表达式。
通过上面两个产生式的组合,特别是产生式2的递归调用,就能推导出所有的加减乘数算术表达式。
比如,对于2*3这个表达式,运用的是产生式1。
对于2+3*5,运用的是产生式2。
我上面用的语法规则的写法,实际上是后面会用到的Antlr工具的写法。你也可以这样书写,就是一般教材上的写法:
A -> M | A + M
M -> int | M * int
我们每个非终结符只用了一个大写字母代表,比较简洁。我在文稿中用比较长的单词,是为了容易理解其含义。
其中的竖线,是选择其一。你还可以拆成最简单的方式,形成4条规则:
A -> M
A -> A + M
M -> int
M -> M * int
上面这些不同的写法,都是等价的。你要能够看习惯,在不同的写法中自由切换。
不知道是否解答了你的疑问。
作者回复: 没错。很好。
既然你已经理解了,那么我再增加一点难度。当前推导是最左推导(LeftMost)推导的算法。也就是总是先把左边的非终结符展开。而且是深度优先的。
你再广度优先推演一下看看?
你再最右推导一下看看?
可能你的感受又不一样。很有意思的。可以作为消遣游戏 :-D
作者回复: 你提的问题特别好!其他同学可能也会有这种疑问。
文法,英文叫做Grammar,是形式语言(Formal Language)的一个术语。所以也有Formal Grammar这样的说法。这里的文法有定义清晰的规则。比如,我们的词法规则、语法规则和属性规则,使用形式文法来定义的。我们的课程里讲解了正则文法(Regular Grammar)、上下文无关文法(Context-free Grammar)等不同的文法规则,用来描述词法和语法。
语法分析中的这个语法,英文是Syntax,主要是描述词是怎么组成句子的。一个语言的语法规则,通常指的是这个Syntax。
问题是,Grammar这个词,在中文很多应用场景中也叫做语法。这是会引起混淆的地方。我们在使用的时候要小心一点就行了。
比如,我做了一个规则文件,里面都是一些词法规则(Lexer Grammar),我会说,这是一个词法规则文件,或者词法文法文件。这个时候,把它说成是一个语法规则文件,就有点含义模糊。因为这里面并没有语法规则(Syntax Grammar)。
为你的认真思考点赞!
作者回复: 为了方便讨论,我们把规则简化一下,去掉乘法那一层。否则在乘法那就已经无限递归下去了。修改后为:
additive -> IntLiteral | additive Intliteral ;
我们假设是最左推导,也就是总是先展开左边的非中介符。
第一遍:additive->IntLiteral,但因为后面还有Token没处理完,所以这个推导过程会失败,要退回来。
这可能是你没理解的地方。我们是要用additive匹配整个Token串,而不仅仅是第一个Token。
第二遍:用第二个产生式,additive->additive->IntLiteral,还是一样失败。
第三遍:additive->additive->additive->IntLiteral。
第四遍:....
这样说,有没有帮助?
作者回复: 哇,这么认真,这么仔细:-)
竖线“|”是或者的关系,怪我忘了强调这一点了。在正则文法、上下文无关文法中,“|”都是代表几个不同的选项。
另外,在前端技术的算法篇,会再把我们对算法的理解提升一下。我尽量做几个示例程序,演示出深度优先和广度优先的差别来。特别是,为什么广度优先的回溯会太多。
当然,如果你能先于我写一个,也可以分享给大家,就省了我的事了 :-)
为你的认真精神点赞!
作者回复: 实际上,这些看上去比较正式的术语,是我在这篇文稿的最后一版才加上去的。其实,你忽略这些术语,也完全能看懂文稿。加上这些术语,是为后面正式讲算法做个铺垫。
我知道编译原理的术语本身就能吓倒很多人。但是这门课程的重点在于帮你建立直觉(Intuition)。建立起直觉来以后,你其实已经明白了语法分析的过程,你已经对它有熟悉感了。之后你再把这些直觉跟术语联系在一起,就不觉得困难了。
再次强调一点,首先建立直觉,然后再追求对术语和算法的严格理解。
学编译原理最大的困难不是这门课本身的难度,而是我们对它的畏惧心理。相信你自己!
作者回复: 你说的很对!
实际上,你提到了递归的优化问题。这是一个专门的研究领域。在SICP(《计算机程序的构造和解释》)这本书中,对这个问题也很重视。
我们下一讲会提到尾递归的情形,也就是线性迭代的递归函数。它实际上可以转化成循环语句,就没有对栈的消耗了。这是在编译技术中常用的一种优化策略。你可以提前了解一下尾递归 : )
作者回复: 算法可以首先尝试产生式1。推导顺序是这样的:
additive -> multiplicative(加法的产生式1)
-> Intliteral(2)(乘法的产生式1)
这时候只消化了一个Token呀。我们是要用一个表达式把这5个Token都消化掉才行。所以会继续尝试乘法的产生式2。
additive -> multiplicative(加法的产生式1)
-> Intliteral * multiplicative (乘法的产生式2)
这次尝试不成功,因为我们下一个Token是加号,不是乘号。
现在,退回来尝试加法的产生式2。
additive -> multiplicative + additive(加法的产生式2)
-> Intliteral(2) + additive
->Intliteral(2) + multiplicative
-> Intliteral(2) + Intliteral(3) 不行,因为还有Token
-> Intliteral(2) + Intliteral(3) * multiplicative 又用上乘法的产生式2了
->Intliteral(2) + Intliteral(3) * Intliteral(5)
这是严格的推导过程。我在示例代码的实现中,因为提取了左公因子,所以没用多次回溯。
这样说,你能明白吗?如果还不明白,就再问。
作者回复: 1.终结符跟非终结符在04讲得更细一点,可以在04讲再体会一下。
2.它的左边,是指:
intDeclaration : Int Identifier ('=' additiveExpression)?;
这个规则,冒号的左边。
作者回复: 是的。
学习也是这么多次迭代的过程。
前面的即使学了,可能也隔着一层。等学到后面,再回过头来看前面,会有新的体会。
这就是最宝贵的直觉。是我们课程里着力培养的。就是你说的“感觉”。有了直觉,直观的理解一件事情了,再去细究,就不难了!
作者回复: 我觉得你在认真分析,点赞!
在讨论左递归会无穷次递归的时候,我们把语法简化了一下,是根本就不要乘法运算了,只看加法运算。这样来推演左递归更加方便一点。
简化后的规则为:
additive -> IntLiteral | additive Intliteral ;
解析过程:
第一遍:additive->IntLiteral,但因为后面还有Token没处理完,所以这个推导过程会失败,要退回来。
第二遍:additive->additive->IntLiteral,还是一样失败。
第三遍:additive->additive->additive->IntLiteral。
第四遍:....
Star就是*号,是一个Token符号。是词法分析过程中形成的。这样的问题建议你看看源代码,甚至运行一下,就更清楚了。
如果不清楚,继续问我。
作者回复: 学完课程,你应该会理解这两个的运作机制。
Babel,只是做语言翻译,只需要前端技术就可以了。翻译成AST,做完语义分析,再转成另一个版本的js。
Node.js基于v8,不仅仅做前端工作,更重要的是在后端运行时做各种优化。
作者回复: 嗯。谢谢你的建议。我看看是否需要把文稿表达得更细致一点。
如果不要乘法那一层,说明起来可能更简洁一些。否则,其实进入到乘法以后,就已经递归个不停了,根本回不到加法规则这来。
修改规则为:
additive -> IntLiteral | additive Intliteral ;
第一遍:additive->IntLiteral,但因为后面还有Token没处理完,所以这个推导过程会失败,要退回来。
第二遍:additive->additive->IntLiteral,还是一样失败。
第三遍:additive->additive->additive->IntLiteral。
第四遍:....
作者回复: 你提得很对。
可能是从右递归的推导过程拷贝过来,没改彻底。
谢谢!
作者回复: 看到你的工程经常更新,我已经在github上加了关注。
简单地用go test运行了一下你的lexer和calculator。运行的输出挺漂亮!
如果有小的建议的话,就是再稍微多写点注释。否则过一阵你自己看代码会想不起来了...
作者回复: 你补充了以后,很详细准确,但是太长了。乘法运算有时还要加上求余数的,越加越长。
所以有时候就用一个单词代表算了:
additive:代表有加有减。
multiplicative:代表乘、除、求余。
assignment:代表=,+=, -=, *=, /=...
作者回复: 感谢分享!
是的,递归的最大优点就是直观、简洁。
当然,如果递归嵌套层数太多,系统开销会比较大。这个时候需要改写和优化。对于嵌套层数有限的场景,用递归就行了。
作者回复: 这个地方确实写得不够细,没有交代清楚什么是非终结符,什么是终结符。后来在下一讲里有更多的描述。
总体来说,终结符,就是我们在词法分析阶段获得的Token。在建立AST的时候,它们是叶子节点。因为不管是表达式也好,语句也好,最终都是由这些Token构成的。
非终结符就相当于AST非叶子节点,它们是由Token构成的一些语法结构,比如表达式、语句。
如果把AST这种直观的理解换成文法的推导过程,那么就是反着来的。从非终结符一步步替换,直到全部替换成终结符。也就是从树根,一步步生成一棵AST。