章节目录

语义边界:这不是通用协程系统

本节阅读量:

本章实现单向、非对称、generator 风格的无栈 coroutine。它只把当前 coroutine 的结构化剩余计算保存起来,等待调用者下一次 resume。

边界是 lowering 成立的前提。越过它以后,程序可能需要保存任意普通函数的机器调用栈,那是另一种设计。

yield 可以穿过哪些结构

当前 pass 区分 flow expression 和 atomic expression。yield 可以沿三种结构传播:

1
2
3
(begin flow flow)
(let name flow flow)
(if flow flow flow)

所以这类程序合法:

1
2
3
4
5
(coroutine
  (let value (yield 40)
    (if value
        (begin (yield value) 42)
        0)))

yielded value 进入暂停 record,并在下次 resume 后作为该表达式的结果交给 continuation。调用者不能发送另一个值。

1
2
3
4
5
cd code/11_coroutines
make
./mini run examples/coroutine_begin.lang
./mini run examples/coroutine_protocol.lang
./mini run examples/coroutine_if.lang

结果都为 42。

普通调用为什么是边界

下面的程序被拒绝:

1
2
3
(coroutine
  ((lambda x x)
   (yield 42)))

若允许 yield 穿过任意普通调用,仅保存当前结构化 continuation 已经不够。被调用函数、调用者和更外层函数都可能有未完成的机器栈帧;正确恢复需要全程序 CPS、显式控制栈或 stackful runtime。

1
./mini run examples/yield_call_error.lang
1
error: yield cannot appear inside a function call

普通 lambda 与 letrec 函数体同样不能藏入当前 coroutine 的 yield。

七类静态错误

示例 原因 关键错误文本
yield_outside_error.lang coroutine 外 yield yield used outside a coroutine
yield_add_error.lang yield 埋进加法 yield is only allowed through begin, let, and if...
yield_record_error.lang yield 埋进字段 同上
yield_call_error.lang yield 跨普通调用 yield cannot appear inside a function call
yield_lambda_error.lang yield 藏进 lambda yield cannot appear inside a lambda body
yield_letrec_error.lang yield 藏进递归函数 yield cannot appear inside a letrec function body
yield_nested_error.lang yield value 再次 yield yield cannot appear inside another yield value
1
2
3
4
5
6
7
./mini run examples/yield_outside_error.lang
./mini run examples/yield_add_error.lang
./mini run examples/yield_record_error.lang
./mini run examples/yield_call_error.lang
./mini run examples/yield_lambda_error.lang
./mini run examples/yield_letrec_error.lang
./mini run examples/yield_nested_error.lang

每条命令打印 error:,状态为 1。core、ir、alloc 和 compile 也由同一个 validation pass 拒绝。

ast 只展示 parser 的 surface AST,所以:

1
./mini ast examples/yield_outside_error.lang

仍会输出 Yield(Int(42))。语法能构造节点,不等于节点在当前控制上下文合法。

嵌套 coroutine 开启新边界

validation 遇到新的 (coroutine body) 时,会为内层重新建立 flow context。内层 yield 只暂停内层:

1
./mini run examples/coroutine_nested.lang

输出 42。这与“lambda body 中不能直接 yield”不冲突;lambda 内创建并返回一个新 coroutine 仍然合法,真正的 yield 发生在以后 resume 新对象时。

两类动态错误

resume 的值不是 closure

1
./mini run examples/resume_integer_error.lang

resume lower 成以 0 为参数的普通 call,因此已有检查报告:

1
error: function call expected a function

解释器状态为 1;生成程序状态为 70。

coroutine 没有独立 runtime tag。实现层面上,resume 一个普通一参数 closure 会像传入 0 的调用一样运行;课程语义只把 (coroutine ...) 产生的 dispatcher 当作 coroutine value。

运行中重入自己

1
./mini run examples/coroutine_reentry_error.lang

dispatcher 在调用 next 前暂时把 state 写成 0。若 step 重入自己,内层 dispatcher 尝试调用 0,得到相同动态函数错误。yield 会安装后续 continuation;完成后的自引用 done closure 每次调用都会把自身重新写回 state,因此任意多次顺序 resume 不受影响。

明确不实现的能力

本章不包含:

  • 独立机器栈或栈拷贝;
  • 从任意普通函数深处 yield;
  • 对称 transfer;
  • 向 coroutine 发送值;
  • 调度器、线程、并发或异步 I/O;
  • 取消与异常跨暂停传播;
  • async/await 的 promise 和事件循环协议。

每次 resume 都是一次正常返回的函数调用。暂停只表示 state 已换成另一个 closure。这个范围足以展示 generator 与状态机的核心,又不会把教程拉进平台栈切换和并发 runtime。


11.6 IR 与后端:没有 coroutine 操作

上一节

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

下一节