第七章:让函数带着环境离开
本节阅读量:前几章已经让函数成为值,也让递归函数能够调用自己。第六章又建立了一套通用的堆对象协议:一个 tagged Value 指向堆对象,header 说明对象种类和 payload 数量。
但到目前为止,普通 lambda 仍然只能使用自己的参数和函数体内部的绑定。下面的程序还缺少一种运行时表示:
|
|
lambda 的参数是 y,函数体却还需要外层的 x。函数创建以后,原来的 let 可能早已结束;单独保存一段代码的地址,无法回答以后应该到哪里寻找 x。
第七章把函数值升级为闭包:
|
|
这个定义里有两个时间点:
- 创建
lambda时,确定并保存以后还需要的外层值; - 调用闭包时,取回这些值,再加入本次调用的参数。
因此上面的程序结果是 42。
本章不增加表面语法
读者写的仍然是已经认识的 lambda 和单参数调用:
|
|
LambdaExpr、CallExpr 和 parser 都不需要新的语法分支。变化发生在 AST 之后:
|
|
解释器也会实现同样的语言行为,只是使用 C++ 对象保存闭包,而不是生成机器布局。
先运行一个闭包
进入本章代码目录并构建:
|
|
输出:
|
|
再用一个同名遮蔽程序确认它采用词法作用域:
|
|
源码是:
|
|
结果仍然是 42,不是 102。调用位置后来出现的 x 不会改变闭包创建时已经确定的名字含义。
AST 没有为闭包增加节点
查看第一个程序的 AST:
|
|
输出:
|
|
这仍然只是 LetExpr、CallExpr、LambdaExpr、AddExpr 和变量节点。AST 记录的是源码结构;“x 应该被捕获”是后续分析得到的事实,不需要塞进 parser。
IR 第一次明确写出捕获值
运行:
|
|
会得到:
|
|
最关键的是这三处:
|
|
第一行在创建位置保存当前的 x0。第二行在求值 argument 前确认 callee 确实是 closure。函数体里的 x1 是与捕获槽对应的局部 IR 名字。self: - 表示普通 lambda 没有递归名字。
没有自由变量的函数也统一创建 closure,只是捕获列表为空:
|
|
这样调用路径不必再区分“裸代码地址”和“带环境的函数”。
closure 复用堆对象协议,但不是 record
第六章已经规定所有堆对象引用共用低位 tag 4。第七章不再发明一个 closure 专用 pointer tag,而是在 header 中增加新的对象 kind:
|
|
closure 的 raw object 布局是:
|
|
header 的 payload count 包含 code pointer,因此捕获 n 个值时:
|
|
这叫复用同一套堆对象协议,不是使用源语言的 (record ...) 来假装闭包。两种对象拥有不同 header kind,所以:
|
|
结果是 0。size 和 get 收到 closure 时也会拒绝操作,而不是把 code pointer 暴露成 record 字段。
编译、链接和观察结果
生成汇编:
|
|
compile 的路径是:
|
|
汇编生成器只消费结构化 IrProgram,不会绕过 IR 再遍历 AST。compile 仍然只写出汇编文件,不调用 cc。
Linux、WSL 和 Intel Mac 可以手动链接:
|
|
Apple Silicon Mac 需要明确生成 x86-64 可执行文件:
|
|
程序不打印内容,退出状态是 42。解释器负责显示完整 Value;编译后的主函数仍用进程退出状态观察整数结果。
本章会新增什么
主要改动集中在这些位置:
|
|
AST 与 parser 只继承第六章已有接口;本章没有新的表面语法。
已有能力不会回退
第七章快照继续支持:
- 整数、负整数、加法、变量和
let; eq?、if、函数值、调用和letrec;- 空 record、任意字段数的 record、
record?、size与安全get; - record 身份比较以及 record 和 closure 互相保存。
本章新增的唯一语言能力是:函数可以读取定义位置的外层变量。可变捕获、共享 cell 和垃圾回收仍留给后续章节。
学完以后应该能回答
读完本章,你应该能够解释:
- 为什么代码地址不足以表示能捕获变量的函数;
- 什么是相对于某个
lambda的自由变量; - 为什么调用位置的同名绑定不能改变闭包行为;
- 解释器为什么保存自由变量对应的 Value;
- IR 怎样把代码标签和捕获 operand 放进一个 closure op;
- closure 怎样复用通用 heap-object tag 和 header;
- 为什么
%rdi要携带 tagged closure Value,函数体又怎样读取捕获槽。