边界:哪些位置可以 yield
本节阅读量:
如果任何表达式内部都能突然暂停,那么编译器必须记录任意深度的求值上下文:加法已经算完左边了吗?函数的 callee 是否检查过?record 已经写了几个字段?这会把本章直接推向通用 CPS 或 CEK machine。
本章刻意选择一条更小、可完整实现的路线:yield 只允许沿当前 coroutine 自己的 begin、let 和 if 控制流出现。
1
2
3
4
5
|
coroutine body
├─ yield
├─ begin 的 first / second
├─ let 的 value / body
└─ if 的 condition / then / else
|
这已经能表达多个暂停点、局部变量、分支和可变状态;但不会要求我们保存普通函数调用或算术指令中间的机器状态。
合法的结构化路径
最小合法 body 是:
begin 可以把“暂停后继续什么”写清楚:
1
2
3
4
|
(coroutine
(begin
(yield 40)
42))
|
let 的 initializer 也可以暂停:
1
2
3
|
(coroutine
(let x (yield 40)
(+ x 2)))
|
恢复时,lowering 会把刚才产出的值交给 x,然后继续 body。
if 的 condition 和两个分支同样可以处在 flow 中:
1
2
3
4
5
6
|
(coroutine
(if (yield 1)
(begin
(yield 40)
42)
0))
|
1
2
|
cd code/11_coroutines
./mini run examples/coroutine_if.lang
|
本语言沿用前面的条件规则:非零整数为真,因此第一次 yield 后恢复到 then 分支;最终结果是 42。没有被选中的 else 分支不会执行。
两个递归入口把 flow 与普通表达式分开
validation 的核心不是维护一张特殊形式名单,而是明确区分两种递归入口:
1
2
3
4
5
|
void validate_flow(const Expr& expr);
void validate_atomic(const Expr& expr,
bool inside_coroutine,
const std::string& yield_error);
|
validate_flow 表示“这里仍处在当前 coroutine 可以暂停的结构化流程中”。它只继续穿过 begin、let 和 if:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
void validate_flow(const Expr& expr) {
switch (expr.kind) {
case ExprKind::yield: {
const auto& yield = static_cast<const YieldExpr&>(expr);
validate_atomic(
*yield.value,
true,
"yield cannot appear inside another yield value");
return;
}
case ExprKind::begin: {
const auto& begin = static_cast<const BeginExpr&>(expr);
validate_flow(*begin.first);
validate_flow(*begin.second);
return;
}
case ExprKind::let: {
const auto& let = static_cast<const LetExpr&>(expr);
validate_flow(*let.value);
validate_flow(*let.body);
return;
}
case ExprKind::conditional: {
const auto& conditional = static_cast<const IfExpr&>(expr);
validate_flow(*conditional.condition);
validate_flow(*conditional.then_branch);
validate_flow(*conditional.else_branch);
return;
}
default:
validate_atomic(
expr,
true,
"yield is only allowed through begin, let, and if in a coroutine body");
return;
}
}
|
注意 yield.value 改走 validate_atomic。这正是“payload 必须先作为一段不能暂停的普通表达式算完”的代码落点。
validate_atomic 则按已有 kind + switch 遍历普通表达式。它仍会递归检查所有子树,但看到属于当前流程的 yield 就报告调用者传入的上下文错误。几个关键分支是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
case ExprKind::lambda: {
const auto& lambda = static_cast<const LambdaExpr&>(expr);
validate_atomic(*lambda.body,
inside_coroutine,
"yield cannot appear inside a lambda body");
return;
}
case ExprKind::call: {
const auto& call = static_cast<const CallExpr&>(expr);
validate_atomic(*call.callee,
inside_coroutine,
"yield cannot appear inside a function call");
validate_atomic(*call.argument,
inside_coroutine,
"yield cannot appear inside a function call");
return;
}
case ExprKind::coroutine:
validate_flow(*static_cast<const CoroutineExpr&>(expr).body);
return;
case ExprKind::yield:
reject_yield(inside_coroutine, yield_error);
|
新的 CoroutineExpr 会重新进入 validate_flow,所以嵌套 coroutine 拥有自己的 yield 边界;普通 lambda 与 call 仍留在 atomic 入口,因此不能偷偷继承外层 coroutine 的暂停能力。
普通表达式内部不允许藏 yield
下面的 yield 位于 + 的右 operand:
1
2
|
(coroutine
(+ 1 (yield 41)))
|
运行:
1
|
./mini run examples/yield_add_error.lang
|
会得到:
1
|
error: yield is only allowed through begin, let, and if in a coroutine body
|
同样,不能把 yield 藏进普通 call:
1
|
./mini run examples/yield_call_error.lang
|
这次错误为:
1
|
error: yield cannot appear inside a function call
|
record 字段、set! 的右侧、eq?、size、get 和 sub1 的内部也都属于普通表达式上下文。它们仍可使用已有计算和副作用,只是其中不能再插入暂停点。
lambda 和 letrec 函数体不是当前 coroutine 的流程
下面的 yield 语法上写在 coroutine 里,但真正执行它的是以后才会被调用的普通函数:
1
2
3
|
(coroutine
(lambda x
(yield x)))
|
1
|
./mini run examples/yield_lambda_error.lang
|
输出包含:
1
|
error: yield cannot appear inside a lambda body
|
letrec 函数体也一样。它可能在很多不同的时刻、递归深度和调用者中运行,不属于创建它的 coroutine 当前步骤。yield_letrec_error.lang 是对应检查。
这条限制也解释了本章“无栈”的含义:我们只保存自己明确生成的 closure,不截获 C++ 调用栈或任意源语言 call stack。
yield 的 payload 不能再 yield
下面的嵌套写法也不合法:
1
2
|
(coroutine
(yield (yield 42)))
|
1
|
./mini run examples/yield_nested_error.lang
|
输出包含:
1
|
error: yield cannot appear inside another yield value
|
外层 yield 在暂停前必须先把 payload 算成一个 Value;若 payload 自己也能暂停,就需要同时决定两个未完成的 pause protocol。本章不把这两个层次混在一起。
yield 必须属于某个 coroutine
单独写:
不是一个可以运行的顶层程序:
1
|
./mini run examples/yield_outside_error.lang
|
输出是:
1
|
error: yield used outside a coroutine
|
这不是 parser 的 arity 错误。parser 会如实构造 YieldExpr,随后 validation 根据它所在的词法位置报告错误;这样 mini ast 仍能展示读者实际写下的 surface 结构。
嵌套 coroutine 重新开始一套边界
一个 coroutine 可以在自己的 body 中创建另一个 coroutine。内层 (yield ...) 属于内层,不会让外层 step 暂停;外层若想暂停,仍要写自己的 yield。
1
|
./mini run examples/coroutine_nested.lang
|
该例子先创建 inner,外层通过 (resume inner) 得到 inner 的值,再在自己的 yield 处暂停。输出为 42,说明两个 state cell 和两条 continuation 链彼此独立。
validation 在哪一条流水线上
ast 是唯一直接观察 surface AST 的命令:
1
|
./mini ast examples/coroutine_protocol.lang
|
run、core、ir、alloc 和 compile 都调用同一个 validate_coroutines,然后才接收去糖后的 core AST:
1
2
3
4
5
6
7
8
9
|
parser
↓
surface AST
├─ ast
└─ validate_coroutines
↓
lower_coroutines
↓
interpreter / IR / alloc / assembly
|
因此一个不合法 yield 不会出现“解释器拒绝了、编译器却悄悄接受”的两套行为。validation error 是 CLI 阶段错误:mini run 会打印 error: 并返回状态 1;它不是原生程序运行到一半的状态 70。
11.1 语言:一次 `resume` 只走到下一个边界
上一节
11.3 AST 与 parser:先保留表面语法,再统一去糖
下一节