章节目录

IR 与后端:没有 coroutine 操作

本节阅读量:

surface-to-core lowering 完成后,编译器面对的是第十章已经能处理的完整语言。本章没有增加 coroutine、yield 或 resume IR op,也没有新增 runtime tag。

1
2
3
4
5
6
7
surface AST
  -> validation / lowering
  -> core AST
  -> IrProgram
  -> representation / liveness / homes
  -> x86-64 assembly
  -> existing C++ copying GC runtime

这正是在检验前面建立的抽象能否组合:控制状态由 closure 表示,可变槽由 cell 表示,暂停结果由 record 表示,它们理应进入同一条后端。

从 core 走到 IR

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

main 的开头会出现:

1
2
3
4
5
6
7
$coro.state.00.cell = cell 0
f.0 = closure lambda0 [$coro.state.00.cell]
$coro.finish.111.cell = cell f.0
f.2 = closure lambda2 [$coro.finish.111.cell, $coro.state.00.cell]
store $coro.state.00.cell f.2
f.5 = closure lambda5 [$coro.state.00.cell]
g29.cell = cell f.5
  • $coro.state.00.cell 保存下一步;
  • f.0 是 finish closure;
  • f.2 是 initial step;
  • store 安装 initial step;
  • f.5 是 dispatcher;
  • g29.cell 是源码 let g 的位置。

两次 resume 已经只是普通调用:

1
2
3
4
5
6
7
8
t.16 = load g29.cell
check-closure t.16
t.17 = call t.16 0
t.18 = load g29.cell
check-closure t.18
t.19 = call t.18 0
t.20 = get t.19 1
return t.20

resume 复用 load、closure_check、call 和 field。

yield 在 IR 中是什么

initial step 的关键操作是:

1
2
3
4
5
6
$coro.yieldvalue...cell = cell 40
f.4 = closure lambda4 [...]
store $coro.state...cell f.4
t.9 = load $coro.yieldvalue...cell
t.10 = record 0 t.9
return t.10

暂停的机器可见效果只有四步:分配 continuation closure、写入 state、分配 (record 0 value)、从当前普通函数返回。下一次 resume 重新进入 dispatcher,再间接调用新 closure。原机器栈已经正常返回,从未被冻结。

自由变量就是保存状态

考虑 coroutine_gc_capture_record.lang 中的结构:

1
2
3
4
5
(let saved (record 40)
  (coroutine
    (begin
      (yield 0)
      (+ (get saved 0) 2))))

暂停后的 continuation 还要读取 saved,所以 closure 捕获它的 cell:

1
dispatcher -> state cell -> continuation -> saved cell -> record 40

这就是“暂停后仍需保存什么”的可执行答案,无需 coroutine frame descriptor。运行:

1
2
./mini run examples/coroutine_gc_capture_record.lang
./mini run examples/coroutine_gc_saved_next.lang

两项都输出 42。第一项先验证 continuation 捕获的外部 record,完成后又触发一轮 GC,并继续检查 done closure 与 completed record 的身份;第二项专门验证 state 中的 next continuation 跨大量分配仍有效。

表示分析继续保守

alloc 不认识“协程变量”。它只应用第十章分类:

来源 分类
closure、cell、record root
load、field、call root
add、sub1、equal 等确定结果 integer
1
./mini alloc examples/coroutine_gc_saved_next.lang

输出中 dispatcher、state、continuation、captured cell 和调用结果进入 %r15 root slots;恢复后的 (+ value 2) 结果仍可进入整数寄存器。

对象引用可以为了读字段短暂进入 scratch register,但不能未受保护地跨越 allocation 或源语言 call。continuation 本体在堆上,与保存它的 tagged pointer 在 safepoint 时位于哪里,是两个问题。

GC 不增加扫描分支

collector 已经会扫描:

1
2
3
record  -> fields
closure -> captures(跳过 code pointer)
cell    -> current Value

coroutine lowering 只构造这三种对象。暂停形成的共享或环仍由 forwarding pointer 处理,不需要第四种 header 或 continuation bitmap。

reentry guard 仍是普通 call 检查

dispatcher 生成的 IR 可以概括为:

1
2
3
4
next = load state
store state 0
check-closure next
result = call next 0

若当前 step 重入同一 dispatcher,内层调用从 state 读到 0,已有 closure_check 进入共同运行时错误出口。完成后的 done step 则会在返回 completed record 前把自身重新写回 state,因此 dispatcher 的临时清空不会破坏重复 resume。编译结果状态仍是 70,没有专用错误码。

emitter 与 runtime 保持原样

compile 仍消费同一种 IrProgram:

1
2
3
write_file(
    output_path,
    mini::compile_to_assembly(mini::lower_to_ir(*core), target));

Linux、WSL 或 Intel Mac:

1
2
3
4
./mini compile examples/coroutine_protocol.lang -o out.s
c++ out.s gc_runtime.o -o out
./out
echo $?

Apple Silicon Mac:

1
2
3
4
./mini compile examples/coroutine_protocol.lang -o out.s --target macos
c++ -arch x86_64 out.s gc_runtime.o -o out
./out
echo $?

状态都应为 42。make 已经生成本章自己的 gc_runtime.o,它与 out.s 通过第九章定义的 C ABI 相连。最终汇编只出现普通 allocation、load/store 和 call,而没有“暂停指令”,正说明 surface-to-core 边界发挥了作用。


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

上一节

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

下一节