第六章 语法制导译在前面已经介绍了编译程序构造的二个重要阶段,即词法分析和语法分析。现在再来介绍编译程序的另一个重要阶段 ——中间代码生成。虽然在实际应用中,是否采用中间代码形式是根据实际情况而定的。但事实上,为了使编译程序的结构清晰、简单、明确,多数编译程序采用了中间代码的形式。尤其是使用了中间代码的形式,使目标代码优化比较容易实现。通常以中间代码生成这一阶段来划分编译程序的前端和后端。对于不同的高级语言只要翻译成相同的中间代码,再接上一个相同的把中间代码翻译成目标代码的后端,
就可以形成不同的编译程序。同一种高级语言只要翻译成相同的中间代码就可以共用一个前端,接上后端可以在不同机型上实现同一语言的编译程序。虽然中间代码的形式很多,
但常见的中间代码有逆波兰式、三元式、四元式、树形表示等。本章讨论如何将高级语言翻译成中间代码。
6.1 中间代码的形式中间代码的形式虽然很多,但组成中间代码的原则是:
(1) 形式比较简单,容易翻译成相应的目标机器代码
(2) 能充分反映源程序的特点比较常用有逆波兰式、三元式、四元式和树型表示。
6.1.1 逆波兰式逆波兰式是由波兰数学家卢卡西维奇发明的一种表示算术表达式或逻辑表达式的方法,它是一种能表示运算符的计算顺序,但没有括号的表达式。在这种表示法中,把运算符直接跟在运算对象后面,因此逆波兰表示法又称后缀表示法。
逆波兰表示法的格式为:
<运算对象 1> <运算对象 2><运算符 >
例如:表达式 (a+b*c/d)*e-f的逆波兰式为:
abc*d/+e*f-
从上可以看出,逆波兰式具有下列性质:
(1) 在中缀式和逆波兰式中,运算对象是按相同的顺序出现的
(2) 在逆波兰式中,运算符是按计算顺序(从左到右)出现的
(3) 运算符紧跟在其运算对象后面出现的当逆波兰式允许单目运算符(仅允许一个运算对象的运算符)出现时,会产生一些问题。如写出表达式 a+(-b*c/d)*e
的逆波兰式为 ab-cd/*e*,此时很难区分运算符 -是单目的还是双目的。即先计算 a-b还是 -b。解决上述问题的方法有二种:
(1) 把单目运算符改成双目,如改写成,a0b-cd/*e*
(2) 引进新的运算符为 @,如,ab@cd/*e*
其它单元目的运算符可参照上述方法处理。对于第一种方法,
把单目运算符处理成双目运算符增加了运算的时间,降低了工作效率。对于第二种方法,要解决的问题是如何把符号‘ -?
处理成不同的符号。处理的时机可以放在词法分析中解决,
如在表达式的起始位(如赋值号后、逗号后、左括号后等)
设置标记 flag为 1,即单元目运算符,在遇到运算对象后设置标记 flag为 0,即双目运算符。
计算逆波兰式表示的算术或逻辑表达式比计算中缀式要简单。这是因为计算逆波兰式不要比较运算符的优先级,只要一遇到运算符就立即可以计算。具体实现中只要用一个栈来存放运算对象,故该栈又称运算对象栈或运算分量栈。在栈中存放未被计算的运算对象,当一旦扫描到运算符时,就从栈中取出运算符所需的运算对象个数进行计算。然后再将计算结果放入栈中,当全部扫描完逆波兰式后栈顶元素即为最后的计算结果。
一般算法语言除了算术表达式和逻辑表达式外,还有其它如赋值语句、条件语句、循环语句等,但只要遵守运算对象后直接紧跟计算它们的运算符这一规则,就可以很方便地将逆波兰式扩充到整个算法语言。
例如,赋值语句 <变量 >:=<表达式 >可改写为 <变量 ><表达式
>:= ; GOTO L 改写成 L jmp (其中 jmp 表示转移的运算符,
L表示逆波兰式的编号或地址)。
对于条件语句,if E then S1 else S2 可以考虑三目运算符 if,如 ES1S2if。但这种表示方法当执行到运算符 if时,E、
S1,S2三个运算对象已经全部计算过可或执行了,由于构造的逆波兰式都是从左到右执行的,此时很难再回到前面去重新执行或跳过相应的逆波兰式。为此可以用二目条件转移来表示,E<op1>jz S1<op2>jmpS2,其中,<op1>,<op2>分别为 S2的开始位置和跟在 S2之后那个符号的位置。 jz和 jmp分别为条件和无条件转运算符。类似可以引进条件转移的逆波兰式为:
<运算对象 1> <运算对象 2><运算符 >
其中 <运算对象 1>是算术值或逻辑值,<运算对象 2>是逆波兰的某个编号或位置; <运算符 >可以是 jl,jg,jle,jge,jz、
jnz等,分别表示小于、大于、小于等于、大于、等于、不等于等转移的运算符。
当然用同样的方式,还可以将逆波兰式扩充至数组、记录或其它数据类型,也可将 for语句,while语句,case语句扩充至逆波兰式。另外需要指出的是:赋值运算符和其它逆波兰式不一样,它要把 <表达式 >的值放入 <变量 >,在栈中只需要变量的地址,而不是值,计算赋值运算符后不要将结果入栈。
例,写出语句 if a>b and b<c then x:=5+3*d/4 else y:=6-y*8
相应的逆波兰式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
a b > b c < and 21 jz x 5 3 d * 4 / +,= 28
20 21 22 23 24 25 26 27 28
jmp y 6 y 8 * -,=
6.1.2 三元式和树中间代码的另一种表示法为三元式,三元式的形式为:
( <运算符 >,<运算对象 1>,<运算对象 2> )
其中,<运算对象 1> <运算对象 2>分别表示变量、常量或三元式的结果等。
例如,表达式 a+(-b*c+d)*e的三元式序列为:
运算符 运算对象 运算对象
( 1)( -,b,_ )
( 2)( *,(1),c )
( 3)( +,(2),d )
( 4)( *,(3),e )
( 5)( +,a,(4) )
其中:( 1)、( 2)、( 3)、( 4)分别为第 1、第 2、第 3、
第 4条三元式的结果。整个表达式的结果可用( 5)表示。
下面把三元式扩充到其它表示。
(jmp,_,p) 表示无条件转向第 p条三元式执行
(jl,a,p) 表示当 a小于 0时转向第 p条三元式执行,否则执行下一条三元式
(jg,a,p) 表示当 a大于 0时转向第 p条三元式执行,否则执行下一条三元式
(jle,a,p) 表示当 a小于等于 0时转向第 p条三元式执行,否则执行下一条三元式
(jge,a,p) 表示当 a大于等于 0时转向第 p条三元式执行,否则执行下一条三元式
(jz,a,p) 表示当 a等于 0时转向第 p条三元式执行,否则执行下一条三元式
(jnz,a,p) 表示当 a不等于 0时转向第 p条三元式执行,否则执行下一条三元式下面介绍用树来表示算术表达式。用树来表示中间代码需要用两个指针分别指向它的左子树和右子树。如果 e1
和 e2是表达式,其相应的树用 T1和 T2表示,则 e1+e2,e1-
e2,e1*e2,e1/e2和 -e1的树如图。
对于表达式 a+(-b*c+d)*e
的三元式则可用图 6-2的树来表示。该图也是表达式 a+(-
b*c+d)*e的三元式( 5)表示树的根,每个子表达式用一棵子树表示,可以看出对于该树的前序遍历、中序遍历
(对不符合运算符的优先级的运算应加上括号)和后序遍历分别产生的是前缀式、
中缀式和逆波兰式,因此树型表示隐含了三元式表示和逆波兰表示。
6.1.3 四元式由于三元式中的结果是用它的编号来表示的,当在三元式组进行优化后,就要用一定的时间来,重新按排三元式的编号,这是很费时的。为了防止优化后的重新编址,在三元式基础上增加了一个存放结果的单元,这就形成了四元式。
四元式是一种最常用的形式。其格式是:
( <运算符 >,<运算对象 1>,<运算对象 2>,<结果 >)
则表达式 a+(-b*c+d)*e的四元式序列为:
( 1)( -,b,_,t1)
( 2)( *,t1,c,t2)
( 3)( +,t2,d,t3)
( 4)( *,t3,e,t4)
( 5)( +,a,t4,t5)
四元式虽然比三元式多了一结果的引用,但减少相互之间的引用,从而有利于优化。为了便于书写四元式也可以写成如下形式:
<结果 >:= <运算对象 1> <运算符 ><运算对象 2>
则表达式 a+(-b*c+d)*e的四元式序列为:
( 1) t1:= - b
( 2) t2:= t1*c
( 3) t3:= t2+d
( 4) t4:= t3 *e
( 5) t5:= a +t4
同样要将算法语言翻译成相应的四元式,也要将四元式扩充到其它运算符,如:( jmp,_,_L)表示无条转向第 L条四元式,(jz/jnz/jg/jl/jle/jge,A,B,,L)表示比较 A和 B分别满足相等,
不等、大于、小于、小于等于或大于等于的 6个条件转移到第 L条四元式执行。其它扩充的四元式在以后本章其它各节中分别介绍。
6.1.4 汇编语言汇编语言是依赖于机器的低级程序设计语言,它是面向具体的计算机系统或相应的计算机系列的,它和三元式、四元式和逆波兰式相比有以下优点:
(1) 能方便地翻译成目标机器指令 由于汇编代码采用的是助记符操作码方式表示的机器指令,它基本上与机器指令一一对应。比三元式、四元式更接近计算机硬件,故更容易翻译成机器指令,且不必由编译程序设计者来开发其翻译程序。
(2) 不必直接计算转移地址 由于在汇编语言中可以用符号名表示数据或机器指令的地址和在汇编语言中的转移均可使用符号地址。它比三元式、四元式的生成转移更方便,更灵活。
(3)可以使用各种数据表示法 由于汇编语言中提供了各种数制的翻译,因此在生成汇编语言时可以使用各种数制,而不必再进行直接转换。
例如,表达式 a+(-b*c+d)*e的类 Intel8088的汇编语言语句为:
mov ax,b
neg ax
mov bx,c
imul bx
mov bx,d
add ax,bx
mov bx,e
imul bx
mov bx,a
add ax,bx
mov ti,ax
其中 a,b,c,d,e表示变量,最后将结果存放在某个临时变量 ti中。
虽然,汇编语言较三元式、四元式有上述三个优点,但汇编语言与它们相比还有一些明显的缺点,就是依赖于计算机系统。这样对于编译程序设计者来说,需熟悉不同的计算机系统的特点。因而使得开发周期延长,工作效率降低。汇编程序实际上也是一个小型的编译程序,因而翻译汇编语言到目标语言的过程要比翻译三元式、四元式到目标语言的过程要复杂。
6.2 语法制导翻译下面讨论编译程序是如何把源程序翻译成相应的中间代码的。由于语义规则的定义要比语法的定义复杂得多,至今没有一种从理论上和实际上都完备的方法。目前大多数采用的方法是所谓的语法制导翻译的方法,语法制导翻译的思想方法是把语言的一些属性附加到代表语言结构的文法的符号上,这些属性值是由附加到文法产生式的“语义规则”计算,
也就是为每个产生式配上翻译子程序,即语义子程序。语义子程序的语义规则的计算可以产生代码、把信息存入符号表、
显示出错信息等等。语法制导翻译的含义是在语法分析过程中,在自顶向下的分析方法中当一个产生式获得匹配或在自底向上的分析方法中用于归约时,则相应产生的语义子程序就进入工作,完成既定的翻译工作。既定的翻译工作主要有两方面,其一是审查语法结构的静态语义,即验证语法结构合法的程序是否真正有意义。其二是如果静态语义正确,则要将源程序翻译成一种中间表示形式,即用中间语言表示源程序的语义。
6.2.1 计算表达式的制导翻译从形式语义的角度来看,语法制导翻译不是一种理论上完备的语义分析方法。形式语义是一门学科,它主要研究语言的语义表示。就目前来说,有各种方法和记号系统推出,
如操作语义学,公理语义学和指称语义学。但无论那一种方法,都存在着缺陷,如其本身的符号系统复杂,其描述文本易读性差,也就是说,目前尚不能借助于这些形式系统来自动完成语义处理任务。因此目前仍只能采用是接近形式化的制导翻译。在这里以属性文法为工具来说明程序设计语言的语义,一个属性文法它包含一个上下文无关文法和一系列语义规则,这些语规则附在文法的每个产生式上,如在自顶向下的语法分析过程中,当归约时就完成附加在所使用的产生式上的语义规则的动作,从而实现语义处理。
在语法制导定义中,每个文法符号有一组属性,对于每个文法产生式 A→α 有一组形式为 b:=f(c1,c2,……c k)的语义规则,
即属性 b是由属性 c1,c2,……c k决定的。其中 b和 c1,c2,……c k
是该产生式中文法符号的属性。
定义 6-1
如果 b是 A的属性,c1,c2,……c k是产生式右部 α中的文法符号的属性或 A的非 b属性,那么 b叫做文法符号 A的综合属性定义 6-2
如果 b是产生式右部 α某个文法符号的属性,c1,
c2,……c k是 A的属性或产生式右部文法符号的属性,那么 b叫做文法符号 A的继承属性。
从上定义可以看出,综合属性是对于产生式左部属性都是由产生式右部文法符号的属性和产生式左部文法符号的其它属性来决定的。即在语法树的角度看是从孩子往的双亲传送属性。而继承属性是产生式右部文法符号属性是由产生式左部文法符号的属性和产生式右部文法符号的其它属性决定的。即在语法树的的角度看是从双亲向孩子传送属性或由兄弟之间传送属性。
显然每个文法符号的综合属性集和继承属性集的交为空,也就是一个属性不可能既是综合属性,又是继承属性。
定义 6-3
如果一个语法制导定义是 S属性的,则每个产生式的属性,
均是综合属性 。
属性文法的语义规则可以写成无函数副作用的语法制导定义。语义规则的函数可以是表达式或相应的函数或过程的调用,也可以是程序段。
例:利用表达式的文法制导定义解释执行表达式的值。
产生式 语义规则
S'→E print(E.val)
E→E 1+T E.val:=E1.val+T.val
E→T E.val:=T.val
T→T 1*F T.val:=T1.val*F.val
T→F T.val:= F.val
F→(E) F.val:=E.val
F→i F.val:=i.lexval
下图是分析 5+7*3时属性传递的注解分析树,其中 5,7,3分别是 i的显示值,即 i.lexval。
显然语义规则的属性 val为均为综合属性,该语法制导定义为
S-属性。
例,描述说明语句中各种变量的类型信息的语义规则产生式 语义规则
D→TL L.in:=T.type
T→int T.type:=integer
T→real T.type:=real
L→L 1,i L1.in:=L.in
addtype(i.entry,L.in)
L→i addtype(i.entry,L.in)
下图为句子 real i1,i2,i3的注解分析树显然语义规则的属性 in为继承属性。
在语法分析过程中,随着分析的进行,每当每个产生式完成时(即在自顶向下的分析技术中全部推导出产生式右部的符号串;在自底向上的分析技术已用产生式归约相应的非终结符号)就执行相应的语义子程序。这种思想也就是所谓的语法制导翻译。但对某一产生式究竟要完成哪些动作,这不仅与要产生的代码有关,而且与编译程序的设计者的风格有关。
例,设有文法 G[S]:
( 0) S→E ( 1) E→E+E ( 2) E→E*E ( 3) E→(E)
( 4) E→i
其中规定符号 +与 *的优先级为 *大于 +,且 *与 +都是左结合的则有 SLR( 1)分析表:
状态 ACTION GOTO
+ * ( ) i # E
0 S2 S3 1
1 S4 S5 acc
2 S2 S3 6
3 r4 r4 r4 r4
4 S2 S3 7
5 S2 S3 8
6 S4 S5 S9
7 r1 S5 r1 r1
8 r2 r2 r2 r2
9 r3 r3 r3 r3
要计算表达式的值,则需为每个产生式配上一个语义动作,如:
S'→E print(E.val)
E→E 1+E2 E.val:=E1.val+E2.val
E→E 1*E2 E.val:=E1.val*E2.val
E→(E 1) E.val:=E1.val
E→i E.val:=i.lexval
下面分析识别 7+9*5,并完成相应的语义动作。为减少归约时的属性值拷贝,这里用属性栈记录属性,也就是用值栈记录文法的属性值 val。对于没有属性 val的文法符号为直观起见,用空表示。其中 7,9,5为 i的显示值步骤 归约动作 状态栈 语义栈(值栈) 符号栈 输入串
1 0 _ # 7+9*5
#
2 03 _7 #i +9*5#
3 r4 01 _7 #E +9*5#
4 014 _7_ #E+ 9*5#
5 0143 _7_9 #E+i *5#
6 r4 0147 _7_9 #E+E *5#
7 01475 _7_9_ #E+E* 5#
8 014753 _7_9_5 #E+E*i #
9 r4 014758 _7_9_5 #E+E*E #
10 r2 0147 _7_45 #E+E #
11 r1 01 _52 #E #
12 acc (执行 S'→E 的语义动作,输出栈顶语义值 52)
为说明语法的制导翻译的翻译方法,在这里先讨论算术表达式和简单赋值语句的制导翻译,其翻译成的中间代为逆波兰式。这里介绍的仍是自底向上的分析技术,即在语法分析中,当句柄归约时,就执行相应规则的语义处理子程序。
现在为了简单起见可以不关心采用何种自底向下的分析方法,
只要关心所归约的句柄。
例,设有文法 G[S]:
S→A
A→V:=E
V→i
E→E+T|T
T→T*F|F
F→(E)|i
试用语法制制导翻译的方法为每个产生式配语义动作以便将赋值语句翻译成相的逆波兰式。
设整个逆波兰式存放在 P数组中,P数组的下标用变量 p
指示,其初值为 1,当归约某个非终结符时,则产生该非终结符代表的逆波兰式。现考虑产生式 F→i 和 V→i 的语义子程序,也就是将 i归约成 V或 F时其中 i是任一标识符或常量,那么在逆波兰表示法中标识符和中缀式中的标识符应该是相同的,也就是单个标识符的逆波兰式,即为标识符本身。故直接输出标识符 i即可。在这里用 i.name表示 i的名字。根据实际情况,也可使用如,i 在符号表中的入口、常量的显示值等作为逆波兰式的运算对象。当用产生式 T→F 归约时,由于
F的逆波兰式已经产生,而 F的逆波兰式和 T的逆波兰式是完全相同的,即 T的逆波兰式也已产生,故在该产生式的语义子程序中,不需完成具体语义动作,即语义子程序为空。
同理用产生式 E→T,S→A 和 F→(E) 归约时的语义子程序均为空。但用产生式 T→T 1*F归约时,T1和 F已经归约,则 T1和
F的逆波兰式已相继产生,因 T1比 F先归约,那么 T1的逆波兰式在 F的逆波兰式前面,T的逆波兰式为 <T1的逆波兰式 ><F
的逆波兰式 >*,与 T的逆波兰式仅相差一个’ *’,因此,产生式 T→T 1*F的语义子程序中只要输出一个’ *’即可。同样,
用产生式 E→E 1+T归约时,只要输出组一个‘ +?就构成了 E的逆波兰式。对于 A→V:=E 产生式的归约只要输出一个,=即可。
综上所述有可得:
产生式 语义动作(语义规则)
(1) S→A 空
(2) A→V:=E P(p):=?:=?;p:=p+1;
(3) V→i P(p):= i.name;p:=p+1;
(4) E→E+T P(p):=?+?;p:=p+1;
(5) E→T 空
(6) T→T*F P(p):=?*?;p:=p+1;
(7) T→F 空
(8) F→(E) 空
(9) F→i P(p):= i.name;p:=p+1;
下面用对于句子 a:=b*(c+d)+e进行分析,观察所产生的逆波兰式,说明制导翻译的正确性。
栈 输入符号串 所用产生式 到目前为止生成的逆波兰式
# a:=b*(c+d)+e#
#a,=b*(c+d)+e# 3 a
#V,=b*(c+d)+e# a
#V:= b*(c+d)+e# a
#V:= b *(c+d)+e# 9 ab
#V:= F *(c+d)+e# 7 ab
#V:= T *(c+d)+e# ab
#V:= T* (c+d)+e# ab
#V:= T*( c+d)# ab
#V:=T*(c +d)+e# 9 abc
#V:=T*(F +d)+e# 7 abc
#V:=T*(T +d)+e# 5 abc
#V:=T*(E +d)+e# abc
#V:= T*(E+ d)+e# abc
#V:= T*(E+d )+e# 9 abcd
#V:=T*(E+F )+e# 7 abcd
#V:= T*(E+T )+e# 4 abcd
#V:=T*(E )+e# abcd+
#V:= T*(E) +e# 8 abcd+
#V:= T*F +e# 6 abcd+*
#V:= T +e# 5 abcd+*
#V:= E +e# abcd+*
#V:= E+ e# abcd+*
#V:= E+e # 9 abcd+*e
#V:= E+F # 7 abcd+*e
#V:= E+T # 4 abcd+*e+
#V:= E # 2 abcd+*e+:=
#A # 1 abcd+*e+:=
#S # acc abcd+*e+:=
此时,接受时恰好生成所需的逆波兰式。
6.3 自底向上的制导翻译
6.3.1 赋值语句的翻译
1,简单变量的引用首先讨论简单赋值语句的翻译。上面已将中缀式翻译成逆波兰式,现可将该语法制导翻译方法作适当的修改可以用在产生四元式上。下面讨论如何将算术表达式翻译成四元式。对于翻译成四元式方法稍作修改就可以翻译成三元式或树。首先考虑赋值语句 a:=b*(c+d)+e是如何生成四元式序列的,希望产生如下四元式序列
( 1)( +,c,d,t1)
( 2)( *,b,t1,t2)
( 3)( +,t2,e,t3)
( 4)(,=,t3,_,a)
显然第一个四元式的产生应该放在规则 E→E+T 的语义子程序中,但当归约 E+T时,代 E和 T的信息 c和 d的已经丢失,从而无法产生四元式( +,c,d,t1)。在产生逆波兰式时却没有这样的问题,这因为生生逆波兰式时当用 F→i 归约 c和 d
时,已经把 c和 d的名字逆波兰序列中了。为了产生四元式就应该使用某种方法保留 c和 d的信息直到使用完它们为止。
这些信息是与非终结符相联系的,此时可以将语义的信息附加到相应的非终结符号上,用 A.place表示存放非终结符号 A
的值的变量或临时变量的名字。 lookup(i)表示从符号表中查找标识符 i,找到返回符号表的入口,否则返回 null。函数
newtemp返回临时变量,依次标记为 t1,t2,…… 。对于形如
A→B 和 A→ ( B)的规则,只要将 B的信息传递给 A,则其语义子程序为,A.place:=B.place;对于规则 E→E 1+T,首先要产生一个新标识符 ti,并把它与 E联系起来,即有 E.place存放临时变量 ti,其次用 gen语义过程来产生一条四元式
(+,E1.place,T.place,E.place }。同样对于产生式 A→V:=E 只要生成一条赋值的四元式;对于产生式 T→T 1*F则要申请临时变量和产生相应的四元式,
因此有,
产生式 语义动作(语义规则)
(1) S→A { 空 }
(2) A→V:=E {gen(:=,E.place,_,V.place) ; }
(3) V→i {V.place=lookup(i) ; }
(4) E→E 1+T {E.place= newtemp;
gen(+,E1.place,T.place,E.place ) ; }
(5) E→T {E 1.place=T.place ; }
(6) T→T 1*F {T.place= newtemp;
gen(*,T1.place,F.place,T.place:=) ; }
(7) T→F {T.place=F.place ; }
(8) F→(E) {F.place=E.place ; }
(9) F→i {F.place=lookup(i) ; }
在程序设计语言中,变量的类型可以是不同的类型,如变量既可以是实型也可以是整型。当实型和整型混合运算时就会产生类型转换问题,在整型 +实型运算时,先要产生一条( itr,a,_,ti)的四元式,并采用运算符 +i和 +r分别表示处理整型 +和实型 +的运算符,为了简单起见,这里仅对于规则( 4)
讨论,对于规则( 2)和规则( 6)可参照此方法修改。
产生式 语义动作
E→E 1+T {E.place= newtemp;
If (E1.type==int &&T.type==int)
{gen(+i,E1.place,T.place,E.place);
E.type=int;
}
else if(E1.type==int &&T.type==real )
{ t= newtemp;
gen(itr,E1.place,_,t);
gen(+r,t,T.place,E.place);
E.type=real ;
}
else if (E1.type==real &&T.type== int )
{t= newtemp;
gen(itr,T.place,_,t);
gen(+r,E1.place,t,E.place);
E.type=real ;
}
else if( E1.type==real &&T.type== real )
{
gen(+r,E1.place,T.place,E.place);
E.type=real ;
}
}
对于一个多维数组如何保存在一维的存储器中是计算数组元素地址的关键。存放形式有二种,一种是所谓按行存放,对于一个 m× n的数组,先存放数组的第一行、
第二行,…… 第 m行,每一行包括 n个元素;另一种所谓按列存放,对于一个 m× n的数组,先存放数组的第一列、
第二列,…… 第 n列,每一行包括 m个元素。如图所示,二维数组
a[1..3,1..4]的存放示意图。
设二维数组的首地址为,则按行存放的 a[i,j]的地址为,
α+(i-1)*n+(j-1)
现将计算地址的公式推广到 n维数组,设有数组 a[l1..u1,
l2..u2,l3..u3,……l n..un]其存放形式为“按行存放”,它的首地址为 α。
令 d1= u1- l1+1 d2= u2- l2+1 d3= u3- l3+1……d n= un- ln+1
即 di是每维元素的个数。
数组元素 a[i1,i2,i3…i n]的地址公式可以通过下列方式获得。如图所示,该数组是“按行存放” (“按列存放”也可以按照此类似方法计算地址 )的。
也就是对于整个数组可表示成从 l1到 u1的 d1个 n-1维的子数组,
它是按顺序 a[l1,*,*,…],a[l 1+1,*,*,…],a[l 1+2,*,*,…],…,
a[u1,*,*,…] 存放的。对于元素 a[i1,i2,i3…i n]前面必定有
[l1,*,*,…],a[l 1+1,*,*,……],a[l 1+2,*,*,…],…,a[i 1-1,*,*,…] 这些 n-1维的子数组,也就是子数组 a[i1,*,*,…] 的首地地址为
α+(i1- l1) d2 d3…d n*k,d2 d3…d n 为 n-1维的子数组的长度,k
为数组元素基本类型的所需存储空间数。同理对于子数组
a[i1,i2,*,…] 的首地址为 α+(i1- l1) d2 d3…d n*k +(i2- l2) d3…d n*k,
d3…d n为 n-2维的子数组的长度,依次类推可以得到数组元素
a[i1,i2,i3…i n]的地址为 α+(i1- l1) d2 d3…d n*k +(i2- l2)
d3…d n*k+(i3- l3) d4…d n*k+… (i n-1- ln-1) dn*k+(in- ln) *k.。
把与 im(m=1,2,3…,n) 有关的部分称为可变部分用 VARPART
表示,其它与 im(m=1,2,3…,n) 无关的部分称为不变部分用
CONSPART表示,用 ADDR表示数组元素 a[i1,i2,i3…i n]的地址,k为基本类型的字节数,整理得:
ADDR= CONSPART+ VARPART
CONSPART=α- (…(l 1 d2 + l2) d3+ l3)d4+…+ l n-1 dn+ ln)*k
VARPART= (…(i 1 d2 + i2) d3+ i3)d4+…+ i n-1 dn+ in)*k
对于 CONSPART一个数组只要计算一次,不同的数组元素只要计算不同的 VARPART。
数组分静态数组和动态数组二大类,所谓静态数组是指数组所需的存储空间的大小是在编译时可以确定的,所谓动态数组是指数组所需的存储空间的大小是在运行时才可以确定。
为了便于检查使用数组合法性和计算地址方便,需把数组的每一维的下界 l、上界 u、长度 d、首地址 α和维数 n、类型( type),CONSPART等信息保存起来,这些信息综合起来称为内情向量,如图表示。静态数组的内情向量在编译时可以确定。把数组的内情向量通常填写在符号表中,以便供编译的其它部分分析使用。 C语言由于不检查下标越界以及下界为定值 0,上界与长度相等,故在内情向量中不必包含 l,u。另外动态数组的上下界 l,u和 d在编译时是无法确定的,需在运行时填写相应的内情向量表。但编译时动态数组的内情向量的长度是可以确定的,因而编译时可以分配在运行时内情向量所需的存储空间,以及生成在运行时填写相应的内情向量表指令集合。
由于数组元素可以表示成一个基址加上一个偏移地址,对于目前的目标计算机一般均有变址指令,故引进新的变址四元式来表示,变址取数和变址存数的四元式分别为:
( =[],C[T],_,X)
( []=,X,_,C[T])
其含义分别是:从基址 C加上变址 T的地址中取出内容存入 X中和将 X的内容存入基址 C加上变址 T的地址中,即相当于 X:=C[T]和 C[T]:= X 。设有赋值语句的文法为:
G[S],S→A
A→V:=E
V→i|i[< 下标表 >]
<下标表 >→< 下标表 >,E|E
E→E+T|T
T→T*F|F
F→(E)|V
为了简单起见设 k为 1,要计算前面所说的 VARPART,在计算 VARPART时,每时每刻需要知道符号 i的在符号表的位置 (以便知道不同的 di值 )。要知道 i在符号表的位置就要先归约 i(归约时可调用相应的查表函数)。如图每当获得一个下标时 ij是就要计算 VARPART= VARPART * dj + ij一次
( VARPART 的初始值为 0),因此将上述文法改写成:
G[S],S→A
A→V:=E
V→i| < 下标表 >]
<下标表 >→< 下标表 >,E| i[E
E→E+T|T
T→T*F|F
F→(E)|V
假定在处理第 i个下标,对于 <下标表 >→< 下标表 >,E的语义子程序应生 VARPART:= VARPART*di+E的代码。下面列出与 <下标表 >和 V的有关的语义信息:
<下标表 >.entry 表示数组名在符号表中的位置,也就是数组名在符号表中的入口位置。
<下标表 >.count 表示正在处理的第 i个下标
V.place 表示存放简单变量或下标变量的值的变量的名字或与其相关的语法属性。对于简单变量可以存入简单变量在符号表中的入口;对于下标变量可以存放组的
CONSPART部分
V.addr 对于简单变量用 null表示;对于下标变量表示
VARPART
函数 getd(数组标识符的符号表入口,第 k个下标 ) 表示已知某数组的符号表入口和第 k个下标,取该数组的 dk。函数
getconspart(数组标识符的符号表入口 )表示取数组的
CONSPART。为了简单起见,在这里不考虑:数组元素的越界检查、类型检查等问题。则其产生的相应的语义子程序如下:
G[S],S→A
{空 }
A→V:=E
{if V.addr=null then
gen(:=,E.place,_,V.place)
else
gen(=[],E.place,_,V.place[V.addr])
}
V→i
{V.place= lookup(i);
V.addr=null;
}
V→< 下标表 >]
{t:=newtemp;
V.place=getconspart(<下标表 >.entry);
V.addr= <下标表 >.addr
}
<下标表 >→< 下标表 1>,E
{t:=newtemp;
<下标表 >.entry= <下标表 1>,entry;
<下标表 >.count= <下标表 1>.count +1;
gen(*,<下标表 >.addr,getd(<下标表 >.entry,
<下标表 >.count),t);
gen(+,t,E.place,t);
<下标表 >.addr=t;
}
<下标表 >→i[E
{<下标表 >.addr=E.place;
<下标表 >.count=1;
<下标表 >.entry= lookup(i);
}
E→E 1+T
{t:=newtemp;
gen(+,,E1.place,T.place,E.place )
}
E→T
{E1.place=T.place
}
T→T 1*F
{T.place= newtemp;
gen(*,T1.place,F.place,T.place)
}
T→F
{T.place=F.place
}
F→(E)
{F.place=E.place
}
F→V
{if V.addr=null
E.place=V.place
else
{
t=newtemp;
gen(=[],V.place[V.addr],_,t);
F.place=t
}
}
3.记录(或结构)的引用对于一个记录(或结构)的引用与对于数组的引用类似,
需计算出其分量的存放位置,但不同的是记录的每个分量的长度一般是不相等的。这里假定一个记录的 n个分量是按下列形式连续存放的。
分量 1,分量 2,分量 3,…… 分量 n
这样编译在处理记录说明语句时可以计算出每个分量与记录首地址的的相对位置,如把每个分量的相对位置填写在符号表中,引用记录的分量及语义子程序如下:
V→< 记录分量 >
{ V.place:= <记录分量 >.place;
V.addr:= <记录分量 >.addr
}
<记录分量 >→< 记录分量 1>,i
{<记录分量 >.place:= lookup2(<记录分量 1>.place,i);
<记录分量 >.addr:= (<记录分量 >.place).offset
<记录分量 >→i
{ <记录分量 >.place:= lookup(i)
}
其中,lookup2(<记录分量 >.place,i)表示查找 <记录分量 >,
place 指出的符号表入口的下一级标识符 i在符号表中的入口。
X.offset表示符号表入口 X的相对位置属性。即 X与记录首地址的差。
6.3.2 说明语句的翻译说明语句的制导翻译除函数、过程及动态数组外一般不产生代码,主要填写各种符号表。说明语句的形式较多,常用的有常量、变量、类型说明以及数组、记录、集合、函数、
过程的说明等。这里介绍几种说明语句的处理。
1,简单说明语句的翻译设有程序设计语言的说明语句的文法为:
D→int L|real L
L→L,i|i
由于该文法在处理说明语句时,是用产生式 L→i 或归约
L→L,i 时,int或 real尚未归约,需用一个队列(或栈)来保存全部标识符表,这样不仅浪费了编译时的存储空间,而且用定长的队列描述标识符表很难确定其队列的最大长度,用不定长的队列如链表形式的队列,则增加了一个指针提高了制导翻译的复杂度。为此需在归约标识符时,先归约 int或 real,因而改变文法:
D→int i|real i|D,I
这样在归约每个标识符时都可以知道该标识符为何种类型并填写符号表。这里属性 D.type表示 D的类型,过程 fillin(标识符,类型 )表示将指定标识符填表,其中包括:查当前标识符是否在符号表中已出现,若是则出错;否则在符号表中申请一个新的登记项,填入该标识符,并将该登记项中的属性类型置为“类型”指出的类型,并为该类型分配相应的运行时的存储空间。故其语义子程序为:
D→int i
{D.type:=int;
fillin(i,int)
}
D→real i
{D.type:=real;
fillin(i,real)
}
D→D 1,i
{D.type:=D1.type;
fillin(i,D1.type)
}
2,数组说明语句的翻译数组说明语句的翻译的工作通常是在符号表中申请内情向的存储空间。在静态数组中填写内情向量;在动态数组中填写可以在编译确定的信息,并产生一些指令以便在运行时填写符号内情向量。在这里仅介绍静态数组说明语句的翻译,
图 6-9为静态数组说明语句翻译的流程图。
设有数组说明语句的文法为:
D→L:array [l 1..u1,l2..u2,l3..u3,……l n..un] of int
D→L:array [l 1..u1,l2..u2,l3..u3,……l n..un] of real
L→L,i|i
在这里每归约一组上下界就将维数加 1,并调用过程将下界 lk,uk,dk填写内情向量表,计算 CONSPART。 fillin2 (标识符链表头,下界,上界 )表示将 lk,uk和计算出的 dk填写由标识符链表指出的每个标识符的内情向量。 dim(标识符链表头 )表示填写每个标识符的维数,conspart(标识符链表头,
C)表示填写每个标识符的计算数组元素的不变部分 C。
type(填写每个标识符的数组元素的类型 ),malloc(n)表示 n个申请运行时的存储空间并返回该存储空间的首地址,build(标识符 )表示建立一个标识符链表,返回该链表头。 add(标识符链表头,标识符 ) 表示将新的标识符加入到标识符链表中,byte(类型 )为该类型的字节数。属性 R.V表示文法符号 V的体积。属性 R.C表示计算的 C。
为此改变文法为:
D→R] of int | R] of real
R→R,l..u
R→L:array [l..u
L→L,i|i
则相应的语义子程序为:
D→R] of int
{dim(R.dim);
type(R.table,int);
conspart(malloc(R.V* byte(int)-R.C)
}
D→R] of real
{dim(R.dim);
type(R.table,real);
conspart(malloc(R.V* byte(real)-R.C)
}
R→R 1,l..u
{R.dim:= R1.dim+1;
d:= u-l+1;
R.C:=R1.C*d+l;
R.V:= R1.V*d;
R.table:= R1.table;
fillin2 (R.table,l,u);
}
R→L:array [l..u
{R.dim:=1;
R.C:=l;
R.V:= u-l+1;
R.table:=L.table;
fillin2 (R.table,l,u);
}
L→L 1,i
{L.table:= L1.table;
add(L.table,i)
}
L→i
{L.table:=build(i);
}
记录或结构是由一些已知类型组成的复合的数据类型。 C语言中的类型文法如下:
<类型 >→struct {< 成员表 >}|char|int|real|pointer
<成员表 >→< 成员表 >;<成员 >|<成员 >
<成员 >→< 类型 > i|<类型 > i[<下标表 >]
<下标表 >→< 下标表 >,n|n
其中,n为常量,fillin3(i,offset)表示将当前记录中成员标识符 i的符号表,填写相对位移值为 offset,属性 len表示该文法符号的长度,即字节数。属性 offset表示相对位置。
对于第一个成员的相对位置为 0。
其语义处理子程序如下:
<类型 >→struct {< 成员表 >}
{<类型 >.len:= <成员表 >.offset
}
<类型 >→char
{<类型 >,len:=1
}
<类型 >→int
{<类型 >,len:=2
}
<类型 >→real
{<类型 >,len:=4
}
<类型 >→pointer
{<类型 >.len:=4
}
<成员表 >→< 成员表 1>;<成员 >
{ fillin3(<成员 >.name,<成员表 1>.offset);
<成员表 >,offset:= <成员表 1>,offset+ <成员 >.len;
}
<成员表 >→< 成员 >
{ fillin3(<成员 >.name,0);
<成员表 >,offset:= <成员 >.len
}
<成员 >→< 类型 > i
{<成员 >.name:=i;
<成员 >,len:=<类型 >.len
}
<成员 >→< 类型 > i[<下标表 >]
{<成员 >.name:=i;
<成员 >,len:=<类型 >.len*<下标表 >.V
}
<下标表 >→< 下标表 1>,n
{<下标表 >.V:= <下标表 1>.V*n
}
<下标表 >→n
{<下标表 >.V:=n
}
注意在这里没有考虑某些机器中对于起始地址必须是偶地址的问题,但对上述翻译稍作修改亦可解决。对于 C中的 union
类型,它们将不同的数据类型分配在同一存储空间中,需用一个标记来描述当前使用的是何种类型。同样 Pascal中的变体部分也需类似的方法解决。
4,过程或函数的说明语句的翻译过程或函数的说明语句的翻译的处理稍为复杂,由于一些过程和函数允许递归,这样每进入一次过程或函数需分配一次内存,因此在处理过程或函数内的其它说明语句时,只要分配每个标识符的相对位置以及求出进入该过程或函数所需的空间总长度。处理过程或函数体时,首先要生成申请所需的存储空间的代码,其次生成过程或函数体的代码 (包括入栈和出栈所需的指令 )。另外在处理过程说明时,还要注意同名标识符在不同层次应翻译成不同的对象,即注意标识符的作用域或可视性。例如 Pascal程序段:
program example(input,output);
var x,y:integer;
procedure change1;
var y:integer;
begin
x:=1;y:=2;
end;
procedure change2(var x:integer;y:integer);
begin
x:=1;y:=2;
end;
begin
x:=0;y:=1;
chamge1;
write(x,y);
change2(y,x)
write(x,y);
end;
则在 change1中使用的 x是主程序中说明的变量,而
change1中使用的 y却是 change1中说明的 y; change2中使用的 x和 y均为 change2中说明的变量 x,y。具体翻译工作在第七章中介绍。
6.3.3 短路表达式的制导翻译程序设计语言一般都有逻辑判断功能,其依据是布尔表达式。布尔表达式又称逻辑表达式,它是由布尔运算符或称逻辑运算符( AND,OR,NOT)(可用 ∧,∨,~表示)作用在布尔变量(或常量)、或关系表达式而形成的。布尔表达式的翻译方法直接对条件语句、当型循环语句,重复语句的翻译有着重要的影响。
1,布尔表达式采用算术表达式的翻译方式布尔表达式的运算次序和它的文法的定义有很大的关系。
如,Pascal中布尔表达式是和一般表达式一起定义的,其定义如下:
<表达式 >→< 简单元表达式 >
|<简单元表达式 ><关系运算符 ><简单元表达式 >
<关系运算符 >→=|<>|<|<=|>|>=|in
<简单元表达式 >→< 项 >|<正负号 ><项 >
|简单元表达式 ><加型运算符 ><项 >
<正负号 >→+| -
<加型运算符 >→+| -|or
<项 >→< 因子 >|<项 ><乘型运算符 ><因子 >
<乘型运算符 >→*|/|div|mod|and
<因子 >→< 变量 >|无符常量 |( <表达式 >) |<函数标识符 >
|<集合 >|not <因子 >
……
这样翻译布尔表达式可用翻译算术表达式的类似方法。
如 Pascal中用非 0表示“真”,0表示“假”,这样其翻译方法就与算术表达式一样。
考虑这一种所谓不优化的翻译方法,翻译 a or b not c
根据其优先级,相应的四元式序列为:
t1:=not c
t2:=b and t1
t3:=a or t2
另外,对于类似关系表达式 a>b,则应翻译成如下四元式代码:
(p) (jg,a,b,p+3)
(p+1) (:=,0,_,ti)
(p+2) (jmp,_,_,p+4)
(p+3) (:=,1,_,ti)
(p+4)

(p) if a>b goto p+3
(p+1) ti:= 0
(p+2) goto p+4
(p+3) ti:=1
(p+4)
这里为记录 p的相对序号,用函数 nextatat表示下一条将要产生的四元式的地址。则对于逻辑表达式文法及语义动作如下:
E→E 1 or E2
{E.place:= newtemp;
gen(or,E1.place,E2.place,E.place)}
E→E 1 and E2
{E.place:= newtemp;
gen(and,E1.place,E2.place,E.place)}
E→not E 1
{E.place:= newtemp;
gen(not,E1.place,_,E.place)}
E→(E 1)
{E.place:= E1.place}
E→i 1 relop i2
{E.place:= newtemp;
gen(relop,i1.place,i2.place,nextstat+3);
gen(:=,0,_,E.place );
gen(jmp,_,_,nextstat+2);
gen(:=,1,_,E.place );}
E→true
{E.place:= newtemp; gen(:=,1,_,E.place)}
F→false
{E.place:= newtemp; gen(:=,0,_,E.place)}
对于布尔表达式的翻译,可以有另一种所谓优化的方法。
如以 a or b为例,当 a的值为 true时,b的值显然可以不计算整个布尔表达式的值为 true。但这样的翻译如对于存在函数副作用的情况下是不太适合的。由于 b中的函数调用会改变一些环境变量的值,若不计算 b时则不会改变。而函数的副作用会导致程序的易读性降低,因此在不允许使用函数的副作用前题下是否采用优化的布尔表达式其结果是一样的。
把上述方法称为短路表达式的翻译,这种翻译方法可以把布尔表达式翻译成没有任何布尔运算的四元式,它只要计算部分表达式,一旦知道布尔表达式的结果就停止计算。用这种方法表达式 a and (b or not c)就等价于:
if a then
if b then true else not c
else flase
即,(p) (j,a,p+1,p+5)
(p+1) (j,b,p+3,p+2)
(p+2) (j,c,p+5,p+3)
(p+3) (:=,1_,ti)
(p+4) (jmp,_,_p+6)
(p+5) (:=,0_,ti)
其中:四元式 (j,a,p,q)表示当逻辑值 a为真时转向第 p条四元式,否则转向第 q条四元式。
下面用 E的属性 E.true和 E.false表示表达式 E中的转向“真”
的链首指针和“假”的链首指针。函数 merge(p1,p2),表示把 p1,p2为链首合并成一条链,并返回合并后的链首指针。
backpatch(p,r),表示将链首指针 p中指出的元素填上 r。用属性 codebegin表示相应文法的首代码的四元式编号。则一种自下而上的制导翻译为:
(1) B→E {B.place:=newtemp;
backpatch(E1.true,nextstat);
gen(:=,1_,B.place);
gen(jmp,_,_,nextstat+1);
backpatch(E1.false,nextstat);
gen(:=,0_,B.place );}
(2) E→E 1 or E2
{backpatch(E1.false,E2.codebegin);
E.codebegin:= E1.codebegin ;
E.true:= merge( E1.true,E2.true );
E.false:= E2.false }
(3) E→E 1 and E2
{ backpatch(E1.true,E2.codebegin);
E.codebegin:= E1.codebegin ;
E.false:= merge( E1.flase,E2.flase);
E.true:= E2.true }
(4) E→not E 1 { E.codebegin:= E1.codebegin ;
E.false:= E2.true
E.true:= E2.false }
(5) E→(E1) { E.codebegin:= E 1.codebegin ;
E.false:= E2,false
E.true:= E2,true }
(6) E→i 1 relop i2 {E.true:= nextstat;
gen(rop,i1.entry,i2.entry,nextstat);
E.false:= nextstat;
gen(jmp,_,_,_);}
(6) E→true {E.true:= nextstat; E.false:=null;
E.codebegin:= nextstat;
gen(jmp,_,_,_);}
(7) F→false { E.false:= nextstat; E.true:= null;
E.codebegin:= nextstat;
gen(jmp,_,_,_); }
其中,rop为,jz,jnz,jg,jl,jle,jge分别为 =,<>,>,<,≤,≥
6.3.4 控制语句的翻译在控制语句中,如,if-then,if-then-else,while、
repeat等语句中的布尔表达式直接控制了程序的转向,而不必求出布尔表达式的值,因此可以直接使用上节中的 E的两个出口真出口和假出口,也就是在控制语句中不必归约成 B,
即不要产生布尔表达式的值。则相应的自下而上的翻译方案为:
(1) E→E 1 or E2 {backpatch(E1.false,E2.codebegin);
E.codebegin:= E1.codebegin ;
E.true:= merge( E1.true,E2.true );
E.false:= E2.false }
(2) E→E 1 and E2 { backpatch(E1.true,E2.codebegin);
E.codebegin:= E1.codebegin ;
E.false:= merge( E1.flase,E2.flase);
E.true:= E2.true }
(3) E→not E 1 { E.codebegin:= E1.codebegin ;
E.false:= E2.true
E.true:= E2.false }
(4) E→(E 1) { E.codebegin:= E1.codebegin ;
E.false:= E2,false
E.true:= E2,true }
(5) E→i 1 relop i2 {E.true:= nextstat;
gen(rop,i1.place,i2.place,nextstat);
E.false:= nextstat;
gen(jmp,_,_,_);}
(6) E→true {E.true:= nextstat; E.false:=null;
E.codebegin:= nextstat;
gen(jmp,_,_,_);}
(7) F→false { E.false:= nextstat; E.true:= null;
E.codebegin:= nextstat;
gen(jmp,_,_,_); }
有了上述在控制语句中的布尔表达式的翻译,下面来讨论类似 Pascal的条件语句,while循环语句、复合语句、开关语句、
for循环语句,exit语句和 goto语句。
1,条件转移设有条件语句的文法
G[S]:
S→if E then S|S→if E then S else S|a
其中 a表示其它非条件语句则对于条件语句 if E then S希望产生下列四元式序列:
(l) E的四元式序列 (其中真出口指向 p,假出口指向 q)
(p) S的四元式序列
(q)
对于条件语句 if E then S1 else S2希望产生下列四元式序列:
(l) E的四元式序列 (其中真出口指向 p,假出口指向 q)
(p) S1的四元式序列
(jmp,_,_r)
(q) S2的四元式序列
(r)
这里使用的是自上而上的语法制导翻译,在归约 E时和 S1以及 if E then S1能产生相应的四元式代码,而在 E归约之后 S1
归约之前,还应该填写 E的真出口。对于 if E then S1 else S2,
在 S1归约之后应产生一条 (jmp,_,_r)的四元式,以便在 S2归约之后 r的值产生之后,再返填相应的 r。这样为此在 E归约之后
S1是归约之前应做一个归约、对于有 else的条件语句,在 S1
是归约之后 S2归约之前还要做一归约。为此将文法改变为:
G[S]:
S→CS
S→T pS
C→if E then
Tp→CS else
以便在归约时,能产生相应的条件语句所需的四元式代码。
用属性 chain表示相应文法符号所有语句的出口构成的链。综上所述产生式和语义动作为:
产生式 语义动作
S→C S 1 {S,chain:=merge(C,chain,S1,chain);}
S→T p S1 { S,chain:=merge(Tp,chain,S1,chain);}
C→if E then {backpatch(E.true,nextstat);
C.chain:=E.false;
}
Tp→C S else {merge(T p,chain,= nextstat;
gen(jmp,__,_,_);
backpatch(c.chain,nextstat);
}
2,while循环语句设有 while型循环语句的文法为:
G[S]:
S→while E do S
对于当型循环语句 while E do S希望产生下列四元式序列:
(l) E的四元式序列 (其中真出口指向 p,假出口指向 q)
(p) S的四元式序列
(jmp,_,_l)
(q)
为记录 while E do S四元式的首地址(即 l的值),在 E归约之前也要做一个归约、为在 E归约这后,S归约之前应填写 E
的真出口,故改变文法为:
G[S]:
S→W d S
Wd→W E do
W→while
以便在归约时,能产生相应的条件语句所需的四元式代码。
综上所述产生式和语义动作为:
产生式 语义动作
S→W d S1 {backpatch(S1,chain,Wd.codebegin);
gen(jmp,__,_,Wd.codebegin);}
Wd→W E do {backpatch(E.true,nextstat);
Wd.chain:=E.false;
Wd.codebegin:= W.codebegin;}
W→while {W.codebegin:=nextstat;}
3,复合语句设有 while型循环语句的文法为:
G[S]:
S→begin L end
S→A
L→L;S
L→S
虽然复合语句不要产生四元式序列,但它要为其它控制转移语句提供该语句的结束位置,以便正确地跳转。用属性
chain表示所需跳转指令的链表(当然如果不需要优化,用多级转跳转,即通过一条指令转达向某条指令执行该条指令仍为转移指令,可以不采用该属性,也不必改变文法和配有相应的语义子程序),则改变方法为:
G[S]:
L→L S S1
L→S
L S →S
综上所述,其产生式和语义动作为:
产生式 语义动作
S→begin L end { S,chain:= L,chain;}
L→L S S { L,chain:= S,chain;}
L→S { L,chain:= S,chain;}
L S →L; { backpatch(L,chain,nextstat);}
4,赋值语句设有赋值语句的文法为:
G[S]:
S→A
由于赋值语句归约成语句时,它的四元式代码已全部产生,可以不产生任何动作,但为了在控制语句和非控制语句
merge函数合并时不会产生没有 S,Chain属性的错误,故为此添加了相应的语义动作:
产生式 语义动作
S→A {S,chain:=null;}
4,开关语句很多程序设计语言为了减少条件语句的嵌套层数,允许使用开关语句( switch,case),但在具体实现中有所有不同。设有开关语句:
switch E of
case V1,S1
case V2,S2
case V3,S3
case V4,S4
……
case Vn-1,Sn-1
default:Sn
则所需产生的四元式为( default部分是可以省略的,即最后一条 goto next和 Ln:Sn是可以省略的):
L1:if E≠V1 goto L2;
S1;
goto next;
L2:if E≠V2 goto L3;
S2;
goto next;
……
Ln-1:if E≠Vn-1 goto Ln;
Sn-1;
goto next;
Ln:Sn;
next,
即,
(L1) (jne,E.place,V1.palce,L2 )
S1的四元式序列
(jmp,_,_,r)
(L2) (jne,E.place,V1.palce,L3 )
S2的四元式序列
(jmp,_,_,r)
……
(Ln-1) (jne,E.place,Vn-1.palce,Ln )
Sn-1的四元式序列
(jmp,_,_,r)
(Ln) Sn的四元式序列
( r)
由于每个 case部分产生的四元式也相同,因此用文法的递归定义。对于每个 case需产生一条比较的四元式 (jne,
E.place,Vi..palce,Li+1 );在每个语句归约后应产生一条无条件转移的四元式 (jmp,_,_,r)、返填上一个语句的条件转移的位置和合并语句的出口链;对于存在 default的 case语句,
需产生一条无条件转移四元式 (jmp,_,_,r)、返填上一个语句的条件转移的位置和合并语句的出口链;最后把所有转向语句的出口合并起来。为此,改变文法为:
S→C a S|MS
Ca→M d S default:
M→M d case V:
Md→M S
M→switch E of case V,
S→C a S1 {S.chain:=merge(Ca,chain,S1.chain)
}
S→MS 1 {S.chain:= merge(M.next,S1.chain);
}
Ca→M d S default,{ Md.chain:=nextstat;
gen(jmp,_,_,_)
backpatch(M.next,nextstat);
Ca,chain:=merge(Md.chain,S.chain);
}
M→M d case V,{ M.next:=nextatat;
gen(jne,E.place,V..palce,_)
}
Md→M S {M d.chain:=nextstat;
gen(jmp,_,_,_)
backpatch(M.next,nextstat);
Md.chain:=merge(Md.chain,S.chain);
}
M→switch E of case V,{ M.next:=nextatat;
gen(jne,E.place,V..palce,_)
}
另外,为了减少 case语句的执行时间,翻译 case语句的情况标号可以用填查散列表的方式来翻译。
6,for循环语句一般程序设计语言除了 while 循环语句外,还有 for 循环语句,虽然 for语句的形式较多,但大致形式相同。为了全面理解 for循环语句,在这里介绍的是 Algol语言中的 for 循环语句的制导翻译。
考虑循环语句:
S→for i:=E 1 setp E2 until E3 do S1
其中 E1为初值,E2为步长,E3终值为简单起见,假定步长总是正的(若为负,在判别循环变量 i≤终值 E3时,只要不等式两边同乘以一负号,改为判断 -i≤-E3)。
因此这种循环语句就等价于:
i:=E1
goto OVER;
AGAIN,i:= i+E2;
OVER,if i≤E3 then begin S1 ;goto AGAIN end;
即:
(p) E1的四元式序列
(:=,E1.palce,_,ENTRY(i))
(m) (jmp,_,_,l)
E2的四元式序列
(+,ENTRY(i),E2.palce,ENTRY(i))
(l) E3的四元式序列
(jg,ENTRY(i),E3.palce,r)
S1的四元式序列
(jmp,_,_,m+1)
(r)
注意为什么不等价于:
i:=E1
AGAIN,if i>E3 then goto OVER;
S1 ;
i:= i+E2;
goto AGAIN;
OVER:
这是因为 S→for i:=E 1 setp E2 until E3 do S1在归约过程中表达式 E1,E2,E3是按照 E1,E2,E3的次序归约的,而上述四元式的次序是 E3的代码在 E2的前面,如要将 E2,E3代码交换次序需大量的辅助存储空间。
要产生前面一种形式的四元式也必须改变文法:
F1→for i:=E 1
F2→F 1 setp E2
F3→F 2 until E3
F4→F 3 do S1
其相应的语义动作为:
F1→for i:=E 1
{gen(:=,E1.palce,_,ENTRY(i));
F1.place:= ENTRY(i)
F1.chain:=nextstat;
gen(jmp,_,_,_);
F1.codebegin:=nextstat;
}
F2→F 1 setp E2
{F2.codebegin:= F1.codebegin;
F2.place:= F1.place;
gen(+,F1.place,E2.palce,F1.place);
backpatch(F1.chain,nextstat);
}
F3→F 2 until E3
{ F3.codebegin:= F2.codebegin;
F3.chain:=nextstat;
gen((jg,F2.place,E3.palce,_)
}
F4→F 3 do S1
{gen (jmp,_,_,F3.codebegin);
backpatch(S1.chain,F3.codebegin);
S.chain:= F3.chain;
}
上述翻译方法对于每一次循环需计算一次表达式 E2和 E3,
这对编译来说是低效的。这此有的编译程序将循环语句等到价于:
i:= E1;
INCR:= E2;
LIMIT:= E3;
goto OVER;
AGAIN,i:=i+ INCR;
OVER,if i≤LIMT then begin S1 ;goto AGAIN end;
相应的语义子程序略。
7,出口和转移语句在这里讨论的是为跳出循环而设置的语句,如 C语言中的 break,continue和 goto语句。 C语言中的 break语句其含义是:跳出当前执行的循环语句或 case语句。这样 break语句实际上也是一种转移语句,但这种转移语句需要在相应的语句翻译完毕才能确定所转移的位置。但是一个 break语句可能在几个嵌套的循环语句或 case语句之中,因此如何找到包含它的最内层循环或 case成为解决这种翻译方法的主要技术。 continue语句的含义为结束一轮循环,进入下一轮循环。
continue语句的翻译相对简单,因为在翻译 continue语句时循环语句( for,while,do-while)的首代码已编译。对于
goto语句应该考虑的问题是:如标号的先使用后说明问题、
怎样区别一个 goto语句是从条件、循环语句等转出还是转入。
为解决这些问题,在编译时可用一个称为循环嵌套栈来实现,栈的每个登记项有二个分量一是存放 while,do-while、
case首代码的四元式编号;对于 for语句可以存放 AGAIN指出的四元式编号(以便处理 continue语句);对于,<标号 >:
<语句 >”的形式,存放标号指出的四元式编号,即标号后语句的四元式编号;对于 exit语句即存放 exit语句的四元式编号。
另一个是存入该四元式编号的类型,它包括,for,while、
do-while,case,break和 label(标号 )。在获得 while,do-
while首代码的四元式编号,for语句的 AGAIN的四元式编号和标号后语句的四元式编号时,将它们和相应的类型入栈。
在归约 continue语句时,从栈顶向栈底查找第一个类型为 for、
while,do-while的元素,若找到取出该登记项的四元式编号,
记为 r,并生成一条四元式( jmp,_,_r)。在归约 exit语句时,
将它入栈并生成一条不完全的四元式( jmp,_,_,_)。在归约
for,while,do-while,case时,逐个退栈至上述四种类型之一,当栈顶类型与归约语句类型一致时退栈,否则出错。退栈时遇 exit类型时用 nextstat返填 exit语句对应的( jmp,_,_,_)
的四元式。
程序设计语言中的转移语句多数是使用 goto语句来实现的。转移的目标称为标号,Pascal的标号为无符号整数,C
的标号为标识符。现以 C的 goto语句为例说明转移语句的翻译。带标的语句的一般形式为:
L,S
当这种语句被处理后,所转向的位置是已知的了,这种已知称为“定义”。如果翻译的 goto L是向前转移语句,也就是转向的是已定义的标号,则先从符号表中取出 L指出的四元式编号 r,并从循环嵌套的栈顶向栈底查找是否存在四元式编号为 r,类型为 label的元素,若有说明不是已终止的循环语句内的标号,故可直接生成一条( jmp,_,_r)的四元式,
否则出错。如果 goto L是一个向后转移语句,也就是转向一个未定义的标号。此时由于标号 L尚未定义,对 goto L只能生成一条不完全的四元式( jmp,_,_,_),它的转移目标须等待 L定义时再填写进去。由于转达 L的 goto语句可能有好几个,
为此要将以 L为转移目标的四元式全部记录下来,以便一旦 L
确定便可返填。
为节省编译程序的存储空间,可用四元式中的尚未填写的转移地址域来链接所有转向 L的四元式如图所示。
因此用产生式
S→< 标号 > S
<标号 >→L:
说明标号时,则产生式 <标号 >→L,的语义动作为:
(1) 若标识符 L不在符号表中,则把它填入,置“类型”为
“标号”,“定义否”为“已”,“地址”为 nextstat。并将 L的地址和类型入循环嵌套栈。
(2) 若 L已在符号表中,但“类型”不为“标号”或“定义否”
为“已”,分别说明该标识符已作它用或该标号多次定义,
故报告出错。
(3) 若 L已在符号表中,则把定义否”的标记改为“已”,然后把地址栏中的链关(记为 q)取出,在循环嵌套栈中查找出第一个循环语句元素的四元式编号,记为 r。则链表中的每个四元式的编号与 r比较,若大于等于 r,则将 nextstat返填至该四元式中,否则存在从循环体外转向循环体内,则报告出错。
8,过程和函数的调用过程和函数的调用实质是把运行程序的控制权转移到子程序,即函数或过程中,为了实现控制权的转移,必须实现参数的传递。参数传递的方式常用的有二大类,一类是所谓传值,另一类称为传地址。对于传值方式,如果实在参数是一个变量、数组的元素或表达式,那么就直接将值存放在子程序中的对应的每个形式参数的形式单元中;对于传地址方式,如果实在参数是一个变量、数组的元素就直接传递它的地址,如果实参是其它表达式,就传递存放该表达式的临时单元的地址,在子程序中对形式参数的任何引用都当作是对形式单元的间接访问。另外,从过程或函数返回时要将运行程序的控制权重新返回调用程序。对于过程返回时,不必返回值。对于函数返回之前将需返回的值存放在某个寄存器中或某个内存单元中。返回相对简单,在这里只讨论参数的传递。
设有 Pascal中的调用过程语句
add(a+b,c)
其中:第一参数为值参,第二参数为地址参数则,翻译成:
par T /* a+b的值存于临时变量 T中 */
par @c /*@c表示地址参数 */
call add
考虑一个描述过程或函数调用语句或表达式的文法:
(1) S→i (< 参数表 >)
(2) <参数表 >→< 参数表 >,E
(3) <参数表 >→E
为了在调用 add之前记住原来参数数的次序使用队列 queue,
用它按序记录每个实参数的地址,为检验参数个数的一致性,
可使用数组中的下标统计方法。如要检查参数的类型一致性,
可用一个队列记录每个参数的类型。
下面是过程或函数调用的语法制导翻译的基本语义动作:
(1) S→i (< 参数表 >)
{for队列 <参数表 >.queue的每一项 p do
if p.type=i的第 <参数表 >.count个元素类型
then if 该参数的传递方式为值传递
then gen(par,_,_p.place)
else gen(par,_,_@p.place);
gen(call,_,_entry(i))}
(2) <参数表 >→< 参数表 1>,E
{把 E.place,和 E.type 组成的元素排在 <参数表 1>.queue的末端
<参数表 >.queue:= <参数表 1>.queue
<参数表 >.count:= <参数表 >.count+1}
(3) <参数表 >→E
{建立一个 <参数表 >.queue,它只包含一个元素,该元素有二个分量,一是 E.place,另一个是 E.type;
<参数表 >.count:=1}
其中,entry(i)表示子程序 i的入口地址,四元式
( par,_,_p.place)和 (par,_,_@p.place)分别表示 p.place的值参数和地址参数。
6.4 自顶向下的制导翻译在自顶向下的制导翻译和自底向上的制导翻译的不同之处是前者是在归约时完成相应的语义运作,而后者是在推导时完成相应的语义动作的。
6.4.1 翻译方案的设计翻译方案和语法制导定义不同之处是它的语义动作放在 {}内,
且可以放在产生式右部的任何地方。这样翻译言的这种动作和分析是交错表示的,即在分析的每时每刻都可以完成语义动作。
定义 6.4 如果一个语法制导定义是 L属性的,则每个产生
A→X 1X2……,X k的某个文法符号 Xi的属性,仅依赖于 A的继承属性和 X1,X2,……,X i-1的综合属性。
例,计算表达式值的翻译方案
E→T{R.i:=T.val}R{E,val:=R.s}
R→+T{R 1.i:=R.i+ T.val }R1{R.s:=R1.s}
R→ -T{R1.i:=R.i- T.val }R1{R.s:=R1.s}
R→ε{ R.s:=R1.i}
T→(E) { T.val:=E.val}
T→num.lexval
显然上例中每个文法符号的属性均为 L属性。在自上顶向下的分析中,也是从左到右扫描输入符号,这样产生式右部的左边非终结符的属性很容易传送到右部文法符号右边的其它属性以及传送到左部文法符号的属性中。故在自上顶向下的分析中适用于 L属性。当一个文法的属性不是 L属性时可以在制导翻译中可以生成属性的依赖图,建立属性的拓朴排序从而完成语法的制导定义。为了简单起见这里仅介绍 L属性的翻译方案。在自上顶向下的分析中,语义动作的执行时机是当一个非终结符找到一串终结符匹配时才执行。
图为计算表达式值的属性传递图,现以 52-18+9为例。
在用上述翻译方案分析中,T.val的值即为 num的显示值(即
num.lexval)子表达式 52-18中的值 52是由图 6-11中的最左边的 T产生的,-18+9是由 T的右边的 R产生的。继承属性 R.i
从 T.val获得值 52。然后通过嵌入在 R→ -TR1中的动作
{R1.i:=R.i- T.val }进行运算,得到继承属性 R1.i的值为 34。
同理通过嵌入在 R→+TR 1中的动作 {R1.i:=R.i+T.val }进行运算,
得到继承属性 R1.i的值 43,然后,将 R.i传送给综合属性 R.s,
最后将 R.s拷贝到 E.val。
翻译方案和制导翻译是等价变换的。对于自顶向下的翻译无论是回溯法、预测分析法还是递归子程序法均不允许使用左递是归。下面介绍有左递归的制导翻译到消除左递归的翻译方案的转换方法。设有文法及综合属性的语义动作为
A→A 1α {A.s:=f(A1.s,a1.a2.a3,……,a n)}
其中,α= X1X2……,X k,Xi∈ Vn∪ Vt (i=1,2,3,……,k)
a1.a2.a3,……,a n 是 Xi的属性
A→β {A.s:=g(b 1,b2,,b3,……,b m)}
其中,β= Y1Y2……,Y l,Yi∈ Vn∪ Vt (i=1,2,3,……,l)
b1,b2,b3,……,b m 是 Yi的属性则消除左递归后的文法为:
A→βR
R→αR|ε
其翻译方案为:
A→β{R.i:= g(b 1,b2,,b3,……,b m) }R{ A.s:=R.s}
R→α{R 1.i:= f(A1.s,a1.a2.a3,……,a n)} R1{R.s:= R1.s}
R→ε{ R.s:= R.i}
递归子程序法翻译器的设计递归子程序法由于方法简单和容易在各过程中添加语义动作,
在一些适合于手工构造编译程序仍使用这种技术,递归子程序法也是一种无回溯的自顶向下分析技术,它应满足 LL( 1)
的条件,也就是其必要条件是在文法中的任何非终结符每个候选式中不能含有左递归、左公因子且该文法必定不是二义性的。下面以表达式为例说明在自顶向下的分析技术中将表达式翻译成相应的四元式设有文法 G[E]:
E→E+T|T T→T*F F→ ( E) |i
为适用于递归子程序法,消去左递归和提左共因子后其:
E→TE? E?→+TE?|ε T→FT? T?→*FT?|ε F→(E)|i
经检验该文法为 LL(1)文法。
则翻译方案为
E→T{E?.i:=T.place} E?{E,place:= E?.s}
E?→+T{E 1?.i:=newtemp; gen(+,E?.i,T,place,E1?.i )}E1?
{ E?.s:= E1?.s}
E?→ε{ E?.s:= E?.i}
T→F{T?.i:=F.place}T? {T,place:= T?.s}
T?→*F{ T 1?,=newtemp; gen(*,T?.i,F,place,T1?.i ) T1? { T?.s:=
T1?.s}}
T?→ε { T?.s:= T?.i}
F→(E) {F.place:=E.place}
F→i {F.place:=lookup(i)}
其递归子程序为:
PROCEDURE E(var E.place);
BEGIN
T(T.place);
E?.i:=T.place;
E?(E?.i,E?.s);
E,place:= E?.s
END;
PROCEDURE E? (E?.i,var E?.s);
BEGIN
IF 下一个符号 =?+? THEN
BEGIN
取下一个符号;
T(T,place );
E1?.i:=newtemp;
gen(+,E?.i,T,place,E1?.i);
E?(E1?.i,E1.s);
E?.s:= E1?.s
END
ELSE
E?.s:= E?.i
END;
PROCEDURE T(var T.place);
BEGIN
F(F.place);
T?.i:=F.place
T?(T?.i,T?.s);
T,place:= T?.s
END;
PROCEDURE T?(T?.i,var T?.s);
IF 下一个符号 =?*? THEN
BEGIN
取下一个符号;
F(F.place);
T1?.i:=newtemp;
gen(+,T?.i,F,place,T1?.i);
T?(T1?.i,T1.s);
T?.s:= T1?.s
END;
PROCEDURE F(var F,place);
BEGIN
IF 下一个符号 =i THEN
BEGIN
取下一个符号;
F,place:= lookup(i)
END
ELSE
IF 下一个符号 =?(? THEN
BEGIN
取下一个符号;
E(E,Place);
IF 下一个符号 =?)? THEN
BEGIN
取下一个符号;
F.place:=E.place
END
ELSE 出错
END
ELSE 出错
END
上述递归子程序中,忽略了所有变量的说明。用值参数传递属性,用变量参数获得所分析文法符号的属性。