第十一章:把“下一步”做成一个值
本节阅读量:前十章已经让这门小语言拥有整数、函数、闭包、record、可变状态、copying GC 和统一的寄存器分配后端。现在我们做一篇选修综合篇:实现一个很小、但真的可以暂停和继续运行的 generator。
先看本章的主例子:
|
|
它看起来像一个函数在中途停下,稍后又从原处继续:
|
|
先把它跑起来:
|
|
与第九、十章相同,make 会同时生成 mini 和独立的 C++ GC runtime object gc_runtime.o。
输出是:
|
|
这里的 42 同时检查了两次 resume 的协议:第一次的状态字段是 0、产出值是 40;第二次的状态字段是 1、完成值是 42。
本章只做一种受限协程
本章实现的是单向、非对称、generator 风格的无栈协程。它有三种固定特殊形式:
|
|
coroutine 创建 generator 值,yield 产出一个值并暂停,resume 继续一个 generator。它不是完整的协程系统:没有向 generator 发送值、没有对称 transfer、没有调度器、线程、异步 I/O、取消、异常传播或另一套机器栈。
这个范围很小,却足以回答一个重要问题:暂停以后,原来函数里那些局部变量、分支位置和后续语句到哪里去了?
本章的答案是:它们不留在冻结的机器栈里,而是由一个 closure 保存为“下一步”。
|
|
第七章的 closure 已经会保存外层环境;第八章的 cell 已经能让某个位置改指向新值;第六章的 record 已经能作为数据协议;第九章的 GC 已经会扫描这些对象;第十章的后端已经能把它们保存在 root slot。第十一章不新发明运行时对象,而是把这些能力接起来。
resume 的结果协议
每次 resume 都返回已有的二字段 record:
| record | 含义 |
|---|---|
(record 0 value) |
本次在 yield 处暂停,字段 1 是产出的值 |
(record 1 value) |
coroutine 已完成,字段 1 是 body 的最终值 |
完成不是重新开始。运行:
|
|
示例先让 (coroutine 42) 完成,再重复恢复它,并用 eq? 检查两个结果是否指向同一个完成 record。输出仍是:
|
|
缓存完成 record 有两个好处:body 的副作用不会重跑,完成后的结果也有稳定的对象身份。
创建不等于执行
coroutine 的 body 不会在创建时运行。下面的程序在创建 g 后立刻检查外层 touched,因此应得到 42:
|
|
对应源码中的 set! 放在 body 内部;只有第一次 resume 真正调用 initial step 时,它才会执行。
两条已有实现路径继续一致
本章保留 surface AST,方便读者看见自己写下的 Coroutine、Yield 与 Resume:
|
|
随后,新加的 core 命令显示去糖结果:
|
|
它会出现 $coro.state.*、Lambda、Set、Record 和普通 Call,但不再出现 Coroutine、Yield 或 Resume。$coro.* 是编译器生成的内部名字;用户语言的普通 identifier 仍只能由英文字母组成,因此不会冲突。
之后两条路径都只处理 core AST:
|
|
因此解释器不需要偷偷维护另一套 coroutine machine,汇编运行时也不需要新的 coroutine tag。run、ir、alloc 和 compile 都先执行同一份 validation 和 lowering。
编译并观察原生程序
先看 lowering 后的 IR:
|
|
开头会出现 state cell、创建 closure 和写回 state 的操作;后面则是已有的 check-closure 与 call。这正是“resume 最终只是调用当前 step closure”的机器层面形状。
生成汇编并运行,在 Linux、WSL 或 Intel Mac 上:
|
|
最后一行应为:
|
|
Apple Silicon Mac 需要生成和链接 x86-64 程序:
|
|
这仍然不是 mini 替你链接:compile 的职责始终只是输出 x86-64 System V ABI 的 AT&T 汇编。第九章以来的 C++ collector 继续由本章 make 构建成 gc_runtime.o;out.s 必须与它一起链接。
这一章要回答什么
我们按下面的顺序前进:
- 定义三种新语法和每次
resume的 result protocol; - 说明哪些
yield位置可以被本章安全地改写; - 在 AST 中保留 surface 形状,再把它降到旧语言的 core AST;
- 一步步展开一个
yield如何保存 continuation closure; - 用解释器、IR、GC 和第十章后端确认这不是纸上的变换。
核心句子始终是:暂停不是冻结机器栈,而是把“接下来还要做什么”保存成一个可以再次调用的值。