常见错误与练习
本节阅读量:闭包的代码量并不大,真正容易出错的是同一条对应关系跨越了自由变量分析、IR、对象布局和调用约定。先用常见错误定位思路,再通过练习把这条链亲手走一遍。
常见错误:使用调用点环境
若 lexical_scope.lang 得到 102,说明函数体从调用位置的环境开始执行了。call 必须复制 closure 保存的定义环境,再加入 self 和参数。
常见错误:解释器复制整份环境
复制整份定义环境通常也能算对简单结果,却掩盖了自由变量分析是否正确,还会无谓保留不相关 Value。本章解释器和编译器都只保存 free_vars 选出的名字。
常见错误:遇到嵌套 lambda 就停止分析
外层函数可能需要把一个值继续传给内层 closure。若在嵌套 lambda 处停止,nested_closure.lang 的外层 closure 会漏掉 x。
常见错误:把 self 当作 capture 0
letrec 的 self 已经由隐藏的实际 callee %rdi 提供。把 self 再写进 payload 会改变所有普通 capture 偏移,还会制造不必要的对象自引用。
常见错误:沿用旧 closure 偏移
offset 0 是 header,offset 8 才是 code pointer,offset 16 才是 capture 0。若从 offset 8 读取 capture,得到的是机器代码地址。
常见错误:只检查通用 heap tag
record 和 closure 的 low tag 都是 4。call、size 和 get 必须继续检查 header kind,否则一种对象的 payload 会被另一种操作错误解释。
常见错误:把 %rdi 永久改成 raw pointer
%rdi 按约定携带 tagged closure Value。需要读 header 或 code 时,复制到 scratch register 再清 tag。递归 self 保存的也必须是原始 tagged Value。
常见错误:在 argument 之后才检查 callee
结构化 IR 必须在 callee lowering 后立即插入 check-closure,然后才 lower argument。否则错误 callee 的程序可能先执行 argument 中的另一项错误。
常见错误:顺手定义 closure 相等
closure 使用堆地址表示,不代表 eq? 应比较地址。函数相等仍不属于语言语义;解释器报错,生成代码进入状态 70 的错误出口。
练习 1:手算自由变量
分别写出下面三个 lambda 的自由变量列表。
|
|
|
|
|
|
检查点:参数不能进入列表;最后一个列表必须保持函数体中首次遇到的顺序。
练习 2:判断 let 的作用域边界
分析这个程序中 lambda 的自由变量:
|
|
先分别回答:
- 内层
let的 value 中,y指向哪一个绑定? - 内层
let的 body 中,y指向哪一个绑定? - closure 是否需要捕获
y? - 程序结果是多少?
把程序保存后分别用 ./mini ast、./mini ir 和 ./mini run 验证。
练习 3:解释嵌套传播
不用运行,分别写出外层和内层 lambda 的自由变量:
|
|
然后运行:
|
|
在输出中找出:
|
|
最后说明:为什么外层函数自己没有直接做 + x ...,仍然必须捕获 x?
练习 4:稳定顺序和去重
预测这个函数的捕获列表:
|
|
正确实现应保留首次出现顺序并去重。把源码保存为临时文件,运行 ./mini ir,确认 closure fields 与 IrFunction::captures 都采用同一顺序。
再思考:如果只把分析结果一端排序,而另一端不排序,机器执行时会发生什么?
练习 5:从源码名追到 IR local
运行:
|
|
画出以下对应关系:
|
|
再对 lexical_scope.lang 重做一遍,找出值为 40 和值为 100 的两个 x 分别得到什么 IR 名字。说明 closure 为什么不会捕获后者。
练习 6:计算 closure header 和大小
不看生成汇编,填写表格:
| capture count | payload count | header | size |
|---|---|---|---|
| 0 | |||
| 1 | |||
| 2 | |||
| 5 |
使用公式:
|
|
然后生成两个程序的汇编核对:
|
|
找出传给 malloc 的大小、写到 offset 0 的 header、offset 8 的 code pointer 和每个捕获槽。
练习 7:callee 检查必须早于 argument
考虑这个完整程序:
|
|
callee 是 record,argument 求值也会因 size 收到 closure 而失败。按本章求值顺序,解释器应该先报告哪一个错误?
把程序保存后运行:
|
|
IR 中应该先出现 callee 的 record op 和 check-closure;argument 产生的 closure op、size op 等全部排在检查之后。若任何 argument op 出现在 check-closure 前,lowering 就改变了语言求值顺序。
练习 8:逐条解释 call guard
分别编译 low-tag 错误和 header-kind 错误:
|
|
在两份汇编中找出:
|
|
回答两个问题:
- 为什么不能先清 tag 并读取 header,再检查 low tag?
- 为什么通过 low tag
4以后仍不能直接执行call *8(...)?
分别手动链接并运行,确认退出状态都是 70。说明整数在哪一层检查失败,record 又在哪一层检查失败。
练习 9:递归 self 不在 payload 中
查看:
|
|
列出:
|
|
验收答案中,closure 只能有 base 一个 capture,因此 header 是 514、size 是 24。若对象中还写入 self,说明实现仍沿用错误的自捕获设计。
练习 10:self 与参数同名
先判断结果:
|
|
运行:
|
|
在 IR 中找出不同的 self local 和 param local。再到汇编中确认 %rdi、%rsi 被保存到不同栈槽,最终返回参数槽。
练习 11:检查对象种类边界
依次运行:
|
|
解释为什么前两个程序都能得到 42,第三个却得到 0。回答时必须区分:
|
|
再分别编译 record_size_closure_error.lang 和 record_get_closure_error.lang,确认两者都在解引用错误 payload 前以状态 70 停止。
练习 12:函数相等不是地址相等
运行:
|
|
解释器应报告函数不能参与 eq?。手动链接生成的汇编后,程序应返回状态 70。
在汇编里找出 emit_equal 为两个 operand 生成的 closure kind 检查。说明为什么“两个 pointer 是否相同”不是本章语言已经定义的函数相等。
练习 13:自己写一个返回 closure 的程序
实现并运行:
|
|
解释器应显示:
|
|
依次回答:
- 外层
lambda的自由变量是什么? - 内层
lambda的自由变量是什么? - 哪个 closure 是 16 字节,哪个是 24 字节?
- 内层 closure 为什么能在外层调用返回以后继续使用
x? - 最终 record 的两个字段分别来自哪两个 Value?
练习 14:完整回归
选择这些正常程序:
|
|
把 <name> 依次替换为上面的每个文件名(不含 .lang),执行:
|
|
Apple Silicon Mac 把链接命令改为:
|
|
然后选择这些错误程序:
|
|
确认解释器给出具体诊断,生成程序统一返回 70。这组回归同时覆盖语义、自由变量、IR、对象布局、调用约定和第六章 record 连续性。