第8章 语法制导翻译和中间代码生成
编译中的语义处理两个功能:
第一,审查每个语法结构的静态语义,即验证语法结构合法的程序是否真正有意义。把这个工作称为静态语义分析或静态审查。
第二,如果静态语义正确,语义处理则要执行真正的翻译,即,要么生成程序的一种中间表示形式(中间代码),要么生成实际的目标代码。
所谓中间代码,也称中间语言。
为什么要生成中间代码?一般,快速编译程序直接生成目标代码,没有将中间代码翻译成目标代码的额外开销。但是为了使编译程序结构在逻辑上更为简单明确,常采用中间代码,并且可以在中间代码一级进行优化工作使得代码优化比较容易实现。
首先简单介绍属性文法。
属性,常用以描述事物或人的特征、性质、品质等等。如,谈到一个物体,可以用“颜色”描述它,谈起某人,可以用“有幽默感”来形容他。对编译程序使用的语法树的结点,可以用“类型”、“值”或“存储位置”来描述它。
8.1 属性文法
形式上讲,一个属性文法是一个三元组, A=(G,V,F),
其中:G为上下文无关文法,V为属性的有穷集;F为关于属性的断言或谓词的有穷集
如,有文法G为:
E→T1+T2|T1 or T2
T→num|true|false
因为T在同一个产生式里出现了两次,使用上脚标区分它们。
对输入串3+4的语法树如图8.1(a)。

属性文法记号中常使用N.t的形式表示与非终结符N相联的属性t。如可把完成对上面表达式的类型检查的属性文法写成图8.2的形式。
与非终结符E的产生式相联的断言指明:两个T的属性必须相同。
E→T1+T2{ T1.t=int AND T2.t=int}
E→T1 or T2{ T1.t=bool AND T2.t=bool}
T→num{T.t:=int}
T→true{T.t:=bool}
T→false{T.t:=bool}
属性可分为:
综合属性和继承属性:(p170)
若A →∂ 为一产生式,任一规则形如:b=f(c1,c2,…,ck)
其中,f 为一函数,c1,c2,…,ck,b为非终结符的属性。
(1)若b为A的属性,且c1,…,ck是产生式右边文法符号的属性或A的其它属性,则b称为综合属性。
(2)若b为产生式右边某个文法符号X的属性,且c1,…,ck是A的属性或产生式右边任何文法符号的属性,则b称为继承属性。
无论哪种情况,我们都称属性b依赖于属性c1,c2,…,ck
在编译的许多应用中,属性和断言以多种形式出现,即,与每个文法符号相联的可以是各种属性、断言、以及语义规则,或者某种程序设计语言的程序段等。再给出一些例子。
例8.1 简单算术表达式求值的语义描述。
产生式 语义规则
(0) L→E print(E.val)
(1)E→E1+T E.val:= E1.val+T.val
(2)E→T E.val:=T.val
(3)T→T1*F T.val:= T1.val*F.val
(4)T→F T.val:=F.val
(5)F→(E) F.val:=E.val
(6)F→digit F.val=digit.lexval
例8.2 说明语句中各种变量的类型信息的语义规则。
产生式 语义规则
(1)D→TL L.in:=T.type
(2)T→int T.type:=integer
(3)T→real T.type:=real
(4)L→L1,id L1.in:=L.in
addtype(id.entry,L.in)
(5)L→id addtype(id.entry, L.in)
其中,过程addtype的功能是把每个标识符的类型信息登录在符号表中相关项中。
例8.2 说明语句中各种变量的类型信息的语义规则。
产生式 语义规则
(1)D→TL L.in:=T.type
(2)T→int T.type:=integer
(3)T→real T.type:=real
(4)L→L1,id L1.in:=L.in
addtype(id.entry,L.in)
(5)L→id addtype(id.entry, L.in)
其中,过程addtype的功能是把每个标识符的类型信息登录在符号表中相关项中。

8.2 语法制导翻译概论
语法制导的翻译:
语法制导的翻译的过程:对输入单词串进行语法分析,构造语法树,然后根据需要构造属性依赖图,遍历语法树并在结点上按语义规则进行计算。
8.2.1 计算语义规则
如何计算这些语义规则?P172
8.2.2 S-属性文法和自下而上翻译
S-属性文法:只含有综合属性的文法, S-属性文法的翻译器通常可以借助LR分析器实现。
再分析:
例8.1 简单算术表达式求值的语义描述。
产生式 语义规则
(0) L→E print(E.val)
(1)E→E1+T E.val:= E1.val+T.val
(2)E→T E.val:=T.val
(3)T→T1*F T.val:= T1.val*F.val
(4)T→F T.val:=F.val
(5)F→(E) F.val:=E.val
(6)F→digit F.val=digit.lexval

语法制导翻译的具体实现并不困难。假定有一个LR语法分析器,现在把它的分析栈扩充,使得每个文法符号都跟有语义值,即把栈的结构看成图8.5所示那样。

同时把LR分析器的能力扩大,使它不仅执行语法分析任务,且能在用某个产生式进行归约的同时,调用相应的语义子程序,完成在例8.1的属性文法中描述的语义动作。每步工作后的语义值保存在扩充的分析栈里“语义值”栏中。采用的LR分析表见图8.6,其中使用d代替digit。

分析和计值2+3*5的过程列在图8.7中。

8.2.3 L-属性文法在自上而下分析中的实现
L-属性文法:若每一个产生式 A →X1 X2 ……Xn ,其中每个语义规则的每个属性或者是综合属性,或者是Xj的继承属性,且这个继承属性仅依赖于:
(1) Xj在产生式左边符号X1 X2 ……Xj-1的属性
(2) A的继承属性
例8.3 将中缀表达式翻译成后缀表达式的文法
E →E addop T print(addop.Lexeme)
E →T
T →num print(num.val)
LR方法分析:2+3-5 见p175图8.8
采用LL(1)方法,如何分析?
因上述文法为左递归,必须改写文法。改写为:
E →TR
R → addop T R | εT →num
为了用LL(1)方法翻译成后缀表达式,应添加如下语义动作
E →TR
R → addop T {print(addop.Lexeme)} R | ε
T →num{ print(num.val) }
LL(1)分析2+3-5 [p175图8.10]
注意,上面是在文法产生式右端的某些位置插入了一些语义动作,用{ }括起来。这种方式称为翻译模式。
8.2.4 L-属性文法在自下而上分析中的实现
L-属性文法涉及到计算继承属性,在自下而上分析中如何计算?
两种办法:从翻译模式中去掉嵌入在产生式之间的动作, 用综合属性代替继承属性。
例如:方法一(翻译模式)
E →TR
R → + T {print(‘+’)} R
R → - T {print(‘-’)} R
R → e
T →num{ print(num.val) }
改为翻译模式:
E →TR
R → + T M R
R → - T N R
R → e
T →num{ print(num.val) }
M → e{print(‘+’)
N → e{print(‘-’)
方法二(用综合属性代替继承属性),须改变文法
例如:pascal语言的类型说明
D →L:T
T →integer | char
L →L, id|id
L只能从第一个产生是的右边T中继承其类型,该文法不可能得到L-属性文法
怎样办?改写文法,用综合属性代替继承属性。
D →id T
L →,id L|:T
T →integer | char
可构造如下属性文法:
D →id L {addtype(id.entry,L.type)}
L →,id L{addtype(id.entry,L.type),L.type:=L.type}
L → :T{L.type:=T.type}
T →integer {T.type:=int}
T → char{T.type:=int}
8.3 中间代码的形式
中间代码常用的形式有:逆波兰式、三元式、四元式和树形表示。
8.3.1 逆波兰式记号
最简单的一种中间代码表示形式。用其表示的表达式也称作后缀式。
原则:运算法的顺序与中序相同;
运算符紧跟在运算数后面。
图给出了程序设计语言中的简单表达式和赋值语句相应的逆波兰式表示形式:

其最大优点是最易于计算机处理表达式。利用一个栈,自左至右扫描算术表达式(后缀形式)。每碰到 运算对象,就把它推进栈;碰到运算符,若该运算符是二目的,则对栈顶部的两个运算对象实施该运算,并将运算结果代替这两个运算对象而进栈。若是一目运算 符,则对栈顶元素执行该运算,并以运算结果代替该元素进栈。最后的结果留在栈顶。
如 B@CD*+(它的中缀表示为-B+C*D,使用@表示一目减)的计算过程为:
1、B进栈;
2、对栈顶元素施行一目减运算,并将结果代替栈顶,即-B置于栈顶;
3、C进栈;
4、D进栈;
5、栈顶两元素相乘,两元素退栈,相乘结果置栈顶;
6、栈顶两元素相加,两元素退栈,相加结果进栈,现在栈顶存放的是整个表达式的值。
后缀式特别适用于解释执行的程序设计语言的中间表示,也方便具有堆栈体系的计算机的目标代码生成。
逆波兰表达式的扩展:
逆波兰表示很容易扩充到表达式以外的范围。
例如:GOTO L写为“L jump”,运算对象L为语句标号,运算符jump表示转到某个标号。
再如条件语句iF E then Sl else S2可表示为:ESlS2¥,把if-then-e1se看成三目运算符,用¥来表示。
又如数组元素A[<下标l>,…<下标n>可表示为:
<下标1><下标2>……<下标n>A subs,运算符Subs表示求数组的下标。
8.3.2三元式和树形表示
三元式表示:
把表达式及各种语句表示成一组三元式。每个三元式三个组成部分是:算符op,第一运算对象ARGl,和第二运算对象ARG2。如,a:=b*c+b*d的表示为:
(1)(* b, c)
(2)(* b, d)
(3)(+ (1), (2))
(4)(:= (3), a)
与后缀式不同,三元式中含有对中间计算结果的显式引用,比如三元式(1)表示的是b*c的结果。三元式(3)中的(1)和(2)分别表示第一个三元式和第二个三元式的结果。
对于一目运算符op,只需选用一个运算对象,不妨规定只用ARG1。至于多目运算符,可用若干个相继的三元式表示。
树形表示是三元式表示的翻版。上述三元式可表示成下面的树表示:

表达式的树形表示的实现:简单变量或常数的树就是该变量或常数自身,如果表达式el和e2的树分别为Tl和T2,那么e1+e2,e1*e2,-e1的树分别为:

二目运算符对应二叉子树,多目运算对应多叉子树,但为了便于安排存储空间,一棵多叉子树可以通过引进新结点而表示成一棵二叉子树。
8.3.3 四元式
四元式是一种比较普通采用的中间代码形式。四元式的四个组成成分是:
(op,ARG1,ARG2,RESULT)
运算对象和运算结果有时指用户自己定义的变量,有时指编译程序引进的临时变量。
例如a:=b*c+b*d的四元式表示如下:
(1)(*,b,c,t1)
(2)(*,b,d,t2)
(3)(+, t1, t2, t3)
(4)(:=, t3,-,a )
四元式和三元式的主要不同在于,四元式对中间结果的引用必须通过给定的名字,而三元式是通过产生中间结果的三元式编号。也就是说,四元式之间的联系是通过临时变量实现的。
有时,为了更直观,也把四元式的形式写成简单赋值形式或更易理解的形式。上述四元式序列写成:
(1) t1:=b*c
(2) t2:=b*d
(3) t3:=t1+t2
(4) a:=t3
把(jump,-,-,L)写成goto L
把(jrop,B,C,L)写成if B rop C goto L
8.4 简单赋值语句的翻译
四元式中的ARG1、ARG2和RESULT是什么?在实际实现中,它们或者是一个指针,指向符号表的某一登录项,或者是一个临时变量的整数码。
为了进一步讨论,我们引入:
- • id表示的语义变量id.name;
- • 用Lookup(id.name)表示审查id.name是否出现在符号表中,如在,则返回一指向该登录项的指针,否则返回nil;语义过程emit表示输出四元式到输出文件;
- • 语义过程newtemp表示生成一临时变量,每调用一次,生成一新的临时变量;
- • 语义变量E.place,表示存放E值的变量名在符号表的登录项或一整数码(若此变量是一个临时变量)。
考虑翻译:

- • 类型转换:实际上,在一个表达式中可能会出现各种不同类型的变量或常数,而不是像图8.12中的id假定为都是同一类型。也就是说,编译程序还应执行这样的语义动作:对表达式中的运算对象应进行类型检查,如不能接受不同类型的运算对象的混合运算,则应指出错误;如能接受混合运算,则应进行类型转换的语义处理。
- • 为进行类型转换的语义处理,增加语义变量,用E.type表示E的类型信息,E.type的值或为int或为real,此外,为区别整型加(乘)和实型加(乘),把+(*)分别写作+i(*i)和+r(*r)。用一目算符itr表示将整型运算对象转换成实型。
8.5 布尔表达式的翻译概论
程序设计语言中的布尔表达式有两个作用:
.计算逻辑值
.用做改变控制流语句中的条件表达式
布尔表达式的形式为E1 rop E2,其中E1和E2都是算术表达式,rop是关系符。为简单起见,只考虑如下文法生成的布尔表达式。
E→E and E|E or E|not E|id rop id|true|false按通常习惯,约定布尔算符的优先顺序(从高到低)为not、∧、∨,且∧和∨服从左结合。
8.5.1 布尔表达式的翻译方法
通常,计算布尔表达式的值有两种办法,第一种办法,如同计算算术表达式一样.步步计算出各部分的真假值,最后计算出整个表达式的值。例如,用数值1表示true.用0表示false。那么布尔表达式1 or (not 0 and 0)or 0的计算过程是:
1 or (not 0 and 0)or 0
=1 or (1 and 0)or 0
=1 or O or 0
=1 or 0
=1
另一种计算方法是采取某种优化措施,只计算部分表达式,例如要计算A or B,若计算出A的值为1,那么B的值就无需再计算了,因为不管B的值为何结果,A or B的值都为1。
上述两种方法对于不包含布尔函数调用的表达式是没有什么差别的。但是,假若一个布尔式中会有布尔函数调用,并且这种函数调用引起副作用(如有对全局量的赋值)时,这两种方法未必等价。
若按第一种办法计算布尔表达式,
布尔表达式 a or b and not c 翻译成四元式序列为:
(1)t1:=not c
(2)t2:=b and t1
(3)t3:=a or t2
对于象 a<b 这样的关系表达式,可看成等价的条件语句if a<b then 1 else 0,它翻译成的四元式序列为:
(1)if a<b goto (4)
(2)t:=0
(3)goto (5)
(4)t:=1
(5)…
其中用临时变量t存放布尔表达式a<b的值,
图8.11给出了按第一种办法将布尔表达式翻译成四元式的描述,其中nextstat给出在输出序列中下一四元式序号。emit过程每被调用一次,nextstat增加1。

8.5.2 控制语句中布尔表达式的翻译
现在讨论if-then; if-then-else和while-do等语句中的布尔表达式E的翻译。
这三种语句的语法为:
S→if E then S1
|if E then S1 else S2
|while E do S1
这些语句的代码结果示意分别在图8.12(a)(b)(c)中。两个出口分别用于表示E为真和假时控制流向的转移。分别叫真出口和假出口。

作为条件转移的E,仅把E翻译成代码是一串条件转移和无条件转移四元式。翻译的基本思路是:
(1)对于E为a rop b的形式生成代码为:
if a rop b goto E.true
goto E.false
其中,使用E.true和E.false分别表示E的“真”“假”出口转移目标。
(2)对于E为E1 or E2的形式,若E1 为真,则可知道E为真即E1的真出口和E的真出口一样。
如果E1为假,那么必须计算E2,E1的假出口应是E2代码的第一个四元式标号,这时E2的的真出口和假出口分别与E的真出口和假出口一样。
(3)类似的考虑E1 and E2的情形。
(4)┐E1的翻译只需调换E1的真假出口即可
布尔表达式a<b or c<d and e>f 翻译为如下四元式序列:
(1) if a<b goto E.true
(2) goto 3
(3) if c<d goto 5
(4) goto E.false
(5) if e>f goto E.true
(6) goto E.false
当然生成的四元式显然不是最优的,如(2)是不需要的。这种问题可留待代码优化阶段解决。
在例8.5中,我们使用E.true和E.false分别表示整个表达式a<b or c<d and e>f 的真、假出口,而E.true和E.false的值并不能在产生四元式的同时就知道的。为了看清这一点,我们把该表达式放在条件语句中考虑,如语句:
if a<b or c<d and e>f then S1 else S2的四元式序列为
(1)if a<b goto (7) /* (7)是整个布尔表达式的真出口 */
(2) goto (3)
(3) if c<d goto (5)
(4) goto (p+1) /* (p+1)是整个布尔表达式的假出口 */
(5) if e>f goto (7)
(6) goto (p+1)
(7) (关于S1的四元式)
…
(p) goto (q)
(p+1) (关于S2的四元式)
…
(q)
上述四元式(1)和(5),(4)和(6)的转移地址并不能在产生这些四元式时得知。例如(1)和(5)的转移地址是在整个布尔表达式的四元式产生完后才得知。因此要回填这个地址。
为了记录需回填地址的四元式,常采用一种“拉链”的办法。把需回填E.true的四元式拉成一链,把需回填E.false的四元式拉成一链,分别称做“真”链和“假”链。拉链的方式是这样的,若有四元式序列:
(10) … goto E.true
…
(20) … goto E.true
…
(30) … goto E.true
则链成 : (10) … goto (0)
…
(20) … goto (10)
…
(30) … goto (20)
地址(30)称作链首,0为链尾标志,即地址(10)为链尾。
- • 我们仍使用E.true和E.false,不过现在分别表示“真”链和“假”链;
- • 用neststat指向下一个四元式的地址,emit的用法也同上.
- • 两个函数:合并merge和回填backpatch 。
merge(p1,p2),把p1和p2为链首的两条链合并为1条,返回合并后的链首值。即将p2的链尾第四区段改为p1,合并后的链首为p2,除非p2是空链。
Bachpatch(p,t),把p所链接的每个四元式的第四区段都填为t。
其中使用语义值codebegin与非终结符E相连,表示表达式E的第一个四元式语句序号。
merge(p1,p2),把p1和p2为链首的两条链合并为1条,返回合并后的链首值。即将p2的链尾第四区段改为p1,合并后的链首为p2,除非p2是空链。
Bachpatch(p,t),把p所链接的每个四元式的第四区段都填为t。
其中使用语义值codebegin与非终结符E相连,表示表达式E的第一个四元式语句序号。

(5) E→id1 rop id2 { E.true:= nextstat;
E.codebegin:= nextstat;
E.false:= nextstat+1
emit(if id1.place rop id2.place goto -);
emit(goto - ) .
(6) E→true { E.true:= nextstat; (7)E→false { E.false:= nextstat;
E.codebegin:= nextstat; E.codebegin:= nextstat;
emit(‘goto’- ) } emit(‘goto’- ) }
根据上述语义动作的描述,当整个布尔表达式所相应的四元式全都产生之后,做为整个表达式的真、假出口(转移目标)仍没填上。它们分别由“真”链和“假”链E.true和E.false记录。
我们仍以表达式a<b or c<d and e<f 为例,将分析产生语法树时的语义动作结果“真”“假”链情况注释在相应结点处,如图8.17所示(注意,上述布尔表达式文法时二义的。图8.17语法树是借助于关系算符的优先级形成的)。
a<b or c<d and e<f语法树:

在归约a<b到E(使用产生式(5))时,产生2个四元式
100:if a<b goto –
101: goto –
这里我们假定四元式编号从100开始即nextstat的初值为100。这时E.codebegin为100。E.true和E.false的值分别为{100}和{101}。
由c<d 归约到E时产生四元式
102: if c<d goto –
103: goto –
这时,E.true和E.false的值分别为{102}和{103}。
继续由产生式(5)把e<f归结成E时产生四元式
104: if e<f goto –
105: goto –
现在用E→E1 and E2归约,完成的一个语义动作将是backpatch({102},104)(E2.codebegin将为104)。另外一个语义动作是merge({103},{105}),即合并E1 和 E2的“假”链,于是产生的6个语句是:
100:if a<b goto –
101: goto –
102: if c<d goto –
103: goto –
104: if e<f goto –
105: goto 103
最后由E→E1 or E2归约,实现backpatch({101},102),实现merge({100},{104})即合并E1,E2的“真”链等产生的四元式为:
100:if a<b goto
101: goto 102
102: if c<d goto 104
103: goto –
104: if e<f goto 100
105: goto 103
“真”链首E.true为104
“假”链首E.false为105
一旦整个布尔表达式的真假出口确定后,则可沿“真”“假”链为相应的四元式填上。如果布尔表达式是做为if-then-else语句的条件表达式,那么当分别扫描到then和else之后,便可回填真假出口。
8.6控制结构的翻译
8.6.1 条件转移
考虑if-then,if-the-else和while-do语句,在图8.12中已给出了它们的代码结构。这里我们着重观察在翻译中使用“回填”和“拉链”技术。
一般使用下面文法G[S]定义这些语句:
G[S]
(1) S→if E then S
(2) | if E then S else S
(3) | while E do S
(4) | begin L end
(5) | A
(6) L→L;S
(7) |S
其中个非终结符号的意义是:
S——语句
L——语句串
A——赋值句
E——布尔表达式
回想在讨论布尔表达式的翻译时,使用:
(1)E.true和E.false分别指待回填“真”、“假”出口的四元式串。
(2)对于条件语句if E then S1 else S2,在扫描到then时才能知道E的“真”出口,而E的“假”出口只有处理了S1之后,到达else时才明确。
另外,E为真时,S1语句执行完成意味着整个语句也已执行完毕,因此应在S1之后产生一无条件转指令,将控制离开整个if-the-else语句。但在完成S2的翻译之前,该无条件转的转移目标无法知道。
对于语句嵌套的情况,如下面的语句:
if E1 then if E then S1 else S2 else S3
在翻译完S2之后,S1后的无条件转移目标仍无法确定,因此它不仅要跨越S2还应跨越S3。
因此,转移目标的确定和语句所处的环境密切相关。
这样我们让非终结符S(和L)含有一项语义值S.CHAIN(和L.CHAIN)。也是一条链,它把所有那些四元式串在一起,这些四元式期待在翻译完S(L)之后回填转移目标。真正的回填工作将在处理S的外层环境的某一适当时候完成。
为了能及时地回填有关四元式串的转移目标,我们对G[S]文法进行改写,改成G′[S]
G′[S]
(1) S→CS1
(2) | TpS2
(3) | WdS3
(4) | begin L end
(5) | A
(6) L→LsS1
(7) | S2
(8) C→if E then
(9) Tp→C S else
(10) Wd→W E do
(11) W→while
(12)Ls→L;
C→if E then /* C—— 一条件子句*/
{ backpatch(E.true,nextstat)
C.CHAIN:=E.false }
S→CS1
{ S.CHAIN:=merge(C.CHAIN,S1.CHAIN )
Tp→C S else /* Tp—— 真部子句*/
{ q:=nextstat
emit(‘GOTO’-)
backpatch(C.CHAIN,nextstat)
Tp.CHAIN:=merge(S.CHAIN,q) }
S→TpS2
{ S.CHAIN:=merge(Tp.CHAIN,S2.CHAIN )
下面将给出这个文法的各个产生式相应的语义动作。
W→while { W.codebegin:=nextstat }
Wd→W E do
{ backpatch(E.true,nextstat)
Wd.CHAIN:=E.false
Wd.codebegin:=W.codebegin }
Ls→L; { backpatch(E.true,nextstat) }
S→WdS3 { backpatch(S3.CHAIN, Wd.codebegin)
emit(‘GOTO’ Wd.codebegin)
S.CHAIN:= Wd. CHAIN }
S→begin L end { S.CHAIN:=L.CHAIN }
S→A { S.CHAIN:=0 /* 空链 */ }
L→LsS1 { L.CHAIN:= S1.CHAIN }
L→S { L.CHAIN:= S.CHAIN }
语句while(A<B) do if(C<D) then X:=Y+Z将被翻译成如下的一串四元式:
100 if A<B goto 102
101 goto 107
102 if C<D goto 104
103 goto 100
104 X:=Y+Z
105 X:=T
106 goto 100
107
8.6.2 开关语句
开关语句(case语句或switch语句)是很多程序设计语言中都有的,方式不尽相同,甚至FORTRAN中的计算GOTO和赋值GOTO也可看做是一种开关语句。我们假定要讨论的开关语句的形式为:
switch E of
case V1:S1
case V2:S2
…
case Vn-1:Sn-1
default:Sn
end
这里的E是一个表达式。开关语句是分情况选择机制,在E被计算之后,测试它的值符合哪种case中的值,而执行和该值相关的语句,并做相应的转移。如果E的值不能与任何Vi(1≤i≤n-1)匹配,便执行“default”时的语句。
直观上,case语句所翻译成如下的一连串语句。
t:=E;
L1: if t≠V1 goto L2;
S1;
Goto next;
L2 : if t≠V2 goto L3;
S2;
Goto next;
…
Ln-1: if t≠Vn-1 goto Ln;
Sn-1;
Goto next;
Ln: Sn;
Next;
也可以按照另外一种方式:
 
switch E of
case V1:S1
case V2:S2
…
case Vn-1:Sn-1
default:Sn
end
翻译为图8.15中的中间代码的过程大致如下:
(1)当看见关键字switch时,产生新的标号test、next和一个临时变量t。
(2)在分析产生式E时,产生计算E值的代码,并把E的结果放到临时变量t中.
(3)见到E后的of则产生四元式 goto test。
(4)每当看见关键字case时,则产生一个标号Li,填进符号表中,把它在符号表中的位置(不妨假定为Pi)连同case语句后的Vi值,即(Vi,Pi)存放于一存储区(用QUEUE)中;接着,按通常方式产生相应的语句Si 的代码,紧跟在Si之后是 goto next 代码。 (5)当看见关键字end后,则能够产生形成n个分支的(对t的测试)代码了。
一般在翻译开关语句的分支的代码时,常常将
(if t=Vi goto Li)写成行如(case,Vi,Li,-)四元式形式。而将四元式(label,next,-,-)加在该四元式序列最后,即形成如下序列:
(case,Vi,Li,-)
(case,V2,L2,-)
…
(case,Vn-1,Ln-1,-)
(case,Vn,Ln,-)
(label,next,-,-)
用case做为四元式操作码的目的在于提示目标代码生成程序对它进行特别处理(优化)。
8.6.3 for循环语句
除了while-do语句外,很多程序设计语言具有下面形式的循环语句:
for i:=E1 step E2 until E3 do S1
我们按ALGOL的意义来翻译这种循环句。为了简单起见,假定E2总是正的。在这种假定下,上述循环句的ALGOL意义等价于:
i:= E1;
goto OVER;
AGAIN: i:=i+ E2;
OVER: if i≤E3 then
begin S1; goto AGAIN end;
注意,在这段程序中有几处用到循环控制变量i,因此,ENTRY(i)必须被保存下来。为了按上述顺序产生四元式,必须改写文法。为此,我们使用如下的产生式:
F1→for i:= E1
F2→F1 step E2
F3→F2 until E3
S→F3 do S1
下面是这些产生式相应的语义动作:

F3→F2 until E3
{ F3.codebegin:= F2.codebegin; q:=nextstat;
emit(‘if’F2.PLACE,‘≤’, E3.PLACE,‘goto’ q+2);
/* 若i≤E3转去执行循环体的第一个四元式 */
F3.CHAIN:=nextstat;
emit(‘goto’-); /* 转离循环*/ }
S→F3 do S1
/* 这里是语句S1的相应代码 */
{ emit(‘goto’ F3.codebegin); /* goto AGAIN */
backpatch(S1.CHAIN, F3.codebegin);
S.CHAIN:= F3.CHAIN /* 转移目标留待处理外层S时回填*/ }
例如,循环语句
for I:=1 step 1 until N do M:=M+1
将被翻译成如下的四元式序列:
100 I:=1
101 goto 103
102 I:=I+1
103 if I≤N goto 105
104 goto 108
105 T:=M+1
106 M:=T
107 goto 102
108
有些语言中,for语句的语法和语义的一些细节与上述不同,在翻译时要予以考虑。编译程序必须考虑何时生成循环参数,何时它使用。而Pascal这样的语言中,循环变量在循环外也是可见的,显然编译的处理不同。又比如,有的语义规定,循环步长和循环终值不得在循环体中改变,这样的for语句必须解释为:
i:= E1;
incr:= E2;
incr:=E3;
goto OVER;
AGAIN: i:=i+ incr;
OVER: if i≤limit then begin S; goto AGAIN end;
其中,incr和limit是编译程序为翻译该循环语句引进的两个变量。这种解释下,每循环一次,E2和E3都不重新计值。如下面的for语句将重复10次执行A:=3。
A:=10;
For i:=1 step 1 unti A do A:=3
8.6.4 出口语句
出口语句是指exit(如Ada语言中的)或break(如C语言),是一种结构化的方式跳出循环而设置的语句,它们的作用是引起内层循环的终止。
(1)结束内循环,跳到外循环
(2)指定跳到的外循环
Ada中的exit可指明欲终止的循环名(缺省名是最内层循环)。不管哪种语言的出口语句,都要翻译成一转移语句,转移的目标是在break(exit)语句之后的循环外才能确定。
FOR …
…
EXIT
…
ENDFOR
因此编译中也一定使用回填技术才能给出转移目标。
但Ada语言的exit语句指明跳出的循环可以是包含该语句的任何一层循环,这在翻译时必须略微作些技术处理。
为处理exit语句,编译程序对每个循环可使用一个称为“循环描述符”的量来记录一些必要的信息。内容:
一个指向直接外层循环描述符的指针;
循环登记项:循环的名字、开始四元式、结束四元式;
exit转移的目标四元式等等:
在标识符表中,将“循环描述符”登录在其中。



打开循环的描述符使用栈式方式组织。图8.19(a)的循环嵌套情况,其描述符栈示意图如图8.19(b)。
FIRSTloop:
…
SECONDloop:
…
loop
…exit
endloop;
…
end loopSECOND;
…
endloop FIRST;
(a)

(b)
8.6.5 go to 语句
(1) 标号定义与标号语句
标号语句 L:S;
goto 语句 goto L
当L:S;语句被处理之后,标号L是“定义了”的。即在符号表中,将登记L的项的“地址”栏中登上语句S的第一个四元式的地址。
(2) 如果goto L是一个向上转移的语句,那么,当编译程序碰到这个语句时,L必是已定义了的。通过对L查找符号表获得它的定义地址p,编译程序可立即产生出相应于这个goto L的四元式如(j,-,-,p)。
(3)如果goto L是一个向下转移的语句,标号L尚未定义,那么,若L是第一次出现,则把它填进符号表中并标志上“未定义”。由于L尚未定义,对goto L只能产生一个不完全的四元式(goto -),它的转移目标须待L定义时在回填进去。
(4)建链的方法是:若goto L中的标号L尚未在符号表中出现,则把L填入表中,置L的“定义否”标志为“未”,把nextstat填进L的地址栏中作为新链首,然后,产生四元式(goto 0),其中0为链尾标志。若L已在符号表中出现(但“定义否”标志为“未” ),则把它的地址栏中的编号(记为q)取出,把nextstat填进该栏作新链首,然后,产生四元式(goto q)。
(5)一旦标号L定义时,我们将根据这条链回填那些待填转移目标的四元式。
一般而言,假定用下面的产生式来定义标号语句
S→label S
Label→I:
当用Label→I:进行归约时,应做如下的语义动作:
1.若i所指的标识符(假定为L)不在符号表中,则把它填入,置“类型”为“标号”,“定义否”为“已”,“地址”为nextstat。
2.若L已在符号表中但“类型”不为“标号”或“定义否”为“已”,则报告出错。
3.若L已在符号表中,则把标志“未”改为“已”,然后,把地址栏中的链首(记为q)取出,同时把nextstat填在表中,最后,执行backpatch(q ,nextstat)。
当翻译goto语句时,还要注意标号的作用域。如:
(1) begin
(2) L:begin
(3) Goto L;
(4) ……
(5) L:…
(6) end
(7) end
当在行(3)处理goto语句时,不知道L的作用域到底是哪个,(因为还没见到语句(5)),因此一定要延迟解决这个标号的使用。
8.6.6过程调用的四元式产生
过程调用:call p(a,a+b,c[7]);
转子指令:现在计算机的转子指令在实现转移的同时就把返回地址放在某个寄存器或内存单元之中。
参数传递:值参数(传值)、变量参数(传地址)、结果引用。我们这里只讨论最简单的一种,即传递实在参数地址(传地址)的处理方式。
目标代码
par 实参1
…
par 实参n
call p;
call p(a,a+b,c[7]);
- • 如果实在参数是一个变量或数组元素,那么,就直接传递它的地址。
- • 如果实参是其它表达式,如A+B或2,那么,就先把它的值计算出来并存放在某个临时单元T中,然后传送T的地址。
- • 在被调用的子程序(过程)中,每个形式参数都有一个单元(称为形式单元)用来存放相应的实在参数的地址,在子程序段中对形式参数的任何引用都当作是对形式单元的间接访问。
- • 当通过转子指令进入子程序后,子程序的第一步工作就是把实在参数的地址取到对应的形式单元中,然后,再开始执行本段中的语句。
传递实在参数地址的一个简单办法是,把实在参数的地址逐一放在转子指令的前面。
例如,过程调用
CALL S(A+B,Z)
将被翻译成:
T:=A+B /* 计算A+B置于T中的代码 */
par T /* 第一个实参地址 */
par Z /* 第二个实参地址 */
call S /* 转子指令 */
根据上述关于过程调用的目标结构的模式,我们现在来讨论如何产生反映这种模式的四元式序列。
考虑到一个描述过程调用语句的文法:
(1) S→call i(<arglist>)
(2) <arglist>→< arglist >1,E
(3) <arglist>→E
为了在处理实在参数串的过程中记住每个实参的地址,以便最后把它们排列在转子指令的前面,我们赋予非终结符arglist一项语义值,叫数据队列QUEUE,用它按序记录每个实在参数的地址。
过 过程调用的目标代码:
Par 1
Par 2
….
Par n
转 过程入口
下面是过程调用语法制导翻译的基本语义描述:
(1) S→call i(<arglist>)
{ for队列arglist.QUEUE的每一项p DO
GEN(PAR,—,—,P);
GEN(call,—,—,ENTRY(i))}
(2) <arglist>→< arglist >1,E
{把E.PLACE排在arglist1.QUEUE的末端;
arglist.QUEUE:=arglist1.QUEUE}
(3) <arglist>→E
{建立一个arglist.QUEUE,它只包含一项E.PLACE}
8.7说明语句的翻译
程序设计语言中的说明语句旨在定义各种形式的有名实体,如常量、变量、数组、记录(结构)、过程、子程序等等,说明语句的种类也多,对象说明、变量说明、类型说明、过程说明等等。
编译程序把说明语句中定义的名字和性质登记在符号表中,用以检查名字的引用和说明是否一致。
许多说明语句的翻译并不生成相应的目标代码。过程说明和动态数组的说明有相应的代码。
8.7.1 简单说明语句的翻译
程序设计语言中最简单的说明语句的语法描述为:
D→integer <namelist>
| real <namelist>
<namelist>→<namelist>,id
| id
即使用关键字integer和real定义一串名字的性质。
如:
integer a1,a2,a3;
上述文法来制导翻译存在着问题:
我们只能把所有的名字都规约成namelist后才能把它们的性质登记进符号表。这意味着namelist必须用一个队列或(栈)来保存所有这些名字。
但我们可以把上述的文法改写成:
D→D1,id
|integer id
|real id
这样,就能把所有说明的性质及时地告诉每个名字id,或者说,每当读进一个标识符时,就可以把它的性质登记在符号表中,不用把它们集中起来最后再成批登记了。
现在来定义这些产生式所对应的语义动作,给非终结符D一个语义变量D.ATT,用以记录说明句所引入的名字的性质(int还是real)。
使用过程enter(id,A)把名字id和性质A登录在名表中。
(1)D→integer id { enter(id,int); D.ATT:=int }
(2)D→real id { enter(id,real);
D.ATT:=real}
(3)D→D1 ,id { enter(id, D1.ATT);
D.ATT:= D1.ATT }
8.7.2 过程中的说明
过程的翻译包括两部分,过程说明和过程调用。
8.6中介绍了过程调用的四元式产生。
(1)需做的工作 处理到过程的说明部分时,要为过程的局部变量安排存储。因此,在建立符号表时,要登录名字和存储的相对地址。
(2)为记录变量的相对地址,可以使用一个变量offset。在处理过程的第一个说明之前,置offset为零。每看见一个新名字,则把名字连同offset的当前值登入符号表,然后offset增加。
offset的增加由其类型决定,称为数据对象的宽度,用属性WIDTH来表示。
假如real型对象宽度为8,则相对应的语义动作为:
D→real id {enter(id, real,offset);
D.ATT:=real;D.WIDTH:=8;
Offset:=offset+D.WIDTH;}
如何计算过程嵌套语言的Offset(如PASCAL)?

- • 利用栈保存OffSET
- • 解决变量的作用域问题
对这种语言,局部于每个过程的名字除了指定相对地址外,还必须保存作用域的信息,可以采用分程序符号表解决)。
8.8数组和结构的翻译
8.8.1数组说明和数组元素的引用
一、数组说明
(1)从逻辑上说,一个数组是由同一类型数据所组成的结构。每维的下标只能在该维的上、下限之内变动。如在FORTRAN中最多只允许七维的数组,并且每维的下限都是1。在有些语言中,数组的维数和上、下限可以是任意的。
 (2)数组的每个元素(也称下标变量)是由数组名连同个维的下标值命名的,如           。每个数组元素在计算机中占有同样大小的存储空间。
(2)数组的每个元素(也称下标变量)是由数组名连同个维的下标值命名的,如           。每个数组元素在计算机中占有同样大小的存储空间。
(3)如果一个数组所需的存储空间的大小在编译时就已知道,则称此数组是一个静态数组;否则,称为可变数组或者动态数组。
数组的存储表示有多种形式,最简单的一种是把整个数组按行(或按列)存放在一片连续存储区中。例如,若A是一个2*3的二维数组,每个元素占一个单元,那么,所谓按行和按列的存储方式分别如图8.19和图8.20所示。

 
               
数组元素的地址计算:
让我们看一看在以行为序的情形下,如何计算数组元素的地址。
例如,假定A是一个的二维数组,各维的下限为1,上限为20,每个元素占用一个机器字(令存储器是以字编址的),数组元素A(i,j)的首地址为:
|  | |||
|  | |||
或等价的表示成:
- • 数组的内情向量:
一般编译程序对数组说明的处理是把数组的有关信息汇集在一个叫做“内情向量”或“信息向量”的表格中,以便以后计算数组的地址时引用这些信息。
 例如,对数组
例如,对数组 
相应的内情向量可设为:

- • 内情向量的保存位置
假定A是一个静态数组,也就是说,它每维的上下限ui、li都是常数。因而,在编译时就能知道数组A需要占用多大的存储空间。在这种情况下,内情向量只在编译时有用,因此,可以把它安排为符号表的一部分,这种内情向量无需带到目标程序运行时刻。 但有时为了统一处理方便起见,我们仍把它带进目标程序。
假定A是一个可变数组,即有些维的上下限ui、li都是变量,那么,某些维的长度di(从而C)需要在运行时才可计算出来。因而,数组所需的存储空间的大小需要在程序运行时才能知道。在这种情况下,编译时应分配数组的内情向量表区、同时必须产生在运行时动态的建立内情向量和分配数组空间的目标指令。
我们不具体的讨论如何产生这些指令,只是指出对于任何可变数组,这些指令大体上都是相同的,因此可把它们组织成一个统一的子程序。
- • 建立内情向量子程序
子程序的入口参数是:数组维数n,界限序列l1,u1,l2,u2,…,ln,un,类型type,内情向量表区地址。这个子程序的作用是,建立内情向量并分配数组空间。
二、数组元素的使用
- • 访问数组元素A[i1,i2,…,in]的目标码
下标变量地址:VARPART和CONSPART
因此,将产生两组计算数组元素地址的四元式。一组计算VARPART,并将它放在某个临时单元T中;另一组计算CONSPART,并将它放在另一个临时单元T1中。
- • 对应“数组元素引用”和“对数组元素赋值”的四元式:
“变址取数”的四元式是:
(=[],T1[T],-,X) /* 相当于X:=T1[T] */
“变址存数”的四元式是:
([]=,X1,-,T1[T]) /* 相当于T1[T]:=X */
下面讨论赋值语句中数组元素的翻译。
- • 含数组元素的赋值语句的文法
A→V:=E
V→i[<elist>]|i
< elist >→<elist>,E|E
E→E+E|(E)|V
例如:x=b[x1,x2,x3]
< elist >→E 变为: x=b[< elist > ,x2,x3]
< elist >→<elist>,E 变为: x=b[< elist >,x3]
< elist >→<elist>,E 变为: x=b[< elist >]
- • 改写文法
为了按前面所说的办法计算数组元素的VARPART,我们需要把关于变量V的文法改写成:
V→<elist>]|i
<elist>→<elist>,E
|i[E
改写的目的是使我们在整个下标串list的翻译过程中随时都能知道数组名i的符号表入口,从而随时能够了解登记在符号表中的有关数组i的全部信息。
为了产生计算VARPART的四元式序列,需要如下的语义变量和过程:
- • elist.ARRAY——表示数组名在符号表中的位置,即数组名的符号表入口。
- • elist.DIM——下标个数(数组维数)计数器。
elist.PLACE——记存业已形成的VARPART的中间结果在符号表中的位置,或是一个临时变量的整数码。
- • LIMIT(ARRAY,K)——一个函数,它给出数组ARRAY的第k维长度。其中,ARRAY是数组名在符号表中的位置。
- • 每个变量V有两项语义值(属性),V.PLACE和V.OFFSET。若V是一个简单变量名i,则V.PLACE就是指此名的符号表入口,而V.OFFSET将是null。若V是一个下标变量名i,则V.PLACE就是指保存CONSPART的临时变量名的整数码,而而V.OFFSET则指保存VARPART的临时变量名的标志。
例如,令A是一个10*20的数组,即d1=10,d2=20。那么,赋值语句X:=A[I,J]的四元式序列:
(*, I ,20,T1 ) /*其中20指d2*/
(+, J , T1, T1 ) /* T1、为VARPART*/
(- , A,21, T2 ) /*相当于T2:= a –C */
(=[ ],T2[T1],_, T3) /* T3= T2[T1]*/
(:=,T3,_ , X )
赋值句A[I+2,J+1]:=M+N的四元序列为:
(+, I , 2 ,T1 )
(+, J ,1 ,T2 )
(*,T1 ,20,T3 )
(+,T2 ,T3 ,T3 ) /* T3= VARPART*/
(- ,A , 21, T4 ) /* T4= ConstPART*/
(+,M, N, T6 )
([ ]=,T5 ,_ ,T4 [ T3 ]) /* T4[T3]= T5 */
8.8.2结构(记录)说明和引用的翻译
结构(记录)是由已知类型的数据组合起来的一种数据类型。比如C语言中由关键字struct, PASCAL语言中由关键字record开头的说明。如:
struct date{
int day;
char month-name[4]
int year
};
是C语言中对结构类型date的声明,date含有三个成员,或说三个分量。
- • 结构说明的文法如下:
<type>→struct { f1};
| int
| char
| pointer
f1→f1; f | f
f→<type> |<type> i[n]
这里,为简单起见,作为分量的数组只允许是一维,n代表无符号整数。
- • 结构类型的说明
结构的简单的存储方式是连续存放。编译时,必须记录所有分量的信息
:
分量名 类型 长度 OFFSET
分量1
分量2
分量n
每个分量记录分量名、类型、长度(所需的存储单元数),这些属性是编译时已知的,所以能知道每个分量的前面各分量的长度总和,用OFFSET也记录下来。
对于非终结符type、f和f1,引入:
语义变量LEN表示长度
i.NAME表示i当前所代表的名字
f.NAME表示分量名
N.VAL表示整数值
语义过程FILN(NAME,L)和FILO(NAME,L)将分别把分量名表中名为NAME的项的长度LEN和OFFSET填为L。
下面是处理结构类型说明的基本语义动作:
每个type的语义值 type.LEN将用来处理说明句
type namelist 中的namelist名列里的每一个名字。
注意,上面在累计区段(栏)f的长度LEN时,我们忽略了某些类型的数据必须起始于整字边界这个事实。为了从整字边界开始,必须放弃一些字节零头,这些领头必须计算在前一区段的长度里。
对于PASCAL和Ada中变体纪录,以及C中联合(union)出现在结构中的情形,只记录类型、长度和区距(offset)是不够的。对于其特殊分量必须给予另外的标志。当然还有更复杂的情形,如结构和数组出现在联合中的情形,其细节这里不予以介绍了 。
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号