编译实验 中间代码生成_编译工程9:中间代码生成

在这部分内容中,我们主要讨论各类语句的翻译,包括声明语句、赋值语句、控制语句等。对这些语句的翻译方法直接决定了编译器底层的实现。

在编译器的分析-综合模型中,前端对源程序进行分析并产生中间表示,后端在此基础上生成目标代码。在理想情况下,和源语言相关的细节在前端分析中处理,而关于目标机器的细节则在后端处理。基于适当定义的中间表示形式,可以把针对源语言

的前端和针对目标机器
的后端组合起来。本部分内容涉及中间代码表示、静态类型检查和中间代码生成。

为什么要生成中间代码?快速编译程序直接生成目标代码,没有将中间代码翻译成目标代码的额外开销。但是为了使编译程序结构在逻辑上更为简单明确,常采用中间代码,并且可以在中间代码一级进行优化工作使得代码优化比较容易实现。

本部分的方法可以用于多种中间表示,包括抽象语法树和三地址代码。之所以命名为三地址代码,主要是因为这些指令的一般形式 x = y op z 具有三个地址:两个运算分量yz,以及结果变量x

中间语言甚至可以是一种真正的语言,譬如C语言是一种程序设计语言,具有良好的灵活性和通用性,可以很方便地把C程序编译成高效的机器代码,并且有很多C的编译器可用,因此C语言也常常被用作中间表示。早期的C++编译器的前端生成C代码,而把C编译器用作后端。


表达式的有向无环图(DAG)

语法树中的各个结点代表了源程序中的构造,一个结点的所有子节点反映了该节点对应构造的有意义的组成部分。为表达式构建的有向无环图指出了表达式中的公共子表达式。

对下面的表达式给出的DAG如下:

16d9de336eccc1acff8c7f26d103ca8a.png

如下图给出的SDD既可以用来构造语法树,也可以用来构造DAG。但是在构造DAG的时候,函数需要在每次构造新结点之前首先要检查是否已经存在这样的结点。

9e5697449401b8adb250e4cca87353a5.png

【上面的5和6中T应该为F】

下图则展示了上面的DAG的构造过程。

1e585d880b08e4b7ce334e225c1c1ece.png

其中,Leaf和Node尽可能地返回已经存在的节点。上面的文法不是LL文法。可以结合规约的过程,理解一下这个DAG的构造过程。

DAG的每个结点通常作为一条记录被存放在数组中,一个记录包括的结点信息有:

  • 结点标号:如果记录表示叶子结点,那么结点标号是该结点的文法符号;如果记录表示内部结点,那么结点标号是运算符;
  • 词法值:如果记录表示叶子结点,那么记录还包括该结点的词法值,通常是一个指向符号表的指针或者一个常量;
  • 左右子结点:如果记录表示内部结点,那么记录还包括该结点的左右子结点。

由于一个DAG的结点都会保存在一个记录数组中,因此我们可以通过数组下标引用某个记录从而获取结点信息,这个数组下标称为相应结点的值编码

071d6dbb0c059646546c6a188168ff6d.png

练习:

为下面的表达式构造DAG。

((x+y)-((x+y)*(x-y)))+((x+y)*(x-y))

[下面很多内容来自参考1和2,在此表示感谢]

三地址代码

三地址代码中,一条指令的右侧最多只有一个运算符,也就是说,不允许出现组合的算术表达式。因此,像 x+y*z 这样的源语言表达式要被翻译成


其中,

是编译器产生的临时名字。因为三地址代码拆分了多运算符算术表达式以及控制流语句的嵌套结构,所以适用于目标代码的生成和优化。因为三地址表达式用名字来表示程序计算得到的中间结果,所以可以方便地进行重组。

三地址代码基于两个基本的概念:地址和指令。简单地说,地址就是运算分量,指令就是运算符,一个地址的表现形式可以是变量名、常量或者编译器生成的临时变量。下面是几种常见的三地址指令形式:

c4db44c9327786a85a757ab6f2f7203f.png

对下面的语句,使用三地址代码的翻译可能有如下的形式:

do i = i +1 ; while(a[i] < v);

25f17ae16466434f4cfb3b9fae64f529.png

上面对三地址指令的描述详细说明了各类指令的组成部分,但是并没有描述这些指令在数据结构中的表示方法。在编译器中,这类指令可以实现为对象,或者是带有运算符字段和运算分量字段的记录。四元式、三元式和间接三元式是三种这样的描述方法。

四元式是一条表示三地址指令的记录,它有4个字段:

  • op:表示一个运算符;
  • arg1:表示第一个运算分量;
  • arg2:表示第二个运算分量;
  • result:表示结果变量。

一个四元式中可能用到了所有4个字段,也可能只用到了其中几个字段,它的几个特例如下:

  • 形如x = minus y的单目运算符指令不使用arg2字段,”minus”表示单目减运算符;
  • 形如x = y的赋值指令不使用arg2字段,并且它的op字段是”=”;
  • 形如param x的参数传递指令不使用arg2和result字段;
  • 条件和非条件转移指令将目标标号放入result字段。

下图是一个例子,左边给出了三地址代码,右边是对应的四元式表示:

661411b04ca1e0bf76c9021d4f1e59d2.png

三元式

通过四元式的表示我们发现,一段三地址代码对应的四元式被存放在一个记录数组中,这一点和DAG结点的记录数组很像;另外,四元式中的result字段主要被用于保存临时变量名,这个临时变量是由编译器生成的。如果我们仿照DAG的记录数组用值编码表示临时变量的地址,那么就可以省略四元式中的result字段,三元式就是由此而来的。

一个三元式只有3个字段:op、arg1和arg2,它们的含义和在四元式中相同。为了取代四元式中的result字段,三元式用值编码表示结果变量的地址。上面的三地址代码的三元式表示如下:

4b3f2c81b967cfc5e065f3d92dbda9f5.png

表格左边的数字是值编码,它表示三元式的结果变量的地址,并且这些值编码可以在三元式中被使用(使用的时候用括号括起来)。另外,对形如x=y的赋值指令,和四元式不同的是,三元式的op字段是”=”,arg1字段是”x”,arg2字段是”y”。

间接三元式

三地址代码的三元式表示存在一个问题,如果记录数组中的某条记录R的位置发生改变,那么所有使用到记录R的值编码的记录都需要更新。举个例子,在下面的图(b)中,如果把第1和第2条记录的位置互换,那么第3和第4条记录的内容都会发生改变。为了解决这个问题,提出了用间接三元式来表示三地址代码。

一个间接三元式在三元式的基础上增加了一个列表,这个列表包含了指向三元式的指针。仍以例子说明,下图是对应于上面三元式的间接三元式:

1e2e384623766929bf2abf6b3b570fcc.png

间接三元式,它使用了一个instruction数组保存要执行的指令,每条指令是一个指向某个三元式的指针。这样的话,当我们改变指令顺序时,就不用再更新三元式了。

静态单赋值形式

静态单赋值(Static Single Assignment,简称SSA)形式是另一种中间表示形式,它和三地址代码的主要区别在于:

第一,SSA中的所有赋值都是针对具有不同名字的变量的,这也是“静态单赋值”名字的由来。这一特性如下图中表示:

5081e7b6db15f91e2fbef3fff8ee20b8.png

SSA的主要用途来自于它如何通过简化变量的属性来,同时简化和改进各种编译器优化的结果。 例如,考虑这段代码:

y := 1
 y := 2
 x := y

可以看到第一个赋值不是必需的,并且第三行中使用的y的值来自y的第二个赋值。 程序必须执行到达定义分析(reaching definition analysis)以确定这一点。 但如果该程序采用SSA形式,则两者都是显而易见的:

y1 := 1
y2 := 2
x1 := y2

类型表达式

语言中最基本的一种语句就是声明语句,对声明语句的翻译主要是收集标识符的类型等信息,并对每一个标识符分配相对地址。因为要收集类型信息,所以,首先来看一下类型表达式。

我们使用类型表达式来表示类型的结构。类型表达式可以是基本类型,也可以是通过把称为类型构造算子的运算符作用于类型表达式得到的。不同语言的基本类型和类型构造算子可能不同。具体地,一个类型表达式可以是:

  • 基本类型。例如C语言的基本类型包括int、float、double、char等;
  • 数组。将类型构造算子array作用于一个数字和一个类型表达式可以得到一个类型表达式;譬如
    就是表示数组类型的类型表达式,其中I表示维度,T表示数组中数据的类型,也即这个类型包括
    型的数据;譬如 int[3]的类型表达式就是array(3,int);int[2][3]的类型表达式就是array(2,array(3,int))等;
  • 记录。一个记录是包含一个或多个字段的数据结构,这些字段都有一个唯一的名字。将类型构造算子record作用于字段的名字和类型可以得到一个类型表达式;【想象成结构体】
  • 函数。使用类型构造算子→可以构造得到函数类型的类型表达式,“从类型s到类型t的函数”记作“s→t”;
  • 如果
    是类型表达式,则其笛卡尔积
    也是类型表达式。引入笛卡尔积主要是为了保证定义的完整性,可以用于描述类型的列表或元组(例如,用于描述函数的参数,譬如可以有(
    )是类型表达式;)。【其中,
    具有左结合性,并且优先级高于
  • 指针也是类型表达式。

可以使用与DAG相似的方式表示一个类型表达式:叶子结点可以是基本类型和类型变量,内部结点是类型构造算子,array运算符有两个参数:一个数字和和一个类型。例如,数组类型 int[3][4] 可以表示成:

d4a83a3b2c11a0d8f09f7f858a8cbf88.png

再看一个例子,对于下面的代码,与各个变量绑定的类型表达式分别是什么呢?

struct stype
{
char[8] name;
int score;
}
stype[50] table;
stype * p;

那么,和stype绑定的类型表达式:

record((name)×array(8,char))×(score × integer))

和table绑定的类型表达式

array(50,stype)

和p绑定的类型表达式

pointer(stype)

声明语句的翻译

(大多数语言中)变量只有在声明后才能使用。对变量进行声明赋予了该变量名字和类型,变量名可以使其在表达式中被引用,变量类型可以在存储分配、类型检查等过程中发挥作用。在声明语句的处理中,需要收集类型等属性信息,并且分配相对地址。

变量类型

一个变量的类型可以是基本类型、数组类型或记录类型。变量的类型可以用下面的文法表示:

    T → BC | record'{'D'}'
    B → int | float
    C → [num]C | ε

在这个文法中:

  • 符号T表示基本类型、数组类型(一维或多维数组)和记录类型(用类型构造算子record构造得到的类型表达式);
  • 符号B表示基本类型,这里只给出了int和float两种;
  • 符号C表示数组的维度,可以是零维(没有中括号)、一维(有一个中括号)或多维(有多个中括号),C加上B才表示一个数组类型;
  • 符号D表示一个声明序列,它是对一个或多个变量的声明,这里用于表示记录类型中的字段。

类型的宽度(width)是指该类型的一个对象所需的存储单元的数量。一个基本类型,比如字符型、整型和浮点型,需要多个字节,在编译的时候就需要分配空间。为了方便访问,为数组和类这样的组合类型数据分配的内存是一个连续的存储字节块。同时,变量的类型和相对地址保存在相应的符号表记录中。

下图中给出的SDT计算了基本类型和数组类型以及它们的宽度。这个SDT对于每个非终结符使用综合属性type和width,还是用了两个变量t和w,变量的目的是将类型和宽度信息从语法分析树中的B节点传递到对应产生式C->ε的结点。在语法制导定义中,w和t是C的继承属性。

ed62a5f0f0d6180fcc73559bcf07071a.png

下面看一下数组类型是如何获得自己的类型和宽度的。

80b448f6e5b05e0060b12abc6d58f0a5.png

变量声明

如果只声明一个变量,一次声明包括一个变量类型和一个变量名并以一个”;”结尾,如int x;。这种声明方式可以扩展成序列的形式,如int x; float y; char z;,这种形式通常用于声明记录中的字段。变量的声明可以用下面的文法表示:

    D → T id;D | ε

所以上面的文法一起就是:

D → T id;D | ε
T → BC | record'{'D'}'    
B → int | float     
C → [num]C | ε

在这个文法中:

  • 符号T表示基本类型、数组类型和记录类型;
  • 符号D表示声明序列,这个序列中至少声明了一个变量;
  • 符号id表示变量名,严格地说,在声明序列中的任何两个变量的名字都不相同。

变量存储

变量的类型告诉我们它在运行时刻需要多大的内存空间,在编译时刻,我们可以使用变量的内存大小信息为每个变量分配一个相对地址。

一个变量的相对地址需要用该变量的起始地址和该变量的类型宽度来刻画。其中,起始地址是用于存储变量的字节块的第一个字节的地址,类型宽度是用于存储变量的字节块包含的字节数。例如,假设x是一个整型变量并且它的起始地址为100,那么x的相对地址是从100开始,到103结束的4字节的字节块。

假设存储变量的方式是连续的(即对变量x和y,x的相对地址是从d1到di的字节块,那么y的起始地址是di+1),则存储变量的关键问题变成了确定每个变量需要多大的存储空间(说白了就是确定每个类型的宽度)。下面我们对计算基本类型、数组类型和记录类型的宽度分别进行说明:

  • 基本类型。基本类型的宽度是由语言事先定义好的,比如Java的int类型的宽度是4个字节;
  • 数组类型。数组类型的宽度是由元素的类型宽度和元素的数量共同决定的,比如数组类型int[3]的宽度是4*3=12个字节;
  • 记录类型。记录类型的宽度是由该记录中所有字段的类型宽度共同决定的,比如记录类型record{ int x; int y; }的宽度是4+4=8个字节。

包括基本类型、数组类型和记录类型在内的声明语句的一个可行的SDT如下:

这里,可以增加一条规则P->D {offset=0};也即在第一个声明之前,offset被设置为0。在进行产生式D->T id; D的处理时,每增加一个变量

时,
而被加入符号表,它的相对地址被设置为Offset的当前值。然后,
的类型宽度被加到offset上。

产生式D->T id; D的语义动作首先执行top.push(id.lexeme,T.type,offset),表示创建了一个符号表条目。这里的top指向当前的符号表,方法top.push为id.lexeme创建一个符号表条目,条目的数据区存放了类型T.type以及相对地址。

对记录类型的宽度进行计算是一个比较复杂的过程,这种情况下,需要把记录中的字段保存到一个新的符号表中(相近的概念是变量的作用域):

11afd7683105bbf8c07de2d11e5163f3.png
  • 首先,在产生式T→record'{'P'}'中,第一个动作对当前的上下文环境进行保存,该动作把指向当前符号表的指针top和记录字段偏移量的变量offset保存到Stack中,并把top指向一个新符号表,把offset重置为0;【这里的stack是用于保存上下文的辅助结构;主要是用于记录每个scope中当前的offset】
  • 然后,在产生式D→T id;D1中,动作把名为id.lexeme的变量加入到符号表中,并将offset加上该变量的类型宽度;
  • 接着,重复第2步直到没有新的变量被声明,即到达产生式D→ε;
  • 最后,在产生式T→record'{'P'}'中,第二个动作计算记录类型的宽度并对之前的上下文环境进行还原,该动作把记录的类型设为record(top),把记录的宽度设为offset,并把top指向第1步保存在Stack中的符号表,把offset设为第1步保存在Stack中的值。

类似于下图所示:

3903bdb6ff6e0281866b84848d59c24e.png

具体来说,譬如从sub到recordtype1的过程,首先是对sub的上下文进行保存,表明现在已经处理到变量

了,变量
本身的偏移是4,也即
占了0~3四个字节的位置;
本身指向一个record,此时record
的width还没有计算出来,然后开辟一个新的符号表,offset为0,所以这个记录中的 整型变量
的offset仍然为0;变量
再次为一个record;开辟新的符号表,此时记录
中的整型变量
的offset也为0,实型变量
的offset为4,本身的width为8,此时更新offset为12;此时recordtype2处理结束,采用相对应产生式中的第二个动作,计算出记录
的宽度位12。接下来stack的栈顶弹出,开始处理recordtype1,因为这里记录b之后也没有其他的变量,所以recordtype1也处理完毕,然后得出来recordtype1的width为16。接下来回到sub,得出sub的width是20。此时可以继续处理program中sub之后的内容。

练习:

如果sub中

之后还有一个整型变量
,请问
的offset是多少?

表达式的翻译

表达式语句的翻译主要是生成相对应的三地址码。一个带有多个运算符的表达式(a+b*c)将被翻译成每条指令最多包含一个运算符的指令序列【t1 = b*c; t2 = a+t1; x=t1;】;一个数组引用将被扩展成计算该引用地址的三地址指令序列。

包括赋值运算、加法运算和取负运算在内的表达式的一个可行的SDT如下:

6889583a2497d13593c16d1b851707ed.png
表达式的三地址代码

在表1中,每个符号的含义是:

  • 非终结符号S表示一个表达式;非终结符号E表示一个子表达式,它的addr属性表示对应变量的变量名/临时变量名/代数值;终结符号id表示一个运算分量,它的lexeme属性是由词法分析器返回的值;
  • top是一个指向当前符号表的指针,top.get(x)表示从符号表中取得标号为x的记录;【如果这里查找不到相应的id,也即发生了变量名未经声明就使用的错误】
  • gen是一个负责生成三地址代码的函数,传递给它的参数就是需要生成的三地址代码。参数中包括变量和字面常量,字面常量需要在左右加上单引号;
  • Temp是一个负责生成临时地址的函数,这个临时地址通常用于编译器产生的临时变量。

以表达式x = a + (-b)为例,它的注释语法分析树和三地址代码如下:

c02e98eff6e4eb9dd7fc3cc95cc09518.png

详细的翻译过程如下:

由于每个语义动作都在产生式的最右端,因此这个SDT可以在自底向上的语法分析过程中实现;

  • 第一次归约发生在图a中的标记1处,这里使用了产生式E→-E,相应的语义动作生成了图b中的第1条指令;
  • 第二次归约发生在图a中的标记2处,这里使用了产生式E→E+E,相应的语义动作生成了图b中的第2条指令;
  • 第三次归约发生在图a中的标记3处,这里使用了产生式S→id=E,相应的语义动作生成了图b中的第3条指令。

数组引用的翻译

将数组引用翻译成三地址码的主要问题就是确定数组元素的存放地址,也就是数组元素的寻址。需要注意的是,这里是数组引用而不是声明,也即,在使用之前,已经声明过了;在进行引用的时候,已经知道了一些基本信息,譬如像在上面介绍的,已经知道数组名字相对应的类型type和宽度width。

将数组的元素存储在连续的存储空间中,就可以快速的访问元素。在C和Java中,具有n个元素的数组中的元素是按照0,1,2....n-1编号的。如果每个元素的宽度是w,那么数组A的第i个元素的开始地址为

base + i * w

其中,base是分配给数组A的内存块的相对地址。也就是说,base是A[0]的相对地址。

类似的计算方法可以推广到二维或者多维数组中。譬如

的地址为:
base + (i1*n2+i2)*w

上面的地址计算是基于数组的按行存放方式,C语言就是使用这种数据布局方法。

练习:
假设type(a) = array(2,array(3,array(4,array(5,int))))
计算a[1][2][3][4]的地址。
计算a[i1][i2][i3][i4]的地址。

注意,以上都是考虑数组下标从0开始的情况,如果数组下标不是从0开始,那么需要进行进一步处理。

假设type(a) = array(n,int),

源程序片段 c = a[i];

addr(a[i]) = base + i * 4

三地址码

数组引用的一个可行的SDT如下:

5295199f5f31388978db4b1f286ae488.png

在上表中,每个符号的含义是:

  • 非终结符号S和E、终结符号id、变量top、函数gen和Temp的含义与之前表中的相同,非终结符号L表示一个数组变量;
  • L.array是指向数组名字对应的符号表条目的指针,假设L.array指向条目a,a.base是数组的基地址,a.type是数组的类型,a.type.elem是数组中元素的类型。例如,对类型为int[2]的数组a,有L.array=a,L.array.base=a[0],L.array.type=int[2],L.array.type.elem=int;
  • L.addr是相对数组基地址偏移的字节数,对距离数组基地址L.addr个字节的元素的引用是L.array.base[L.addr];
  • L.type是数组中元素的类型,等同于L.array.type.elem,L.type.width表示数组中元素的类型宽度。例如,对类型为int[2]的数组a,有L.type=int,L.type.width=4;对类型为int[3][4]的数组b,有L.type=int[4],L.type.width=16。

以表达式a[k] = b[i][j]为例,它的注释语法分析树和三地址代码如下:

f38ea3a21f470d3094d4303c3fce3f4f.png

详细的翻译过程如下:

由于每个语义动作都在产生式的最右端,因此这个SDT可以在自底向上的语法分析过程中实现;

  • 第一次归约发生在图a中的标记1处,这里使用了产生式L→id[E],相应的语义动作生成了图b中的第1条指令;此时需要注意的是,这里使用了top.get来获得数组
    的相关信息,这里包括判断是否声明、进行类型检查、数组有无越界等各种操作;
  • 第二次归约发生在图a中的标记2处,这里使用了产生式L→id[E],相应的语义动作生成了图b中的第2条指令;此时通过在符号表中进行查找,获得了关于数组
    的各种信息,包括它的type是int[3][4];同时计算第一维的偏移;
  • 第三次归约发生在图a中的标记3处,这里使用了产生式L→L[E],相应的语义动作生成了图b中的第3和第4条指令;
  • 第四次归约发生在图a中的标记4处,这里使用了产生式E→L,相应的语义动作生成了图b中的第5条指令;
  • 第五次归约发生在图a中的标记5处,这里使用了产生式S→L=E,相应的语义动作生成了图b中的第6条指令。

接下来我们讨论控制流的翻译。if-else,while语句这类语句的翻译和布尔表达式的翻译是结合在一起的,在程序设计语言中,布尔表达式经常用来:

  1. 改变控制流。布尔表达式用于语句中改变控制流的条件表达式。这些布尔表达式的值由程序到达的某个位置隐式地指出。例如,在if(E) S 中,如果运行到语句S,就意味着表达式E的值为1/true。
  2. 计算逻辑值。一个布尔表达式的值可以表示true或false。这样的布尔表达式也可以像算术表达式一样,使用带有逻辑运算符的三地址进行求值。

布尔表达式的使用意图要根据其语法上下文来确定。例如,跟在关键字if后面的布尔表达式用来改变控制流,而一个赋值语句右部的表达式用来表示一个逻辑值。有多种方式可以描述这样的上下文:可以使用两个不同的非终结符号,也可以使用集成属性,还可以在语法的分析过程中设置一个标记。此外,还可以建立一棵语法分析树,并调用不同的过程来处理布尔表达式的两种不同使用。

下面主要讨论用于改变控制流的布尔表达式,为此,引入了一个新的非终结符B。

控制流和布尔表达式的翻译

考虑由如下文法生成的布尔表达式

B -> B || B | B && B | !B | (B) | E rel E | true | false

具体实例【if(true);if(true&&true); if(x>y); if(1>0);if(x>y && x<z)】

通过使用属性rel.op指明运算符 <、>、<=、==、!=、>和>=;按照惯例,||和&&是左结合的,||的优先级最低,其次是&&,最后是!。

给定表达式B1 || B2,如果已经确定B1为真,那么不用再计算B2就可以断定整个表达式为真。同样地,给定B1 && B2,如果B1为假,则整个表达式为假。布尔表达式遵循短路运算。在短路(跳转)代码中,布尔运算符&&、||和!被翻译成跳转指令,运算符本身不出现在代码中,布尔表达式的值是通过代码序列中的位置来表示的。

譬如一条语句:S-> if B then S1 else S2

c99ae7cd25957abae49eedea15d7a696.png

S.next是一个地址,存放了紧跟在S代码之后的指令(S的后继指令)的标号;S1和S2都有.next属性,指向同一个地址,S.next

B因为需要有两个出口,所以分别设置了B.true和B.false,分别表示当B为真和为假时跳转到不同的地址,也即需要产生标号

考虑语句:

if(x<100 || x>200 && x!=y) x = 0; 

我们人工翻译一下,可以被翻译成这样:

c87c6a8d4e93c806d626f79c208ac50e.png

那现在的问题就是,在代码中的这些标号是如何产生的?以及,布尔表达式是如何被翻译的?所以接下来主要讨论的是控制流语句和布尔表达式的翻译。

控制流语句的代码:

5bf61b08fd25cab6bde06d1d75bbb025.png

下面是控制流语句的语法制导定义:这里,newlabel()每次都能产生一个新的标号;并且label(L)将标号L附加到即将生成的下一条三地址指令上。

76da47bce78b906e91adb4bce991131b.png

下面是布尔表达式语义规则:

915969286c752c314d16681692c92e2f.png

使用以上的语义规则,

if(x<100 || x>200 && x!=y) x = 0; 

可以被翻译如下:

1a7938f87decb4a07f693d23b7d92530.png

首先,使用产生式和规则

ffe320ad69795090c536e4d4dfe7f348.png

主要在S1的代码(x=0)之前生成了标号L2,在S1的代码之后生成了标号L1。L2对应于B.true,而L1对应于B.false。

接着,使用布尔表达式的翻译规则,对x<100 || x>200 && x!=y 进行翻译。

74026212b14b8c29ba9ff3c7b8b261bd.png

因为&&的优先级高于||,所以首先对x<100进行了处理,以及代码生成。

9527b5b780bed973e43b7d27004a0fb9.png

此时生成了代码

if x < 100 goto B.true ; 
goto B.false;

作为|| 运算符的左边部分,(x<100).true此时就是整个||布尔表达式的B.true;也即L2;B.false要生成L3,但是L3还没有确定下来在哪里;

在(x<100)处理完成,开始处理(x>200 && x!=y )之前,定下来L3的位置;

接下来,使用上面的relop的规则,处理 x > 200,产生了代码if x > 200 goto B.true ; goto B.false;

if x < 100 goto L2 ; 
goto L3;
if x > 200 goto B.true ; 
goto B.false

接下来,处理 x != y,生成代码if x != y goto B.true ; goto B.false;

if x < 100 goto L2 ; 
goto L3;
if x > 200 goto B.true ; 
goto B.false
if x != y goto B.true ; 
goto B.false;

此时使用

eb8973b34db6000c4abbf842bc4b5ab0.png

此时生成了标号B1.true L4;也即接下来要计算B2;代码变成:

if x < 100 goto L2 ; 
goto L3;
if x > 200 goto L4 ; 
goto B.false
L4:
if x != y goto B.true ; 
goto B.false;

B1.false、B2.true以及B2.false取决于B的整体的true和false属性,结合||的语义,我们知道B.false=L1;B.true=L2

74026212b14b8c29ba9ff3c7b8b261bd.png

所以代码是:

if x < 100 goto L2 ; 
goto L3;
L3:
if x > 200 goto  L4 ; 
goto L1
L4:
if x != y goto L2 ; 
goto L1;

调整一下格式:

    if x < 100 goto L2 ; 
    goto L3;
L3: if x > 200 goto  L4 ; 
    goto L1
L4: if x != y goto L2 ; 
    goto L1;
L2: x = 0;
L1:

避免生成冗余的goto指令

在上面的结果中,有一些冗余,譬如

    if x < 100 goto L2 ; 
    goto L3;
L3: 

以及

L3: if x > 200 goto  L4 ; 
    goto L1
L4:

可以将上面的指令替换为

L3: ifFalse x > 200 goto L1;
L4:

if False指令利用了控制流在指令序列中会从一个指令自然流动到下一个指令的性质,因此当x > 200时,控制流直接“穿越“到标号L4,从而减少了一个跳转指令。

在上面的if和while的代码布局中,

的代码紧跟在布尔表达式B的代码之后,通过使用特殊标记“fall”(即,不要生成任何跳转指令),我们可以修改上面的语义规则,支持控制流从B的代码直接穿越到
的代码,譬如对于
,新语义规则是

fe7d8e7add40ea1c26599c3a899763ce.png

if-else和while语句也将B.true设置为fall。

接下来我们将修改布尔表达式的语义规则,使之尽可能地允许控制流穿越。在B.true和B.false都是显式的标号时,也就是它们都不等于fall时,那么新规则和旧规则一样,产生两条指令;否则,分别产生一条指令:如果B.true为显式标号,那么B.false一定是fall,因此不必为B.false生成goto语句;反过来,如果B.false是显式的标号,那么可以产生一条ifFalse的指令。如果B.true和B.false都是fall,那么不产生任何的跳转指令。

52752aeb753f1772646a635ac38ecc82.png

此时

3695085ac01dc3f242295e937529a3f2.png

所以,相比之前的代码,主要是将B1.false的标号给省略了。

74026212b14b8c29ba9ff3c7b8b261bd.png

请问

的语义规则应该是怎样的?【主要是省略了B1.true的标号】
B1.true = fall
B1.false = if B.false != fall then B.false else newlabel()
B2.true = B.true
B2.false = B.false
B.code = if B.false != fall then B1.code || B2.code
         else B1.code || B2.code || label(B1.false)

使用上述的规则,可以将

if(x<100 || x>200 && x!=y) x = 0; 

翻译成

fd109096f436b60e04b662cf3b454ab3.png

此时,当应用

的语义规则时,继承属性B.true是fall(B.false是L1);所以根据图6-40的规则,创建新的标号L2,当B1为真时有一个跳转指令可以跳过B2的代码。因此,B1.true为L2;而B1.false为fall,因为B1为假时必须计算B2的值。

当开始处理表达式 x < 100的时候,使用的是

的规则,此时B.true不为fall且B.false为fall,因此生成指令 if x < 100 goto L2。

接下来处理的是 x>200 && x!=y,此时作为整体,它是

中的B2。而B2.true等于B.true,而B.true是fall;B2.false是B.false,而B.false是S1.next,也即L1。接下来将x>200看做是B3, x!=y看做是B4,则
B3.true = fall
B3.false = B2.false = L1
B4.true = B2.true = fall
B4.false = B2.false = L1

所以,根据图6-39的规则,生成两条iffalse语句。


回填

为布尔表达式和控制流语句生成目标代码时,关键问题之一是将一个跳转指令和该指令的目标匹配起来。譬如,对 if (B) S 中的布尔表达式的翻译结果包括一条跳转指令,当B为假时,这条指令跳转到S之后的代码。之前使用的是继承属性进行传递,但是如果希望在一趟中完成继承属性的分析,只能结合LL文法进行;因此,需要多做一趟处理,才能将标号和具体的地址联系起来。

这里介绍一种称为回填(backpatching)的补充性技术,它把一个由跳转指令组成的列表以综合属性的形式进行传递【因此可以在LR语法分析中完成属性分析】。具体来说,生成一个跳转指令时暂时不指定该跳转指令的目标,这样的指令都被放入到一个由跳转指令组成的列表中。等到能够确定正确的目标标号时才去填充这些指令的目标标号。同一个列表中的所有跳转指令具有相同的目标标号。这样生成的代码与之前生成的相同,但是处理方法不同。

布尔表达式的一个可行的SDT如下:

d70a78691422f325e20c7241a68d2cbf.png

在上表中,每个符号的含义是:

非终结符号B表示一个布尔表达式,它的truelist属性是一个包含指令地址的列表,这些地址是当B为真时控制流应该跳转到的指令地址,它的falselist属性也是一个包含指令地址的列表,这些地址是当B为假时控制流应该跳转到的指令地址;

  • 非终结符号E表示一个表达式,它的addr属性表示对应变量的代数值;
  • 符号M是一个标记非终结符号,它的instr属性负责记录下一条指令的地址;
  • 变量nextinstr表示下一条指令的地址,即下一次生成的三地址代码会被放在nextinstr所指向的地址上;
  • 函数makelist(i)负责创建一个只包含指令地址i的列表,并返回一个指向新创建列表的指针;
  • 函数merge(p, q)负责将p和q指向的列表进行合并,并返回一个指向合并的列表的指针;
  • 函数backpatch(p, i)的功能比较复杂,首先,p是一个指向列表的指针,对p指向的列表中的每个指令地址j,地址j上的指令是一个未填写目标跳转地址的转移指令(如goto _);其次,i是一个地址,这个地址是一个目标跳转地址;最后,函数backpatch用i填写每个j上的转移指令的目标跳转地址。

以表达式x<100 || x>200 && x!=y为例,它的注释语法分析树和三地址代码如下:

55128d185309903b2c9c6465ffbd7374.png

详细的翻译过程如下:

由于每个语义动作都在产生式的最右端,因此这个SDT可以在自底向上的语法分析过程中实现;

为了美观,truelist、falselist和instr都用它们的首字母表示,nextinstr用ni表示。假设初始时指令地址从100开始,即nextinstr指向地址100,如图b(1)所示;

  • 第一次归约发生在图a中的标记1处,此时nextinstr指向地址100。这里使用了产生式B→E rel E,相应的语义动作把地址100放入B.truelist中,把地址101放入B.falselist中,并生成了图b(2)中的两条转移指令,这两条转移指令的目标跳转地址都未被填写;
  • 第二次归约发生在图a中的标记2处,此时nextinstr指向地址102。这里使用了产生式M→ε,相应的语义动作把M.instr设为102;
  • 第三次归约发生在图a中的标记3处,此时nextinstr指向地址102。这里使用了产生式B→E rel E,相应的语义动作把地址102放入B.truelist中,把地址103放入B.falselist中,并生成了图b(3)中的两条转移指令,这两条转移指令的目标跳转地址都未被填写;
  • 第四次归约发生在图a中的标记4处,此时nextinstr指向地址104。这里使用了产生式M→ε,相应的语义动作把M.instr设为104;
  • 第五次归约发生在图a中的标记5处,此时nextinstr指向地址104。这里使用了产生式B→E rel E,相应的语义动作把地址104放入B.truelist中,把地址105放入B.falselist中,并生成了图3(b)(4)中的两条转移指令,这两条转移指令的目标跳转地址都未被填写;
  • 第六次归约发生在图a中的标记6处,此时nextinstr指向地址106。这里使用了产生式B→B1 && MB2,相应的语义动作设置了B.truelist和B.falselist,并用地址104填充地址102上的转移指令的目标跳转地址,如图b(5)所示;
  • 第七次归约发生在图a中的标记7处,此时nextinstr指向地址106。这里使用了产生式B→B1 || MB2,相应的语义动作设置了B.truelist和B.falselist,并用地址102填充地址101上的转移指令的目标跳转地址,如图3(b)(6)所示;
  • 最终的三地址代码如图b(7)所示,在第六和第七次归约中填充转移指令的目标跳转地址的技术称为回填,回填技术用来在一趟扫描中完成对布尔表达式或控制流语句的目标代码生成。

理解一下,在

中,可以立刻判断出B1.false的位置,也即B2之前,所以在B2前添加一个M就可以算出来B2的第一条指令的地址,从而可以使用backpatch(B1.falselist, M.instr);
也类似,能很快确定下来B1.true的位置,从而可以使用backpatch(B1.truelist, M.instr)。

在布尔表达式B_1 || B_2本身的处理中不能确定的是B.true,也即,B1.true和B2.true所指向的位置。此时要等到处理控制流语句,如if的时候才能确定。也不能确定B2.false,也即B.false的位置,此时要等到控制流语句本身处理完成,找到S.next才能确定。

控制流语句的翻译

控制流语句的一个可行的SDT如下:

00e52dba2ac140fb919e2cc9b9ac4b50.png

每个符号的含义是:

  • 非终结符号S表示一个表达式,L表示一个语句列表,A表示一个赋值语句,B表示一个布尔表达式;
  • S的nextlist属性是一个包含指令地址的列表,这些地址是紧跟在S代码之后的转移指令的地址;L的nextlist属性与此类似;
  • 符号M是一个标记非终结符号,它的instr属性负责记录下一条指令的地址;
  • 符号N是一个标记非终结符号,它的nextlist属性是一个包含指令地址的列表,这些地址上的指令是无条件转移指令。

以表达式if (x<100) y=1 else y=2为例,它的注释语法分析树和三地址代码如下:

81549f67b940fb0bc55f2375ee37f6da.png

详细的翻译过程如下:

由于每个语义动作都在产生式的最右端,因此这个SDT可以在自底向上的语法分析过程中实现;

为了美观,truelist、falselist、nextlist和instr都用它们的首字母表示,nextinstr用ni表示。假设初始时指令地址从100开始,即nextinstr指向地址100,如图b(1)所示;

  • 第一次归约发生在图a中的标记1处,此时nextinstr指向地址100。这里使用了产生式B→E rel E,相应的语义动作把地址100放入B.truelist中,把地址101放入B.falselist中,并生成了图b(2)中的两条转移指令,这两条转移指令的目标跳转地址都未被填写;
  • 第二次归约发生在图a中的标记2处,此时nextinstr指向地址102。这里使用了产生式M→ε,相应的语义动作把M.instr设为102;
  • 第三次归约发生在图a中的标记3处,此时nextinstr指向地址102。这里使用了产生式S→A,相应的语义动作把S.nextlist设为空,并生成了图b(3)中的一条赋值指令;
  • 第四次归约发生在图a中的标记4处,此时nextinstr指向地址103。这里使用了产生式N→ε,相应的语义动作把地址103放入N.nextlist中,并生成了图b(4)中的一条转移指令,这条转移指令的目标跳转地址未被填写;
  • 第五次归约发生在图(a)中的标记5处,此时nextinstr指向地址104。这里使用了产生式M→ε,相应的语义动作把M.instr设为104;
  • 第六次归约发生在图(a)中的标记6处,此时nextinstr指向地址104。这里使用了产生式S→A,相应的语义动作把S.nextlist设为空,并生成了图(b)(5)中的一条赋值指令;
  • 第七次归约发生在图(a)中的标记7处,此时nextinstr指向地址105。这里使用了产生式S→if (B) MS1N else MS2,相应的语义动作设置了S.nextlist,并用地址102填充地址100上的转移指令的目标跳转地址,用地址104填充地址101上的转移指令的目标跳转地址,如图(b)(6)所示;
  • 最终的三地址代码如图(b)(7)所示,在第六次归约中用到了回填技术。

参考:

  1. https://blog.csdn.net/jzyhywxz/article/details/78720620
  2. https://blog.csdn.net/jzyhywxz/article/details/78788288
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值