“设编译阶段 $C$ 把源中间表示程序 $P$ 成功变换为目标程序 $Q=C(P)$。先用小步语义为两层语言定义完整行为,包括有限可观察事件迹 $t$ 后正常返回 $v$、以错误或 trap…”
形式陈述 ​
编译器中间表示(intermediate representation, IR)不是一份任意的内部数据结构,而是一门供编译阶段读写的中间语言。一个足以讨论正确性的 IR 可写成三元组
其中
一个编译过程可以拥有多层 IR。前端 IR 保留词法作用域、异常或高阶构造,中端 IR 强调显式控制流与数据依赖,后端 IR 则暴露寄存器类别、寻址模式与调用约定。层次的区分来自不变量和语义接口,而不只是文件后缀;若两个阶段共享同一节点类却采用不同的未定义行为或求值顺序,它们已经不是同一语义对象。
直觉
源程序像带墙壁和房间名称的建筑图,机器指令像施工后的管线;IR 是工程中间的结构图。它会舍弃某些源级细节,也会把原先隐含的事实显式化。例如表达式 “a+b*c” 在树形 IR 中保留嵌套,在低层 IR 中可拆成两个带临时量的指令;条件语句则常拆成比较、条件跳转和若干基本块。改变画法本身不保证程序正确,关键是每次重画都能说明可观察行为如何对应。
IR 设计的核心是为变换提供稳定契约。语法决定变换能重排哪些节点,控制流决定到达关系和合流点,语义决定溢出、陷阱、内存访问与外部调用是否可观察。若只给语法而没有语义,“把加法结合律用于整数加法”无法判断在有符号溢出会陷阱的语言中是否合法;若只给指令语义而没有良构 CFG,又无法排除跳到块中部或传错块参数。
例子与边界
考虑源片段 “若 p 则 x:=a+1,否则 x:=a-1;返回 x”。一个块式 IR 可含入口块
边界首先出现在内存和副作用。把两次读取同一地址合并,只有在中间没有可能写该地址、该地址不是易失设备寄存器且语言允许时才成立。IR 若以单一 “load” 节点掩盖 volatile、原子序和可能陷阱,优化证明会建立在错误接口上。其次,调试位置、栈展开信息与资源消耗是否属于观察,也必须由编译契约决定;它们有时是元数据,有时直接影响可见行为。
IR 也不等于某一种具体形态。三地址码、栈式字节码、续延表示和“节点之海”图式都可以成为 IR;能打印成文本不使它成为“三地址”,处在编译器内部也不自动使任意缓存成为一门 IR。判断标准是它是否具有明确的合法程序集合与阶段间语义责任。
推论与应用
把 IR 明确为语言后,每个 pass 都可写成部分函数
多层 IR 还允许逐层选择证明粒度:高层保留结构便于证明类型与异常,SSA 层暴露定义—使用关系,低层暴露机器约束。层越低并不天然越精确;它只是显式化另一组事实,同时可能遗忘源级名称。可靠编译依赖每次遗忘都有一份可陈述、可复核的对应,而非依赖 IR 名称带来的直觉。
参考资料
- Steven S. Muchnick, Advanced Compiler Design and Implementation, Morgan Kaufmann, 1997, Chapters 4–5.
- Keith D. Cooper and Linda Torczon, Engineering a Compiler, 3rd ed., Morgan Kaufmann, 2023, Chapter 4.
- Andrew W. Appel, Modern Compiler Implementation in ML, Cambridge University Press, 1998, Chapters 7–9.