章节目录

边界与验收:第七章真正承诺什么

本节阅读量:

第七章完成后,不只是 closure.lang 能得到 42。解释器、结构化 IR 和生成代码必须在词法作用域、捕获顺序、递归 self、对象种类与错误路径上互相对齐,同时完整保留第六章已有能力。

这一节把要求集中列出,便于实现和审阅时逐项检查。

语言语义

本章不增加表面语法,必须满足:

  • 求值 lambda 创建 closure,不执行函数体;
  • closure 只保存函数体真正需要的外层 Value;
  • 变量含义由定义位置决定,不由调用位置决定;
  • call 先求 callee,确认它是 closure,再求 argument;
  • 调用环境由 captures、可选 self 和参数组成;
  • 参数是比 self 更内层的绑定,允许同名遮蔽;
  • closure 可以返回 closure,也可以保存或被保存进 record;
  • closure 不能参与 eq?。

用这些程序做语义验收:

1
2
3
4
5
6
cd code/07_closures
./mini run examples/closure.lang
./mini run examples/lexical_scope.lang
./mini run examples/nested_closure.lang
./mini run examples/recursive_closure.lang
./mini run examples/self_param_shadow.lang

结果依次应为 42、42、42、15、42。

前端连续性

AST 和 parser 不新增 closure 专用节点:

  • 普通函数仍使用 LambdaExpr;
  • 调用仍使用 CallExpr;
  • letrec 仍使用 LetRecExpr;
  • 普通 identifier 规则保持一个或多个英文字母;
  • 第六章的可变长度 record、空 record、record?、size 和非负字面量 index get 全部保留。

closure 是否捕获变量不属于语法判断。相同 LambdaExpr 可以在不同定义环境中创建不同 closure Value。

自由变量分析

front/free_vars.* 必须做到:

  • 参数不属于当前 lambda 的自由变量;
  • let 名字只绑定 body,不绑定 value;
  • letrec 的 self 和参数在 function body 中已绑定;
  • 嵌套 lambda 中仍来自更外层的名字会向外传播;
  • 所有继承的 AST kind 都用显式 switch 遍历;
  • 同名自由变量只保留一次;
  • 结果保持第一次遇到的稳定顺序;
  • shadowing 由词法绑定栈判断,不按全局字符串集合猜测。

分析结果同时供解释器捕获环境和 IR lowering 使用,不能让两条路线各自维护一套略有差异的规则。

解释器

解释器 closure 必须保存:

1
2
3
4
param
body
self_name
captured environment

其中:

  • 普通 lambda 的 self_name 为空;
  • letrec 的 self_name 是递归名字;
  • captured environment 只含 free_vars 选中的名字;
  • captured environment 不保存递归 closure 自身;
  • call 时用实际 callee 建立 self 绑定;
  • call 时不能直接使用调用点环境执行 body。

错误诊断至少覆盖:

1
2
3
4
调用非 closure              function call expected a function
closure 参与 eq?            eq? does not support function values
对 closure 使用 size        size expected a record value
对 closure 使用 get         get expected a record value

结构化 IR

IR 继续使用 OperandKind、OpKind 和结构体字段,不退回字符串操作列表。第七章新增的关键种类是:

1
2
OpKind::closure
OpKind::closure_check

IrFunction 必须精确区分:

1
2
3
4
5
6
label
param
self
captures
ops
result

验收要求:

  • closure op 的 target 保存代码标签;
  • closure op 的 fields 保存捕获 operands;
  • IrFunction::captures 与 fields 等长、同序;
  • 普通 lambda 的 self 为空;
  • letrec 的 self 是独立 local,不出现在 fields;
  • self、参数和 captures 都做 alpha-renaming;
  • lowering 顺序为 callee ops、closure_check、argument ops、call;
  • branch 后仍紧跟 then label;
  • compile 先调用 lower_to_ir,assembly emitter 只接收 IrProgram。

查看代表输出:

1
2
3
4
5
./mini ir examples/closure.lang
./mini ir examples/multiple_captures.lang
./mini ir examples/nested_closure.lang
./mini ir examples/recursive_closure.lang
./mini ir examples/self_param_shadow.lang

通用堆对象协议

record 和 closure 必须共用:

1
2
3
kHeapObjectTag = 4
header = (payload_count << 8) | heap_kind
object_size = 8 * (1 + payload_count)

kind 分配是:

1
2
record   1
closure  2

不能恢复 closure 专用 pointer tag,也不能只检查 tag 4 就认定对象是 closure 或 record。

closure 布局

布局固定为:

1
2
3
offset 0       header
offset 8       raw code pointer
offset 16+8i   capture i tagged Value

必须满足:

capture count header size
0 258 16
1 514 24
2 770 32

self 不计入 capture count,也不占 payload。recursive_closure.lang 只捕获 base,所以使用 header 514、大小 24。

机器调用约定

调用约定必须是:

1
2
3
%rdi  tagged closure Value
%rsi  tagged argument Value
%rax  tagged result Value

closure_check 生成:

  1. low tag 是否为 4;
  2. 清 tag 后 header kind 是否为 closure;
  3. 失败时跳到状态 70 的错误出口。

最终 call op 使用临时 raw address,从 offset 8 取 code pointer,但不能把 %rdi 永久改成 raw pointer。函数入口:

  • self 非空时,把 tagged %rdi 保存为 self local;
  • 把 %rsi 保存为 param local;
  • 用 %rdi 的临时 untagged 副本读取 offset 16+8i;
  • 把每个 capture Value 保存为对应 local。

固定栈帧大小向上对齐到 16 字节,外部 malloc、exit 和间接 call 都要满足 System V ABI 对齐。

record 能力必须完整继承

第七章不能退回旧的二字段 record。下面这些程序都必须继续工作:

1
2
3
4
5
./mini run examples/empty_record.lang
./mini run examples/many_fields.lang
./mini run examples/record_get_third.lang
./mini run examples/record_predicate.lang
./mini run examples/record_size.lang

预期输出依次是:

1
2
3
4
5
(record)
(record 10 20 30 40)
30
1
4

种类交叉测试也必须成立:

1
2
3
./mini run examples/closure_captures_record.lang
./mini run examples/record_holds_closure.lang
./mini run examples/record_predicate_closure.lang

结果为 42、42、0。

生成代码的错误状态

编译后的这些错误统一以状态 70 结束:

示例 失败原因
call_integer_error.lang callee 是整数,low tag 不是 heap object
call_record_error.lang callee 是 record,不是 closure
function_eq_error.lang closure 参与 eq?
record_size_closure_error.lang size 收到 closure
record_get_closure_error.lang get 收到 closure
record_get_bounds_error.lang record index 越界

以一个例子验证:

1
2
3
4
./mini compile examples/call_record_error.lang -o out.s
cc out.s -o out
./out
echo $?

Apple Silicon Mac 的链接命令改为:

1
cc -arch x86_64 out.s -o out

退出状态应为 70。

分支仍然短路

错误路径只有真正执行时才失败:

1
(if 1 42 ((record 0) 1))

解释器和生成代码都必须返回 42,不能因为 else 分支中存在非法调用而提前报错。运行:

1
2
3
4
5
./mini run examples/short_branch.lang
./mini compile examples/short_branch.lang -o out.s
cc out.s -o out
./out
echo $?

Apple Silicon Mac 仍使用 cc -arch x86_64 out.s -o out 链接。两条执行路径的结果都应是 42。

这同时检查 lowering 没有把分支内的 closure_check 提到 branch 之前。

CLI 与平台边界

本章仍提供:

1
2
3
4
mini run
mini ast
mini ir
mini compile

compile 只输出 x86-64 System V ABI 的 AT&T 汇编。它不调用 cc,不负责链接,也不执行结果。Linux 和 macOS 的外部符号拼写由 target 选择处理。

非目标

第七章不实现:

  • 多参数函数;
  • 可变变量和共享 cell;
  • closure 相等;
  • 逃逸分析或栈上 closure;
  • 主动 free、引用计数 runtime 或垃圾回收;
  • 通用尾调用优化。

这些边界不影响本章目标:闭包已经能在词法作用域下保存自由变量、返回和嵌套,递归函数也能同时使用 self 与真正的外层捕获值。


7.6 闭包:从一行源码走到一次间接调用

上一节

7.8 常见错误与练习

下一节