六万字长文!让你懂透编译原理——第七章 语义分析和中间代码产生
Posted Leokadia Rothschild
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了六万字长文!让你懂透编译原理——第七章 语义分析和中间代码产生相关的知识,希望对你有一定的参考价值。
六万字长文!让你懂透编译原理(七)——第七章 语义分析和中间代码产生
编译原理最后一篇,完结撒花,555,马上考试了,学不完了,同样长文预警!
系列文章传送门:
万字长文+独家思维导图!让你懂透编译原理(一)——第一章 引论
万字长文!让你懂透编译原理(二)——第二章 高级语言及其语法描述
近三万字长文!让你懂透编译原理(三)——第三章 词法分析
三万多字长文!让你懂透编译原理(四)——第四章 语法分析—自上而下分析
六万多字长文!让你懂透编译原理(五)——第五章 语法分析—自下而上分析
三万五千字长文!让你懂透编译原理(六)——第六章 属性文法和语法制导翻译
六万字长文!让你懂透编译原理(七)——第七章 语义分析和中间代码产生
E→E1op E2
E是由两个子表达式通过op运算符连接而成
E.code:= E1.code || E2.code ||op
那么他的语义说的是,构造好的复杂的表达式后缀形式由op前面的子表达式E1的后缀形式和op后面的子表达式E2的后缀形式这两个后缀形式依次连接,再放上op运算符,这样的出来的就是整个表达式的后缀形式。
(E的后缀式是E1的后缀式+E2的后缀式+op)
E→ (E1)
第二条规则,从语法上看,一个表达式加上括号还是一个表达式
E.code:= E1.code
语义说的是,带括号的表达式的后缀形式,就是括号内的子表达式的后缀形式
E→id
第三条规则说的是,单独的一个标识符,也是一个表达式
E.code:=id
语义规则说的是一个标识符构成的表达式,他的后缀形式就是标识符自己
E→E1op E2{POST[k]:=op;k:=k+1}
说的是E的后缀式是E1的后缀式+E2的后缀式+op
当你用E1,E2归结到E的时候,E1和E2是已经分析翻译完了的,可以认为E1,E2翻译出来的后缀式已经在post里面了,这个时候要得到整个的E或者说是E1opE2的话,后缀式只要在这两个E1’和E2’的后面再放上op运算符,则整个的这个部分就是归约后的E所对应的后缀式,
那么对于E定义为(E1)这一条规则
根据前面后缀式的定义以及这里的语义规则
E→ (E1) E.code:= E1.code
说的是E的后缀式就是括号里面的子表达式的后缀式就是里面的E1的后缀式,所以说当你要把(E1)归约到E的时候,这个时候E的代码怎么构成?此时E1已经分析完毕,E1的代码已经在POST里面了,此时对应的语义动作是什么都不做,因为这个时候POST最后段的E1’这个后缀式就是这整个的带括号表达式的后缀式,所以什么都不做。就相当于完成了这条规则。
同理,对于
E→id E.code:=id
可是这个表示符本身就是自己的后缀式,所以这个时候只要把这个标识符id放到POST当前位置上,然后k+1走到后面就行了,这样这一项就是E的后缀式
E→id {POST[k]:=i;k:=k+1}
举例:输入串a+b+c的分析和翻译
7.1.2 图表示法
图表示法
- DAG
- 抽象语法树
父节点具有该运算符作用于其子节点的对应的值之后的结果,在DAG中代表公共子表达式的节点可以具有多个父节点,这就是有向无环图与抽象语法树的区别,一个子节点可能有多个父节点,因此他是图,不是树
赋值语句中的俩个子表达式b*(-c)在抽象语法树中对应有两棵独立的子树,这两棵子树是一样的,而在有向无环图中,这两颗子树被合并了,和并成了一个子树,消除了冗余的子树,这样一来,这个+节点他的左右两个子树都是乘法的结果,因此右边的这个图是个有向无环图,省去的都是由父节点指向子节点,本来是有箭头的,将箭头省掉了,带上箭头,这里面肯定是没有环的
建立一个
7.1.3 三地址代码
首先,所有涉及的三元式指令放在三元式表中,一共有4条指令,重复的求负以及乘法指令在这里面没有重复保存,只留下了一个求负指令,一个乘法指令,然后在这个加法左操作数是(1)的结果,由操作数也是(1)的结果,那么整个赋值语句,的计算的顺序,体现在间接码表中,说的是先计算0号指令,再计算(1)号指令,再计算(2)号指令,再计算(3)号指令,看上去好像顺序一样,但是如果将来因为优化,要删除增加语句,或者要调整语句顺序,你只要修改间接码表,对间接码表进行增加一项,删除一项或者是调整顺序,而三元式表不改动,三元式表不改动意味着每个三元式指令的下标不会变,而且三元式内部对下标的引用也不会变。
再看一个复杂一点的例子,说明间接三元式是如何进行优化的。
7.2 赋值语句的翻译
7.2.1 简单算术表达式及赋值语句
设计上采取了递归的思想:
S→id:=E S.code:=E.code || gen(id.place ‘:=’ E.place)
整个赋值语句的三地址代码是由赋值号右边的E语法单位也就是表达式它翻译成的三地址代码再加上gen函数生成的赋值指令构成。
gen函数是将输入的各个参数拼接成一个三地址代码,这里有三个参数,一个是变量在符号表的入口id.place,然后赋值号,然后存放E的值的单元的名字/地址,这样gen函数会根据这三个参数,生成三地址指令,这里说明一下,由于可以通过名字串找到标识符在符号表中的入口,进而找到对应的地址,所以在三地址指令中,要引用一个单元,我们可以用名字,地址,甚至是符号表中的入口,都是一样的,以后就不再区分了,这是第一条规则。
对于E→E1+E2
这条规则,语义规则如上:
先产生一个名词变量newtemp,将来这个名词变量用来存放+运算的结果,我们把该名词变量的名字记录在E.place这个属性中,注意E.place表示存放E值的单元或者是名字或者是地址,那么E的三地址代码是怎么构造出来的,语义规则是这样说的:整个大的+法表达式的三地址代码是由加法前面的子表达式E1翻译出来的三地址代码在连接上+后面的子表达式E2所翻译出来的三地址代码连接,再加上一个赋值指令构成,注意gen函数产生的是一个+法赋值指令,赋值号的左边是刚才分配的这个临时单元的名字或者地址,右边有一个+,+号左边是E1.place,E1.place里面就是说E1这个子表达式的值的存放的单元或者是名字,E2.place是说+后面表达式所计算出来的值的存放的单元或者是名字,或者是地址。
gen函数就利用这几个参数产生一个+赋值指令,这三部分连接起来,就是整个E的代码,E1.code 计算+前面的子表达式,E2.code 计算+后面的子表达式,gen(E.place ‘:=’ E1.place ‘+’ E2.place)这条指令负责将两条结果加起来送给结果单元,这就是这个+法规则的语义描述
同理E→E1*E2
这条语义规则与E→E1+E2类似
对于E→-E1
这个指令
首先也是产生这个临时单元newtemp用来存放求-的结果,然后再E1代码后面再加上一个求负赋值指令
对于E→ (E1)
这种形式,它的语义规则说的是E.place:=E1.place; E.code:=E1.code
说的是整个的代括号的表达式E他所对应的计算结果的存放单元就是括号里面的这个子表达式的结果单元,那么带括号的这个表达式的三地址代码序列就是括号内的这个子表达式所翻译出来的三地址代码序列
最后
说的是E的结果单元就是变量对应的单元,单独的一个变量所对构成的表达式本身没有对应的三地址代码
这样我们就得到了一个为赋值语句生成三地址代码的S-属性文法,其中,这里面的code,place都是综合属性。
下面我们对照语义规则看看各产生的语义规则怎么设计:
首先通过lookup函数在符号表中查找赋值号左边的id在符号表中是否出现,如果找到了那么lookup就会返回该表示符在符号表中的入口,此时p就不会为nil空,那么就产生一条三地址指令送到输出文件中调emit,此时调emit的时候如果被定义值的变量id这个标识符它真正的标识符是a(p是a)的话,赋值号右边的表达式E计算出来的结果存放的单元比如(E.place记录的是T单元的名字)实际上发射出来的指令就是a:=T(a赋值为T)这样一条三地址代码送到输出文件当中,如果lookup在查找赋值号左边的标识符的时候,没有找到该标识符,说明这个标识符还没有什么,属于未定义表示符就被使用了,就报错。
这是第一条语义规则的动作。
对于
首先产生一个临时变量newtemp,将来这个粮食变量就用来存放+运算结果,把该临时变量的名字、地址记录在E.place属性中,然后在生成一条加法赋值的三地址指令,这个赋值指令以用来存放E1/E2单元的地址进行加法运算,结果结果单元就是我们公共生成的这个临时变量,如果刚刚生成的临时变量的名字是T的话,赋值号右边E1和E2这两个子表达式的计算结果的存储单元分别对应的是T1和T2的话,那么emit实际上发出的指令就是T:=T1+T2这种形式。T被赋值为T1+T2,发出的这样一条指令送到输出文件当中,注意,当用E1和E2归约的时候,E1和E2对应的三地址代码已经翻译好了,输出到了输出文件中,现在只要在输出文件中再加一条+赋值指令,也就是emit生成的+赋值指令,那么就完成了这个产生式的语义动作。
它的语义动作也类似
先分配一个临时单元,然后再分配一条乘法赋值指令
注意E.place存放的是整个乘法表达式计算结果的存放单元的名字/地址
对于
它的语义动作,先分配一个临时单元,然后交给place,将来这个临时单元用来存放求负这个运算的结果,我们把该临时变量的名字/地址存放在E.place里面。然后再生成一条求负赋值指令,这个求负赋值指令引用存放E1的单元的名字/地址进行求负运算,结果就存放在刚刚分配好的临时单元,临时变量当中。
如果刚刚分配的临时变量是T而赋值号右边的E1子表达式,它的计算结果单元是T1,那么实际上发射出来的指令就是T:=-T1
设计出来的语义动作是
E1.place用的是那个单元,我E就用哪个单元。
而对于E.code:=E1.code这条语义规则,当我们把(E1)归约到E,E的代码已经翻译完毕,到冲突文件的末尾了,而三地址代码就是新归约得到E的三地址代码,所以不需要生成新的代码,这里的语义动作就只有E.place:=E1.place
最后
说的是E的结果单元就是变量对应的单元,单独一个变量构成的表达式本身没有对应的三地址代码,所以说它的语义动作
就是通过调用lookup函数在符号表中查找变量标识符id,如果找到了lookup就会返回该标识符的入口,此时p就不空,那么就将id对应的单元或者是地址用E.place保存,这里是将p交给E.place保存。
如果lookup在符号表中,查找id找不到,就说明这个标识符没有定义,就需要报错了。
再总览一下
7.3.2 数组元素的引用
编译程序完成了把数组元素的引用,转换成对存储单元的访问这一工作,编译程序会对数组名和下标表达式列表进行分析和翻译,将目标程序/中间语言程序中插入计算出要访问的数组元素的地址的代码,这样,将来在实际执行目标代码的时候,就会执行插入的地址计算代码,从而得到数组元素的地址,进而按照该地址完成存储单元的访问。
下面我们来学习,带有数组元素引用的赋值语句的翻译:
蓝色的不变信息在数组声明的时候就确定了,因此在编译的时候处理完变量,在数组变量的声明语句就可以把蓝色部分计算出来,蓝色部分不变的值对于该数组的所有数组元素的引用都可以直接使用不变部分的计算结果。
可变部分随着数组元素的下标表达式的不同而不同,但是总可以按照红色部分公式按照相同的模式来计算,就是说可以对下标表达式列表进行分析,一遍分析,一遍累计计算公式中的可变部分,也就是红色部分。
在下面的处理中,我们进一步将蓝色不变部分分成两组,一部分是base,第二个和数组声明的维数,各维的上下界,元素占用空间的大小相关,但和数组元素引用的信息无关,我们把这一部分记为C,原来部分就是Base-C
首先我们给出语法规则,在赋值语句中,我们让允许标识符出现的地方都允许出现数组元素的引用,我们把标识符对应的简单变量和数组元素对应的代表的下标变量都统一为L,L可以由标识符来充当也就是简单变量,也可以由一个标识符加一个表达式列表Elist对应的是表达式列表,放在方括号中间,作为下标表达式列表,那么这个叫做数组元素的引用,我们有时候也叫做下标变量。
为了便于按照数组元素的公式进行计算,我们这个公式特别适合于从左到右不断累积运算,累计计算红色部分,对刚才语法规则进一步改造
L定义为Elist|id
Elist定义为Elist,E|id[E
这么改造的目的是为了方便红色部分的计算,使得我们每分析完一维下标表达式,也就得到了一个新的E,我就做一次归约,就扩展计算红色公式的一部分,下回Elist又有一个,E的时候,再做一次归约,根据刚刚计算出来的E,再扩展计算红色部分,那么Elist的第二个候选,就代表数组元素引用的开始,并且分析完了第一维的表达式E,L是当Elist碰到右括号,表示数组元素引用的结束。
因为我们要从左到右分析下标表达式列表,就要知道目前处理到第几维,我们用Elist.ndim来做计数器记录我们处理到了第几维。
Elist.place:保存临时变量的名字,这些临时变量存放已形成的Elist中的下标表达式计算出来的值
Elist.array:记录数组名查找数组相关的信息时就是用数组名来查找
举例:这里有8个产生式,前面的产生式都是原来简单的算数表达式或者赋值语句,L代表下标变量和简单变量,Elist代表下标表达式列表,下面我们将为每个产生式配上合适的语义动作,就可以得到翻译模式,这个翻译模式适合于自上而下的翻译模式结合在一起通过一遍扫描完成语法分析和翻译
如果L.offset不等于null,也就是L是个下标变量是个数组元素的引用,那么也生成一条三地址指令
赋值号的左边是个变值访问,跟刚刚是个变量的名字/地址不一样,是个变值访问,它是利用L.place记录的地址计算的不变部分和L.offset记录的可变部分做访问,访问到的单元就是接受E的结果的数组元素所对应的存储单元。
动作和原来简单表达式的一样的:
首先产生一个名词变量用来存放+运算结果,将该名词/变量的结果记录在E.place里面
然后再生成一条加法赋值三地址指令,这个赋值指令引用存放E1和E2的值的单元的名字或者是地址,将他们两个做加法,结果交给刚刚生成的临时变量E.place
动作也和原来一样
E对应的计算结果单元就是E1的结果单元,
E对应的三地址代码序列就是E1的三地址代码序列
不需要增加新的代码,只需要记录E.place:=E1.place
这个动作就要区分L是简单变量还是下标变量,要做分别处理
如果L是一个简单变量,E的存放结果的单元E.place就是L.place中保留的变量的名字或者是地址或者是简单变量在这个符号表中的入口,E.place就记录成L.place的值
如果L是一个下标变量,那么也先分配一个临时单元,交给E.place然后产生一个赋值指令,注意:赋值号的右边是一个变值访问,利用L.place记录的地址计算不变部分和L.offset记录的地址计算的可变部分做变值访问,访问的单元里面的内容送到刚刚分配给E的临时单元里面。
下面讲一下Elist单元所对应的语义动作
当用这个产生式做归约的时候,应该处理到了第一维的向量表达式
因此可以先将可变部分红色部分那里的计算算一下,因此它的语义动作就是把存放第一维计算结果的单元信息存放到Elist.place里面,这里回顾一下:Elist代表的是从左到右已经分析完的下标列表,Elist.place里面记录了根据分析完的下标列表,计算出来的可变部分前一阶段的值,这个值总是随着下标列表的不断的识别与分析,从左到右逐步的做乘法加法,再做乘法加法累计计算出来,我们总是把Elist前面的值乘上这一维的个数再加上这一维的表达式的值,按照这种模式,不断的像滚雪球一样,把可变部分的值计算出来,这是Elist.place的语义
然后将下标表达式的个数计数器Elist.ndim置上1,表示当前已经处理完了第一维,最后把数组名标识符id的信息记录在Elist.array里面,因为后面的下标表达式的翻译还需要根据数组名查询各维的信息,所以数组名这个符号归约完了之后,从栈里面弹出去了,我们要把数组名这个信息放到新规约后的Elist这个符号的array属性里面,让他代下去,这是数组元素饭呢西刚开始时候的语义动作。
随着分析的进行
假设处理到了中间的某一维,第m维
这个时候,前面已经分析完的结果都在Elist1里面,M分析完了后归约到了E,现在把Elist1,归约到Elist,那么这个产生式就表明继续识别后面各维的下标表达式,前面识别的若干维下标表达式都归约在了Elist1里面,现在又有一维表达式已经识别出来,记为E,那么对应到的公式,我们分析到了这个地方
前面的计算结果都有了,那么应该乘上这个E所在的第m维的长度再加上这个E自身的值,就得到了可变部分最新的结果,所以说语义动作是,首先分配一个临时变量t=newtemp
,这个变量就是用来记录地址计算的可变部分的最新的值,下面生成的几条指令,就是计算可变部分,首先 m=Elist1.ndim+1
根据下标计数器ndim计算出当前归约得到的E的维数,前面Elist有多少维在Elist1.ndim中,+1就是新归约后的E的维数,我们把它存在m变量里面,然后调用emit函数得到这个数组的第m维的个数emit(t':=' Elist1.place'*' limit(Elist1.array,m));
也就是第m维的下标个数或者它的长度,我们在这里面引用了Elist1里面的array属性所带来的数组名,根据这个数组名和我要查的现在这个E所对应的第m维,找到这一维的limit返回这一维的长度,这长度值就是nm,limit返回值就是nm,这个
nm乘以Elist1.place(公式里面的紫色部分:已经分析完的表达式的可变部分的计算结果单元),之前都归约成了Elist1,它的计算结果都在Elist1.place里面,也就是这个紫色部分。前面的结果乘上这一维的长度就计算到了nm之后,然后emit(t ‘:=’ t ‘+’ E.place);
这个结果放在t里面,t再加上新归约后的最新的下标表达式的值,它的值在E.place里面,就是Im的值加到t里面去,结果还是给t。这样t对应的临时变量里面就有了地址计算确定可变部分最新的值,这个值就是在分配的临时单元里面。
下面Elist.place=t;
把t交给新归约后的Elist.place,所以说在任何时候Elist.place属性都记录了一个单元的名字或者是地址,这个单元就是Elist所对应的分析完了的下标表达式列表计算出来的可变部分的最新结果
Elist.array=Elist1.array;
Elist.place=t;
Elist.ndim=m
将处理完的维数m更新到Elist.ndim属性中继续传递下去,同时也要把Elist1.array中记录的数组名信息传递给Elist.array这个属性,让它继续往下传递,因为当我把Elist1,E归约到新的Elist这个符号上的时候,Elist1,E都弹出栈了,他们的属性也变成不可访问,因此我必须要把Elist1.array记录的所有数组名传递到新归约后的Elist的属性里面。
随着一维一维的分析,我们最终总会碰到,把最后一维分析完,碰到右括号,这个时候用到第5条规则:
此时应该采取的语义动作是,先分配一个临时单元交给L.place记录下来,前面我们说过,L.place里面放的是不变部分的名词变量名字或者是地址,这里就是给他分配一个临时单元,下面就要生成计算不变部分的指令,不变部分的指令
Elist.array数组名,记录了这个base(首地址),而绿色部分,涉及到的是有多少维,每一维的下界上界元素的个数以及元素的宽度,这些信息在数组声明的时候就可以按照绿色的公式给他计算出来,存放到我们编译程序的常量C里面,因此这里发射出来的指令,是Elist.array-C,放到L.place刚刚分配的临时单元里面,这里面就是不变部分的最终结果了。下面处理可变部分,将可变部分分配的临时单元记录在L.offset里面。可变部分计算的大部分工作已经完成了,计算的结果是在Elist.place属性当作,下面产生一个乘法指令,将紫色部分与w相乘给分配的临时单元L.offset中,因此发射的这条指令emit(L.offset ‘:=’ w ‘*’ Elist.place)
就是可变部分最后的计算,最后的结果就在L.offset记录的临时变量当中,这个临时变量就具有了可变部分最终的结果。注意到这,数组元素可变计算的部分和不变部分的计算代码都已经生成完毕。最后在L.place当中指明了存放不变部分的最后结果的临时变量名字,L.place当中记录了存放可变部分的最后结果的临时变量名字
最后还有个L定义为简单变量id,所以就将L.place设置为id.place,L.offset置为空
7.3 类型转换
比之前的语义动作多了类型转换的处理:
首先建立一个临时变量用来存放加法运算的结果:我们把该临时变量的地址记录在E.place里面,然后检查操作数的类型,根据操作数的类型,来确定结果的类型,生成相应的代码,如果要进行类型转换,就需要添加类型转换相应的代码,此外还要注意,生成的代码中的运算符也要带上类型信息,具体看下如何根据E1,E2的类型来进行处理,如果E1是整型,E2也是整型,那么无需进行类型转换,直接产生整型+和赋值的指令,结果也是整型.
如果E1是实型,E2也是实型,那么无需进行类型转换,直接产生实型+和赋值的指令,结果也是实型.
复杂一点的是,如果E1是实型,E2是整型,那么需要对E2进行类型转换,先产生将E2的结果转换成实型,并且存放到刚刚分配的的临时单元的指令,再产生一个实型+赋值的指令,结果是实型.
如果上述四种情况均不满足就是类型错误。
7.4 布尔表达式的翻译
布尔表达式的两个基本作用:
- 用于逻辑演算,计算逻辑值;
- 用于控制语句的条件式.
产生布尔表达式的文法:
E→E or E | E andE | not E | (E) | i rop i | i
计算布尔表达式的两种方法
- 数值表示法
- 带优化的翻译法
7.4.1 数值表示法
关于布尔表达式的数值表示法的翻译模式
分配一个临时单元给or运算结果存放的地方,交给E.place属性记录。注意每个E都对应一个子布尔表达式,这个子布尔表达式的计算结果一定在一个存储单元中,这个存储的单元的地址就存放在E.place里面,然后在输出文件中发送一条这样的指令,一条定制指令,左边是刚建立的临时变量,右边是or连接的两个单元的地址,左边操作数的地址是E1.place,右边操作的地址在E2.place的两个语义变量里面。刚刚我们说了E1.place里面是两个临时单元的地址,放的是E1这一部分的子表达式的计算结果单元,E2.place里面这个单元是E2所对应的bool表达式的计算结果单元
我们可以看一下,如果我们对应的是一个自上而下的分析,当用产生式E → E1 or E2
做归约的时候,栈里面已经有了E1,E2这两个符号,这两个符号都已经分析完了,也翻译完了,那么E1所对应的代码和E2所对应的代码已经发送到了输出文件当中,而且E1.place里面,假设我们记录的临时单元是T1,E2.place里面记录的是T2,这个时候,我们要把E1和E2用产生式归约成E,那么我们就要执行刚刚说的语义动作,第一个产生一个临时单元E.place=T,然后第二个动作就发射一条指令,注意,这个E.place=T,E1.place=T1,E2.place=T2,所以说发射这一条指令,就是这种形式:T:=T1 or T2
这就是或运算产生式对应的语义动作。
类似的给出其他产生式的语义动作:
前面给出了翻译运算的例子,每个翻译运算给出了四条指令,现在对照这个例子我们设计语义动作来翻译这些指令:注意我们现在要写语义子程序,能够把关系运算翻译成四条指令。
如果左操作数和由操作数存在这样一个关系运算的话,goto nextstat+3
把0给刚刚生成的临时变量,也就是条件不满足
跳过goto语句的下一条指令,到达下下条指令
E→id 这样的表达式无需生成计算指令,甚至无需为其准备存放结果的临时单元,因为bool变量id的值就是整个构成的bool表达式E的值,所以bool表达式E的结果存放单元就是变量id的存储单元。
举例
将布尔表达式a<b or c<d and e<f
翻译成三地址指令
首先将a<b按照关系运算到bool表达式的定义归约为E,就会执行,你用这个产生式E→id1 relop id2
做规约就执行相应的语义动作。首先产生一个临时单元T1就记录在E.replace里面,然后连续发送四条指令,假设我们指令的编号是从100开始,发送的第一条指令if a<b goto 103
接着把or移进来,把c<d移进来,对他们进行相应的归约到E,又用这个产生式做归约,归约就得执行相应的语义动作,给他分配临时单元T2就记录在E.replace里面,然后连续发送四条指令,我们指令的编号现在是从104开始,发送的第一条指令if a<b goto 107
接着把and移进来,e<f移进来,归约到E,又用这个产生式做归约,归约就得执行相应的语义动作,给他分配临时单元T3就记录在E.replace里面,然后连续发送四条指令,我们指令的编号现在是从108开始,发送的第一条指令if a<b goto 111
,在连续发送三条指令。
接着这个时候栈里面有E or E and E,这个时候语法分析栈告诉我,得把E and E归约到一个更大的E,用的是最后一个产生式,执行相应的语义动作,产生一个临时单元T4就记录在归约后的E.replace里面,然后发射一条指令,emit(E.place ‘:=’ E1.place ‘and’ E2.place)
同样,现在栈里面有两个E再加上or的运算,把他们归约为E,用第三条产生式,执行相应的语义动作,产生一个临时单元T5就记录在归约后的E.replace里面,然后发射一条指令,emit(E.place ‘:=’ E1.place ‘or’ E2.place)
7.4.2 作为条件控制的布尔式翻译 (带优化的翻译法)
如果计算部分子表达式就能确定整个表达式的值的时候,那么其余的表达式就不需要计算了。
如果A是真,整个表达式就是真,就不用再算B了,而数值表达式需要把两个都计算出来。
如果A是假的话,B的结果就是整个布尔表达式的结果。
有带优化,B并不是总是都要算。
如果A成立,B的结果决定了整个布尔表达式的结果,如果A是假的话,B就不用算了,整个逻辑表达式确定为假。
逻辑或运算 E→E1 or E2
E有两个属性,E1.true和E.false,分别放着的是E为真的跳转目标标号和E为假的跳转目标标号,如果E的标号都确定了
那么E1为真应该去整个表达式为真的地方。
①E1.true:=E.true;
E1为假应该去哪里,E1为假应该跳转到将来要执行E2的代码,所以为E1.false产生一个新标号,将来这个标号就像这里一样要放在E2代码之前
② E1.false:=newlabel;
E.code:=E1.code || gen(E1.false ‘:’)
|| E2.code
E2.true代表着E2为真的时候应该去整个表达式为真的地方
③E2.true:=E.true;
所以E2.true就直接引用E1.true的标号就行了
E2.false应该去整个布尔表达式为假的地方,引用E.false的标号
④ E2.false:=E.false;
整个或的运算构成更大的这个E的代码应该是由E1的代码或之前的代码和或之后E2的代码拼接而成,注意E2的代码之前放上E1.false的标号,这样就把代码的跳转关系串接起来,gen发射一个标号,形成一个字符串放在输出文件里面,放在E2的开头,E1的结尾
逻辑与运算E→E1 and E2
类似的给出and运算
的语义规则
E1为假的时候E2就不用算了,跳到整个bool表达式为假的地方,E1为真的时候倒是要执行E2,E2的结果决定了到底是去整个表达式为真的地方还是去整个表达式为假的地方。
前四条计算E1和E2的继承属性,最后一条计算新得到的更大的语法单位E的综合属性code:
E1.true:=newlabel;
E1为真去的地方用一个新的标号来标记,将来这个标号放在E2的开头
E1.false:=E.false;
E1为假的去整个bool表达式为假的地方
E2.true:=E.true;
E2为真的去整个bool表达式为真的地方
E2.false:=E.fasle;
E2为假的去整个bool表达式为假的地方
E.code:=E1.code || gen(E1.true ‘:’) || E2.code
整个或的运算构成更大的这个E的代码应该是由E1的代码和E2的代码拼接而成,注意E2的代码之前放上E1.true的标号来标记,这样E1为真的时候,将来引用标号跳转就跳转到了E2的开头
逻辑非运算E→not E1
E1.true:=E.false;
E1为真去整个布尔表达式为假的目标
E1.false:=E.true;
E1为假去整个布尔表达式为真的目标
E.code:=E1.code
E的代码就是E1的代码
跳转目标互换就执行了逻辑非运算的效果
括号表达式E→ (E1)
E1.true:=E.true;
E1.false:=E.false;
E.code:=E1.code
括号内表达式E1的计算结果就是整个带括号的表达式E的计算结果,所以括号内的表达式E1为真或者是为假要去的目标就是,带括号的整个表达式E为真或者是为假要去的目标,而这些目标分别放在了E.true和E.false里面
关系运算E→id1 relop id2
现在给出关系运算表达式所对应的语义规则:
E→id1 relop id2
E.code:=gen(‘if ’ id1.place relop.op id2.place ‘goto’ E.true) || gen(‘goto’ E.false)
整个关系运算布尔表达式代码就是两条指令:
第一条,产生为真的跳转,注意,跳转目标引用到的是E的继承属性E.true(条件为真应该去的地方)
否则的话,就会 gotoE.false 从这个出口出去,因此一个关系运算,产生两条指令
常量条件表达式E→true/E→false
直接跳转到真/假
E→true
E.code:=gen(‘goto’ E.true)
E→false
以上是关于六万字长文!让你懂透编译原理——第七章 语义分析和中间代码产生的主要内容,如果未能解决你的问题,请参考以下文章
从零开始学自然语言处理-十万字长文带你深入学习自然语言处理全流程
面试官一上来就问我Chrome底层原理和HTTP协议(万字长文)
半万字长文学习 MySQL 主从复制原理,面试必问,建议收藏!