章节目录

解释器:执行 core,而不是认识协程

本节阅读量:

surface coroutine 已经被改写成普通的 let、lambda、函数调用、set!、begin 和 record。因此解释器不需要增加一套“暂停 C++ 调用栈”的机制。

1
source -> surface AST -> validation / lowering -> core AST -> 原有解释器

解释器看到的暂停状态是一张普通对象图:dispatcher closure 捕获 state cell,state cell 保存下一步 continuation closure。

先观察 surface 与 core

1
2
3
cd code/11_coroutines
make
./mini ast examples/coroutine_one_yield.lang

ast 保留读者写下的节点:

1
Let(g, Coroutine(Begin(Yield(Int(40)), Int(42))), ...)

再看真正交给解释器的 AST:

1
./mini core examples/coroutine_one_yield.lang

这里已经没有 Coroutine、Yield 或 Resume,取而代之的是 $coro.* 内部名字、closure、Set、Call 和 Record。完整 dump 很长,先确认 surface 节点已经消失即可。

CLI 明确守住这条边界:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
auto expr = mini::parse(source);

if (command == "ast") {
    std::cout << expr->dump() << "\n";
    return 0;
}

auto core = mini::lower_coroutines(std::move(expr));

if (command == "run") {
    std::cout << mini::format_value(mini::eval(*core)) << "\n";
    return 0;
}

ast 是唯一停在 surface AST 的观察点;run、core、ir、alloc 和 compile 都经过同一个 pass。

旧解释器规则已经足够

core 结构 已有语义 在 coroutine 中的职责
lambda 捕获自由变量的 Location 保存 continuation
函数调用 执行 closure body 执行一次 step
let 先求值,再分配 Location 建立 state、finish 和临时值
set! 写回已有 Location 更换下一步
begin 顺序执行 先安装下一步,再返回结果
record 从左到右求字段并分配 表示 paused/done 协议

第八章已经让 closure 捕获 Location,而不是某次读取出的 Value。dispatcher、当前 step 和后续 continuation 共享同一个 state Location;一个 continuation 执行 (set! state next) 后,下一次 dispatcher 调用才能读到新的 closure。

走一遍一次暂停

coroutine_one_yield.lang 的核心是:

1
2
3
4
5
(let g
  (coroutine (begin (yield 40) 42))
  (begin
    (resume g)
    (get (resume g) 1)))

创建 g 时只做这些事:

1
2
3
4
分配 state Location,初值为 0
建立 finish closure
建立 initial step,并写入 state
返回捕获 state 的 dispatcher

body 尚未执行。运行:

1
./mini run examples/coroutine_lazy.lang

应输出 42,说明创建 coroutine 没有修改 body 中的 touched。

第一次 (resume g):

1
2
3
4
5
6
dispatcher 读出 initial step
state 暂时写成 0
调用 initial step
求出 yield value 40
把“从 42 继续”的 closure 写入 state
返回 (record 0 40)

第二次 resume 读到刚保存的 continuation。它求值 42,调用 finish,分配 (record 1 42),再建立一个自引用的 done closure。done 每次先把自己写回 state,再返回同一个完成对象。

1
./mini run examples/coroutine_one_yield.lang

输出为 42。

state 临时写成 0:拒绝重入

dispatcher 的 core 形状是:

1
2
3
4
5
(lambda ignored
  (let next state
    (begin
      (set! state 0)
      (next 0))))

它先保存当前 step,再把共享 state 临时写成 0。正常 step 会在 yield 或完成时安装新 closure;若 step 尚未返回就再次 resume 自己,内层 dispatcher 会尝试调用 0,复用已有动态检查报告错误:

1
./mini run examples/coroutine_reentry_error.lang
1
error: function call expected a function

这里没有新增 coroutine 状态 tag,只是让“正在运行”暂态不可调用。

完成后不重新开始

finish 先分配完成 record,再让 state 指向捕获该 record 的自恢复 closure:

1
2
3
state -> done closure -> completed record
          |
          +-- 每次调用前,把自己重新写回 state

dispatcher 会在调用 done 前照常把 state 暂时清成 0;done 随即把自己写回,再返回 completed。所以完成 record 只分配一次,body 也只执行一次,而且第三次、第四次乃至更多次恢复仍然有效:

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

两项都输出 42。前者用 record 身份相等验证结果是同一对象,后者验证 body 中的计数器只增加一次。

独立 coroutine 是独立对象图

两个 coroutine 各有 dispatcher 和 state。交错 resume 不需要调度器:调用哪一个 dispatcher,就推进哪张对象图。

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

结果都为 42。嵌套 coroutine 在 validation 时开启新的 yield 边界;lowering 后只是另一组 closure 和 cell。

surface 泄漏是内部错误

解释器仍显式列出全部 kind:

1
2
3
4
5
case ExprKind::coroutine:
case ExprKind::yield:
case ExprKind::resume:
    throw std::runtime_error(
        "surface coroutine expression reached the interpreter");

这些分支不是第二份 coroutine 实现。正常 CLI 永远不会到达它们;它们负责在某条入口忘记 lowering 时立即暴露 pass 边界错误。

本节最重要的结论是:解释器没有冻结控制栈。它只是在执行 record、closure 和共享位置语义;暂停来自变换后的对象结构,而不是新的求值机器。


11.4 Lowering:把暂停后的计算装进 closure

上一节

11.6 IR 与后端:没有 coroutine 操作

下一节