11.5 短路布尔代码生成
短路运算符是伪装成表达式的控制流。在 A() && B() 中,只有 A() 为真才运行 B();在 A() || B() 中,只有 A() 为假才运行 B()。若编译器急切计算两个操作数,就会改变副作用、异常、性能,甚至程序是否合法。
一个特别实用的降低接口是 lowerCondition(expr, trueBlock, falseBlock):它直接发射跳转,而不是先制造一个布尔临时值。
这种表示称为控制形态(control form):条件的含义体现在“执行流向哪里”,而不是某个保存 true 或 false 的临时值。它天然适合 if、while 和条件表达式,还能避免生成一个马上又只用于 branch 的中间布尔值。
用目的地组合条件
递归规则非常紧凑:
- 对
A && B,把A的真边接到一个计算B的新块,把假边直接接到最终假目的地;
- 对
A || B,把A的假边接到B,把真边直接接到最终真目的地;
- 对
!A,交换真、假目的地。
这些规则无需物化中间布尔值,就能自然处理任意嵌套,同时精确保留哪些调用、加载和检查被跳过。
; A() && B()
entry:
%a = call @A()
br %a, rhs, false
rhs:
%b = call @B()
br %b, true, false对于嵌套表达式 A() && (B() || !C()),继续递归应用同样规则:
entry:
%a = call @A()
br %a, eval_b, false
eval_b:
%b = call @B()
br %b, true, eval_c
eval_c:
%c = call @C()
br %c, false, true ; !C 交换目的地执行轨迹最能说明这张图:若 A() 为假,轨迹只有 A;若 A 为真且 B() 为真,轨迹是 A、B;只有 A 为真且 B 为假时才会调用 C。仅检查最终真值,无法判断这些跳过行为是否被正确保留。
x < y 这样的比较是叶子条件:降低器先计算两个操作数,发射 comparison,再依据结果分支。布尔变量也是叶子:加载或引用它的值,然后 branch。AND、OR、NOT 的递归规则把这些叶子组合成任意规模的条件。
什么时候必须得到布尔值
直接跳转很适合 if 与 while,但 x = A() && B() 必须产生一个值。此时先把条件降低到 true/false 两块,再在汇合点物化结果:
true: br merge
false: br merge
merge:
%x = phi [true, true], [false, false]这叫值形态(value form)。当布尔结果需要被存储、作为参数传递、返回,或参与非控制运算时,就必须生成一个值。降低器可以这样实现 lowerValue(booleanExpr):创建 true、false 与 merge 块,委托 lowerCondition 产生控制流,再在 merge 中用 phi 得到结果。反过来,值形态布尔量被 if 使用时,可以直接依据该值跳转。
不要过早物化。先急切计算 A、B,再生成 and %a, %b,只适用于非短路布尔运算,或两个操作数已经是安全且无副作用的值。对于语言级 && 和 ||,CFG 边才是默认正确表示。
测试被跳过的行为
只做真值表测试还不够。应让右操作数带有可见效果,例如递增计数器、写入轨迹或故意触发陷阱,并断言这些效果不会出现在短路路径上。还要测试嵌套组合、取反与值物化。关键性质不只是最终真假,而是产生结果的执行轨迹。
完整测试集至少要让每种边都执行一次:AND 在左侧失败、AND 进入右侧、OR 在左侧成功、OR 进入右侧、NOT 交换目的地,以及嵌套表达式到达每个叶子。对值形态,还应验证 merge 的前驱恰好对应 phi 中的 true/false 输入。即使普通真值表结果碰巧一致,这些结构断言也能抓住意外的急切求值。