11.1 AST 降低到 IR
抽象语法树(Abstract Syntax Tree,AST)适合表达源程序的结构;中间表示(Intermediate Representation,IR)适合执行、分析与变换。降低(lowering) 是二者之间保持语义的桥:它把高级语言构造拆成行为更明确的小操作。
以 score += bonus * 2 为例,AST 可以用一个有意义的 += 节点表示它;三地址 IR 则要显式写出读取 score、乘法、加法与写回。如果语言承诺溢出检查或精确源码位置,降低时还必须保留这些事实。
降低通常发生在名字解析和类型检查之后。因此,降低器拿到的 AST 不应还是一棵需要猜测含义的语法树:score 已经绑定到某个具体声明,bonus * 2 已经拥有确定类型,必要的隐式转换也已经记录下来。降低器无需再猜 + 表示整数加法、浮点加法还是字符串连接;它负责把前一阶段已经做出的语义决定翻译为合法 IR。
可以把这个阶段边界概括为:
已检查 AST + 符号/类型事实 + 语言语义规则
↓ lowering
带类型的指令 + 基本块 + 源码元数据输出比 AST 冗长,是因为隐藏工作被显式化了。这种冗长并非缺点:后续优化可以直接观察每一次 load、check、call 和 branch,而不必从高级节点中重新猜出它们。
降低结构,但不降低语义
一条降低规则不只是树重写,它还是一份契约:
- 每个源子表达式只按语言要求的次数求值;
- 保持规定的求值顺序与可观察副作用;
- 保持数组越界、算术溢出等异常行为;
- 保留足够的源码元数据,服务诊断与调试;
- 生成类型、基本块和终结指令都合法的 IR。
例如降低 items[next()] 时,不能因为边界检查和地址计算都需要下标,就调用两次 next()。正确做法是只计算一次,并复用临时值:
%i = call @next()
%len = array.len @items
guard.in_bounds %i, %len
%addr = element_addr @items, %i
%value = load %addr注意这段 IR 的依赖顺序:%i 只定义一次,同时供边界检查与地址计算使用;检查必须发生在 load 之前。如果 next() 抛出异常,后面的检查和读取都不会发生;如果检查失败,也不能读取内存。这些顺序不是实现细节,而是源程序行为的一部分。
复合赋值更微妙,因为左侧既表示一个位置,也提供旧值。降低 items[next()] += read() 时,应只计算一次数组元素地址,读取旧值,再调用 read()、相加并写回:
%i = call @next()
%addr = checked_element_addr @items, %i
%old = load %addr
%rhs = call @read()
%new = add %old, %rhs
store %new, %addr如果最后写回前再次调用 next(),就可能修改另一个元素;如果在 read() 之后重新 load,且 read() 能修改 items,语义也会变化。因此,一条可靠的降低规则必须写明“何时捕获地址”和“何时读取旧值”。
可维护的降低架构
编译器通常为表达式提供 lowerExpr(node) -> Value,为语句提供 lowerStmt(node)。降低上下文保存当前基本块、新临时变量生成器、符号到存储位置的映射,以及源码位置元数据。这样,机械的 IR 构造与语言语义策略可以分开维护。
IR builder 应在创建指令时立即维护局部不变量。例如,整数 add 的两个操作数必须具有兼容位宽;load 必须接收指向预期元素类型的地址;分支目标必须属于当前函数。越早拒绝错误,越容易定位到具体降低规则,而不是让畸形 IR 流入多个后续阶段才神秘崩溃。
更稳妥的实现方式是:为每种 AST 节点写清局部语义契约;通过会拒绝非法操作的 IR builder 发射指令;再用小程序比较源解释器与 IR 解释器的执行轨迹。降低正确的标准是可观察行为相同,而不是生成文本“看起来像 IR”。
测试也不能只覆盖普通数值。每条降低规则至少应包含边界值、带可见副作用的子表达式,以及会抛出异常或触发 trap 的子表达式。降低结束后还应运行 IR verifier。差分执行检查动态语义,verifier 则检查类型一致、跳转目标存在、每个可达块只有一个终结指令等结构性质;二者共同守住 AST 到 IR 的边界。