章节目录

常见错误、练习与全书收束

本节阅读量:

本章没有增加后端指令,却同时调用了 parser、AST pass、词法作用域、closure、共享 cell、record、调用、寄存器分配和 GC。错误往往发生在这些边界的组合处。

1
2
cd code/11_coroutines
make

常见错误

  • 直接在解释器里特殊处理 yield:会让解释器和编译器形成两套语义。
  • 创建 coroutine 时执行 body:body 的副作用和错误都应等到首次 resume。
  • 返回 paused record 后才保存 continuation:当前调用已经返回,后续状态会丢失。
  • 每次完成后重新分配 record,或 done 返回前不把自身写回 state:前者破坏对象身份,后者会让下一次 resume 读到 dispatcher 留下的 0。
  • 把 continuation AST 复制进两个分支:嵌套 if 会造成指数膨胀。
  • 使用 state、next 作为内部名:这些都是合法用户变量,会发生捕获。
  • 认为 closure 可达就不用 root:safepoint 时 tagged pointer 仍必须位于 collector 扫描的位置。

练习 1:手推三次 resume

1
2
3
./mini ast examples/coroutine_begin.lang
./mini core examples/coroutine_begin.lang
./mini run examples/coroutine_begin.lang

画出 state 的变化:

1
2
3
4
创建后          -> initial step
第一次暂停后    -> 从第二个 yield 继续
第二次暂停后    -> 从最终 42 继续
完成后          -> stable done closure

写出三次返回 record,解释前两次 tag 为 0、最后一次为 1。

练习 2:证明创建是懒执行

1
2
./mini run examples/coroutine_lazy.lang
./mini core examples/coroutine_lazy.lang

在 core 中找到 source body 对应的 Set。它位于哪个 lambda body?为什么创建 dispatcher 只构造 closure,不执行这条 Set?

把 body 改成必然类型错误但不 resume,程序仍应完成;加入一次 resume 后错误才出现。

练习 3:yield value 怎样回到 let

运行:

1
2
./mini core examples/coroutine_protocol.lang
./mini run examples/coroutine_protocol.lang

寻找 $coro.letcont、$coro.yieldvalue 和调用 continuation 的位置。解释为什么第一次 resume 返回 (record 0 40),第二次 resume 中的 x 仍得到 40,但调用者没有发送值。

练习 4:验证左到右顺序

1
2
3
./mini run examples/coroutine_yield_order.lang
./mini run examples/coroutine_resume_operand_order.lang
./mini run examples/coroutine_if_unselected.lang

列出三个程序实际发生的 set! 顺序。思考如果提前执行两个分支,或在 yield value 求完前安装下一步,哪项回归会先失败。

要区分编译期访问两条分支的 AST 与运行时执行分支;lowering 可以递归构造两边,生成的 core 仍只能执行被选择的一边。

练习 5:读懂 reentry guard

1
2
./mini core examples/coroutine_reentry_error.lang
./mini run examples/coroutine_reentry_error.lang

在 dispatcher 中找到“读取 state 到 next、写 state 为 0、调用 next”。回答:

  1. 为什么先保存 next?
  2. 正常 yield 在哪里恢复 closure?
  3. 完成在哪里安装自引用 done closure?done 每次调用为何还要把自身写回 state?
  4. 为什么重入复用了普通函数错误?

思考删掉写 0 后,示例会怎样重新进入同一个 step。

练习 6:独立状态机

1
2
./mini core examples/coroutine_interleaved.lang
./mini run examples/coroutine_interleaved.lang

找出两组不同的 $coro.state.*,分别画出 left/right 的 dispatcher、state 与 continuation。改变 resume 顺序,只要正确读取协议字段,二者不应互相干扰。

练习 7:追踪 suspended root

1
2
./mini alloc examples/coroutine_gc_saved_next.lang
./mini run examples/coroutine_gc_saved_next.lang

在 alloc 中找出 dispatcher、state、next closure、yielded value cell,以及恢复后的加法结果。说明哪些是 root、哪个可进入 integer register、collector 搬移对象后从哪里更新引用。

练习 8:完成结果的对象身份

1
2
./mini run examples/coroutine_done_repeat.lang
./mini core examples/coroutine_done_repeat.lang

找到 completion record 的唯一构造位置和捕获它的自引用 done closure。若把 record 构造移进 done,字段仍相等,为什么 eq? 会变成 0?若删除 done body 中的 (set! state done),为什么第二次完成后 resume 能返回、再下一次却会报函数错误?

再说明 coroutine_done_no_restart.lang 如何从副作用方向验证 body 没有重新执行。

练习 9:检查代码规模

写一个 coroutine,让多个 if 顺序出现,每个分支都可能 yield,最后共享同一尾部。逐渐增加 if,运行:

1
2
./mini core your_example.lang
./mini ir your_example.lang

记录输出长度。正确 lowering 把共同后续绑定成 continuation closure,规模近似线性;若 clone 整棵 continuation AST,规模会快速翻倍。

练习 10:静态边界的正反例

先看非法写法,yield 跨进了普通 lambda:

1
(coroutine (lambda x (yield x)))

再看合法写法,lambda 只负责创建一个拥有自己控制边界的新 coroutine:

1
(lambda x (coroutine (yield x)))

为第二项补一个完整程序,用 ast、core、run 确认 surface 节点存在、lowering 成功、只有 resume 后才执行内层 body。

进阶练习:改变协议前先写语义

若想实现 (resume coroutine value),先明确:首次发送值是否忽略;yield 恢复后得到什么;完成后继续发送是否报错;嵌套 coroutine 如何区分接收者。

这会同时改变 parser、surface AST、continuation 参数和测试协议,属于课后扩展。先把语义写成示例,再改 lowering,仍不要给解释器和编译器各写一套。

全书收束:你已走完一条编译器路径

第一章只有整数和加法:

1
source -> AST -> interpreter / IR -> x86-64

随后每章只增加一个主要困难:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let                  词法作用域与名字解析
if / eq?             控制流与基本块
function             调用约定与栈帧
recursion            自引用 closure
record               堆上复合数据
closure              自由变量与环境捕获
set! / begin         共享位置与副作用顺序
copying GC           对象搬移、root 与 safepoint
register allocation CFG 活跃性、冲突与 spill
coroutine            surface-to-core 与显式控制状态

它们没有成为十一套互不相干的演示。第十一章仍经过同一个解释器语义、同一种结构化 IR、同一套寄存器分配、同一个汇编 emitter 和 copying collector。这种连续性说明前面建立的边界足够稳定,可以承载后来没有预先写死的新抽象。

你现在已经亲手处理过这些核心关系:

  • surface 特性可以 lowering 到较小的 core;
  • 词法名字通过环境找到共享位置;
  • closure 的自由变量决定暂停后保存什么;
  • 堆可达性最终落实为 safepoint 处的机器位置;
  • 控制流同时影响语义、活跃性与寄存器冲突;
  • ABI、动态错误和 GC 必须在同一条后端中成立。

本书没有实现宏、静态类型、模块、异常、并发或通用协程,也没有把小语言包装成生产工具。它完成的是更基础、也更可迁移的路径:从文本出发,经过可逐层检查的数据结构和 pass,最后成为真正由机器执行的 x86-64 程序。

到这里,编译器不再是只能远观的黑箱。你已经把它拆开过,也把它重新接起来了。


11.8 验收:120 个程序走同一条流水线

上一节

专栏首页

下一节