语义边界:这不是通用协程系统
本节阅读量:本章实现单向、非对称、generator 风格的无栈 coroutine。它只把当前 coroutine 的结构化剩余计算保存起来,等待调用者下一次 resume。
边界是 lowering 成立的前提。越过它以后,程序可能需要保存任意普通函数的机器调用栈,那是另一种设计。
yield 可以穿过哪些结构
当前 pass 区分 flow expression 和 atomic expression。yield 可以沿三种结构传播:
|
|
所以这类程序合法:
|
|
yielded value 进入暂停 record,并在下次 resume 后作为该表达式的结果交给 continuation。调用者不能发送另一个值。
|
|
结果都为 42。
普通调用为什么是边界
下面的程序被拒绝:
|
|
若允许 yield 穿过任意普通调用,仅保存当前结构化 continuation 已经不够。被调用函数、调用者和更外层函数都可能有未完成的机器栈帧;正确恢复需要全程序 CPS、显式控制栈或 stackful runtime。
|
|
|
|
普通 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 |
|
|
每条命令打印 error:,状态为 1。core、ir、alloc 和 compile 也由同一个 validation pass 拒绝。
ast 只展示 parser 的 surface AST,所以:
|
|
仍会输出 Yield(Int(42))。语法能构造节点,不等于节点在当前控制上下文合法。
嵌套 coroutine 开启新边界
validation 遇到新的 (coroutine body) 时,会为内层重新建立 flow context。内层 yield 只暂停内层:
|
|
输出 42。这与“lambda body 中不能直接 yield”不冲突;lambda 内创建并返回一个新 coroutine 仍然合法,真正的 yield 发生在以后 resume 新对象时。
两类动态错误
resume 的值不是 closure
|
|
resume lower 成以 0 为参数的普通 call,因此已有检查报告:
|
|
解释器状态为 1;生成程序状态为 70。
coroutine 没有独立 runtime tag。实现层面上,resume 一个普通一参数 closure 会像传入 0 的调用一样运行;课程语义只把 (coroutine ...) 产生的 dispatcher 当作 coroutine value。
运行中重入自己
|
|
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。