11.3 分支与循环的控制流翻译
源语言中的 if、while 和 for 具有结构化外形;IR 用条件跳转、无条件跳转和终结指令连接基本块。降低的任务,是把嵌套结构变成控制流图(Control-Flow Graph,CFG),同时精确保留哪些路径能够执行。
一个 if-else 通常产生条件块、两个分支块和一个汇合块。如果某个分支提前返回,它就不会流向汇合点。因此,是否需要汇合和 phi 输入由 CFG 决定,而不是由源码缩进决定。
降低控制流时,最好让每个辅助函数都明确表达“后续执行位置”。语句降低器接收当前基本块,自行创建所需的新块,并返回正常执行继续所在的块。如果所有路径都以 return 或 throw 终结,它甚至可以不返回 continuation。这样就不会为了继续线性发射指令而伪造不存在的边。
降低一个分支
对于 if (score >= 60) pass(); else retry();,一种直接形态是:
entry:
%c = icmp.ge %score, 60
br %c, then, else
then:
call @pass()
br join
else:
call @retry()
br join
join:
...每个可达基本块都必须恰好以一条终结指令结束。IR builder 应拒绝在同一块的 ret、throw 或无条件跳转之后继续发射普通指令。
没有 else 的 if 仍需要假分支目的地,此时汇合块就是假边的目标:
entry:
br %condition, then, join
then:
call @work()
br join
join:
; 从这里继续执行如果 then 块以 return 结束,它就不应再跳到 join;此时只有条件假边到达 continuation。这个差异会进一步决定后续是否需要 phi 节点。
循环就是基本块加跳转目标
前测循环常见形态是 preheader → header → body → latch → header,header 还有一条退出边。do-while 在第一次测试前先进入 body;for 通常为更新表达式单独建立一个块。
这些差异会直接影响 continue:在 while 中,它一般跳到条件 header;在 for 中,它必须先跳到 update,再回 header。break 则跳到循环出口。维护一组当前活跃的 break/continue 目标栈,就能自然降低嵌套循环。
下面是 for (i = 0; i < n; i++) body(i) 的基本骨架:
preheader:
store 0, @i
br header
header:
%i0 = load @i
%keep = icmp.lt %i0, %n
br %keep, body, exit
body:
call @body(%i0)
br update
update:
%i1 = load @i
%i2 = add %i1, 1
store %i2, @i
br header
exit:
...body 中的 continue 指向 update,break 指向 exit。在基于 SSA 的降低器中,反复 load/store 的 i 可能变成 header 中的循环携带 phi,但控制流骨架仍然相同。
验证拓扑,而不是只看打印结果
高价值测试应断言图性质:所有块都有终结指令、每个跳转目标存在、不可达块是有意产生的、continue 到达正确的更新或测试块、break 离开最内层活跃循环。CFG 验证器能抓住许多漂亮 IR 文本掩盖的错误。
嵌套循环尤其依赖正确的目标管理。进入循环时,把 {breakTarget, continueTarget} 压栈;离开时弹栈。内层循环中的 break 总是选择栈顶目标,而不会意外退出外层循环。若语言支持带标签的 break,则可以沿目标栈查找匹配标签。
最后还要区分“不可达代码”和“尚未完成的基本块”。无条件 return 后的代码确实不可达,不能继续追加到已终结块。编译器可以省略它、为了诊断放入单独标记的 unreachable 块,或向用户发出警告,但绝不能制造一条不可能存在的 fallthrough 边。