边界与验收:第八章真正承诺什么
本节阅读量:第八章不是只让三个新示例“碰巧算出 42”。它必须在第七章的完整语言上增加共享可变位置,并让 parser、自由变量分析、解释器、结构化 IR 和汇编生成器继续描述同一种语言。
这一页把承诺写成可执行的验收清单。修改实现以后,可以按顺序检查,而不必依靠肉眼猜测某一层是否仍然一致。
表面语言
本章新增两个特殊形式:
|
|
应满足以下语义:
set!只修改已经存在的最近词法绑定,不创建新变量。set!的目标只能是普通 identifier,不能是(get r 0)等一般表达式。- 先确认目标绑定存在,再求值 RHS;RHS 成功后才写入。
set!返回写入的新 Value。begin先完整求值第一项并丢弃其结果,再求值第二项。begin返回第二项的 Value。- 一个
begin固定接收两个表达式;更长序列通过嵌套表达。 - 普通 identifier 规则仍是
letter+。set!只在列表 head 位置作为固定特殊形式识别。
最小验收:
|
|
三条命令都应输出:
|
|
名字、位置和值
本章的核心不变量是:
|
|
它带来这些可观察行为:
- 每次
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 |
可以直接运行:
|
|
AST、parser 与自由变量分析
实现必须满足:
ExprKind显式包含set和begin。SetExpr保存目标名字与 RHS 子树。BeginExpr保存有序的两个子树。- parser 对缺少参数、多余参数和非 identifier 的赋值目标给出诊断。
set!的 target 是一次变量使用;若它没有在当前分析边界内绑定,就必须进入自由变量列表。- 分析
set!时还要继续遍历 RHS。 - 分析
begin时按 first、second 顺序遍历两棵子树。 - 所有 AST 消费者继续使用显式
kind + switch,并完整覆盖 record 等既有节点。 - 自由变量仍然去重并保留首次出现顺序。
检查 AST:
|
|
应输出:
|
|
再查看只写不读的捕获:
|
|
输出中必须出现:
|
|
若 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 上,而不是使用字符串列表。
新增操作的约定是:
|
|
完整要求:
- lowering Env 把源码名字映射到 cell Operand。
let生成cell。- 每次变量读取生成
load。 set!生成store,并返回 RHS Operand。begin只通过 op 顺序表达,不需要单独的beginop。- 每个
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:
|
|
汇编与对象协议
record、closure 和 cell 必须继续使用第六章建立的共同堆对象协议:
|
|
cell 的布局固定为:
|
|
编译器要求:
cell通过malloc(16)分配,并写入 header 与初值。load清除 cell Value 的 low tag 后读取 offset8。store清除 cell Value 的 low tag 后写入 offset8。- closure payload 中保存完整的 tagged cell Value。
- closure 的 offset
8仍是 raw code pointer;capture 从 offset16开始。 - 调用前同时检查共同 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 操作的既定诊断。
抽查:
|
|
这些结果依次应为 42、15、42、0、30、42 和 0。
正常程序的编译验收
compile 只能写出 x86-64 System V ABI 的 AT&T 汇编,不能替用户调用 cc。
先生成汇编:
|
|
Linux、WSL 和 Intel Mac 手动链接:
|
|
Apple Silicon Mac 使用:
|
|
退出状态应是:
|
|
至少还应对这些程序重复编译、链接和运行:
|
|
错误程序的验收
未绑定赋值目标在解释器和 lowering 阶段直接报错:
|
|
诊断应包含:
|
|
这些错误程序在解释器中应得到具体诊断:
|
|
对应错误分别覆盖非函数 callee、函数相等、越界访问和对象 kind 混淆。
把后六个程序分别编译、链接并运行,生成程序都必须在非法解引用或间接调用之前进入统一错误出口,退出状态为:
|
|
明确的非目标
本章有意不实现:
- 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 对象和最终汇编,并且前七章的所有行为仍然成立。