11.2 表达式翻译
表达式降低会把嵌套语法变成明确的求值计划。(a + b) * c 描述了要算什么;IR 还必须说明中间结果放在哪里、运算以什么顺序发生。
常见接口是 lowerExpr(e) -> Value:字面量返回常量,名字返回加载值或 SSA 名字,运算符先递归降低操作数,再发射一条新指令。
AST 已经表达了优先级,降低器不需要重新解析运算符。对于 (a + b) * c,乘法节点的左孩子就是加法节点。按后序遍历会自然得到:
%a = load @a
%b = load @b
%sum = add %a, %b
%c = load @c
%result = mul %sum, %c每个临时值都代表一项已经完成的计算。定义-使用(def-use)关系因此变得明确,后续遍就能精确地折叠常量、删除死计算或分配寄存器。
从树到临时值,再到共享图
直接降低 a * b + a * b 会发射两次乘法。若运算是纯的,且操作数中途没有改变,有向无环图(Directed Acyclic Graph,DAG)或公共子表达式消除可以共享结果:
%t1 = mul a, b
%t2 = add %t1, %t1这种共享并非总是合法。加载、函数调用、volatile 操作、陷阱和可变内存都可能让两个文本相同的表达式产生不同的可观察行为。降低阶段应先完整保留语义;只有证明充分时,优化阶段才共享计算。
例如 random() - random() 中两个调用的文本完全相同,但程序故意需要两个结果。再如 a[i] + (a[i] = 0) + a[i],两次外观相同的读取之间隔着一次写入。若把它们错误合并为同一个 DAG 节点,程序行为就会悄悄改变。
由类型决定具体运算
源码运算符 + 并不唯一对应某条 IR 指令,操作数类型与语言规则共同选择实现:
| 已检查的源码形式 | 可能的 IR 配方 |
|---|---|
i32 + i32 | 整数 add |
f64 + f64 | 浮点 fadd |
String + String | 调用运行时 string.concat |
i32 + f64 | 先扩展整数,再执行 fadd |
隐式转换必须出现在明确位置。如果 count + 0.5 的类型是 f64,降低器可先发射 %wide = sitofp %count to f64,再执行 fadd %wide, 0.5。显式记录转换既保留类型检查器的决定,也让后续遍能够分析精度与成本。
求值顺序也是含义
在 f() + g() 中,加法虽然满足交换律,但两个调用可能打印、修改状态、抛出异常或读取同一片内存。若语言规定从左到右求值,IR 就必须先调用 f。即使语言没有规定顺序,编译器也要在生成的控制流中选择一种合法、内部一致的顺序。
这也限制了看似显然的代数化简。若计算 x 可能抛出异常,就不能轻易把 x * 0 直接替换为 0;若 x 与 y 带有有序副作用,也不能随意交换 x + y。优化器必须同时考虑结果值和产生该值所必需的事件。
值与位置是两种需求
表达式降低器往往需要两个相关接口:
lowerValue(e)计算e的值;
lowerPlace(e)计算e对应的可写位置。
items[i] = value 左侧需要地址,而 x = items[i] 右侧需要加载后的值。把二者区分开,可以避免多余加载,也能让赋值、复合更新、字段访问与解引用的降低更清晰。
对于普通变量,lowerPlace(x) 可以返回绑定到 x 的栈槽或全局地址,lowerValue(x) 再从这个位置加载。对于 record.field,位置降低会计算对象基址加字段偏移;对于 *pointer,在完成空指针或安全检查后,指针值本身就是目标位置。
并非所有表达式都有位置。a + b 的临时结果可以作为值使用,却通常不能出现在赋值左侧。若降低器在类型层面编码 Value 与 Place 的区别,而不只是靠注释提醒,就能从结构上阻止一大类非法 store。