1、1,第四章 语法制导翻译生成中间代码,语法制导翻译是处理语义的基本方法,它以语法分析为基础,在语法分析得到语言结构的结果时,对附着于此结构的语义进行处理,如计算表达式的值、生成中间代码等。与语法分析部分的讨论不同,本章的内容更注重于实际方法的讨论。 主要内容包括:语法制导翻译的基本概念中间代码简介符号表简介典型声明语句与可执行语句的翻译上机作业第三部分:语法制导翻译绘制函数图形,2,4.1 语法制导翻译简介 4.1.1 语法与语义, 语法与语义的关系语法是指语言的结构、即语言的“样子”;语义是指附着于语言结构上的实际含意 ,即语言的“意义”。 对于语法和语义:语义不能离开语法独立存在;语义远比
2、语法复杂;同一语言结构可以包含多种含意,不同语言结构表示相同含意;语法与语义之间没有明确的界线。,例1:猫吃老鼠与老鼠吃猫 例2:程序设计语言中的分情况结构:,1case condition iscase1: stat1;case2: stat2;.end case;,2switch (condition) case condition1:stat1; case condition2:stat2; .,break; break;,3,4.1.1 语法与语义(续1), 语义分析的两个作用检查是否结构正确的句子所表示的意思也合法;执行规定的语义动作,如:表达式求值符号表填写中间代码生成等语义分析的
3、方法语法制导翻译,4,4.1.2 属性与语义规则, 语法制导翻译的基本思想通俗地讲:以语法分析为基础,伴随语法分析的各个步骤,执行相应的语义动作。具体方法:1将文法符号所代表的语言结构的意思,用附着于该文法符号的属性表示;2用语义规则规定产生式所代表的语言结构之间的关系(即属性之间的关系),即用语义规则实现属性计算。语义规则的执行:在语法分析的适当时刻(如推导或归约)执行附着在对应产生式上的语义规则,以实现 对语言结构语义的处理,如计算、查填符号表、生成中间代码、发布出错信息等。,5,4.1.2 属性与语义规则(续1), 属性的抽象表示 .attr 例如:E.val(值) E.type(类型)
4、E.code(代码序列)E.place(存储空间)对文法的约定本章关注的是语法分析的基础上的语义处理,忽略语法分析。为了简单,本章的文法一般为二义文法。默认解决二义的方法是规定常规意义下的优先级和结合性。,6,4.1.2 属性与语义规则(续2), 属性的定义* 定义4.1 对于产生式A,其中是由文法符号X1X2.Xn组成的序列,它的语义规则可以表示为(4.1)所示关于属性的函数:b := f(c1, c2, ., ck) (4.1) 语义规则中的属性存在下述性质与关系。(1) 若b是A的属性,c1, c2, ., ck是中文法符号的属性,或者A的其它属性,则称b是A的综合属性。(2) 若b是中
5、某文法符号Xi的属性,c1, c2, ., ck是A的属性,或者是中其它文法符号的属性,则称b是Xi的继承属性。(3) 称(4.1)中属性b依赖于属性c1, c2, ., ck。(4) 若语义规则的形式如下述(4.2),则可将其想像为产生式左部文法符号A的一个虚拟属性。属性之间的依赖关系,在虚拟属性上依然存在。f(c1, c2, ., ck) (4.2) ,(4.1)中属性之间的依赖关系,实质上反映了属性计算的先后次序,即所有属性ci被计算之后才能计算属性b。,EE1+E2 E.val:=E1.val+E2.val,EE1+E2 print(E.val),7,4.1.3 语义规则的两种形式,
6、语法制导定义用抽象的属性和运算符号表示的语义规则;(公式,做什么)翻译方案用具体属性和运算表示的语义规则。(程序段,如何做)语义规则也被习惯上称为语义动作。忽略实现细节,二者作用等价。(设计与实现),8,4.1.3 语义规则的两种形式(续1),例4.1 将中缀形式的算术表达式转换为后缀表示的语法制导定义和翻译方案。虚拟属性print(E.post)可想象为L.p:=print(E.post)。,产生式 LE EE1+E2Enum,语法制导定义 print(E.post) E.post:=E1.post|E2.post|+; E.post:=num.lexval;,翻译方案1 print_pos
7、t(post); post(k):=+; k:=k+1;post(k):=lexval; k:=k+1;,翻译方案中需要考虑的问题: 1采用什么样的语法分析方法; 2为属性分配存储空间; 3考虑计算次序。,产生式 翻译方案2 LE EE1+E2 print(+); Enum print(lexval);,语法制导定义算法 翻译方案程序实现,多种方法,翻译方案1,自下而上计算,LR分析。 (以3+5+8为例,归约时翻译),post:(3 5 + 8 +),9,4.1.3 语义规则的两种形式(续2), 属性作为分析树的注释 将属性附着在分析树对应文法符号上,形成注释分析树。,产生式 语法制导定义
8、翻译方案 LE print(E.post); EE1+E2 E.post:=E1.post print(+);|E2.post|+; Enum E.post:=num.lexval; print(lexval);,例4.2 3+5+8的分析树和注释分析树:,.post=3,.post=5,.post=8,.post=35+,.post=35+8+,(print(35+8+),10,4.1.3 语义规则的两种形式(续3), 注释分析树上看继承属性与综合属性 继承属性是自上而下计算的综合属性是自下而上计算的 提醒:除非特别提醒,本章讨论的语法制导翻译是综合属性。,11,4.1.4 LR分析翻译方案
9、的设计,LR分析中的语法制导翻译实质上是对LR语法分析的扩充:扩充LR分析器的功能:当执行归约产生式的动作时,也执行产生式对应的语义动作。由于是归约时执行语义动作,因此限制语义动作仅能放在产生式右部的最右边;扩充分析栈:增加一个与分析栈并列的语义栈,用于存放分析栈中文法符号所对应的属性值。,例如: EE1+E2 valtop:=valtop+valtop+2;,对于表达式: 5+3,当归约为左部E时, 同时也得到了值8。,12,4.1.4 LR分析翻译方案的设计(续1),例4.3 3+5*8的语法制导翻译。,语法制导定义 print(E.val) E.val:=E1.val+E2.val; E
10、.val:=E1.val*E2.val; E.val:=n.lexval;,翻译方案 print(valtop); valtop:=valtop+valtop+2; valtop:=valtop*valtop+2; valtop:=lexval;,产生式 LE EE1+E2 EE1*E2 En,分析栈 语义栈 输入 语义动作 # # 3+5*8# shift #n #3 +5*8# En,valtop:=lexval #E #3 +5*8# shift #E+ #3? 5*8# shift #E+n #3?5 *8# En,valtop:=lexval #E+E #3?5 *8# shift
11、#E+E* #3?5? 8# shift #E+E*n #3?5?8 # En,valtop:=lexval #E+E*E #3?5?8 # EE1*E2; valtop:=valtop*valtop+2; #E+E #3?40 # EE1+E2,valtop:=valtop+valtop+2; #E #43 # acc,13,4.1.5 递归下降分析翻译方案的设计,递归下降方法是用程序实现对非终结符的展开和对终结符的匹配。翻译方案的设计需要解决两个问题: 1如何在递归下降子程序中嵌入语义动作:产生式右部的任何位置; 2如何为文法符号的属性设计存储空间:函数返回值、参数、变量等。,例 函数绘图
12、语言的解释器中语法制导翻译的设计:1递归子程序可以设计为函数,用于返回必要的属性值;2适当设计子程序中的临时变量,用于保存属性值;3将语义动作嵌入在子程序的适位置,正确计算属性值。 (第三次上机课介绍),阅读:95页的例4.4,14,4.2. 中间代码简介,编译器各阶段的完整输出,均可以被认为是源程序的某种中间表示。本章讨论的是中间代码生成器输出的中间表示,称之为中间代码。中间代码实际上应起一个编译器前端与后端分水岭的作用。要求中间代码具有如下特性,以便于编译器的开发移植和代码的优化:1便于语法制导翻译;2既与机器指令的结构相近,又与具体机器无关。中间代码的主要形式:树、后缀式、三地址码等。,
13、15,4.2.1 后缀式, 后缀式的特征操作符在前,操作数紧随其后,无需用括号限制运算的优先级和结合性。计算后缀式的虚拟机,算法4.1 后缀式计算 输入 后缀式 输出 计算结果 方法 采用下述过程进行计算,最终结果留在栈中。,x := first_token; while not end_of_exp loop if x in operatorsthen push x; - 操作数进栈else pop(operators); - 算符,弹出操作数push(evaluate); - 计算,并将结果进栈end if;next(x); end loop; ,16, 后缀式计算 4.2.1 后缀式(续
14、1),算术表达式3+5+8的后缀式为35+8+。 算法4.1的计算: (# 35+8+# 进栈) (#3 5+8+# 进栈) (#35 +8+# 弹出3和5,计算3+5,结果进栈) (#8 8+# 进栈) (#88 +# 弹出8和8,计算8+8,结果进栈) (#16 # ),x := first_token; while not end_of_exp loop if x in operatorsthen push x; - 操作数进栈else pop(operators); - 算符,弹出操作数push(evaluate); - 计算,并将结果进栈end if;next(x); end loo
15、p;,17, 将后缀式推广到其他语句 4.2.1 后缀式(续2),后缀式并不局限于二元运算的表达式,可以推广到任何语句,只要遵守操作数在前,操作符紧跟其后的原则即可。语句: if e then x else y它的后缀式可以写为:e x y if-then-else (1) 上述表示中,e、x和y均需计算。 而实际上,根据条件e的取值,x和y不能都计算:e p1 jez x p2 jump p1: y p2: (2) 其中:p1和p2分别是标号;p1 jez表示e的结果为0(假)则转向p1;p2 jump表示无条件转向p2。 与 (1)比较,(2)中的if-then-else被分解,首先计算e
16、,根据e的结果是否为真,决定计算x还是计算y。,18,4.2.2 三地址码, 三地址码的直观表示 语法: 语义:,例如:赋值句x := a + b * c的三地址码序列:T1 := b * cT2 := a + T1x := T2 注意:直观表示与源程序中赋值句的区别。,result := arg1 op arg2 或 result := op arg1 或 op arg1,结果存放在result中的二元运算arg1 op arg2 结果存放在result中一元运算op arg1 一元运算op arg1,19, 三地址码的种类,序号 三地址码 四元式 (1) x := y op z (op,
17、y, z, x) (2) x := op y (op, y, , x) (3) x := y (:=, y, , x) (4) goto L (j, , , L) (5) if x goto L (jnz, x, , L) (6) if x relop y goto L (jrelop, x, y, L) (7) param x (param, , , x) (8) call n, P (call, n, , P) (9) return y (return, , , y) (10) x := yi (=, yi, , x) (11) xi := y (=, y, , xi) (12) x :=
18、 &y (=&, y, , x) (13) x := *y (=*, y, , x) (14) *x := y (*=, y, , x),20, 三地址码的实现:三元式与四元式, 三元式三元式: (i) (op, arg1, arg2)三地址码:(i) := arg1 op arg2,例4.5 表达式x:=a+b*c 的三元式:(1) (*, b, c )(2) (+, a,(1)(3) (:=,x,(2)标识符a,b,c,x分别表示它们的存储位置,序号(1)、(2)、(3)分别是它们在三元式表中的位置。 ,序号的双重含义:既代表此三元式,又代表三元式存放的结果。 存放方式:数组结构,三元式在
19、数组中的位置由下标决定。 弱点:给代码的优化带来困难。因为代码优化常使用的方法是删除某些代码或将某些代码移动位置,而一旦进行了代码的删除或移动,则表示某三元式的序号就会发生变化,从而使得其他三元式中对原序号的引用无效。,21, 三元式的语法制导翻译 三地址码的实现(续1),属性 .code:三元式代码,指示标识符的存储单元或三元式表中的序号; 属性 .name:标识符的名字; 函数trip( op,arg1,arg2 ):生成一个三元式,返回三元式的序号; 函数 entry(id.name):返回标识符在符号表中的位置或存储位置。,产生式与语义规则: (1) Aid:=E (2) EE1+E2
20、 (3) EE1*E2 (4) E(E1) (5) E-E1 (6) Eid,A.code:=trip(:=,entry(id.name),E.code) E.code:=trip(+,E1.code,E2.code) E.code:=trip(*,E1.code,E2.code) E.code:=E1.code E.code:=trip(,E1.code, ) E.code:=entry(id.name),22,例4.6 生成x:=a+b*c的三元式(LR分析) 三地址码的实现(续2),(1) Aid:=E A.code:=trip(:=,entry(id.name),E.code) (2)
21、 EE1+E2 E.code := trip(+,E1.code,E2.code) (3) EE1*E2 E.code := trip(*,E1.code,E2.code) (4) E(E1) E.code := E1.code (5) E-E1 E.code := trip(,E1.code, ) (6) Eid E.code := entry(id.name),.code=a,.code=b,.code=c,.code=(1)(*,b,c),.code=(3)(:=,x,(2),.code=(2)(+,a,(1),三元式序列:(1) (*, b, c )(2) (+, a,(1)(3) (
22、:=,x,(2),23, 四元式 三地址码的实现(续2),四元式是对三元式的改进,在将表示计算结果的三元式序号用一个显示的变量表示,从而避免了三元式的值与三元式在三元组中的位置相关的弱点。 四元式的语法:(op,arg1,arg2,result) 所表示的计算:result := arg1 op arg2四元式与三元式的唯一区别是将由序号所表示的运算结果改为了由临时变量来表示。,这一改变使得四元式具有了运算结果与四元式在四元式序列中的位置无关的特点,它为代码的优化提供了极大的方便,因为这样可以删除或移动四元式而不会影响运算的结果。三地址码与四元式形式的一致性。,三元式: (i) (op, ar
23、g1, arg2) (i) := arg1 op arg2,24, 四元式的语法制导翻译 三地址码的实现(续2),属性.code: 表示存放运算结果的变量; 函数newtemp: 返回一个新的临时变量,如T1,T2,.等。 过程emit( op,arg1,arg2, result):生成一个四元式。若一元,则arg2可空;,产生式与语义规则: (1)Aid:=E (2)EE1+E2(3)EE1*E2(4)E(E1) (5)E-E1 (6)Eid,A.code:=newtemp; emit(:=,E.code, , A.code)emit(:=,E.code, , entry(id.name)
24、E.code:=newtemp;emit(+,E1.code,E2.code,E.code) E.code:=newtemp;emit(*,E1.code,E2.code,E.code) E.code:=E1.code E.code:=newtemp; emit(,E1.code, , E.code) E.code:=entry(id.name),25,4.2.3 图形表示, 树作为中间代码 语法树真实反映句子结构,对语法树稍加修改(加入语义信息),即可以作为中间代码的一种形式(注释语法树)。 例4.8 赋值句x:=(a+b)*(a+b)的树的中间代码表示:,T1/(1),T2/(2),T3/
25、(3),T4/(4),26, 树的语法制导翻译,(1) A id := E (2) E E1 + E2 (3) E E1 * E2 (4) E ( E1 ) (5) E - E1 (6) E id,A.nptr:= mknode(:=,mkleaf(entry(id.name),E.nptr)E.nptr:=mknode(+,E1.nptr,E2.nptr)E.nptr:=mknode(*,E1.nptr,E2.nptr)E.nptr:=E1.nptrE.nptr:=mknode(,E1.nptr, )E.nptr:=mkleaf(entry(id.name),属性.nptr:指向树节点的指针
26、; 函数mknode(op,nptr1,nptr2): 生成一个根或内部节点,节点数据是op, nptr1和nptr2分别指向的左右孩子的子树。若仅有一个孩子,则nptr2为空; 函数mkleaf(node): 生成一个叶子节点。,27, 树的优化表示DAG,如果树上若干个节点有完全相同的孩子,则这些节点可以指向同一个孩子,形成一个有向无环图(Directed Acyclic Graph, DAG)。DAG与树的唯一区别是多个父亲可以共享同一个孩子,从而达到资源(运算、代码等)共享的目的。,DAG的语法制导翻译与树的语法制导翻译相似,仅需要在mknode和mkleaf中增加相应的查询功能。首先
27、查看所要构造的节点是否已经存在,若存在则无需构造新的节点,直接返回指向已存在节点的指针即可。,28, 树与其他中间代码的关系,树表示的中间代码与后缀式和三地址码之间有着内在的联系。对树进行深度优先的后序遍历,得到的线性序列就是后缀式,或者说后缀式是树的一个线性化序列。树的每个内部节点和它的孩子,对应一个三元式或四元式。,例4.9 赋值句x:=(a+b)*(a+b)的注释语法树:,后缀式:xab+ab+*:= 三元式:,(1)(+, a, b ) (2)(+, a, b ) (3)(*,(1),(2) (4)(:=,x, (3),四元式:,(1)(+, a, b, T1) (2)(+, a, b
28、, T2) (3)(*, T1,T2,T3) (4)(:=,x, T3,T4),因此,现代的编译器基础架构均用语法树作为中间表示。,29,4.3 符号表简介,符号表的作用:连接声明与引用的桥梁,记住每个符号的相关信息,如作用域和绑定等,帮助编译的各个阶段正确有效地工作。 符号表设计的基本要求:目标是合理存放信息和快速准确查找。正确存储各类信息。适应不同阶段的需求;便于有效地进行查找、插入、删除和修改等操作; 空间可以动态扩充;,30,4.3.1 符号表条目,逻辑上讲:每个声明的名字在符号表中占据一栏,称为一个条目,用于存放名字的相关信息。 符号表中的内容:保留字、标识符、特殊符号(包括算符、分
29、隔符等)等等。不同类别的符号存放在不同的子表中,如变量名表、过程名表、保留字表等。 存放方式:关键字属性。 关于组合关键字:, int x; double x;struct x float y, z; ; ,为C+构造的符号表中,组合关键字至少应该包括三项:名字作用域类型。当一个名字x在同一作用域中允许有多于一个的声明,则对x的引用时需要根据上下文确定x到底属于哪个对象。因此程序设计语言在语法上规定了不允许这样的声明,以简化编译时的处理。,31,4.3.2构成名字的字符串的存储,定长数据 变长数据 直接存放 间接存放,名字(直接存储) 属性 sort proc, . a int, . read
30、array proc, . draw_a_red_line_for_object_a boolean, .,名字(间接存储) 属性 101 (或101/4) proc, . 106 (或105/1) int, . 108 (或106/9) proc, . 118 (或105/28) boolean, .,sort#a#readarray#draw_a_red_line_for_object_a# 101,sortareadarraydraw_a_red_line_for_object_a 101,间接存储的方法实际上解决了复杂信息的存储问题,将其推广到属性,则任何一个复杂的属性,均可以为其另辟
31、空间(空间本身可以是复杂结构,如数组的内情向量等),而仅需要将指向此空间的指针放在此属性在符号表中的对应位置即可。,32,4.3.3 名字的作用域,程序设计语言的名字可以出现在不同的范围内,并且可以具有不同的意义。 两种划分范围的方式:并列的和嵌套的。 不同的语言采用不同的方式:如Pascal的过程定义可以是嵌套的,而C的过程定义是并列的,但是C允许程序块是嵌套的。,名字的作用域:名字在哪个范围内起作用。并列的两个范围内的名字作用域互不相干,但是分别在嵌套的两个范围内的名字,其作用域的问题就需要制定规则来限定,以使得任何一个名字在任何范围内涵义都是无二义的。,名字的作用域规则:规定一个名字在什
32、么样的范围内应该表示什么意义。,33,4.3.3 名字的作用域(续1), 静态作用域原则(static-scope rule):编译时就可以确定名字的作用域,也可以说,仅从静态读程序就可确定名字的作用域。最近嵌套原则(most closely nested):以程序块为例,也适用于过程。 程序块B中声明的作用域包括B; 如果名字x不在B中声明,那么B中x的出现是在外围程序块B的x声明的作用域中,使得(a) B有x的声明,并且(b) B比其它任何含x声明的程序块更接近被嵌套的B。,通俗地讲,名字的声明在离其最近的内层起作用,即在名字引用处从内向外看, 它处在所遇到的第一个该名字声明的作用域。 例
33、子:找人 张三;软件学院的张三;计算机学院的张三;西电软件学院的张三,34,4.3.3 名字的作用域(续2),例4.10 说明符合作用域规则的C+程序。 void main() int a=0, b=0; /* B0层 */ int b=1; /* B1层,被B0嵌套 */ int a=2, c=4, d=5; /* B2层,被B1嵌套 */printf(“%d %dn“, a, b); /* 结果为:2,1 */ int b=3; /* B3层,与B2并列 */printf(“%d %dn“, a, b); /* 结果为:0,3 */printf(“%d %dn“, a, b); /* 结果
34、为:0,1 */printf(“%d %dn“, a, b); /* 结果为:0,0 */ ,声明与作用域:,声 明 作用域 int a=0 B0-B2 int b=0 B0-B1 int b=1 B1-B3 int a=2 B2 int b=3 B3,35,4.3.4 线性表,线性表应是一个栈,以正确反映名字的作用域,即符号的加入和删除,均在线性表的一端进行。,线性表上的操作:关键字:名字作用域; 查找:从表头(栈顶)开始,遇到的第一个名字; 插入:先查找,再插入在表头;,1 void main() 2 int a=0, b=0; / B0 3 int b=1; / B1 4 int a=2
35、, c=4, d=5; / B2 7 int b=3; / B3 11 ,36,4.3.4 线性表(续1),1 void main() 2 int a=0, b=0; / B0 3 int b=1; / B1 4 int a=2, c=4, d=5; / B2 7 int b=3; / B3 11 ,线性表上操作的效率(n个条目): 一个名字的查找:成功查找(平均):(n+1)/2;不成功查找:n+1建立n个条目的符号表(最坏): = (n+1)(n+2)/2,删除: (a) 暂时:将在同一作用域的名字同时摘走,适当保存; (b) 永久:将在同一作用域的名字同时摘走,不再保存。 修改:与查找类
36、似,修改第一个遇到的名字的信息。 修改可以用插入删除代替。,37,4.3.5 散列表, 散列表的构成将线性表分成m个小表。构造hash函数,使名字均匀地散布在m个子表中。如果散列均匀,则时间复杂度会降到原线性表的1/m。,每个名字挂在两个链上(便于删除操作):哈希链(hash link): 链接所有具有相同hash值的元素,表头在表头数组中;作用域链(scope link):链接所有在同一作用域中的元素,表头在作用域链中。,其中: S1、S2、S4在同一作用域S3在另一作用域,38, 散列表上的操作 4.3.5 散列表(续1),1查找:首先计算散列函数,然后从散列函数所指示的入口进入某个线性表
37、,在线性表中沿hash link,象查找单链表中的名字一样进行查找。 2插入:首先查找,以确定要插入的名字是否已在表中,若不在,则要分别沿hash link和scope link插入到两个链中,方法均是插在表头,即两个表均可看作是栈。 3删除:把以作用域链连在一起的所有元素从当前符号表中删除,保留作用域链所链的子表,为后继工作使用(如果是临时删除,则下次使用时直接沿作用域链加入到散列链中即可)。,39,4.3.5 散列表(续2),设:hash(s)=ord(s)-ord(a),1 void main() 2 int a=0, b=0; / B0 3 int b=1; / B1 4 int a=
38、2, c=4, d=5; / B2 7 int b=3; / B3 11 ,分析在B2中:,退出B2进入B3:,40, 散列函数的计算 hash: string integer,减少冲突,分布均匀充分考虑程序设计语言的特点若有变量:V001,V002,V300,且首字母的值作为hash值。会发生什么?,一种可行的hash函数计算方法:从串s=c1c2ck的字符ci确定正整数h:令: h0=0, 计算:hi=hi-1+ci, 1ik,得到:h=hk=1或是素数,如=65599。,思考题:根据上述方法,设计一个hash函数并测试之。根据测试结果讨论各参数值对函数结果的影响。, 取一素数m, 令 h=h mod m。,41, 散列函数的计算(续1),思考题(P108):对于下列函数: #include const int PRIME=211; const int EOS=0; int hashpjw(char *s) char *p; unsigned h=0, g;for (p=s; *p!=EOS; p+) h=(h24); h=hg; return h%PRIME; (1) 为每行语句写注释; (2) 写出此函数的计算公式; (3) 若参数是“abcd“,试给出返回值。,结 束,