6 Intermediate Representation¶
中间表示 (IR) 是编译器前端和后端之间的桥梁。前端将源代码转为 IR,后端将 IR 转为目标代码。引入 IR 的核心动机有二:
- 模块化——前后端独立开发演进;
- 可移植性——一种语言对 \(M\) 种后端只需 \(N\) 个前端 + \(M\) 个后端,而非 \(N \times M\) 个组合。
6.1 IR Overview¶
IR 按抽象层次分为三级:
| 层次 | 特点 | 示例 |
|---|---|---|
| High-level | 贴近源语言,保留类型/结构信息 | AST |
| Middle-level | 平衡分析效率和机器特征 | 三地址码 |
| Low-level | 贴近目标机器,显式寄存器/内存 | LLVM IR, Register Transfer Language |
按结构特征分为三类:
- 结构化 (Structural):树 (Tree)、DAG。保留表达式嵌套关系,适合局部分析。
- 线性 (Linear):三地址码
x = y op z,指令序列化,适合全局分析和优化。 - 混合 (Hybrid):基本块 + CFG。块内是线性指令,块间用控制流图连接。主流编译器(LLVM、GCC)采用此方式。
三地址码 (Three-Address Code, TAC):每条指令最多三个地址(两个源、一个目标)。常见指令类型:
| 类型 | 形式 | 说明 |
|---|---|---|
| 赋值 | x = y op z |
二元运算 |
| 复制 | x = y |
直接复制 |
| 无条件跳转 | goto L |
|
| 条件跳转 | if x relop y goto L |
|
| 过程调用 | call f, n / param x |
|
| 索引赋值/读取 | x = y[i] / x[i] = y |
实现方式:四元组 (op, arg1, arg2, result) 使用临时变量名引用结果,适合优化和重排;三元组 (op, arg1, arg2) 用指令序号隐式引用结果,更紧凑但不便重排。
6.2 IR Tree Nodes¶
Tiger 编译器使用树形 IR,介于 AST 与汇编之间,保留树结构但包含内存/寄存器等底层细节。节点分两类:
表达式 (Exp) — 有返回值:
| 节点 | 含义 | 示例 |
|---|---|---|
CONST(i) |
整数常量 | CONST(42) |
NAME(n) |
符号标签(Label) | NAME(L1) |
TEMP(t) |
临时变量(虚拟寄存器) | TEMP(t42) |
BINOP(o, e1, e2) |
二元运算 | BINOP(PLUS, e1, e2) |
MEM(e) |
内存读取(解引用) | MEM(BINOP(PLUS, TEMP fp, CONST 8)) |
CALL(f, args) |
函数调用 | CALL(NAME g, [sl, arg1]) |
ESEQ(s, e) |
先执行语句 s(副作用),再返回表达式 e 的值 |
语句 (Stm) — 无返回值,只有副作用:
| 节点 | 含义 |
|---|---|
MOVE(dst, src) |
赋值到寄存器 TEMP(t) 或内存 MEM(e) |
EXP(e) |
计算表达式并丢弃结果(用于调用等纯副作用场景) |
JUMP(e, labs) |
无条件跳转到 e 指定的标签 |
CJUMP(o, e1, e2, t, f) |
条件跳转:若 e1 o e2 则跳 t,否则跳 f |
SEQ(s1, s2) |
顺序执行 s1 后 s2 |
LABEL(n) |
在此处定义标签 n |
MEM 是关键节点——作为子表达式时是读内存(右值),作为 MOVE 的目标时是写内存(左值)。
6.3 Translation to IR Trees¶
将 AST 翻译为 IR 树时,表达式根据出现位置有三种翻译上下文:
| 上下文 | 含义 | 产生 | 示例 |
|---|---|---|---|
| Ex | 产生值 | Exp |
a + b,函数返回值 |
| Nx | 无值(副作用) | Stm |
print(x),x = 1 作为语句 |
| Cx | 条件(跳转) | Stm + 两个标签 |
if/while 的条件 |
三种上下文之间需要互相转换,写翻译函数时命名约定为 exp2ex、exp2nx、exp2cx 等。
Variables¶
局部变量在栈帧中有固定偏移,通过 FP 寻址:
非局部变量(嵌套函数访问外层变量)沿静态链追踪:
x (深度差 2, 偏移 8) → MEM(BINOP(PLUS,
MEM(BINOP(PLUS,
MEM(BINOP(PLUS, TEMP fp, CONST sl_offset)), ← 跟 1 步
CONST sl_offset)), ← 跟 2 步
CONST 8)) ← 读变量
每多一层嵌套深度差,就多一层 MEM(BINOP(PLUS, ..., CONST sl_offset)) 解引用静态链。
Arrays & Records¶
Tiger 中数组和记录都是指针语义(变量存的是堆上对象的地址)。访问元素 a[i]:
MEM(BINOP(PLUS, MEM(a_base), BINOP(MUL, i, CONST word_size)))
└─读基址──┘ └─── 下标 × 字长 = 偏移 ───┘
└──────────── 基址 + 偏移 = 元素地址 ─────────┘
└── 读元素地址处的内容 ──┘
数组访问需要边界检查:若 i 越界则引发 subscript error,翻译为 CJUMP 包围的 ESEQ。
Control Flow¶
if-then-else:
CJUMP(cond, t_label, f_label) → SEQ(LABEL(t), then_body, JUMP(join))
→ SEQ(LABEL(f), else_body, LABEL(join))
短路求值 (Short-Circuit):
a & b:翻译为if a then b else false。先算a,若假直接跳false标签(不计算b),若真再算b。a | b:翻译为if a then true else b。先算a,若真直接跳true标签,若假再算b。
while 循环:
for 循环改写为 while,特殊处理:上限 hi 要先存入临时变量(防止循环体内修改上限),且 i+1 需防溢出(当 i = maxint 时 i+1 会溢出为负数,导致循环永不终止)。
break:直接翻译为 JUMP(done_label),跳转到最内层循环的结束标签。
Function Calls & Declarations¶
调用:CALL(NAME f, [static_link, arg1, arg2, ...])。静态链作为第一个隐式参数传递,使被调用函数能访问其静态父帧。
声明分为 Prologue 和 Epilogue:
- Prologue:
MOVE(TEMP sp, BINOP(MINUS, TEMP sp, CONST framesize))— 分配栈帧;保存 callee-save 寄存器;保存静态链。 - Epilogue:将返回值放入
rv(返回值寄存器);恢复 callee-save 寄存器;MOVE(TEMP sp, BINOP(PLUS, TEMP sp, CONST framesize))— 回收栈帧;JUMP到返回地址。
整个函数体实际上是一个 SEQ(prologue, SEQ(body, epilogue)) 的嵌套结构。