实验3 中间代码生成
help- assignment 代码已完成
除了语法树,编译器里另一个核心数据结构就是中间代码(Intermediate Representation,IR)。中间代码是编译器从源语言到目标语言之间采用的一种过渡性质的代码形式,往往介于语法树和汇编代码之间,其表示独立于机器,易于分析和翻译成汇编代码。你可能会有疑问:为什么编译
器不能把输入程序变成语法树,然后直接翻译成目标代码,而要额外引入中间代码生成呢?
答案当然是可以的。但中间代码的引入有两个好处。一方面,中间代码将编译器自然地分为前端和后端两个部分。当我们需要改变编译器的源语言或目标语言时,如果采用了中间代码,我们只需要替换原编译器的前端或后端,而不需要重写整个编译器。著名的LLVM 框架就是一个后端平台的例子,我们可以将C++,Rust或者自定义的其他语言统一转为LLVM中间代码,再由它负责优化和目标代码生成,这极大的简化了新的编程语言开发难度。另一方面,中间代码既不像语法树那样抽象,也不像汇编代码那样底层,因此在某些分析和优化任务会更加方便。
而在本实验中,我们将会定义一种简单的中间代码结构,然后将你在实验2中实现的语法树转换成对应的中间代码。
中间代码定义
我们先看一个阶乘函数的例子,直观感受一下本实验要求实现的中间代码长什么样子:
我们有如下说明:
·中间代码是大小写敏感的,如 GOTO 和goto 是不同的,所以请大写所有关键字。
·x,y,z都是临时变量或者标识符,而#k则是整型常量。在中间代码中同一个函数内不存在作用域的概念,因此你要避免重复命名不同的变量。
·PARAM语句在每个函数开头使用,对于函数中形参的数目和名称进行声明。例如,若一个函数func有两个形参a、b,则该函数的函数体内前两条语句为:PARAM a,PARAMb.在调用一个函数之前,我们先使用ARG语句传入所有实参,随后使用CALL语句调用该函数并存储返回值。
以函数 func(x,y) 为例,如果我们需要依次传入两个实参x、y,并将返回值保存到临时变量t1中,则可分别表述为:ARGx、ARGy和t1= CALL func.在实验三中你不需要特殊处理main函数,即在main 函数最后你同样只需要写RETURN语句即可。
·当函数参数是数组时,ARG语句的参数为数组的地址,即以传引用的方式实现函数参数传递。
·解释器内置了两个函数来完成输入输出,即read 和write. read函数不需要参数,可以从控制台读入一个整型变量,而write函数则需要指明一个参数,将其值写到控制台上。调用这两个函数和调用其他函数没有任何区别。
·变量声明语句 DEC用于为一个函数体内的局部变量声明其所需要的空间,该空间的大小以字节为单位。这个语句是专门为数组变量这类需要开辟一段连续的内存空间所准备的。例如,如果我们需要声明一个长度为10的int类型数组a,则可以写成DEC a 40.
·生成的中间代码函数必须要有RETURN语句,即使函数返回值为空,也需要写成RETURN,不能省略。
·全局变量需要在函数定义前先进行定义。
语法制导的代码生成
讨论完了中间代码的定义,下一步我们就要把经过语义检查和推断的语法树转换成中间代码。从语法树生成中间代码并没有你想象的那么困难,和打印语法树一样,我们要做的就是遍历语法树的节点,然后根据节点的类型生成对应的中间代码。其核心和语义分析类似,我们要实现一个translate_X 函数,x可以对应表达式,语句等等。
表达式代码生成
让我们从最简单的常量表达式开始,例如1,我们可以生成如下的中间代码:
t1 =#1
这看起来非常简单,但是有一个显然的问题是这个临时变量t1是从哪里来的呢?表达式本身并不会给出这一信息。这启示我们在设计translate_exp函数时,需要传入一个参数,用于指定表达式的结果的存放位置。例如,对于1这个表达式,我们可以传入一个临时变量t1,然后生成上面的中间代码。所以,你可以这么定义translate_exp函数:
translate_exp(exp,symbol_table,place)
其中,exp是表达式节点,symbol_table 是符号表,place 是表达式的结果的存放位置。
为什么要符号表呢?答案是显然的,因为对于形如x的变量表达式,我们需要查阅符号表来获取该变量在中间表示下的名字。我们的中间表示并没有作用域的概念,因此对于那些在语法树中重复命名的变量,如在一个Block里定义的变量和外层的变量重名时,你需要自行处理。
而对形如 exp1 + exp2这样的二元运算表达式,我们生成新的临时变量,递归调用二元运算两个子节点的translate_exp函数,最后生成代码将它们加起来,即:
code1 = translate_exp(exp1,symbol_table,t1)
code2 = translate_exp(exp2,symbol_table,t2)
place = t1 + t2
同理,这个存储结果的位置t3也是由translate_exp函数的参数传入的。而t1和t2则是由内部生成的。
最后是函数调用表达式,我们只需要递归调用实参的translate_exp函数,然后生成代码将它们作为参数传递给函数,最后将函数的返回值存放到place 即可。
我们可以将表达式的翻译规则总结如下:
语句代码生成表达式生成的核心是translate_exp函数,而其中的参数place是表达式的结果的存放位置。那这个参数由谁来生成传递呢?那就是语句的生成函数translate_stmt(stmt, symbol_table).最简单的就是表达式语句,只需要直接翻译里面的表达式即可。赋值语句的生成也很简单,分别生成左右两边的表达式,然后将右边的结果存放到左边的位置即可。条件语句的生成则要复杂些,我们所定义的LABEL指令有了用武之地。直觉上来说,If 语句应该生成如下的中间代码
而理想中translate_cond 应该在里面的条件为真时跳转到L1,条件为假时跳转到L2。而这也就是 If 语句的翻译流程:
·生成新的标签L1,L2和L3,分别用于条件为真时的跳转,条件为假时的跳转,以及条件语句结束后的跳转。
·调用 translate_cond 生成条件表达式的中间代码,传入L1和L2作为条件为真时和条件为假时的跳转位置。
·而对于具体的语句,只需要递归调用translate_stmt 即可。
·在上图一样在合适的位置中加上LABEL 和 GOTO 语句,完成控制流的跳转。
其余类型的条件语句本质上是一样的,我们不再一一赘述。我们总结语句翻译的规则如下:
条件判断语句代码生成
你可能已经注意到了在翻译语句的时候,我们专门为条件判断语句定义了一个函数translate_cond,这是因为条件判断语句的翻译和普通语句有所不同,我们多传入了两个参数,true_label 和 false_label,分别表示条件为真时和条件为假时的跳转位置。
你可能觉得这和二元表达式没什么区别,只需要递归调用两边的表达式的translate_exp函数,然后生成代码将它们进行比较,最后根据结果跳转到对应的位置即可。对于一般的条件判断例如大于等于或者小于等于确实如此。
增加该函数复杂性的一大原因是SysY语言和C语言相同,是支持短路的。也就是说在翻译 Exp1 && Exp2时,如果第一个表达式为假,那么第二个表达式就不需要计算了,直接跳转到 false_labe1 即可。而对于Exp1 |]Exp2也是类似的。那么我们该如何实现这一点呢?答案还是控制流跳转。在这种情况,我们需要额外生成一个标签以供跳转,以Exp1 && Exp2为例,我们可以生成如下的中间代码:
而条件判断如果不是关系运算,例如简单的if (x),只需要当成if(x!=0)处理。
我们总结条件判断翻译的规则如下:
相比表达式和语句的生成,函数的生成则要简单的多。在定义函数时将参数以 PARAM形式声明,然后循环生成函数体内的语句即可。
全局变量和数组代码生成
上述代码生成中我们忽略了全局变量和数组相关的表达式求值和定义翻译。这两种类型的变量实质上都是地址/指针,它们的使用和赋值是相似的。
定义
我们为全局变量的定义单独设计一个GLOBAL指令,其初始化使用.WORD #k指令来完成。你可以假设全局变量的初始化一定是一个INT_CONST.例如,全局变量int a[4] = {1,2,3,4};int c;的中间代码翻译如下: