语言:一次 `resume` 只走到下一个边界
本节阅读量:协程最容易被含混地描述为“函数可以暂停”。本章先不用这个说法,而是给三个表达式明确的求值规则。只要每次 resume 的结果可预测,后面的 lowering 才有可靠的目标。
coroutine 创建一个尚未运行的 generator
语法是:
|
|
求值它时,body 不执行。结果是一个以后可以交给 resume 的 generator 值。
因此:
|
|
结果是 0,不是 1。创建 generator 只准备“第一步应该做什么”,并没有走出第一步。
coroutine_lazy.lang 用 if 把这条规则检查成最终的 42:
|
|
yield 产出一个值,并把它留给下一次恢复
语法是:
|
|
当前这一次 resume 会先求 expr,然后暂停并返回:
|
|
其中 value 是 expr 的结果。下一次 resume 不会重新计算 expr;它从 yield 后面继续,并把这个已经得到的 value 作为整个 (yield expr) 的表达式值。
例如:
|
|
第一次恢复时,yield 产出 40,整个 let 还没有建立 x。第二次恢复时,yield 的结果变成已有的 40,于是建立 x,再计算 (+ x 2) 得到 42。
这就是“本章不向 coroutine 发送值”的精确含义:恢复者没有提供新 argument,恢复后的 yield 也不凭空得到一个新值;它继续使用自己已经产出的值。
body 正常结束也会产生 record
若 body 没有再遇到 yield,本次 resume 返回:
|
|
1 表示 done,字段 1 是 body 的最终值。对主例子:
|
|
完成后再次 resume 不会回到 body 开头,也不会重复最后一次计算。它反复返回第一次完成时创建的那个 record。
|
|
示例中的:
|
|
结果为真,说明两个 done 结果是同一个 record 对象,而不只是字段碰巧相同的两个 record。
resume 恢复一个 generator
语法是:
|
|
它先按普通表达式的从左到右规则求 expr,然后调用其中保存的 generator step。这个顺序可观察:
|
|
该示例在 resume 的 operand 中先执行 set!,再取得 g;最后结果是 42,说明 operand 没有被跳过或提前改写。
从 core AST 的角度,resume 会降成“以隐藏整数 0 调用一个一元 closure”。这个 0 不是 send 值,也不是用户可见的协议字段;它只是复用已有一元函数调用形状的占位 argument。
本章不为 coroutine 另设 runtime tag。因此 resume 能进行的动态检查与普通调用相同:operand 必须是 closure。下面会触发已有的 closure 错误:
|
|
输出包含:
|
|
(coroutine ...) 产生的 closure 才保证遵守本节的二字段 record 协议;普通 closure 不是这个协议的一部分。
一个 generator 可以暂停多次
本章的 begin 固定为二元。嵌套 begin 足以写出多个暂停点:
|
|
运行:
|
|
三次恢复依次经过:
|
|
示例忽略前两次暂停 record,读取第三次的完成值,因此输出 42。
不同 generator 各有自己的 state,不会互相抢走下一步:
|
|
它交错恢复两个 generator,仍然得到 42。这不是调度器;只是两个独立 closure 各自捕获了自己的 cell。
yield 的值先算完,状态随后才更新
yield 的 payload 也是普通表达式。它先完整求值,随后才安装下一步 closure 和返回暂停 record。
|
|
该程序的 payload 先把 value 改成 1,恢复后剩余 body 再把它改成 2。最终 42 证明两次副作用恰好分别发生在暂停前和恢复后。
若 payload 在求值时出错,则既没有 pause record,也不能错误地替换当前 state。这个顺序会在 lowering 一节具体实现。
重新进入正在执行的 coroutine 是错误
当一个 step 已经开始运行时,本章不允许它再次恢复自己。进入 step 的瞬间,lowering 临时把 state cell 写成整数 0;若 body 递归执行 (resume g),已有的 closure guard 会发现 0 不是函数并报告错误。
|
|
输出包含:
|
|
这条规则避免“同一个 step 同时运行两次”的难题。它不实现 generator 的重入、协作式 transfer 或独立机器栈;这些都超出本章边界。