编译器介绍 — 例子篇
系列文章
文章目录
- 编译器介绍 --- 例子篇
- 1) 源程序
- 2) Lexer (source program -> Tokens)
- 3) Parser (tokens -> AST)
- 4) semantic analysis (AST -> AST)
- 5) translate (AST -> IR)
- 6) instruction selection (IR -> infinite registers MIPS)
- 7) liveness analyse (infinite registers MIPS -> igraph)
- 8) register allocation + emission (infinite registers MIPS + igraph -> final MIPS)
上一篇文章讲了那么多的抽象原理,这篇文章就让我们一起用一个例子看一下,一个程序从高级语言到汇编语言,中间到底发生了什么事情吧。
1) 源程序
为了简单起见 (也为了篇幅不要过长),我们用一个稍微简单一点的程序,包含了一个函数调用,和一个if语句。
/* a very simple sum program */
let
var x := 3
var y := 4
function sum (a : int, b : int) : int = a + b
in
if x > y then sum(x, 2) else sum(y, 3)
end
2) Lexer (source program -> Tokens)
将源程序转换为一系列的tokens,同时省略注释内容。
LET
VAR ID(x) ASSIGN INT(3)
VAR ID(y) ASSIGN INT(4)
FUNCTION ID(sum) LPAREN ID(a) COLON ID(int) COMMA ID(b) COLON ID(int) RPAREN
COLON ID(int) EQ ID(a) PLUS ID(b)
IN
IF ID(x) GT ID(y)
THEN ID(sum) LPAREN ID(x) COMMA INT(2) RPAREN
ELSE ID(sum) LPAREN ID(y) COMMA INT(3) RPAREN
END
EOF
这里我隐去了所有tokens的位置信息,并且尽可能的按照源程序的结构组织了一下输出。实际代码输出是每个token占一行,然后token后面跟着它出现的位置信息 (如: VAR 8)。可以看到整个程序被转换为了一系列的tokens,有的token会包含其他信息 (如: ID(x)),有的没有 (如: VAR)。
3) Parser (tokens -> AST)
将产生的tokens组合成不同的语句。
LetExp([
VarDec(x, false, NONE, IntExp(3)),
VarDec(y, false, NONE, IntExp(4)),
FunctionDec[
(sum, [(a, false, int), (b, false, int)], SOME(int),
OpExp(PlusOp, VarExp(SimpleVar(a)), VarExp(SimpleVar(b))))]
],
SeqExp[
IfExp(
OpExp(GtOp, VarExp(SimpleVar(x)), VarExp(SimpleVar(y))),
CallExp(sum, [VarExp(SimpleVar(x)), IntExp(2)]),
CallExp(sum, [VarExp(SimpleVar(y)), IntExp(3)]))
])
可以看到lexer输出的一系列tokens已经根据语义被组合成了不同的expression,假如我们的程序有语法错误,那么在这个阶段编译器就会报出错误信息并停止执行。
4) semantic analysis (AST -> AST)
该阶段主要是对前一阶段产生的AST进行处理和分析,由于我们的程序较为简单,没有任何变量"逃逸",所以产生的AST和上一阶段的一模一样,这里就不再重复了。
针对语义错误,由于我们的程序并没有任何错误,所以不会有错误信息的输出。考虑以下这个程序,有一个错误的assign语句:
let
var x : int := "hello"
in
()
end
此时我们会得到这样的一个错误信息:type mismatch expected: int, but was: string
(当然这个错误信息具体是什么取决于你的实现)
5) translate (AST -> IR)
上一阶段检查完确保程序没有静态错误之后,我们该阶段就要将和源语法相关的AST转换为无关的IR,因为我们做的是intraprocedure的分析,所以这里每一个函数都会产生一个单独的fragment (包含该函数的body expression和frame相关的信息,如名字,有几个变量,每个变量的位置)。如下,为最后产生的IR,L0就是我们的sum函数。
PROC fragment{
body=
MOVE(
TEMP t100,
ESEQ(
SEQ(
MOVE(TEMP t113, CONST 3),
MOVE(TEMP t114, CONST 4)),
ESEQ(
SEQ(
CJUMP(GT, TEMP t113, TEMP t114, L1, L2),
SEQ(
LABEL L1,
SEQ(
MOVE(TEMP t117, CALL(NAME L0, TEMP t101, TEMP t113, CONST 2)),
SEQ(
JUMP(NAME L3),
SEQ(
LABEL L2,
SEQ(
MOVE(TEMP t117, CALL(NAME L0, TEMP t101, TEMP t114, CONST 3)),
LABEL L3)))))),
TEMP t117)))
frame={name=main, formals=[InFrame(0)], index=1}
}
PROC fragment{
body = MOVE(TEMP t100, BINOP(PLUS, TEMP t115, TEMP t116))
frame={name=L0, formals=[InFrame(0), InReg(t115), InReg(t116)], index=1}
}
由于上面的IR含有很多实现的细节,这里我们将其去掉之后,只保留核心的部分再看一下。可以看到这里已经没有原来程序的语法了,赋值语句全部变成了MOVE指令,函数调用则用CALL来表示等等。
main:
MOVE(TEMP t113, CONST 3) # x = t113
MOVE(TEMP t114, CONST 4)) # y = t114
CJUMP(GT, TEMP t113, TEMP t114, L1, L2)
LABEL L1 # then case
MOVE(TEMP t117, CALL(NAME L0, TEMP t101, TEMP t113, CONST 2))
LABEL L2 # else case
MOVE(TEMP t117, CALL(NAME L0, TEMP t101, TEMP t114, CONST 3))
LABEL L3 # finish
MOVE(TEMP t100, TEMP t117) # return value
L0:
MOVE(TEMP t100, BINOP(PLUS, TEMP t115, TEMP t116))
6) instruction selection (IR -> infinite registers MIPS)
转换为IR之后,下一步就是将IR翻译成汇编语言,以下就是翻译结果。可以看到我们并没有使用实际的机器寄存器 ($t0-$t9等),而是使用的t143, t144等,并且可以很明显的看出,有很多的move指令是多余的,但是我们现阶段不需要考虑这两个问题,我们只需要保证产生的程序逻辑正确即可,这些问题会在后面解决掉。
main:
PROCEDURE main
L5:
sw $a0, 0($fp)
addi t148, $zero, 3
move t143, t148 # x = t143
addi t149, $zero, 4
move t144, t149 # y = t144
bgt t143, t144, L1
L2: # else case
la t151, L0
move $a0, $fp # static link
move $a1, t144
addi t152, $zero, 3
move $a2, t152
jalr t151 # sum (y + 3)
move t150, $v0
move t147, t150
L3:
move $v0, t147 # return value
j L4
L1: # then case
la t154, L0
move $a0, $fp # static link
move $a1, t143
addi t155, $zero, 2
move $a2, t155
jalr t154 # sum (x + 2)
move t153, $v0
move t147, t153
j L3
L4:
jr $ra
END main
L0:
PROCEDURE L0
L7:
sw $a0, 0($fp)
move t145, $a1
move t146, $a2
add t156, t145, t146
move $v0, t156
j L6
L6:
jr $ra
END L0
7) liveness analyse (infinite registers MIPS -> igraph)
liveness这个阶段主要是为了下一个阶段服务,只会产生一个interference graph给下一阶段使用,但不会对程序的输出做任何的修改。由于main函数的igraph打印出来内容较多,这里只展示一下sum这个函数对应的igraph,读者有个大致的了解即可。
Node: $v0 -> Adjacent: $zero, $sp, $fp, $ra
Node: $a0 -> Adjacent:
Node: $a1 -> Adjacent:
Node: $a2 -> Adjacent: t134
Node: $zero -> Adjacent: $v0, t134, t135, t145
Node: $sp -> Adjacent: $v0, t134, t135, t145
Node: $fp -> Adjacent: $v0, t134, t135, t145
Node: $ra -> Adjacent: $v0, t134, t135, t145
Node: t134 -> Adjacent: $a2, $zero, $sp, $fp, $ra, t135
Node: t135 -> Adjacent: $zero, $sp, $fp, $ra, t134
Node: t145 -> Adjacent: $zero, $sp, $fp, $ra
8) register allocation + emission (infinite registers MIPS + igraph -> final MIPS)
有个liveness信息之后,我们就可以对每个函数进行寄存器分配,并生成最终的代码。注意直到这个阶段我们才可以最终确定每个函数的frame上面到底需要保存多少个变量(要看寄存器分配有没有变量溢出),从而才能计算出准确的frame size,所以也是在这个阶段,我们需要给每个函数加上prolog和epilog (calling convention相关的内容)。
.ent tig_main
tig_main:
# prolog start
sw $fp, 0($sp) # save old FP
move $fp, $sp # update FP
addi $sp, $sp, -24 # allocate frame
sw $ra, -8($fp) # store the return address
# prolog end
L5:
sw $a0, -4($fp)
addi $t0, $zero, 3
addi $a1, $zero, 4
bgt $t0, $a1, L1
L2:
la $t0, L0
move $a0, $fp
addi $a2, $zero, 3
jalr $t0
L3:
j L4
L1:
la $t1, L0
move $a0, $fp
move $a1, $t0
addi $a2, $zero, 2
jalr $t1
j L3
L4:
# epilog start
lw $ra, -8($fp)
move $sp, $fp # deallocate frame
lw $fp, 0($sp) # restore old FP
jr $ra
# epilog end
.end tig_main
.ent L0
L0:
sw $fp, 0($sp) # save old FP
move $fp, $sp # update FP
addi $sp, $sp, -8 # allocate frame
L7:
sw $a0, -4($fp)
add $v0, $a1, $a2
j L6AA
L6:
move $sp, $fp # deallocate frame
lw $fp, 0($sp) # restore old FP
jr $ra
.end L0
可以看到,此时的程序,所有的寄存器使用的都是实际机器寄存器,而不是之前的那种临时寄存器,同时已经基本没有了MOVE指令 (除了个别必须的)。
到这一步,我们的程序就已经完成了从高级语言到汇编语言的"华丽"变身!
拓展:实际编译器中,下一步就是将我们产生的汇编程序,连同一些runtime library一起输入给assembler进行编译连接最终产生可执行文件 (机器码);简单起见,我们可以直接将runtime library和这个文件合并成一个.s文件,然后利用spim (一个MIPS仿真器)来执行观察结果。