编译原理
把 typed AST 翻译成 IR Tree:ESEQ / CJUMP / CALL 等节点的设计,以及前端友好的中间表示如何为后续优化与代码生成铺路。
Chapter 6 把运行时的样子抽象好了——F_access 告诉你"变量在 frame 还是 reg",Tr_level 帮你管嵌套 scope。但 typed AST 到这里还没有变成可执行的东西。Chapter 7 接着问:
怎么把 typed AST 真正翻译成机器无关的 IR?
答案是 IR Tree + Translate 模块——把每个 Tiger 语法结构(变量、数组、条件、循环、函数调用)映射成一棵小小的 Tree。
Chapter 7 的三个主题:
直接把 AST 翻成目标机器代码行不行?行,但糟糕:
┌────────┐ ┌────────┐ ┌────────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Tiger AST│ │ C AST │ │ Java AST│ │Tiger│ │ C │ │Java │
└───┬────┘ └───┬────┘ └───┬────┘ └──┬──┘ └──┬──┘ └──┬──┘
↓ × M 种机器 ↓ ↓ ↘ ↓ ↙
N×M 个编译器 IR ← 前端都落到这里
↙ ↓ ↘
MIPS x86 ARM ← 后端各自翻译
IR 的两个核心特性:
一个好的 IR:前端好产出、后端好翻译、每个构件语义极简——这样优化变换写起来才干净。
最基本的指令形式:
x = y op z
一条指令四个字段:一个操作符 + 三个地址(源 1、源 2、目的)。
地址可以是:
a, x)3)t1)约束:右侧最多一个运算符。复杂表达式要用临时量一步步拆开。
2*a + (b-3)t1 = 2 * a
t2 = b - 3
t3 = t1 + t2
read x read x
if 0 < x then t1 = x > 0
fact := 1; if_false t1 goto L1
repeat fact = 1
fact := fact * x; label L2
x := x − 1 t2 = fact * x
until x = 0; fact = t2
write fact t3 = x − 1
end x = t3
t4 = x == 0
if_false t4 goto L2
write fact
label L1
halt
_:fact = 1 → (asn, 1, fact, _)Three-address code 没有标准形式,不同语言可能要扩展(SSA 是其中著名变体)。
Tiger 用的不是三地址码,而是表达式树。
每条 IR 只描述一件极简的事。上层复杂的语法结构 → 一组 IR;一组 IR → 一条或多条真实机器指令。
T_exp:有返回值的表达式| 节点 | 含义 |
|---|---|
CONST(i) | 整型常量 i |
NAME(n) | 符号常量 n(汇编 label) |
TEMP(t) | 临时量 t(虚拟寄存器) |
BINOP(o, e1, e2) | 二元运算(PLUS/MINUS/MUL/DIV/AND/OR/XOR/LSHIFT/RSHIFT/ARSHIFT) |
MEM(e) | 从地址 e 处取 wordSize 字节;作为 MOVE 左孩子时表示 store,其他地方表示 fetch |
CALL(f, l) | 函数调用,参数从左到右求值 |
ESEQ(s, e) | 先执行语句 s(副作用),再把 e 的值作为结果 |
T_stm:只做副作用、没返回值的语句| 节点 | 含义 |
|---|---|
MOVE(TEMP t, e) | 把 e 的值搬到寄存器 t |
MOVE(MEM(e1), e2) | 把 e2 的值存到地址 e1 |
EXP(e) | 求值 e 丢弃结果(只要副作用) |
JUMP(e, labs) | 跳到 e;可以是 NAME(lab) 也可以是算出来的地址 |
CJUMP(o, e1, e2, t, f) | 比较 e1 op e2,真跳 t,假跳 f |
SEQ(s1, s2) | 先 s1 再 s2 |
LABEL(n) | 定义当前地址为 n |
NAME(n)vsLABEL(n):NAME 是使用一个符号,LABEL 是定义一个符号。
一个 Tiger 表达式翻译出来可能长三种样子,Translate 层用 Tr_exp 统一表达:
| 味道 | 意思 | 载体 |
|---|---|---|
| Ex | "有返回值的表达式" | T_exp |
| Nx | "没有返回值,只有副作用" | T_stm(procedure、while) |
| Cx | "条件"——能跳 true-label / false-label | T_stm + 两条 patch list |
struct Cx { patchList trues; patchList falses; T_stm stm; };
struct Tr_exp_ {
enum { Tr_ex, Tr_nx, Tr_cx } kind;
union {
T_exp ex;
T_stm nx;
struct Cx cx;
} u;
};
Tr_*是 Translate 模块的包装,T_*是底层 Tree 模块的 IR 节点。
翻译 a > b | c < d 时,还不知道 true / false 要跳到哪里——那是外层 if / while 才会告诉你的。所以:
doPatch(list, label) 一次性回填void doPatch(patchList tList, Temp_label label) {
for ( ; tList; tList = tList->tail)
*(tList->head) = label;
}在不同上下文需要把 Tr_exp 转成指定形式:
static T_exp unEx(Tr_exp e); // 要一个值
static T_stm unNx(Tr_exp e); // 要一个语句
static struct Cx unCx(Tr_exp e); // 要一个条件unEx 最重要 ——Cx 转 Ex 的标准展开:
MOVE(TEMP r, 1) // 预置 r = 1
<Cx.stm> // 跑条件,true → t, false → f
LABEL(f)
MOVE(TEMP r, 0) // false 分支改成 0
LABEL(t)
TEMP(r) // 最后值
对应代码:
Temp_temp r = Temp_newtemp();
Temp_label t = Temp_newlabel(), f = Temp_newlabel();
doPatch(e->u.cx.trues, t);
doPatch(e->u.cx.falses, f);
return T_Eseq(T_Move(T_Temp(r), T_Const(1)),
T_Eseq(e->u.cx.stm,
T_Eseq(T_Label(f),
T_Eseq(T_Move(T_Temp(r), T_Const(0)),
T_Eseq(T_Label(t), T_Temp(r))))));一个在当前 frame 的局部变量 v,在 offset k 处:
MEM(+(TEMP fp, CONST k))
MEM
│
+
╱ ╲
TEMP fp CONST k
TEMP fp:frame pointer 寄存器k:v 在 frame 里的偏移Semant 不能直接碰 Tree 或 Frame 模块,要通过 Translate:
Tr_exp Tr_simpleVar(Tr_access, Tr_level);Semant 递给 Translate:变量的 Tr_access + 当前使用点所在的 Tr_level。
/* frame.h */
Temp_temp F_FP(void);
extern const int F_wordSize;
T_exp F_Exp(F_access acc, T_exp framePtr);F_Exp 把 F_access 翻成访问树:
InFrame(k) → MEM(+(TEMP FP, CONST k))InReg(t832) → TEMP t832如果变量在外层 frame,要沿 static link 往上爬。爬 n 层:把 TEMP FP 换成 MEM(+(TEMP FP, CONST slOff)) 嵌套 n 次。
不同语言对数组变量语义不同:
| 语言 | a = b(数组) | 说明 |
|---|---|---|
| Pascal | 复制整个数组 | array 变量就代表内容 |
| C | 非法 | 数组名是常量指针 |
| Tiger / Java / ML | 指针赋值 | 只改指向,不拷贝元素 |
Tiger 的数组和 record 都是指向堆对象的指针(scalar):
let type intArray = array of int
var a := intArray[12] of 0
var b := intArray[12] of 7
in a := b end
(* a 和 b 指向同一个"12 个 7",原来的"12 个 0"被扔了 *)L-value = 能出现在赋值左边的表达式(有位置,可写入):x, y.p, a[i+2]。
R-value = 只能出现在赋值右边:a + 3, f(x)。
为了支持结构体赋值,T_Mem 多一个 size 参数:
T_exp T_Mem(T_exp, int size);Mem(+(TEMP fp, CONST k), S)——S 是这个对象的字节数。
Tiger 里数组和记录都是指针(一个字长,比如 4 字节),MEM(e) 默认取 wordSize 就够了。但 Pascal 里数组变量就是数组本身:
var a, b: array[1..100] of integer;
a := b; (* 要把 100 个整数全部拷过去,搬 400 字节! *)普通的 MEM(e) 只取一个字,不够用。所以给 MEM 加 size,让 IR 知道要搬多少字节。
Tiger 基本用不到这个扩展——所有变量都是 scalar,size 永远等于 wordSize。这个设计是为 Pascal/C 这类有 structured l-value 的语言准备的。
数组:
address(a[i]) = (i − l) × s + a
a = 数组首址l = 下界(Tiger 是 0)s = 元素大小记录:
address(a.f) = a + offset(f)
否则 a[i] 就得不到 a 的地址去做算术。正确做法:
把 l-value 翻成表示其地址的 Tree,只有当它出现在需要 r-value 的上下文时,才外加一个
MEM。
为什么? 假设数组 a 在 frame 偏移 k 处,你要翻译 a[i]:
如果一上来就翻成 MEM(+(TEMP fp, CONST k))——这已经是取出来的值了,你没法在一个值上做地址运算(算 a + i × W)。
正确做法是先只保留地址 +(TEMP fp, CONST k),自由地做算术算出 a[i] 的地址,等到最后确定上下文再决定:
比喻:地址 = 门牌号("长安街 10 号"),MEM = 进门取东西。你要算"第 10 号往后第 3 家",得先有门牌号才能 +3。一上来就进门取了包裹,就没法在门牌号上做算术了。
a[i] 的 IR Tree因为 Tiger 的 a 本身是指针(放在 frame 里),要先 MEM 把它取出来:
MEM
│
+
├── MEM(e) ← e 是 a 的地址,MEM(e) 取出指针
└── BINOP
├── MUL
├── i
└── CONST W ← W 是 word size
MEM(+(MEM(e), BINOP(MUL, i, CONST W)))
Tiger 里 MEM 既代表 fetch 又代表 store:看它是不是 MOVE 的左孩子。所以 l-value 可以先带着 MEM,等到上下文确定再解释。
Tiger 每个整型算术运算都直接对应一个 Tree 节点。没有一元运算符:
-n → 0 - n(减法)not n → n xor -1(XOR 全 1)-0.0 没法用减法表示(0 - (-0.0) = +0.0,不对)结论:Tree 语言对一元运算不友好,但 Tiger 也用不上。
x < 5 翻成 Cx:
stm = CJUMP(LT, x, CONST 5, NULLt, NULLf)
trues = {t} ← 指向 NULLt 的指针,待回填
falses = {f} ← 指向 NULLf 的指针,待回填
a > b | c < d(短路或)SEQ
├── CJUMP(GT, a, b, NULLt, z) ← a > b 真 → 跳 NULLt(外层填),假 → 跳 z
└── SEQ
├── LABEL z
└── CJUMP(LT, c, d, NULLt, NULLf)
patch lists:
trues = [&cjump1.true, &cjump2.true] ← 外层两个 true 目的地都要回填
falses = [&cjump2.false]
if e1 then e2 else e3最直接的翻法:
unCx(e1)
LABEL t
r ← unEx(e2)
JUMP join
LABEL f
r ← unEx(e3)
JUMP join
LABEL join
TEMP r
优化:
if x<5 then a>b else 0)——直接用 unEx 会产生一堆 jump/label 缠起来,要特判if x < 5 then a > b else 0等价于 if x<5 & a>b then 1 else 0:
SEQ(
s1(z, f), ← x<5:真跳 z,假跳 f
SEQ(LABEL z,
s2(t, f))) ← a>b:真跳 t,假跳 f
通用模板:
test:
if not(condition) goto done
body
goto test
done:
在 body 里的 break(不嵌在更内层 while 里)翻成 JUMP done——问题是怎么知道 done 的 label?
给
transExp加一个形参break,传入"最近一层外层循环的 done label"。嵌套时内层 while 覆盖这个参数,退出后恢复。
朴素想法:重写成 let/while:
for i := lo to hi do body
≡
let var i := lo
var limit := hi
in while i <= limit do (body; i := i + 1) endbug:当 limit = maxint 时,i + 1 溢出。
修正模板:先判空,再把自增放到比较前(更准确:比较用 >= 提前检测,然后才加):
if lo > hi goto done
i := lo
limit := hi
test:
body
if i >= limit goto done
i := i + 1
goto test
done:
f(a1, ..., an) → CALL(NAME lf, [sl, e1, e2, ..., en])
lf:f 的 labelsl:static link——作为隐式第一参传入,支撑嵌套作用域static link 的值 = 按 Chapter 6 规则算出来的 caller 传给 callee 的 frame 指针。
transDec 是 side-effect:它不返回 IR(或只返回 no-op),但会修改 frame 数据结构和产出 fragment。
Tr_allocLocal)let var x := 5 in body end等价于:在 body 前加一条 MOVE(x_access, 5) 的 IR。
transDec 返回 EXP(CONST 0)(no-op)一个 Tiger 函数 → 一段"汇编语言片段",三部分:
_global name
name: ... ← prologue
assembly code for body ← body(translated expression)
... ← epilogue
return 指令:跳回 return address每个函数产出一个 fragment(F_frag),两种类型:
struct F_frag_ {
enum { F_stringFrag, F_procFrag } kind;
union {
struct { Temp_label label; string str; } stringg;
struct { T_stm body; F_frame frame; } proc;
} u;
};void Tr_procEntryExit(Tr_level level, Tr_exp body,
Tr_accessList formals);
F_fragList Tr_getResult(void); // 拿到所有 fragment每翻译完一个函数就调 Tr_procEntryExit 入队,全部翻完后 Tr_getResult 取列表传给下游。
N+M 而不是 N×M;优化写一次就能惠及所有前端/后端。
Tree 表达更自然,每个节点只描述极简的一件事——fetch / store / add / jump。
不同上下文要的"形状"不一样,unEx / unNx / unCx 按需转换;Cx 用 patchList + backpatching 延迟绑定 true/false label。
把 l-value 翻成地址表达式,等上下文要 r-value 时再外加 MEM。Tiger 里 MEM 双用(fetch/store)靠位置区分。
Translate 层根据 caller / callee 层次关系算出 static link 值,作为隐式第一参传进去。
if / while / for / break 都能套标准的 label + CJUMP + JUMP 模式;for 要小心溢出。
每个函数 → 一个 procFrag(frame + IR body);字符串常量 → stringFrag。Translate 阶段最终交付给下游的就是这个 fragment list。
frame 的"进入"和"退出"动作并不在函数体 IR 里——由后续阶段根据 frame 信息合成,这里只是搭好框架。
| Chapter | 问 | 答 |
|---|---|---|
| 5 | AST 语义对吗? | Symbol table + type checking |
| 6 | 运行时变量放哪? | Activation record + Frame / Temp 抽象 |
| 7 | typed AST → IR 怎么翻? | IR Tree + Translate (Ex/Nx/Cx) |
| 8 | IR 怎么整理? | Canonicalization (basic blocks, traces) |
| 9 | IR → 目标机器? | Instruction selection |
到这里,前端算是真正落地了:
source → tokens → CST → AST → typed AST
→ typed AST + frame info → IR fragments ← 你在这里
→ canonical IR → machine code
下一章 Chapter 8 要把这些 IR tree 规整成 basic block + trace 的形式,为指令选择做准备。
按以下顺序推进,效率最高:
F_access、Tr_level、static link——Ch7 整个前半部分就是拿这些接口实际产 IRT_exp 和 T_stm 的表格背下来——特别是 MEM 的双重含义、ESEQ、CJUMP 这三个a > b | c < d 的树unEx 的展开(MOVE 1; Cx; LABEL f; MOVE 0; LABEL t; TEMP r)doPatch 和 joinPatch 两个操作按简单到复杂:
MEM(+(TEMP fp, CONST k))a[i]:两层 MEM——取指针 + 下标偏移if-then-else:三 label 模板,特判 e2/e3 是 Nx 或 CxtransDec 是 side-effect,不产值挑一个含嵌套函数 + if + while + 数组下标的 Tiger 片段,从 AST 翻到 IR Tree——能手画出来,这章就真掌握了。
i<=limit then i+=1,而是先 body 再判再加Written by
Comments