章节目录

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

本节阅读量:

第八章不是只让三个新示例“碰巧算出 42”。它必须在第七章的完整语言上增加共享可变位置,并让 parser、自由变量分析、解释器、结构化 IR 和汇编生成器继续描述同一种语言。

这一页把承诺写成可执行的验收清单。修改实现以后,可以按顺序检查,而不必依靠肉眼猜测某一层是否仍然一致。

表面语言

本章新增两个特殊形式:

1
2
(set! name expr)
(begin first second)

应满足以下语义:

  • set! 只修改已经存在的最近词法绑定,不创建新变量。
  • set! 的目标只能是普通 identifier,不能是 (get r 0) 等一般表达式。
  • 先确认目标绑定存在,再求值 RHS;RHS 成功后才写入。
  • set! 返回写入的新 Value。
  • begin 先完整求值第一项并丢弃其结果,再求值第二项。
  • begin 返回第二项的 Value。
  • 一个 begin 固定接收两个表达式;更长序列通过嵌套表达。
  • 普通 identifier 规则仍是 letter+。set! 只在列表 head 位置作为固定特殊形式识别。

最小验收:

1
2
3
4
5
cd code/08_state
make
./mini run examples/set.lang
./mini run examples/set_result.lang
./mini run examples/begin_order.lang

三条命令都应输出:

1
42

名字、位置和值

本章的核心不变量是:

1
2
Environment:name -> Location
Store:       Location -> Value

它带来这些可观察行为:

  • 每次 let 都创建新 Location;同名内层绑定不会修改外层 Location。
  • 每次函数调用都为参数创建新 Location。
  • closure 保存自由变量的 Location,不保存创建时读出的 Value。
  • 多个 closure 可以保存同一个 Location,并互相看到对方的写入。
  • 从函数返回的 closure 仍能访问定义时的 Location。
  • 两次调用工厂函数会创建两组独立 Location,不会意外共享状态。
  • letrec 的函数体和外层 body 共享递归名字的 self Location。
  • 对 letrec 名字执行 set! 后,旧 closure 中通过递归名字进行的调用也会读到新 Value。

对应示例:

示例 检查点 结果
set_shadow.lang 内层同名 cell 不修改外层 cell 42
parameter_set.lang 参数有本次调用独有的 cell 42
captured_read_after_set.lang closure 读取外层后来的写入 42
captured_set.lang closure 写入,外层随后读取 42
write_only_capture.lang 只出现在 set! target 的名字也被捕获 42
two_closures_share.lang 两个 closure 共享同一个 cell 42
returned_counter.lang 外层调用结束后 cell 仍然可达 42
independent_counters.lang 两次工厂调用拥有独立 cell 42
recursive_binding_set.lang 可变递归名字使用共享 self cell 42
self_param_shadow.lang 同名参数最后绑定并遮蔽 self 42

可以直接运行:

1
2
3
4
5
6
7
8
9
./mini run examples/set_shadow.lang
./mini run examples/parameter_set.lang
./mini run examples/captured_read_after_set.lang
./mini run examples/write_only_capture.lang
./mini run examples/two_closures_share.lang
./mini run examples/returned_counter.lang
./mini run examples/independent_counters.lang
./mini run examples/recursive_binding_set.lang
./mini run examples/self_param_shadow.lang

AST、parser 与自由变量分析

实现必须满足:

  • ExprKind 显式包含 set 和 begin。
  • SetExpr 保存目标名字与 RHS 子树。
  • BeginExpr 保存有序的两个子树。
  • parser 对缺少参数、多余参数和非 identifier 的赋值目标给出诊断。
  • set! 的 target 是一次变量使用;若它没有在当前分析边界内绑定,就必须进入自由变量列表。
  • 分析 set! 时还要继续遍历 RHS。
  • 分析 begin 时按 first、second 顺序遍历两棵子树。
  • 所有 AST 消费者继续使用显式 kind + switch,并完整覆盖 record 等既有节点。
  • 自由变量仍然去重并保留首次出现顺序。

检查 AST:

1
./mini ast examples/set.lang

应输出:

1
Let(x, Int(1), Begin(Set(x, Int(41)), Add(Var(x), Int(1))))

再查看只写不读的捕获:

1
./mini ir examples/write_only_capture.lang

输出中必须出现:

1
f.0 = closure lambda0 [x0.cell]

若 capture 列表为空,说明自由变量分析漏掉了 set! target。

解释器

解释器要求:

  • Location 是稳定句柄;标准快照使用 std::size_t。
  • 一份显式 Store 保存所有运行时 Value。
  • Environment 只保存名字到 Location 的对应关系。
  • 普通变量读取经过 Store。
  • let 在 initializer 成功后才分配 Location。
  • set! 写回原 Location,并返回同一个新 Value。
  • closure 只捕获 free_vars 选出的 Location。
  • 调用 closure 时复用同一份 Store,并为参数分配新 Location。
  • letrec 先分配 self Location,再创建 closure,最后把 closure 写回该 Location。
  • 参数绑定最后加入调用环境,因此可以遮蔽同名 self;普通 lambda 中与参数同名的外层绑定不应进入 capture。
  • 解释器与编译器采用相同的左到右求值和错误顺序。

使用 Location 索引而不是 shared_ptr<Value> 还有一个重要结果:closure 环境只保存索引,不拥有 Store。递归 closure 与 self cell 可以构成语言层面的对象环,却不会在 C++ 层形成 shared_ptr 强引用环。

结构化 IR

IR 必须建立在第七章的 Operand、OpKind、Op、IrFunction 和 IrProgram 上,而不是使用字符串列表。

新增操作的约定是:

1
2
3
cell(dst, initial-value)
load(dst, cell)
store(cell, new-value)

完整要求:

  • lowering Env 把源码名字映射到 cell Operand。
  • let 生成 cell。
  • 每次变量读取生成 load。
  • set! 生成 store,并返回 RHS Operand。
  • begin 只通过 op 顺序表达,不需要单独的 begin op。
  • 每个 IrFunction 的第一条 op 把原始参数 Value 装入参数 cell。
  • closure fields 保存 cell Operand,不提前 load。
  • 普通 lambda 的 self_cell 为空。
  • letrec 先生成 self cell;closure field 0 保存该 cell,普通自由变量从 field 1 开始。
  • IrFunction::self_cell 对应 field 0,captures 与后续 fields 按位置一一对应。
  • 函数 lowering 环境按 self、普通 captures、参数 cell 建立,让参数最后遮蔽同名绑定。
  • call 的 op 顺序必须是 callee、closure_check、argument、call。
  • branch 后必须立即是 then label;假值跳向 else,真值顺序进入 then。
  • record、record?、size、get 和 closure 操作全部继续保留。
  • 所有 IR 消费者用显式 OpKind + switch 覆盖全部操作。

查看关键 IR:

1
2
3
4
./mini ir examples/set.lang
./mini ir examples/write_only_capture.lang
./mini ir examples/self_param_shadow.lang
./mini ir examples/short_branch.lang

汇编与对象协议

record、closure 和 cell 必须继续使用第六章建立的共同堆对象协议:

1
2
3
4
5
pointer low tag = 4

header kind 1 = record
header kind 2 = closure
header kind 3 = cell

cell 的布局固定为:

1
2
3
offset 0   header = (1 << 8) | 3 = 259
offset 8   current tagged Value
size       16 bytes

编译器要求:

  • cell 通过 malloc(16) 分配,并写入 header 与初值。
  • load 清除 cell Value 的 low tag 后读取 offset 8。
  • store 清除 cell Value 的 low tag 后写入 offset 8。
  • closure payload 中保存完整的 tagged cell Value。
  • closure 的 offset 8 仍是 raw code pointer;capture 从 offset 16 开始。
  • 调用前同时检查共同 heap tag 和 closure header kind。
  • callee 检查在任何 argument 操作之前执行。
  • %rdi 传完整 tagged closure Value,%rsi 传 tagged argument Value。
  • 函数入口只在 scratch register 中清除 %rdi 的 tag,再读取 self cell 和普通 captures。
  • 函数值仍不参与 eq?;解释器报错,生成代码进入状态 70 的错误出口。
  • record? 对 closure 返回 0;size 和 get 收到 closure 时失败。
  • record?、size 和 get 不会把内部 cell 误当成源语言 record。
  • 汇编生成器只消费 IrProgram,不能绕过 IR 重新遍历 AST。
  • 所有分配仍由 malloc 完成,本章不主动释放。

累积语言能力不能回退

第八章代码快照还必须保留前七章已经支持的能力:

  • 整数、负整数、加法和 sub1。
  • let、变量读取和词法遮蔽。
  • eq?、truthy 规则和 if 的短路分支。
  • 单参数 lambda、函数作为值、函数参数和函数返回值。
  • letrec 直接递归以及捕获外层变量的递归 closure。
  • 词法 closure、稳定顺序自由变量、嵌套 closure 和多捕获。
  • 空 record、任意字段数的不可变 record。
  • record?、size、非负字面量索引的安全 get。
  • record 身份相等、record 保存 closure、closure 捕获 record。
  • 非函数 callee、函数 eq?、错误 record 操作的既定诊断。

抽查:

1
2
3
4
5
6
7
./mini run examples/negative_add.lang
./mini run examples/recursive_closure.lang
./mini run examples/nested_closure.lang
./mini run examples/empty_record_size.lang
./mini run examples/record_get_third.lang
./mini run examples/record_holds_closure.lang
./mini run examples/record_predicate_closure.lang

这些结果依次应为 42、15、42、0、30、42 和 0。

正常程序的编译验收

compile 只能写出 x86-64 System V ABI 的 AT&T 汇编,不能替用户调用 cc。

先生成汇编:

1
./mini compile examples/counter.lang -o out.s

Linux、WSL 和 Intel Mac 手动链接:

1
2
3
cc out.s -o out
./out
echo $?

Apple Silicon Mac 使用:

1
2
3
cc -arch x86_64 out.s -o out
./out
echo $?

退出状态应是:

1
42

至少还应对这些程序重复编译、链接和运行:

1
2
3
4
5
6
7
8
9
set.lang
parameter_set.lang
write_only_capture.lang
two_closures_share.lang
independent_counters.lang
recursive_binding_set.lang
self_param_shadow.lang
record_get_third.lang
short_branch.lang

错误程序的验收

未绑定赋值目标在解释器和 lowering 阶段直接报错:

1
2
./mini run examples/set_undefined_error.lang
./mini ir examples/set_undefined_error.lang

诊断应包含:

1
undefined variable: missing

这些错误程序在解释器中应得到具体诊断:

1
2
3
4
5
6
./mini run examples/call_integer_error.lang
./mini run examples/call_record_error.lang
./mini run examples/function_eq_error.lang
./mini run examples/record_get_bounds_error.lang
./mini run examples/record_get_closure_error.lang
./mini run examples/record_size_closure_error.lang

对应错误分别覆盖非函数 callee、函数相等、越界访问和对象 kind 混淆。

把后六个程序分别编译、链接并运行,生成程序都必须在非法解引用或间接调用之前进入统一错误出口,退出状态为:

1
70

明确的非目标

本章有意不实现:

  • record 字段修改或 set-field!;record 仍然不可变。
  • 以任意表达式作为 set! 目标。
  • 一等引用、取地址、解引用或把内部 cell 暴露给源语言。
  • cell 的源语言 constructor、predicate、load 或 store 形式;这些只属于内部 IR。
  • 单个语法节点包含任意数量表达式的 variadic begin。
  • 多参数函数或新的调用语法。
  • assigned-variable analysis、选择性装箱或未装箱局部变量优化。
  • closure 逃逸分析、栈上 cell、栈上 closure 或对象标量替换。
  • 尾调用优化。
  • 垃圾回收或主动释放堆对象;这些留到第九章。
  • 线程、原子操作、锁、调度器或并发内存模型。

统一为所有绑定创建 cell 会产生较多 malloc,但它让本章只解决一个核心困难:多个词法作用域怎样共享同一个可变位置。性能优化不能以破坏这条语义为代价。

完成标准不是“看到了 store 字样”,而是同一个 Location 能从解释器环境一路对应到 closure capture、结构化 IR、cell 对象和最终汇编,并且前七章的所有行为仍然成立。


8.5 汇编:把位置落成 cell 对象

上一节

8.7 常见错误与练习

下一节