章节目录

练习

本节阅读量:

这一页适合读完第二章后回头做,不必一次全部完成。练习 1~4 主要检查作用域和环境;练习 5~8 对照 IR 与栈槽;最后一题需要修改代码。

练习 1:手画 AST

画出下面程序的 AST:

1
(let x 7 x)

参考形状:

1
2
3
4
Let
  name = ...
  value = ...
  body = ...

再指出:新建立的 x 对 value 和 body 中的哪一部分可见?

练习 2:追踪 shadowing 环境

不用写代码,只写出环境变化:

1
(let x 10 (+ (let x 32 x) x))

参考形式:

1
2
3
4
5
进入外层:...
进入内层:...
内层 x 查到:...
离开内层:...
最后那个 x 查到:...

练习 3:分清 value 和 body 的环境

先不要运行,判断下面程序中每个 x 对应哪一层绑定:

1
(let x 1 (let x (+ x 1) (+ x x)))

分别写出:

1
2
3
计算内层 value 时的环境
计算内层 body 时的环境
程序的最终结果

然后运行下面两个命令,检查你的推导:

1
2
./mini run <file>
./mini ir <file>

重点观察:内层绑定要等 value 处理完成后,才会加入解释器环境或 lowerer 环境。

练习 4:判断结果

判断下面程序的结果:

1
(let x 5 (let y 6 (+ x y)))

再判断:

1
(let x 5 (let x 6 (+ x x)))

说明第二个程序为什么没有修改外层的 x。

练习 5:手写 IR

给下面程序写出 IR:

1
(let x (+ 20 22) x)

参考形式:

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

注意 value 里的加法会先生成临时结果,变量引用本身不会生成新的操作。

练习 6:区分语法错误和未绑定变量

下面程序语法合法,但变量没有绑定:

1
(+ x 1)

分别运行:

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

解释为什么 ast 能成功,而解释器和编译器会在各自的环境查找阶段报错。

练习 7:从 shadowing 画出栈帧

编译:

1
(let x 10 (+ (let x 32 x) x))

先找出:

1
2
3
外层 x 的 IR 名字和栈槽
内层 x 的 IR 名字和栈槽
最终加法结果的 IR 名字和栈槽

再回答:

1
2
3
三个栈槽一共需要多少字节?
向上对齐后,subq 应该分配多少字节?
离开内层 let 时,内层 x 的栈槽会不会立刻释放?

最后画出包含 %rbp、%rsp、x0、x1 和 t.0 的栈帧。

练习 8:变量引用会生成 IR 吗

分别查看下面两个程序的 IR:

1
(let x 7 x)
1
(let x 7 (+ x x))

数一数每个程序生成了多少条 copy 和多少条 add,再回答:

1
2
body 里的 x 为什么没有生成新的 IR 操作?
两个 x 为什么可以直接成为 add 的操作数?

结合 lowerer 的 ExprKind::variable 分支解释答案。

练习 9(代码):让同名变量连续编号

当前 lowerer 使用一个全局的 next_local_ 为所有 let 绑定编号。对于依次绑定 x、y、x 的程序:

1
(let x 1 (let y 2 (let x 3 (+ x y))))

生成的内部名字是:

1
2
3
x0
y1
x2

修改内部名字的生成方式,让每个源码名字分别计数,得到:

1
2
3
x0
y0
x1

修改后重新构建并运行:

1
2
3
make
./mini ir <file>
./mini run <file>

确认 IR 中同名变量的编号连续,同时程序结果仍然是 5。这个改动只影响 IR 名字是否容易阅读,不应该改变变量查找、shadowing 或栈槽分配的行为。


2.7 本章总结

上一节

3.0 有无相生,前后相随

下一节