340 likes | 531 Views
第三章. 静态语义分析-属性与属性的计算. 概述. 语义分析是编译器前端的最后一个阶段,其作用是检查更多语法分析阶段无法检查的错误和处理语法分析无法处理的上下文相关的语言特性,最后生成中间代码。 语义分析的内容可以分为两大类: 作用域检查 ( scope checking 或 scoping )和 类型检查 ( type checking 或 typing )。 作用域检查 用于发现是否所有名字都被恰当地声明,或者一个名字在什么样的范围内可以被访问等; 类型检查 用于检查一个名字是否从事的是合法的操作。
E N D
第三章 静态语义分析-属性与属性的计算
概述 语义分析是编译器前端的最后一个阶段,其作用是检查更多语法分析阶段无法检查的错误和处理语法分析无法处理的上下文相关的语言特性,最后生成中间代码。 语义分析的内容可以分为两大类:作用域检查(scope checking或scoping)和类型检查(type checking或typing)。 作用域检查用于发现是否所有名字都被恰当地声明,或者一个名字在什么样的范围内可以被访问等;类型检查用于检查一个名字是否从事的是合法的操作。 由于动态语义处理的复杂性,使得绝大部分的程序设计语言都采用静态作用域规则和静态类型,即仅通过对源程序的分析,而无需考虑程序的运行,就可以进行作用域检查和类型检查。
概述(续) 静态语义分析的基本方法是语法制导翻译,更准确的说法应该是属性与属性计算,即将反映语言结构的语义信息(如作用域信息或类型信息等)设计为属性并将其附着于对应的文法符号上,根据语法分析的结果进行属性的计算,以达到语义分析的目的。 本章主要内容: • 从原理的角度讨论属性与属性计算; • 重点讨论如何在语法分析的过程中同步进行语义处理; • 特别讨论如何在自下而上分析方法中实现语义的同步计算; • 简单介绍Yacc源程序中的语法制导翻译 • 上机题的讨论。
3.1 属性与属性计算 3.1.1 属性与语义规则 对于产生式A→α,其中α是由文法符号X1X2...Xn组成的序列,它的语义规则可以表示为(3.1)所示关于属性的函数: b:=f(c1, c2, ..., ck) (3.1) 语义规则中的属性存在下述性质与关系。 (1) 若b是A的属性,c1, c2, ..., ck是α中文法符号的属性,或者A的其它属性,则称b是A的综合属性。 (2) 若b是α中某文法符号Xi的属性,c1, c2, ..., ck是A的属性,或者是α中其它文法符号的属性,则称b是Xi的继承属性。 (3) 称(3.1)中属性b依赖于属性c1, c2, ..., ck。 (4) 若语义规则的形式如下述(3.2): f(c1, c2, ..., ck) (3.2) 则可将其认为是产生式左部文法符号A的一个虚拟属性。属性之间的依赖关系,在虚拟属性上依然存在。即可以将(3.2)认为是如下形式: A.dummy:=f(c1, c2, ..., ck) (3.3)
3.1 属性与属性计算(续) 属性之间的依赖关系实质上反映了属性计算的先后次序,即所有属性ci被计算之后才能计算属性b(包括A.dummy)。 根据属性表示的抽象程度,语义规则可以有两种表示方式。用抽象的属性和运算符号表示的语义规则称之为语法制导定义,而用具体属性和运算表示的语义规则称之为翻译方案,语义规则也被习惯上称为语义动作。 语法制导定义主要考虑“做什么”,而翻译方案还需考虑“如何做”。忽略实现细节,二者的作用是等价的。从某种意义上讲,语法制导定义类似于算法,而翻译方案类似于程序。
文法符号上附加了属性的分析树/语法树被称为注释分析树/注释语法树。文法符号上附加了属性的分析树/语法树被称为注释分析树/注释语法树。 3.1.2 综合属性 注释分析树很好地反映了属性的性质和属性之间的关系:继承属性从前辈和兄弟的属性计算得到,综合属性从子孙和自身的其他属性计算得到,通俗地讲就是继承属性“自上而下,包括兄弟”,综合属性“自下而上,包括自身”。 若一个语法制导定义中仅含有综合属性,则称此语法制导定义具有S_属性性质,或简称为它是S_属性的。 综合属性在分析树上的计算次序,与LR语法分析剪句柄的过程完全一致,即对虚拟的注释分析树一次深度优先后序遍历可以计算所有S_属性。语法分析与语义分析同步的方法也被称为增量分析(incremental processing)。
产生式 语法制导定义 翻译方案 L→E E→E1+E2 E→num 例3.1将算术表达式的中缀表示转换为后缀表示的语法制导定义和翻译方案: 3.1.2 综合属性(续) print(E.post) print(+); E.post:=E1.post||E2.post||'+'; print(lexval); E.post := num.lexval; 3+5+8:
3.1.3 继承属性 <1> 属性计算的深度优先遍历方法-dfvisit 若语法制导定义中既有综合属性又有继承属性,则可以通过对注释分析树的深度优先遍历(dfvisit)来完成属性计算。 procedure dfvisit(n:node) is begin for n的每个子节点m,从左到右loop end loop; end dfvisit; 计算m的继承属性;-- 遍历子节点m前计算继承属性(先序遍历) dfvisit(m); -- 递归遍历子节点m 计算n的综合属性; -- 遍历节点n后计算综合属性(后序遍历)
例3.2 C形式的变量声明文法G3.1和语法制导定义: 3.1.3 继承属性(续1) D→TL L.in := T.type; T→real T.type := real; (G3.1) L→L1, id L1.in := L.in; addtype(id.entry, L.in); L→id addtype(id.entry, L.in); dfvisit输入序列real id1,id2,id3的注释分析树,计算属性值。 (G3.2)
for n的每个子节点m,从左到右 loop 计算m的继承属性; dfvisit(m); end loop; 计算n的综合属性; 3.1.3 继承属性(续2) 根据序dfvisit的遍历次,第一个被计算的是T.type属性,在退出访问T之前它得到属性值real。第二个被计算的是L1.in 属性,访问L1之前它得到从T.type传来的属性值real。 接下来L2和L3以同样的方式得到.in的值。从L3退出前,计算L3的综合属性addtype,它的两个参数此时均已得到,因此可以将id1的类型信息记录为real。 依此类推,id2和id3的类型也是real。其中依赖自身属性的箭头被忽略。 右兄弟
<2> dfvisit与递归下降分析 3.1.3 继承属性(续3) 如果一个语法制导定义的所有属性均可由dfvisit计算,则可用递归下降子程序进行增量分析。 因为递归下降子程序是以程序的方式模拟分析树的构造,从左到右分析输入序列并从上到下构造分析树,与dfvisit遍历分析树的过程完全一致。 语义动作可以加入在子程序的任何部位。当一个非终结符A的子程序被调用时,它的父亲节点和左兄弟节点均已被构造,故调用前可以根据父亲和左兄弟的属性计算A的继承属性;当子程序返回时,它的所有孩子节点均已被构造,故可以根据孩子的属性计算A的综合属性。
例3.3文法G3.1中L产生式的EBNF形式L→id {, id}和它的递归下降子程序: 3.1.3 继承属性(续4) procedure L(in : obj_type) is -- in是类型属性,如real等 begin match(id); addtype(id.entry, in); -- 记录in类型到符号表 while lookahead="," loop match(","); match(id); addtype(id.entry, in);--记录in类型到符号表 end loop; end L; 将.in作为L子程序的参数,因为.in是L的继承属性,在调用L之前已经得到。当match(id)之后得到id.entry,于是可以计算虚拟属性addtype。这与分别记录下每个id的entry,最后在L返回前一并计算是等价的。
<1> dfvisit对文法的限制 3.1.4 依赖图与属性计算 分析树上的任何节点只能得到上、下、左三个方向的属性,而得不到来自右兄弟的属性。换句话说,dfvisit无法计算依赖于右兄弟的继承属性。 例3.4 文法G3.1改造为G3.2(语义规则不变): D→L:T L.in := T.type; T→real T.type := real; (G3.2) L→L1, id L1.in := L.in; addtype(id.entry, L.in); L→id addtype(id.entry, L.in); 对于id1,id2,id3:real, .in由依赖于左兄弟改变为依赖于右兄弟,使得.in属性无法从dfvisit遍历中得到。
<2> 分析树的依赖图 3.1.4 依赖图与属性计算(续1) 对于一个任意的语法制导定义,可能需要对分析树进行多次遍历才可能完成对所有属性的计算。 理论上可以采取如下步骤:首先构造输入序列的注释分析树,然后根据属性的依赖关系构造分析树的依赖图,若依赖图中无环则找到一个拓扑排序序列,根据此序列计算属性。 分析树的依赖图是一个有向图: 1.为文法符号的每个属性(包括虚拟属性)分配一个节点; 2.若属性b依赖于属性ci,则从ci到b引一条有向边。 构造方法: for 分析树的每个节点n-- 第一个循环构造节点 loop为节点上的每个属性a构造一个节点; end loop; for 分析树的每个节点n -- 第二个循环构造边 loop for n的产生式的每条语义规则b:=f(c1, c2, ..., ck) loop 从每个ci到b构造一条有向边; end loop; end loop;
例3.5 构造输入序列real id1, id2, id3 和id1, id2, id3:real分析树的依赖图: 3.1.4 依赖图与属性计算(续2) D→TL L.in := T.type; T→real T.type := real; (G3.1) L→L1, id L1.in := L.in; addtype(id.entry, L.in); L→id addtype(id.entry, L.in); D→L:T L.in := T.type; T→real T.type := real; (G3.2) L→L1, id L1.in := L.in; addtype(id.entry, L.in); L→id addtype(id.entry, L.in);
<3> 属性的计算次序 loop 找出G中所有没有前驱的节点,并为这些节点编号; 删除这些已编号节点,形成G的子图Gnew; end loop; 3.1.4 依赖图与属性计算(续3) if Gnew=Φ then 正确结束; -- 全部节点被编号,找到一个拓扑排序 else end if; if Gnew=G -- 存在非空的强连通子图 then 错误结束;-- 语法制导定义中有环 else G=Gnew; -- 否则继续 end if; 得到的是一个组的全序(n1,n2,n3,n4)(n5)(n6,n7)(n8,n9)(n10) (组内无序)
3.1.4 依赖图与属性计算(续4) 由分析树构造依赖图,再按照依赖图的拓扑排序规定的次序计算属性,这样的方法被称为分析树方法,它原则上可以处理任意的属性,唯一的约束是语法制导定义无环。 事实上被广泛应用的属性计算并不采用上述的分析树方法,而采用与语法分析同步计算的增量分析,也称为非分析树的方法。 非分析树方法既适用于LL分析也适用于LR分析,它的优点是属性的计算不必显式构造依赖图,从而提高了编译器的效率,而弱点是并不是所有属性均可与语法分析同步计算。
3.2 L-属性的增量分析3.2.1 语法制导定义 若文法G的产生式A→X1X2...Xk的语义规则中的属性都是综合属性,或者Xi(1≤i≤n)的继承属性依赖于: • X1, X2, ..., Xi-1的属性,和 • A的继承属性 • 则称G的语法制导定义是L_属性定义,或称G是L_属性的。 • 由此可以得出三点结论: • S_属性是 L_属性; • 若语法制导定义是L_属性的,则所有属性值均可用dfvisit计算得到; • 若语法制导定义是L_属性的,则LL分析可同步计算所有属性值。
S_属性是 L_属性; • 若语法制导定义是L_属性的,则所有属性值均可用dfvisit计算得到; • 若语法制导定义是L_属性的,则LL分析可同步计算所有属性值。 3.2.1 语法制导定义(续) • 结论1是显而易见的。 • 令A→X1X2...Xk在分析树上的节点是:n, m1, m2,...mk,则: • 任何节点n, mi, 其综合属性均可在dfvisit(n)或dfvisit(mi)返回之前得到计算; • n的继承属性在调用dfvisit(n)之前已被计算; • mi左边各节点属性在dfvisit(mi)之前均已被计算。 因此结论2成立。 • 由于dfvisit和递归下降分析存在下述关系: • LL分析的过程是为输入序列从上到下、从左到右构造一棵分析树; • LL分析构造分析树的过程与dfvisit遍历分析树的过程完全重合。 • 因此结论3也成立。
3.2.2 翻译方案 <1> 如何嵌入语义动作 • 翻译方案与语法制导定义的本质差别在于,语义动作需要考虑实现的细节: • 如何为各属性安排存储空间(语义变量); • 如何安排属性的计算次序。 • 翻译方案中语义动作的计算的安排: • 语义动作嵌入在{ }中且可以出现在产生式右部任何位置; • { }对应分析树中的一个节点,它在分析树中的位置,由其在产生式右部的位置决定; • 对分析树dfvisit的次序,就是对语义动作的计算次序。
例 3.6 将算术表达式转化为后缀式 E→E op T { print(op.lex); } E→T (G3.3) T→num {print(num.val);} 3.2.2 翻译方案(续1) 若输入序列为:9-5+2, 则它的分析树如下: dfvisit分析树,得到分析结果95-2+。
<2> dfvisit对翻译方案设计的限制 若翻译方案中既有继承属性又有综合属性,则语义动作的计算必须满足: 3.2.2 翻译方案(续2) • 产生式右部符号的继承属性必须在先于该符号的动作中计算; • 一个动作不能引用该动作右部符号的属性;(L属性自然满足) • 产生式左部非终结符的综合属性只能在它引用的所有属性都计算完成后才能计算。(语义动作放在最右边) 由于L属性自然满足条件2,而在我们设计语义动作时,又可以先将它们全部安排在最右边(满足条件3)。因此只需要考虑如何调整不能安排在最右边的语义动作,以满足条件1。
例3.7 用下述翻译方案对aa进行分析: S→A1A2 {A1.in:=1; A2.in:=2;}(G3.4) A→a {print(A.in);} 3.2.2 翻译方案(续3) 得到分析树: 将文法修改为: S→ {A1.in:=1; A2.in:=2;} A1 A2 (G3.4') A→ a {print(A.in);} 又得到分析树:
数学排版问题的翻译方案 例3.8将数学排版问题的语法制导定义转换为翻译方案。 上述数学排版格式可以描述为:E sub 1 .val, 其中sub 1表示将正文1缩小并下移。 可设计语法制导定义如下: 首先引入属性与函数: 继承属性.ps:表示点的大小,决定正文实际高度; 综合属性.ht:表示正文的实际高度; 综合属性text.h:参数值,语法分析时可作为常数; 函数max(a,b):取a和b的最大值作为返回值; 函数shrink(a):将a按一定比例缩小; 函数disp(a,b):将b向下偏置,然后返回二者的最大值。
数学排版问题的翻译方案(续1) 语法制导定义如下: • (1) S→B B.ps:=10; S.ht:=B.ht; • (2) B→B1 B2 B1.ps:=B.ps; B2.ps:=B.ps; • B.ht:=max(B1.ht, B2.ht); • | B1 sub B2 B1.ps:=B.ps; (G3.5) • B2.ps:=shrink(B.ps); B.ht:=disp(B1.ht, B2.ht); • (4) | text B.ht:=text.h*B.ps; 这是一个L_属性定义,因为产生式右部B的.ps属性仅依赖于其左部非终结符的.ps属性,从分析树的角度看,.ps是从其父亲的.ps属性继承而来,与其兄弟属性无关。 另外所有语义规则均在产生式的最右边,若将此语法制导定义改写为翻译方案,它自然满足限制条件的2和3。 因此仅需将部分语义动作向左移至适当位置,使其满足限制条件1即可。
S→B • (2) B→B1 B2 • (3) | B1 sub B2 (G3.5) • (4) | text 数学排版问题的翻译方案(续2) 根据限制条件1“产生式右部符号的继承属性必须在先于该符号的动作中计算”,将对文法符号继承属性的计算移到该文法的左边,形成翻译方案如下。 • (1) S→{B.ps:=10;} B {S.ht:=B.ht;} • (2) B→{B1.ps:=B.ps;} B1 • {B2.ps:=B.ps;} B2 {B.ht:=max(B1.ht, B2.ht);} • | {B1.ps:=B.ps;} B1 sub(G3.5') • {B2.ps:=shrink(B.ps);} B2 • {B.ht:=disp(B1.ht, B2.ht);} • (4) | text {B.ht:=text.h*B.ps;}
设text.h=2,shrink=0.7,则对于输入E sub 1 .val,注释分析树如下: 数学排版问题的翻译方案(续3) 编写语法制导定义和翻译方案同样是一种程序设计,但是抽象程度更高,没有清晰的控制流与数据流,从书面上看各语义变量和动作的控制流向十分困难,需要对语法分析过程和属性计算次序有深刻认识。 这一编写过程既要有理论支持也要有经验积累,因此更具挑战性。
3.3 L_属性的自下而上计算 回顾LR分析的语法制导翻译,它是在语法分析的基础上进行两点扩充: • 扩充分析栈:增加一个与分析栈并列的语义栈,用于存放文法符号的属性值; • 扩充分析器的驱动程序:在归约产生式后执行该产生式的语义动作。 由于在归约后执行语义动作,语义规则只能放在产生式右部的最右边。对于产生式A→X1X2...Xk,只有当X1X2...Xk全部被移进栈中后才可能归约出A。 也就是说,分析X1X2...Xk时A还没有出现,因此所有的Xi也就无法得到A的继承属性。 问题:能否将需要用到的所有继承属性在被使用前均已放进语义栈中?
3.3 L_属性的自下而上计算(续) 解决方法: • 引入标记非终结符M(marker nonterminals)并构造一个空产生式M→ε。M对语言的结构没有贡献,但是可以为M产生式配上语义规则,使得在对M产生式进行归约时执行该语义动作。 • 利用复写规则(copy rule)A.att:=B.att,用等价的B.att取代对A.att的引用。
3.3.1 去除翻译方案中的嵌入动作 LR分析中要求所有语义动作均在产生式右部的最右边。对于不满足要求的嵌入语义动作,可以通过引入标记非终结符来将嵌入动作移出去。 例3.9对于下述算术表达式的翻译方案: E→TE' E'→+T{print(+)}E'|-T{print(-)}E'|ε (G3.6) T→num {print(num.val)} E→TE' E'→+TME'|-TNE'|ε (G3.6') T→num {print(num.val)} M→{print(+)} N→{print(-)} 用标记非终结符取代嵌入动作,使得所有语义动作均可在产生式归约时执行:
3+5-4的分析树(G3.6) 3.3.1 去除翻译方案中的嵌入动作(续) • dfvisit两棵分析树,两者的输出结果是相同的:35+4-。 • 但消除嵌入语义动作的翻译方案可以进行LR的增量分析。 3+5-4的分析树(G3.6')
LR方法的增量分析 完成一次归约,进行一次语义计算