练习

本节阅读量:

这一页适合读完第一章后回头做。不要急着一次做完,先能看懂输出,再尝试改代码。

练习 1:手画 AST

画出下面程序的 AST:

1
(+ (+ 1 2) 3)

参考形状:

1
2
3
Add
  ...
  ...

练习 2:追踪 parser

不用写代码,只写出 parser 的动作顺序。

程序:

1
(+ -1 3)

参考形式:

1
2
3
4
advance() 拿到 ...
expect(...) 检查 ...
parse_expr() 读到 ...
最后得到 Add(...)

这个练习的目的不是背函数名,而是确认你知道:

1
parser 先认出加法,再读两个子表达式。

练习 3:手写 IR

给下面程序写出计算步骤:

1
(+ (+ 1 2) (+ 3 4))

参考形式:

1
2
3
4
t.0 = ...
t.1 = ...
t.2 = ...
return t.2

练习 4:观察真实输出

运行:

1
2
3
4
cd code/01_numbers
./mini ast examples/nested_add.lang
./mini ir examples/nested_add.lang
./mini compile examples/nested_add.lang -o out.s

对照三个输出:

1
2
3
AST 是树。
IR 是步骤。
汇编是机器指令。

练习 5:手写简单汇编

尝试给下面程序手写汇编:

1
(+ 10 32)

目标结果是 42

你应该至少用到:

1
2
3
movq
addq
retq

练习 6:改一个示例

新建或修改一个示例文件,让它表示:

1
(+ (+ 10 20) 12)

然后依次运行:

1
2
3
./mini run <file>
./mini ir <file>
./mini compile <file> -o out.s

检查解释器结果、IR 和汇编是否能对应起来。

练习 7:自己实现减法

把第一章的语言扩展一点点,让它支持二元减法:

1
(- 10 3)

这个程序的结果应该是 7

再试一个嵌套例子:

1
(- (+ 10 5) (- 4 1))

这个程序的结果应该是 12

你可以按这条线索改:

1
2
3
4
5
6
AST:增加减法表达式 kind
parser:在 "(" 后允许操作符是 "-"
解释器:先求左右两边,再计算 lhs - rhs
IR:增加减法操作 kind
汇编:生成 subq 指令
示例:新增一个减法示例文件

注意,-1 仍然是一个整数;(- 10 1) 才是减法表达式。

改完后至少运行:

1
2
3
4
5
6
7
./mini run <file>
./mini ast <file>
./mini ir <file>
./mini compile <file> -o out.s
cc out.s -o out
./out
echo $?

检查解释器结果、IR 和汇编运行结果是不是一致。

练习 8:给坏程序写错误信息

这一题不要求加入新语法。目标是让错误更清楚。

试着运行或构造下面这些坏程序:

1
2
3
4
(+ 1)
(+ 1 2 3)
(+ 1 2
(1 2)

然后改 parser,让它们失败时尽量说清楚原因。

你可以先做到这种程度:

1
2
3
4
缺少右边表达式
加法后面有多余内容
缺少右括号
列表开头不是 +

这道题的重点不是写出很漂亮的错误系统,而是练习 parser 在哪里知道“现在不对了”。

练习 9:手算栈槽

不要先运行编译器。先手算下面程序可能需要哪些临时变量:

1
(+ (+ 1 2) (+ (+ 3 4) 5))

写出类似这样的表:

1
2
3
4
5
6
7
t.0 = ...
t.1 = ...
t.2 = ...

t.0 -> -8(%rbp)
t.1 -> -16(%rbp)
t.2 -> -24(%rbp)

然后运行:

1
2
./mini ir <file>
./mini compile <file> -o out.s

对照真实 IR 和汇编,看看你的临时变量顺序、栈槽位置和编译器是否一致。

不一致也没关系。只要解释器结果和汇编结果一致,不同的临时变量安排也可以是正确的。

练习 10:观察退出码截断

第一章的汇编程序用返回值看结果。这个办法很方便,但它不是完整的数字输出。

试试这些程序:

1
2
3
4
42
300
(+ 200 100)
-1

每个程序都编译、链接、运行:

1
2
3
4
./mini compile <file> -o out.s
cc out.s -o out
./out
echo $?

记录你看到的结果。

想一想:

1
2
3
语言里的结果是多少?
echo $? 显示的是多少?
为什么它们不总是一样?

这道题是为了确认一件事:退出码只是操作系统保存的一小段结果,不等于语言以后真正的打印功能。

练习 11:想一想下一章

如果下一章加入变量:

1
(+ x 1)

和现在的:

1
(+ 40 2)

会有什么不同?

先不用实现,只要想:

1
2
解释器怎么知道 x 是多少?
编译器怎么知道 x 放在哪里?

这就是第二章要解决的问题。


1.9 本章总结

上一节

2.0 无名,天地之始;有名,万物之母

下一节