闭包:从一行源码走到一次间接调用
本节阅读量:前几节分别看了语义、自由变量、解释器、IR 和汇编。本节只跟踪一个程序,把同一个 x 在五层表示中的位置连起来。
起点:定义位置有一个 x
源码是:
|
|
从语言语义看:
|
|
这个结论先于实现。解释器和编译器必须得到相同结果。
第一层:AST 只保存源码结构
运行:
|
|
输出:
|
|
AST 能看见两个变量引用,却还没有“capture slot”概念。parser 也不需要判断哪个 Var(x) 属于哪份机器环境。
第二层:自由变量分析得到 x
分析 Lambda(y, ...) 时,初始 bound 是 [y]:
|
|
结果是 [x]。解释器和 IR lowering 都消费这份结果。
第三层:解释器保存 x 的 Value
求值外层 let 后,当前环境是:
|
|
求值 lambda 创建:
|
|
调用时先确认 callee 是 closure,再求值参数 2,然后建立:
|
|
解释器在这个环境中求值 (+ x y),返回 number Value 42。
第四层:IR 把捕获关系显式化
运行:
|
|
最终 IR 是:
|
|
可以沿着 x 画一条线:
|
|
x0 和 x1 是两个不同执行位置的 IR local,中间由 closure payload 连接。
check-closure f.0 位于 argument 和 call 之前。当前 argument 只是字面量 2,看不出区别;对复杂 argument,它保证 callee 类型错误先发生。
第五层:closure object 落到堆上
一个 capture 对应:
|
|
malloc 返回 raw address p 后,对象是:
|
|
源语言 closure Value 是:
|
|
整数 40 在 capture slot 中仍是完整 tagged Value,不会因为进入对象而改用另一套表示。
第六层:检查与调用
执行 check-closure f.0 时:
|
|
通过以后,call 约定是:
|
|
调用端用 scratch register 清掉 %rdi 的 tag,从 offset 8 取出 .Llambda0,执行间接 call。
第七层:函数入口恢复名字
.Llambda0 入口做两件与闭包有关的事:
|
|
随后普通 add emitter 只看 IR operands x1 和 y2。它不需要知道这两个值分别来自 closure 和参数寄存器。
结果 tagged 42 放进 %rax 返回。主函数解码整数,并把 42 作为退出状态。
五层表示对照
| 层次 | 函数代码 | 外层 x | 参数 y |
|---|---|---|---|
| 源语言 | lambda body |
自由变量名 | 参数名 |
| AST | LambdaExpr::body |
VarExpr("x") |
LambdaExpr::param |
| 解释器 | Closure::body |
Closure::env |
call 时加入 call_env |
| IR | IrFunction |
closure field 与 capture local | IrFunction::param |
| 机器 | offset 8 code pointer | offset 16 capture | %rsi 后进入栈槽 |
表中的外形不同,但都在实现同一句话:函数体中的 x 由定义位置决定,y 由本次调用决定。
再看嵌套 closure
运行:
|
|
第一条命令输出 42。IR 中外层函数创建内层 closure:
|
|
此时 x1 来自外层 closure,y2 来自外层调用参数;对内层 closure 来说,它们都是创建位置可用的 Value,于是按普通捕获规则写入两个槽。
没有任何一步需要复制外层 closure 对象本身。内层对象只保存自己以后会读取的两个 Value。
再看递归 closure
运行:
|
|
创建位置是:
|
|
函数头是:
|
|
沿着两条线看:
|
|
base 是定义环境中的自由变量,所以进入 payload。adddown 是实际 callee 的递归名字,所以来自 %rdi。二者不能都笼统地叫“捕获值”。
record 与 closure 的交叉验证
两个示例检查统一对象协议的边界:
|
|
输出依次是:
|
|
前两个结果证明两种对象可以互相保存 tagged 引用;最后一个结果证明它们仍由 header kind 区分。
一条实用的排错路径
若闭包程序结果不对,可以按层检查:
- AST 中变量是否位于预期
lambdabody; free_vars是否排除了参数和 self,又保留真正外层名字;- closure op 的 fields 是否解析到正确外层 IR operands;
IrFunction::captures是否与 fields 同序;- header、code 和 capture 是否写在
0、8、16 + 8i; %rdi是否仍是 tagged closure,临时 raw pointer 是否只用于读取;- 函数入口是否把 capture
i装入对应 local。
按这条链逐层核对,比只盯着最后一条 call 更容易定位问题。