编译器介绍 --- 例子篇

编译器介绍 — 例子篇

系列文章

上一篇文章讲了那么多的抽象原理,这篇文章就让我们一起用一个例子看一下,一个程序从高级语言到汇编语言,中间到底发生了什么事情吧。

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仿真器)来执行观察结果。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值