E N D
第六章 语法制导译 在前面已经介绍了编译程序构造的二个重要阶段,即词法分析和语法分析。现在再来介绍编译程序的另一个重要阶段——中间代码生成。虽然在实际应用中,是否采用中间代码形式是根据实际情况而定的。但事实上,为了使编译程序的结构清晰、简单、明确,多数编译程序采用了中间代码的形式。尤其是使用了中间代码的形式,使目标代码优化比较容易实现。通常以中间代码生成这一阶段来划分编译程序的前端和后端。对于不同的高级语言只要翻译成相同的中间代码,再接上一个相同的把中间代码翻译成目标代码的后端,就可以形成不同的编译程序。同一种高级语言只要翻译成相同的中间代码就可以共用一个前端,接上后端可以在不同机型上实现同一语言的编译程序。虽然中间代码的形式很多,但常见的中间代码有逆波兰式、三元式、四元式、树形表示等。本章讨论如何将高级语言翻译成中间代码。
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。解决上述问题的方法有二种:当逆波兰式允许单目运算符(仅允许一个运算对象的运算符)出现时,会产生一些问题。如写出表达式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语句扩充至逆波兰式。另外需要指出的是:赋值运算符和其它逆波兰式不一样,它要把<表达式>的值放入<变量>,在栈中只需要变量的地址,而不是值,计算赋值运算符后不要将结果入栈。当然用同样的方式,还可以将逆波兰式扩充至数组、记录或其它数据类型,也可将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的树如图。下面介绍用树来表示算术表达式。用树来表示中间代码需要用两个指针分别指向它的左子树和右子树。如果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)表示树的根,每个子表达式用一棵子树表示,可以看出对于该树的前序遍历、中序遍历(对不符合运算符的优先级的运算应加上括号)和后序遍历分别产生的是前缀式、中缀式和逆波兰式,因此树型表示隐含了三元式表示和逆波兰表示。对于表达式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的汇编语言语句为:例如,表达式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中。其中a,b,c,d,e表示变量,最后将结果存放在某个临时变量ti中。 虽然,汇编语言较三元式、四元式有上述三个优点,但汇编语言与它们相比还有一些明显的缺点,就是依赖于计算机系统。这样对于编译程序设计者来说,需熟悉不同的计算机系统的特点。因而使得开发周期延长,工作效率降低。汇编程序实际上也是一个小型的编译程序,因而翻译汇编语言到目标语言的过程要比翻译三元式、四元式到目标语言的过程要复杂。
6.2 语法制导翻译 下面讨论编译程序是如何把源程序翻译成相应的中间代码的。由于语义规则的定义要比语法的定义复杂得多,至今没有一种从理论上和实际上都完备的方法。目前大多数采用的方法是所谓的语法制导翻译的方法,语法制导翻译的思想方法是把语言的一些属性附加到代表语言结构的文法的符号上,这些属性值是由附加到文法产生式的“语义规则”计算,也就是为每个产生式配上翻译子程序,即语义子程序。语义子程序的语义规则的计算可以产生代码、把信息存入符号表、显示出错信息等等。语法制导翻译的含义是在语法分析过程中,在自顶向下的分析方法中当一个产生式获得匹配或在自底向上的分析方法中用于归约时,则相应产生的语义子程序就进入工作,完成既定的翻译工作。既定的翻译工作主要有两方面,其一是审查语法结构的静态语义,即验证语法结构合法的程序是否真正有意义。其二是如果静态语义正确,则要将源程序翻译成一种中间表示形式,即用中间语言表示源程序的语义。
6.2.1 计算表达式的制导翻译 从形式语义的角度来看,语法制导翻译不是一种理论上完备的语义分析方法。形式语义是一门学科,它主要研究语言的语义表示。就目前来说,有各种方法和记号系统推出,如操作语义学,公理语义学和指称语义学。但无论那一种方法,都存在着缺陷,如其本身的符号系统复杂,其描述文本易读性差,也就是说,目前尚不能借助于这些形式系统来自动完成语义处理任务。因此目前仍只能采用是接近形式化的制导翻译。在这里以属性文法为工具来说明程序设计语言的语义,一个属性文法它包含一个上下文无关文法和一系列语义规则,这些语规则附在文法的每个产生式上,如在自顶向下的语法分析过程中,当归约时就完成附加在所使用的产生式上的语义规则的动作,从而实现语义处理。 在语法制导定义中,每个文法符号有一组属性,对于每个文法产生式A→α有一组形式为b:=f(c1, c2,……ck)的语义规则,即属性b是由属性c1, c2,……ck决定的。其中b和c1, c2,……ck是该产生式中文法符号的属性。
定义 6-1 如果b是A的属性,c1, c2,……ck是产生式右部α中的文法符号的属性或A的非b属性,那么b叫做文法符号A的综合属性 定义 6-2 如果b是产生式右部α某个文法符号的属性,c1, c2,……ck是A的属性或产生式右部文法符号的属性,那么b叫做文法符号A的继承属性。 从上定义可以看出,综合属性是对于产生式左部属性都是由产生式右部文法符号的属性和产生式左部文法符号的其它属性来决定的。即在语法树的角度看是从孩子往的双亲传送属性。而继承属性是产生式右部文法符号属性是由产生式左部文法符号的属性和产生式右部文法符号的其它属性决定的。即在语法树的的角度看是从双亲向孩子传送属性或由兄弟之间传送属性。
显然每个文法符号的综合属性集和继承属性集的交为空,也就是一个属性不可能既是综合属性,又是继承属性。显然每个文法符号的综合属性集和继承属性集的交为空,也就是一个属性不可能既是综合属性,又是继承属性。 定义 6-3 如果一个语法制导定义是S属性的,则每个产生式的属性,均是综合属性。 属性文法的语义规则可以写成无函数副作用的语法制导定义。语义规则的函数可以是表达式或相应的函数或过程的调用,也可以是程序段。
例:利用表达式的文法制导定义解释执行表达式的值。例:利用表达式的文法制导定义解释执行表达式的值。 产生式 语义规则 S'→E print(E.val) E→E1+T E.val:=E1.val+T.val E→T E.val:=T.val T→T1*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→L1,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)分析表:
要计算表达式的值,则需为每个产生式配上一个语义动作,如:要计算表达式的值,则需为每个产生式配上一个语义动作,如: S'→E print(E.val) E→E1+E2 E.val:=E1.val+E2.val E→E1*E2 E.val:=E1.val*E2.val E→(E1) E.val:=E1.val E→i E.val:=i.lexval 下面分析识别7+9*5,并完成相应的语义动作。为减少归约时的属性值拷贝,这里用属性栈记录属性,也就是用值栈记录文法的属性值val。对于没有属性val的文法符号为直观起见,用空表示。其中7、9、5为i的显示值
为说明语法的制导翻译的翻译方法,在这里先讨论算术表达式和简单赋值语句的制导翻译,其翻译成的中间代为逆波兰式。这里介绍的仍是自底向上的分析技术,即在语法分析中,当句柄归约时,就执行相应规则的语义处理子程序。现在为了简单起见可以不关心采用何种自底向下的分析方法,只要关心所归约的句柄。为说明语法的制导翻译的翻译方法,在这里先讨论算术表达式和简单赋值语句的制导翻译,其翻译成的中间代为逆波兰式。这里介绍的仍是自底向上的分析技术,即在语法分析中,当句柄归约时,就执行相应规则的语义处理子程序。现在为了简单起见可以不关心采用何种自底向下的分析方法,只要关心所归约的句柄。 例:设有文法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→T1*F归约时,T1和F已经归约,则T1和F的逆波兰式已相继产生,因T1比F先归约,那么T1的逆波兰式在F的逆波兰式前面,T的逆波兰式为<T1的逆波兰式><F的逆波兰式>*,与T的逆波兰式仅相差一个’*’,因此,产生式T→T1*F的语义子程序中只要输出一个’*’即可。同样,用产生式E→E1+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进行分析,观察所产生的逆波兰式,说明制导翻译的正确性。
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→E1+T,首先要产生一个新标识符ti,并把它与E联系起来,即有E.place存放临时变量ti,其次用gen语义过程来产生一条四元式(+,E1.place ,T.place, E.place }。同样对于产生式A→V:=E只要生成一条赋值的四元式;对于产生式T→T1*F则要申请临时变量和产生相应的四元式,
因此有: 产生式 语义动作(语义规则) (1) S→A {空} (2) A→V:=E {gen(:=, E.place,_, V.place) ;} (3) V→i {V.place=lookup(i) ;} (4) E→E1+T {E.place= newtemp; gen(+,E1.place ,T.place, E.place ) ;} (5) E→T {E1.place=T.place ;} (6) T→T1*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)可参照此方法修改。在程序设计语言中,变量的类型可以是不同的类型,如变量既可以是实型也可以是整型。当实型和整型混合运算时就会产生类型转换问题,在整型+实型运算时,先要产生一条(itr,a,_,ti)的四元式,并采用运算符+i和+r分别表示处理整型+和实型+的运算符,为了简单起见,这里仅对于规则(4)讨论,对于规则(2)和规则(6)可参照此方法修改。 产生式 语义动作 E→E1+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个元素。如图所示,二维数组对于一个多维数组如何保存在一维的存储器中是计算数组元素地址的关键。存放形式有二种,一种是所谓按行存放,对于一个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,……ln..un]其存放形式为“按行存放”,它的首地址为α。 令d1= u1- l1+1 d2= u2- l2+1 d3= u3- l3+1……dn= un- ln+1 即di是每维元素的个数。 数组元素a[i1,i2,i3…in]的地址公式可以通过下列方式获得。如图所示,该数组是“按行存放”(“按列存放”也可以按照此类似方法计算地址)的。
也就是对于整个数组可表示成从l1到u1的d1个n-1维的子数组,它是按顺序a[l1,*,*, …], a[l1+1,*,*, …], a[l1+2,*,*, …] ,…, a[u1,*,*, …]存放的。对于元素a[i1,i2,i3…in]前面必定有[l1,*,*, …], a[l1+1,*,*, ……], a[l1+2,*,*, …] ,…, a[i1-1,*,*, …]这些n-1维的子数组,也就是子数组a[i1,*,*, …]的首地地址为α+(i1- l1) d2 d3…dn*k,d2 d3…dn为n-1维的子数组的长度,k为数组元素基本类型的所需存储空间数。同理对于子数组a[i1, i2,*, …]的首地址为α+(i1- l1) d2 d3…dn*k +(i2- l2) d3…dn*k,d3…dn为n-2维的子数组的长度,依次类推可以得到数组元素a[i1,i2,i3…in]的地址为α+(i1- l1) d2 d3…dn*k +(i2- l2) d3…dn*k+(i3- l3) d4…dn*k+… (in-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…in]的地址,k为基本类型的字节数,整理得:把与im(m=1,2,3…,n)有关的部分称为可变部分用VARPART表示,其它与im(m=1,2,3…,n)无关的部分称为不变部分用CONSPART表示,用ADDR表示数组元素a[i1,i2,i3…in]的地址,k为基本类型的字节数,整理得: ADDR= CONSPART+ VARPART CONSPART=α- (…(l1 d2 + l2) d3+ l3)d4+…+ ln-1 dn+ ln)*k VARPART= (…(i1 d2 + i2) d3+ i3)d4+…+ in-1 dn+ in)*k 对于CONSPART一个数组只要计算一次,不同的数组元素只要计算不同的VARPART。 数组分静态数组和动态数组二大类,所谓静态数组是指数组所需的存储空间的大小是在编译时可以确定的,所谓动态数组是指数组所需的存储空间的大小是在运行时才可以确定。
为了便于检查使用数组合法性和计算地址方便,需把数组的每一维的下界l、上界u、长度d、首地址α和维数n、类型(type)、CONSPART等信息保存起来,这些信息综合起来称为内情向量,如图表示。静态数组的内情向量在编译时可以确定。把数组的内情向量通常填写在符号表中,以便供编译的其它部分分析使用。C语言由于不检查下标越界以及下界为定值0,上界与长度相等,故在内情向量中不必包含l、u。另外动态数组的上下界l、u和d在编译时是无法确定的,需在运行时填写相应的内情向量表。但编译时动态数组的内情向量的长度是可以确定的,因而编译时可以分配在运行时内情向量所需的存储空间,以及生成在运行时填写相应的内情向量表指令集合。为了便于检查使用数组合法性和计算地址方便,需把数组的每一维的下界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