编译原理大家是怎么学习的?

2021-09-17 13:06:27 +08:00
 paranoiddemon

非科班,最近在看 Enginnering a compiler 。看第二章 scanner 部分讲正则和自动机还勉强能理解。 第三章 parser 讲 CFG 引出了一堆符号和概念,感觉完全看不明白。

不知道大家是怎么学的,有没有更基础的视频课程推荐或者其他更入门的书推荐的?

10315 次点击
所在节点    程序员
69 条回复
levelworm
2021-09-17 21:31:10 +08:00
还有一本书,game scripting mastery,手把手教你写一个脚本语言加虚拟机。
swsh007
2021-09-17 21:45:13 +08:00
浙大有出过一本讲 lemon 的
纯粹是从代码开始刷
我觉得比啃各种动物书要好理解多了
pcslide
2021-09-17 22:48:45 +08:00
很正常,编译原理前面的基础知识,如果做为计算理论的一部分在大学上,可能会讲 1 到 2 个月,但是在上编译原理的时候只花两周就带过了。。。。
建议参考 J. Glenn Brookshear 的 Theory of Computation,这本写得简单易懂
如果找不到上面这本书,找 Michael Sipser 的那本(有电子版)也不错.

当然有一个捷径,如果你数学有一定基础,就是直接去看 Donald Knuth 写的关于 LR parser 的原文 On the translation of languages from left to right,这文章基本是从头讲起,写得简洁易懂,从头到底看下来,弄懂 7 成,其他基本就不用看了。
levelworm
2021-09-18 00:14:33 +08:00
@DianQK 惭愧,我之前看 craftintepreter 也是卡在 CFG 那里了。他说什么要按照顺序来排我就一直没想明白这个。
namelosw
2021-09-18 00:23:24 +08:00
强烈推荐先看这本 Crafting Interpreters http://craftinginterpreters.com/ 写得很简单易懂。前半截是一个解释器,后半截 是一个 bytecode VM,而且该接触的都接触了,大部分东西都既手写又简单,很完整。

Engineering a compiler 我还没看,但是你 CFG 就卡住了后面应该很难看下去,先把 Crafting Interpreters 快速看完做一遍。

---

具体说 CFG,就是比正则稍微强力一点,可以递归引用规则的语法而已。不用特别纠结你发的这些 formal 定义到底懂不懂,接着往后走,直接看一些实际 BNF 的例子,能照猫画虎地翻译成 recursive descent parser,写一遍之后就懂了。
namelosw
2021-09-18 00:34:46 +08:00
@levelworm

> 惭愧,我之前看 craftintepreter 也是卡在 CFG 那里了。他说什么要按照顺序来排我就一直没想明白这个。

top down 和 bottom up ?不用太纠结这个,直接看他写的 recursive descent parser 代码就行了。

这个其实是个简单算法问题,recursive descent 本质上是用一个 N 叉树的 DFS 生成一个 AST,比如你看 binary expression 之类的解析,其实就是二叉树的后序遍历。

因为是递归,递归其实就是一个隐式的栈运算,所以看着有点上下反着的感觉,但是后序遍历比较适合生成树,因为可以接 children 的返回值。

从上到下递归调用,所以叶子节点优先级最高,但是人习惯的理解逻辑是从上到下,从整体到局部的。所以理解完代码,回头再看对应的 BNF 就很好理解了。
levelworm
2021-09-18 01:43:34 +08:00
@namelosw 多谢,我是这里没明白:
https://craftinginterpreters.com/parsing-expressions.html
(上面半页基本的 CFG 还是很简单的)

引用开始:
Each rule here only matches expressions at its precedence level or higher. For example, unary matches a unary expression like !negated or a primary expression like 1234. And term can match 1 + 2 but also 3 * 4 / 5. The final primary rule covers the highest-precedence forms—literals and parenthesized expressions.
引用结束

他说的 precedence rule 我明白,就是优先度的问题,比如说加减乘除,乘除高于加减。但是什么叫做 each rule here only matches expressions at its precedence level or higher?

这是他最后的结果,能够看到每一行实际上都引用了下一行的东西:

expression → equality ;
equality → comparison ( ( "!=" | "==" ) comparison )* ;
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
term → factor ( ( "-" | "+" ) factor )* ;
factor → unary ( ( "/" | "*" ) unary )* ;
unary → ( "!" | "-" ) unary
| primary ;
primary → NUMBER | STRING | "true" | "false" | "nil"
| "(" expression ")" ;

我感觉他这里很巧妙的就把之前比较复杂的东西,比如说 expression 简化成 equality 了,但是对于我这个看的人来说,似乎就需要一个个调用下去,才能真正知道这行到底对应的是什么东西。
levelworm
2021-09-18 01:49:59 +08:00
接楼上。我现在又看了一下,是看懂了,但是你让我自己推出来这个,我就有些困难了,不是完全做不到,而是想不到这么做。
ch2
2021-09-18 01:53:35 +08:00
@levelworm #47 文法产生式,你直接拿来用就行了,设计的话需要你有经验才能设计的出来复杂语言的规则。你想自己搞的话可以先从 json 开始做一个解析器,标准 LL/LR 的好处是算法框架搭好了,切换到新语言只需要重新写产生式就行了
levelworm
2021-09-18 01:57:46 +08:00
@ch2 明白了,看来我硬着头皮看下去就行了。我之前看过另外一本书,好像就不是用这个办法,用的是状态机好像。
namelosw
2021-09-18 02:11:03 +08:00
@levelworm

> 但是什么叫做 each rule here only matches expressions at its precedence level or higher?

意思就是说优先级低的(也就是 BNF 靠上的、行数小的)可以兼容他自己那行和优先级更高的那行(也就是 BNF 靠下的,行数大的)。比如调用 factor,它不仅自己可以解析乘除,而且还可以递归解析 unary 和 primary,但是他不能解析 term 。

> 我感觉他这里很巧妙的就把之前比较复杂的东西,比如说 expression 简化成 equality 了,但是对于我这个看的人来说,似乎就需要一个个调用下去,才能真正知道这行到底对应的是什么东西。

这个就是 recursive descent 的特点,因为 CFG 是可以递归嵌套的( expression 里面有 binary,binary 里还能有 binary ),我们用的编程语言的表达式是可以互相任意无限嵌套的,所以需求就是递归的,如果不这样一层层调用其他的方式其实看起来更复杂。如果 BNF 没有这层递归,那么就只能得到一个比 basic 还 basic 很多的语言了,写起来可能跟汇编一样。

至于实现 BNF,其实本质上就是照着 BNF 结构做一个 N 叉树 DFS,而且代码很像 BNF 本身很直观。

private Expr equality() {
Expr expr = comparison();

while (match(BANG_EQUAL, EQUAL_EQUAL)) {
Token operator = previous();
Expr right = comparison();
expr = new Expr.Binary(expr, operator, right);
}

return expr;
}

其实就和平常刷 LeetCode 差不多,只不过多了 match 判断到底该用几叉树和构建哪种 AST:

fn traverse(root) {
left = traverse(root.left)
right = traverse(root.right)
return AST(left, right)
}

如果也就是说,上面的 equality match 的结果为 true 时,这段代码的骨架等于二叉树后序遍历,就在原地建起来 Binary:

fn equality() {
left = comparison()
right = comparison()
return Binary(left, right)
}

如果 match 为 false 时,那就交给子规则去递归,有点像链表(一叉树)遍历但不干活,有点像 LeetCode 那种删除链表后 N 个节点那种题里,前面节点 length - N 个节点无操作只递归一样:

fn equality() {
exp = comparison()
return exp
}

可以尝试做一下二叉树的序列化和反序列化,还有根据前序中序 / 中序后序结果反推二叉树的题。然后把这几道题扩展一下变成多态,其实就能得到 recursive descent 了。

本质上就是序列化字符串或者源码本身是一个隐式树,你控制代码递归遍历这个隐式树来创建一个真树。如果你把 N 叉树的遍历搞熟了,假如你从来没听说过 recursive descent,有人给你讲明白 BNF 的需求,你很可能也会凭空发明出 recursive descent 。
err1y
2021-09-18 08:30:59 +08:00
我最近也在学,给你两个我看的资料
[自己动手写编译器]
https://pandolia.net/tinyc/
[编译原理(哈工大)-哔哩哔哩] https://b23.tv/ceo4sd
zxCoder
2021-09-18 09:16:10 +08:00
硬着头皮看肯定能看明白,但是如果不用的话,也没啥用,很快就忘了
DianQK
2021-09-18 09:55:19 +08:00
@levelworm 优秀,现在我也只是可以看懂,让我设计一个那只能设计出一个辣鸡。
后面的章节会不断增加这个语法规则(就更复杂了),不过本质上还是不变的,某个规则是可以向下(规则)解析的,(也可能向上,向上就是递归了,比如 primary 又回到 expression 这个)
PS. 11 13 17 22 23 24 几个章节看起来可能也蛮辛苦的,其中 17 的 Pratt parser 跟 BNF 差不多的绕 /带劲,层主加油
loryyang
2021-09-18 10:09:47 +08:00
先问问自己为啥要学。。。说实话,知识是很多的,具体学什么要好好选择一下
我工作快十年了,至少我觉得这个东西作用不大,学编译原理还不如去学 linux 的源码,有本书的,不记得名字了。里面写了各种系统里面的设计,包括缓存、分页,CPU 调度等设计原理,这个对你设计好一个系统真的帮助挺大的
jones2000
2021-09-18 10:15:00 +08:00
大学里面学的。计算机专业必修课, 上了 2 个学期。毕业以后再找些开源的编译器看下,一般没什么问题。
weiwenhao
2021-09-18 10:24:56 +08:00
@namelosw +1 我也是看了这本,照着做最后就是一个解释器+vm
zouzou0208
2021-09-18 10:48:43 +08:00
@misaka19000 感谢。
whosesmile
2021-09-18 11:04:15 +08:00
@Mistwave 我虽然也看英文文档,但是一般是基本概念已经有了的情况,再去看文档都是查细节或者深入了解下,通常不会再概念还不了解,大脑基本空白的状况下去读,那样理解太累,各种专业词汇和术语,这和读英文小说和诗歌不一样,我也读不下去,毕竟英文很一般。
所以借这个机会问一个一直以来的疑惑,你们的英文都怎么学的?是双语成长环境?还是留学有了氛围后天努力的?
whosesmile
2021-09-18 11:08:27 +08:00
@loryyang 有用的,比如我一直好奇 Mustach 基于字符串的模版引擎和 Vue 、React 这种基于数据结构的 DOM 引擎在程序上如何实现的,解析思路怎么设计出来的,模板语法怎么分析的,有什么差异;这些没有编译原理的功底,去理解这个代码太费力了,我不是计算机专业毕业的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/802520

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX