从零编写现代编译器 09 - 条件、循环和可执行程序
第 08 篇让编译器支持了函数定义和调用,递归与前向引用都已经工作。但每个函数体仍然是一条直线——从第一条语句顺序执行到最后一条,中间没有任何分支或重复。写不出 if,写不出 while,程序能做的事极其有限。
本篇加入条件分支和循环,让 Sprout 成为一门图灵完备的语言。完成后,sum_to(10) 将编译成本机程序并输出 55。
if/else 进入 AST 和 HIR
if 的语法结构包含三部分:条件表达式、then 分支、可选的 else 分支。条件必须是 bool 类型,类型检查器直接拒绝整数条件。
1 | |
AST 节点记录条件、then 块和 else 块。降低到 Typed HIR 时,条件必须为 Bool,两个分支按语句处理。当前 if 只作为语句;将来作为表达式时,两个分支的类型必须一致。
错误示例:while 42 { ... } 触发类型错误——条件期望 Bool,收到 i64。类型检查统一拦截,不合格的程序不进入代码生成。
while 循环
while 由条件和循环体组成。每次迭代先检查条件,为真执行循环体,为假退出。条件一开始就为假时,循环体一次也不执行。
1 | |
零次执行是必须覆盖的边界:while false { print_i64(999); } 不产生任何输出。把条件检查错放在循环体之后,这个测试立刻暴露问题。
LLVM IR 中的控制流
LLVM IR 是 SSA 形式,控制流通过基本块和跳转指令表达。
if/else 生成三个基本块:then、else、merge。入口块用 br i1 %cond, label %then, label %else 做条件跳转,两个分支完成后无条件跳到 merge。没有 else 时,条件为假直接跳 merge。
while 同样三个块:loop.header、loop.body、loop.exit。header 计算条件,为真跳 body,为假跳 exit。body 完成后跳回 header,形成回边。
1 | |
这个结构保证条件检查在循环体之前,零次迭代自然正确。
临时方案:alloca + load/store
循环体内 sum 和 i 在每次迭代中被修改。真正的 SSA 需要 phi 节点来合并回边带来的不同值,但我们还没有构建自己的 SSA。
本篇采用临时方案:每个局部变量用 alloca 在栈上分配一个槽位,读变量生成 load,写变量生成 store。这样每个变量只有一个地址,不需要 phi 节点。
1 | |
代价是 LLVM 的 SSA 优化无法直接看穿内存操作。LLVM 的 mem2reg pass 能把简单 alloca 提升回寄存器,但不应长期依赖它。第 10–12 篇将构建自己的 CFG 和 SSA,用 phi 节点替换这些 alloca。
提前返回
return 可以出现在循环内部。sum_to 的写法把 return 放在循环之后,但另一种写法可能在循环中间提前返回:
1 | |
在 LLVM IR 中,return 生成一条到函数返回块的无条件跳转,返回块执行 ret。return 之后当前基本块已终结,不能再追加指令。代码生成器遇到 return 后必须开始新的基本块。
里程碑:sum_to(10) = 55
以下是本篇的标志性程序,完整经过所有编译阶段:
1 | |
Token 流(节选):fn sum_to ( n : i64 ) -> i64 { let mut sum … while i <= n { …
AST 中 while 节点持有条件子树 i <= n 和循环体块。HIR 确认 i <= n 结果为 Bool。LLVM IR 中 sum_to 包含 entry、loop.header、loop.body、loop.exit 四个基本块。编译为本机程序:
1 | |
参考解释器执行同一程序同样输出 55。两条路径结果一致,是差分测试的基础。
测试覆盖
零次循环:while false { print_i64(999); } 编译运行后无输出,解释器同样无输出。
提前返回:find_first_ge(5) 在循环第六次迭代时命中 return,返回 5。编译结果和解释器一致。
类型错误:while 42 { ... } 被类型检查拒绝,报告条件期望 Bool 却收到 i64,附源码位置。
练习与资料
- 用
while和if实现欧几里得 GCD 算法。对gcd(48, 18)验证解释器和编译器都输出 6。 - 在 LLVM IR 输出中找到
sum_to的回边(从loop.body跳回loop.header),解释把它改成跳到loop.exit会发生什么。 - 把
while的条件检查移到循环体之后(变成 do-while 语义),用while false { print_i64(999); }验证行为差异。
上一篇:08 - 函数调用、递归和返回值。下一篇:10 - 基本块、跳转与 CFG。
