跳转至

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) 顺序执行 s1s2
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 的条件

三种上下文之间需要互相转换,写翻译函数时命名约定为 exp2exexp2nxexp2cx 等。

Variables

局部变量在栈帧中有固定偏移,通过 FP 寻址:

x (深度 0, 偏移 8)  →  MEM(BINOP(PLUS, TEMP fp, CONST 8))

非局部变量(嵌套函数访问外层变量)沿静态链追踪:

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 循环

LABEL(test)
CJUMP(cond, body, done)
LABEL(body)
  ... loop body ...
  JUMP(test)
LABEL(done)

for 循环改写为 while,特殊处理:上限 hi 要先存入临时变量(防止循环体内修改上限),且 i+1 需防溢出(当 i = maxinti+1 会溢出为负数,导致循环永不终止)。

break:直接翻译为 JUMP(done_label),跳转到最内层循环的结束标签。

Function Calls & Declarations

调用CALL(NAME f, [static_link, arg1, arg2, ...])。静态链作为第一个隐式参数传递,使被调用函数能访问其静态父帧。

声明分为 Prologue 和 Epilogue:

  • PrologueMOVE(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)) 的嵌套结构。

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!