IR lowering:把 AST 变成分支和汇合
本节阅读量:上一节已经确定目标 IR 的形状。这一节进入实现,看看 lowerer 怎样生成谓词、label、跳转和共享结果名。
代码在:
|
|
lower_expr 的约定没有改变
先回顾 lowerer 的核心约定:
|
|
它做两件事:
|
|
例如:
|
|
因此,lowering if 并不是立刻求出 then 或 else 的值。lowerer 在编译期间会遍历两个分支,把两边的操作都生成出来;将来程序运行时,branch 才会决定执行哪一边。
要特别区分这两个时刻:
|
|
lowerer 会先后遍历 then 和 else,但两边都从进入这个 if 时的同一个 lowerer 环境出发。某个分支内部如果出现 let,它会在该分支 lowering 期间 push 绑定,并在离开作用域前 pop;then 中的局部绑定不会因为先生成,就泄漏到 else。
IR 在代码里怎样保存
操作种类定义在 ir.h:
|
|
equal 负责产生普通整数结果,branch、jump 和 label 负责控制下一步去哪里。
每条操作都保存在同一个 Op 结构里:
|
|
不同 OpKind 使用不同字段:
|
|
例如,一条 branch:
|
|
在结构里保存的是:
|
|
没有使用到的字段会放占位值。真正决定一条 Op 应该怎样解释的是 kind;IR 打印和下一节的汇编生成器都会用 switch 分派。
把重复动作抽成两个辅助函数
第二章直接在各个分支里生成临时名字、追加 copy。第三章需要这些动作的地方变多了,所以把重复代码抽成两个辅助函数:
|
|
new_temp() 统一生成 t.0、t.1 这样的临时名字,供加法、eq? 和 if 的共享结果使用。emit_copy() 统一追加 dst = value,既用于原有的 let,也用于把 then 和 else 的结果保存到同一个名字。
它们没有引入新的 IR 概念,只是把多处相同的实现集中起来。
eq? 怎样 lowering
eq? 是前面说过的谓词表达式:它提出一个判断,并把结果表示成整数 0 或 1。
谓词本身不会改变控制流。它和加法一样,读取操作数、生成一个临时名字,再把结果写进去。后面的 if 可以读取这个 0 或 1,再决定走哪个分支。
eq? 读取两个操作数:
|
|
这里也保持从左到右的 lowering 顺序:先生成 lhs 所需的操作,再生成 rhs,最后追加 equal。
例如 (eq? (+ 20 22) 42) 得到:
|
|
判断某个表达式是否为零也用同一条路径,例如 (eq? x 0)。equal 的结果一定是 0 或 1,但在 IR 中仍然只是普通整数操作数,可以继续交给 if、加法或其他表达式使用。
if 怎样 lowering
先不看 C++ 细节,把 lowering 骨架写成步骤:
|
|
这份骨架先解决控制流,再把两个分支的值汇合到同一个内部名字。对照它阅读完整代码:
|
|
第一步递归 lower condition。condition 自己可能包含加法、变量甚至另一个 if:
|
|
接着为这次 if 创建三个互不冲突的 label,以及一个共享结果名:
|
|
然后按线性布局追加两条路径:
|
|
then 和 else 都通过 emit_copy() 写入 result,但运行时只会执行其中一个 copy。当前布局把 then label 紧跟在 branch 后面,让非零条件可以顺序落入 then;零条件则跳到 else。then 结束后必须 jump end_label,避免继续落入 else。
最后返回共享结果名:
|
|
调用者不需要知道这个结果来自哪个分支。它只知道:执行到 end_label 后,result 中就是整个 if 的值。
next_label_ 为每个 if 分配独立编号,所以嵌套条件会得到 then0、then1 等不同名字,不会跳进别的 if。
到这里,AST 已经 lowering 成带控制流的 IR。下一节把这些 IR 操作翻译成 x86-64 汇编。