章节目录

练习

本节阅读量:

这一页适合读完第三章后完成,不必一次全部做完。练习 1~3 检查语言语义,练习 4~8 对照控制流 IR 与汇编,练习 9 做整条路径验证,最后一题需要修改代码。

练习 1:画出条件 AST

画出:

1
(if (eq? (+ 1 2) 3) 10 20)

参考形状:

1
2
3
4
If
  condition = ...
  then      = ...
  else      = ...

再运行 ./mini ast <file> 对照。

练习 2:追踪 condition 和最终结果

先不要运行。对下面每个程序,分别写出:

1
2
3
condition 的结果
被选择的分支
整个 if 的最终结果
1
2
3
4
(if 0 10 20)
(if -7 10 20)
(if (eq? (+ 20 22) (+ 40 2)) 1 99)
(if (eq? (eq? 1 2) 0) 42 7)

注意区分两件事:谓词表达式产生的是 0 或 1,但整个 if 的结果来自被选中的分支,不一定也是 0 或 1。

练习 3:追踪 environment 和分支

程序:

1
(let x 0 (if (eq? x 0) (let x 42 x) x))

写出:

1
2
3
4
5
进入外层 let 后的环境
condition 的结果
被选择的分支
进入内层 let 后的环境
最终结果

确认内层 x 不会修改外层绑定。

练习 4:手写 IR

给下面程序写 IR:

1
(if (eq? 1 2) 3 4)

必须包含:

1
2
3
4
5
eq? 操作
branch
then、else、end label
then 后的 jump
两个分支共享的结果名

然后运行 ./mini ir <file> 对照名字和顺序。

练习 5:找出基本块

查看 examples/if.lang 的 IR,把每条操作分别归入:

1
2
3
4
入口块
then0
else0
end0

再画出块之间的箭头,并标出每条边来自 branch、jump 还是 fallthrough。入口块有两条出边,then0 和 else0 最后都到达 end0。

练习 6:同一个结果栈槽

编译:

1
(if 1 10 20)

在 IR 中找出两个分支共同写入的结果名,再在汇编里找出它对应的栈槽。

解释为什么栈槽分配器第二次看到同名 dst 时不能再分配一个新位置。

再计算这个程序有几个不同的结果栈槽、原始需要多少字节,以及向上对齐后栈帧会分配多少字节。

练习 7:追踪比较和跳转

对下面程序生成汇编:

1
(if (eq? 3 4) 10 20)

沿执行顺序填写:

1
2
3
4
5
6
eq? 的 cmpq 比较是否相等?
sete 之后 %al 是多少?
movzbq 之后 %rax 是多少?
branch 再与 0 比较后,je 会不会跳转?
最终执行 then0 还是 else0?
程序结果是多少?

最后回答:lowerer 生成两个分支,是否意味着运行时也会执行两个分支?

练习 8:嵌套 if 的标签

查看下面程序的 IR:

1
(if 1 (if 0 1 2) 3)

列出所有 then、else 和 end label,确认内外两层 if 没有使用相同名字。

练习 9:解释器与编译器一致

自己写一个同时包含 let、shadowing、eq? 和 if 的程序,并让正确结果位于 0 到 255 之间。

分别验证:

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

解释器输出与可执行程序退出码应该相同。

练习 10(综合):把 not 做成语法糖

给语言增加一个一元形式:

1
(not expr)

它的语义是:

1
2
3
先求值 expr
如果结果是 0,not 返回 1
如果结果不是 0,not 返回 0

也就是说,(not expr) 等价于:

1
(eq? expr 0)

这道题的重点不是给后端增加新指令,而是练习怎样把一个表面语法复用到已有语义上。推荐先使用 parser 脱糖:

1
在 parser 里把 (not expr) 直接构造成已有的 EqExpr(expr, 0)

这样 AST 后面的解释器、IR 和汇编生成器都不需要新增分支。进阶做法是增加 NotExpr,再在解释器和 lowering 中复用“是否等于 0”的语义;这会涉及更多模块,不必作为第一版答案。

完成后测试:

1
2
3
(not 0)
(not 42)
(if (not (eq? 1 2)) 10 20)

修改代码后先重新运行 make,再分别检查 run、ast、ir 和 compile,确认解释器与编译器结果一致。

确认解释器和编译器的结果一致。


3.8 本章总结

上一节

4.0 函数成为值

下一节