练习
本节阅读量:这一页适合读完第一章后回头做,不必一次全部完成。练习 1~6 主要帮助你对照各阶段输出;练习 7~8 需要修改代码;练习 9~10 继续理解栈槽和退出码;练习 11 是一项汇编生成进阶练习;最后一题用来思考下一章的问题。
练习 1:手画 AST
画出下面程序的 AST:
|
|
参考形状:
|
|
练习 2:追踪 parser
不用写代码,只写出 parser 的动作顺序。
程序:
|
|
参考形式:
|
|
这个练习的目的不是背函数名,而是确认你知道:
|
|
练习 3:手写 IR
给下面程序写出计算步骤:
|
|
参考形式:
|
|
练习 4:观察真实输出
运行:
|
|
对照三个输出:
|
|
练习 5:手写简单汇编
尝试给下面程序手写汇编:
|
|
目标结果是 42。
在 Linux 或 WSL 上,可以从这个最小函数骨架开始填写中间三行:
|
|
保存为 add.s 后,用 cc add.s -o add 构建,再运行 ./add 和 echo $? 检查结果。macOS 需要把入口标签改成 _main;Apple Silicon Mac 还需要按照第 0 章的说明指定 x86-64 架构。
练习 6:改一个示例
新建或修改一个示例文件,让它表示:
|
|
然后依次运行:
|
|
检查解释器结果、IR 和汇编是否能对应起来。
练习 7(综合):自己实现减法
把第一章的语言扩展一点点,让它支持二元减法:
|
|
这个程序的结果应该是 7。
再试一个嵌套例子:
|
|
这个程序的结果应该是 12。
你可以按这条线索改:
|
|
注意,-1 仍然是一个整数;(- 10 1) 才是减法表达式。
改完后至少运行:
|
|
检查解释器结果、IR 和汇编运行结果是不是一致。
练习 8:给坏程序写错误信息
这一题不要求加入新语法。目标是让错误更清楚。
试着运行或构造下面这些坏程序:
|
|
然后改 parser,让它们失败时尽量说清楚原因。
你可以先做到这种程度:
|
|
这道题的重点不是写出很漂亮的错误系统,而是练习 parser 在哪里知道“现在不对了”。
练习 9:手算栈槽
不要先运行编译器。先手算下面程序可能需要哪些临时变量:
|
|
写出类似这样的表:
|
|
然后运行:
|
|
对照真实 IR 和汇编,看看你的临时变量顺序、栈槽位置和编译器是否一致。再回答三个问题:
|
|
最后尝试画出这次编译对应的栈帧,把 %rbp、%rsp 和每个临时变量标在图上。
其他编译器可以采用不同的临时变量安排;但这道练习的目标是读懂当前 lowerer 和汇编生成器。如果你的结果与实际输出不同,请沿着递归 lowering 的顺序找出从哪一步开始出现差异。
练习 10:观察退出码截断
第一章的汇编程序用返回值看结果。这个办法很方便,但它不是完整的数字输出。
试试这些程序:
|
|
每个程序都编译、链接、运行:
|
|
记录你看到的结果。
想一想:
|
|
这道题是为了确认一件事:退出码只是操作系统保存的一小段结果,不等于语言以后真正的打印功能。
练习 11(进阶):处理 addq 放不下的大整数
本章解释器用 long 保存整数。在常见的 64 位 Linux 和 macOS 环境中,long 可以保存 2147483648,所以先分别解释下面两个程序:
|
|
|
|
两次 mini run 都应该得到:
|
|
再分别生成汇编并尝试链接:
|
|
如果你在 Apple Silicon Mac 上,仍然要按照第 0 章的说明使用:
|
|
观察生成的汇编,你可能会发现一个不对称现象:大整数在左边时通常可以先放进 %rax,大整数在右边时,当前生成器却会尝试生成:
|
|
x86-64 的 addq 虽然计算的是 64 位结果,但它直接携带的整数只有 32 位,并按有符号数扩展到 64 位。因此这种立即数的范围是:
|
|
2147483648 超出了这个范围,汇编器会拒绝这条指令。这里不是加法结果超出了 64 位,而是指令本身放不下这个直接写入的整数。
修改汇编生成器:当 add 的右操作数是超出上述范围的整数时,先把它放进一个 scratch register,再执行寄存器加法。例如:
|
|
修改后,至少检查下面四个边界值:
|
|
还要交换它们在加法左右两边的位置,确认解释器和编译器不再因为操作数顺序而表现不同。
这道练习要帮助你分清三件事:
|
|
练习 12:想一想下一章
如果下一章加入变量和局部绑定:
|
|
和现在的:
|
|
会有什么不同?
先不用实现,只要想:
|
|
这就是第二章要解决的问题。