程序员不得不会的计算机科班知识——编译原理篇(下)

200 阅读28分钟

计算机科班知识整理专栏系列文章:

【1】程序员不得不会的计算机科班知识——操作系统篇(上)
【2】程序员不得不会的计算机科班知识——操作系统篇(下)
【3】程序员不得不会的计算机科班知识——数据库原理篇(上)
【4】程序员不得不会的计算机科班知识——数据库原理篇(下)
【5】程序员不得不会的计算机科班知识——数据结构与算法篇(上)
【6】程序员不得不会的计算机科班知识——数据结构与算法篇(下)
【7】程序员不得不会的计算机科班知识——软件工程篇(上)
【8】程序员不得不会的计算机科班知识——软件工程篇(中)
【9】程序员不得不会的计算机科班知识——软件工程篇(下)
【10】程序员不得不会的计算机科班知识——编译原理篇(上)
【11】程序员不得不会的计算机科班知识——编译原理篇(中)
【12】程序员不得不会的计算机科班知识——编译原理篇(下)
【13】程序员不得不会的计算机科班知识——计算机网络篇(上)
【14】程序员不得不会的计算机科班知识——计算机网络篇(中)
【15】程序员不得不会的计算机科班知识——计算机网络篇(下)

第五章 自底向上的分析

5.1 引入

5.1.1 概述

  • 从分析树的底部(叶节点)向顶部(根节点)方向构造分析树
  • 可以看成是将输入串w归约为文法开始符号S的过程
  • 自顶向下的语法分析采用最左推导方式
  • 自底向上的语法分析采用最左归约方式(反向构造最右推导)
  • 自底向上语法分析的通用框架:移入-归约分析(Shift-Reduce Parsing)
  • 移入-归约分析过程:它的实现思想是对输入符号串自左向右进行扫描,并将输入符逐个移入一个后进先出栈中,边移入边分析,一旦栈顶符号串形成某个句型的句柄时,(该句柄对应某产生式的右部),就用该产生式的左部非终结符代替相应右部的文法符号串,这称为一步归约。重复这一过程直到归约到栈中只剩文法的开始符号时则为分析成功,也就确认输入串是文法的句子。

5.1.2 移入-规约分析例子

逐步分析如图右侧“动作”,很明确,按上述的移入-归约分析过程进行操作。

注意到,每一个蓝色虚线划分的区域中,栈+剩余输入的内容,总是构成一个相同的“规范句型”。

5.1.3 移入-归约分析器可采取的4种动作

  • 移入:将下一个输入符号移到栈的顶端;
  • 归约:被归约的符号串的右端必然处于栈顶。语法分析器在栈中确定这个串的左端,并决定用哪个非终结符来替换这个串;
  • 接收:宣布语法分析过程成功完成;
  • 报错:发现一个语法错误,并调用错误恢复子例程。

5.1.4 移入-归约分析中存在的问题

如图,对一个语句 var iA , iB : real 进行分析,最后剩余输入中已经没有任何符号了,而栈中并没有归约出文法的开始符号<S>,因此分析失败。

但是,这个文法其实是合法的句子,那么问题出在:错误地识别了句柄(应该识别<IDS>,iB,但是上面这种分析变成了识别iB,造成分析错误),如上图;应该如下图一样进行分析。

那么,如何避免这种错误?正确识别句柄呢?

将在接下来地LR分析技术中详细介绍。

5.2 LR分析法

5.2.1 概述

  • LR文法(Knuth, 1963) 是最大的、可以构造出相应移入-归约语法分析器的文法类
    • L:对输入进行从左到右的扫描
    • R:反向构造出一个最右推导序列
  • LR(k)分析:
    • 需要向前查看k个输入符号的LR分析
    • k = 0 和 k = 1 这两种情况具有实践意义;当省略(k)时,表示k =1

5.2.2 LR分析法的基本原理

  • 自底向上分析的关键问题是什么?如何正确地识别句柄。
  • 句柄是逐步形成的,用“状态”表示句柄识别的进展程度。

5.2.3 LR分析器(自动机)的总体结构

与移入规约不同的是,LR分析器还包含一个与符号栈平行的状态栈。

5.2.4 LR分析表的结构

  • sn表示,将符号a、状态n压入栈;
  • rn表示,用第n个产生式进行归约(r是reduce的缩写)
  • GOTO中表示,在某一状态,遇到某一非终结符,进入到后继状态。
  • acc是成功接收的意思。

5.2.5 LR分析表的结构:应用例子

针对上面的分析表,现在输入 b a b ;下面的表格将阐述栈与剩余输入的快照(分析过程)。

状态栈符号栈剩余输入
0$bab$

0号状态遇到b,移入动作,入栈。

状态栈符号栈剩余输入
04$bab$

查表得,4号状态与b,对应动作r3,用第3个产生式进行归约,也就是说b出栈,B进栈。

状态栈符号栈剩余输入
0$Bab$

查表得,0号状态与B,对应GOTO 2,则2号状态进栈。

状态栈符号栈剩余输入
02$Bab$

以此类推:

状态栈符号栈剩余输入
023$Bab$
状态栈符号栈剩余输入
0234$Bab$
状态栈符号栈剩余输入
023$BaB$
状态栈符号栈剩余输入
0236$BaB$
状态栈符号栈剩余输入
0236$BaB$
状态栈符号栈剩余输入
02$BB$
状态栈符号栈剩余输入
025$BB$
状态栈符号栈剩余输入
02$BB$
状态栈符号栈剩余输入
01$S$

1$对应的acc代表成功接收。

5.2.6 LR 分析算法

  • 输入:串w和LR语法分析表,该表描述了文法G的ACTION函数和GOTO函数。
  • 输出:如果w在L(G)中,则输出w的自底向上语法分析过程中的归约步骤;否则给出一个错误指示。
  • 方法:初始时,语法分析器栈中的内容为初始状态s0,输入缓冲区中的内容为w$。然后,语法分析器执行下面的程序。

可以看出,LR分析算法较为简单,重点在于如何构造LR分析表,包括四种,将在后面分别进行介绍:

  • LR(0)分析
  • SLR分析
  • LR(1)分析
  • LALR分析

5.3 LR(0)分析法

5.3.1 基本概念

  • 拓广文法: 对于文法 G = (VN, VT, P , S ) , 增加如下产生式:S’->S ,其中, S’ ∈ VN∪ VT , 得到 G 的拓广文法,G’ = (VN ’, VT, P ’ , S’ ) 。其实就是增加了一条右部为开始符号的产生式,就变成了拓广文法。
  • 可归前缀: 采取归约过程前符号栈中的内容,称做可归前缀。 这种前缀包含句柄且不包含句柄之后的任何符号。
  • 活前缀: 对于文法 G = (VN, VT, P , S ) , 设 S’是其拓广文法的开始符号(即有产生式 S’-> S), 且α,β∈(VN∪VT)* , ω∈VT*。 若 S’ ==>α A ω 且 A ->β, 即 β 为句柄,则 αβ 的任何前缀 γ 都是文法 G 的活前缀。 注:由于 S’ ==>S’ 且 S’ -> S, 故 S 是 G 的活前缀 。 也就是说可归前缀的所有前缀(包括可归前缀)都是活前缀。

例:文法 G[S] :
(1) S -> AB
(2) A -> aA
(3) A -> ε
(4) B -> b
(5) B -> bB

句子 aaab 是一个句型,其唯一的句柄为:ε (aaaεb); 活前缀有:ε,a,aa,aaa。

  • LR(0)项目:
    • 在文法G中每个产生式的右部适当位置添加一个圆点构成项目。
    • 每个项目的含义是:欲用改产生式归约时,圆点前面的部分为已经识别了的句柄部分,圆点后面的部分为期望的后缀部分。
    • 分类:(其中a∈VT , α,β∈(VN∪VT)*, A,B∈VT)
      • 移进项目:原点后为终结符的项目,形如 A -> α• aβ,对应移进状态,把a移进符号栈。
      • 待约项目:原点后为非终结符的项目,形如 A -> α • Bβ,对应待约状态,需要等待分析完非终结符B的串再继续分析A的右部。
      • 归约项目:归约结束,原点在最右端, 形如 A -> α •,句柄已形成,可以归约。
      • 接受项目:左端为 S’ 的归约项目,形如 S’ -> S •。
      • 初始项目:形如 S’ -> • S。
      • 后继项目:表示同属于一个产生式的项目,但是圆点的位置仅相差一个文法符号,则称后者为前者的后继项目。

例:

对于产生式S -> aAcBe,它有6个项目:
S -> ·aAcBe
S -> a·AcBe
S -> aA·cBe
S -> aAc·Be
S -> aAcB·e
S -> aAcBe·

5.3.2 LR(0) 有限状态机的构造方法

5.3.2.1 用闭包函数(CLOSURE)来求DFA一个状态的项目集

CLOSURE(I)是这样定义的:首先I的项目都属于CLOSURE(I);如果A->α• Bβ,则左部为B的每个产生式中的形如B->·γ项目,也属于CLOSURE(I)。

例子:

已知文法G[E]如下:

(1) E -> E+T

(2) E -> T

(3) T ->( E )

(4) T -> d

可以直到它的拓广文法G'[E']为 :

(0) E'-> E

(1) E -> E+T

(2) E -> T

(3) T -> ( E )

(4) T -> d

令I0 = CLOSURE({E'->.E})

则I0 = {

E’ -> • E,

E -> • E+T,

E -> • T,

T -> •( E ),

T -> • d

}

5.3.2.2 LR(0) FSM 的状态转移函数

GO (I,X) = CLOSURE(J)

其中,I为LR(0) FSM 的状态(闭包的项目集),X为文法符号, J={ A -> αX•β | A -> α• Xβ∈I} ;

表示对于一个状态项目集中的一个项目A -> α• Xβ,在下一个输入字符是X的情况下,一定到另一个新状态 A -> αX•β。

5.3.2.3 LR(0) 有限状态机的构造

从 LR(0) FSM 的初态出发 ,先求出初态项目集的闭包(CLOSURE({S’->.S})),然后应用上述转移函数,通过项目分析每种输入字符下的状态转移,若为新状态,则就求出新状态下的项目集的闭包,级可逐步构造出完整的 LR(0) FSM。

例子:

给定文法G[E]:

(1)E —> E + T | T
(2)T —> T * F | F
(3)F —> ( E ) | id

构造LR(0) FSM

解:

  1. 先画出I0,使用增广文法,构造E’->E,然后求E的闭包得到E -> E + T、E -> T、T -> T * F、T -> F、F -> ( E )、F -> id,以上构成I0集合,然后在他们前面都加上一个点,得到:

  1. 对 E 进行GOTO操作,就是把I0中所有包含有 ▪E 的项拿出来,变成 E▪,构建成为I1

  1. 对 T 进行GOTO操作,得到I2

  1. 对F进行GOTO操作,得到I3

  1. 对( 进行GOTO操作,得到I4,由于此时▪后为非终结符,故可以进行闭包操作,对F求闭包

  1. 对 id 进行GOTO操作,得到I5

此时对I0的操作全部结束,图应为这样:

  1. 对I1中的E’->E进行归约操作,得到accept,注意只有开始符号才可以进行这样的操作。

  1. 对+进行GOTO操作,由于此时▪后为非终结符,故可以进行闭包操作,得到I6

  1. 之后,对I2、I3由于已经为归约项目,所以无操作)、I4、I5(由于已经为归约项目,所以无操作)都进行跟之前类似的操作,可以得到I7、I8

第二轮画完后是这样,为了便于区分,这里使用红色标记第二轮,值得注意的是I4部分很容易出现漏画的情况

  1. 对新生成的I6、I7、I8进行处理,得到I9、I10、I11

此时自动机的图为这样:

  1. 最后对I9、I10、I11处理得到完整的自动机

至此,LR(0)自动机构建完毕。

此时我们再对归约式分析时就可以采用这样的方法来进行分析

5.3.3 LR(0) 分析法

5.3.3.1 LR(0) 文法定义

文法 G 是 LR(0) 文法,当且仅当它的LR(0)FSM中的每个状态都满足:
①不同时含有移进项目和归约项目,即不存在移进-归约冲突。
②不含有两个以上归约项目,即不存在归约-归约冲突。

例如对于下面的LR(0) 有限状态机,状态2、9存在冲突现象:存在移进/归约冲突。

如果LR(0)分析表中没有语法分析动作冲突,那么给定的文法就称为LR(0)文法。

5.3.3.2 LR(0)分析表的构造

ACTION 表项和 GOTO表项可按如下方法构造:

  • 若项目A ->α • aβ属于 Ik 且 GO (Ik, a)= Ij, 期望字符a 为终结符,则置ACTION[k, a] =sj (j表示新状态Ij);
  • 若项目A ->α • Aβ属于 Ik,且GO (Ik, A)= Ij,期望字符 A为非终结符,则置GOTO(k, A)=j (j表示文法中第j个产生式);1
  • 若项目A ->α •属于Ik, 那么对任何终结符a, 置ACTION[k, a]=rj;其中,假定A->α为文法G 的第j个产生式;
  • 若项目S’ ->S • 属于Ik, 则置ACTION[k, #]为“acc”;
  • 分析表中凡不能用上述规则填入信息的空白格均置上“出错标志”

翻译一下:

  • 如果圆点不在项目k最后且圆点后的期待字符a为终结符,则ACTION[k, a] =sj (j表示新状态Ij);
  • 如果圆点不在项目k最后且圆点后的期待字符A为非终结符,则GOTO(k, A)=j (j表示文法中第j个产生式);
  • 如果圆点在项目k最后且k不是S’ ->S,那么对所有终结符a,ACTION[k, a]=rj (j表示文法中第j个产生式);
  • 如果圆点在项目k最后且k是S’ ->S,则ACTION[k, #]为“acc”;

例子:

考虑文法G[S] :
S → (S) | a
相应的LR(0) FSM如下,构造其LR(0)分析表。

解:

  • 对于状态0,则需要从I0看,因为S'->·S,则使用规则2得GOTO[0,S]=1(因为Go(I0,S)=I1);因为S->·(S)、S->·a,则使用规则1得ACTION[0,(]=S2(因为Go(I0,()=I2)、ACTION[0,a]=S3(因为Go(I0,a)=I3)。
  • 对于状态1,则需要从I1看,因为S'->S,则使用规则4得ACTION[k, #]=acc。
  • 对于状态2,则需要从I2看,因为S->(·S),则使用规则2得GOTO[2,S]=4(因为Go[I2,S]=I4);因为S-·(S)、S->·a,则使用规则1得ACTION[0,(]=S2(因为Go(I0,()=I2)、ACTION[0,a]=S3(因为Go(I0,a)=I3)。
  • 对于状态3,则需要从I3看,因为S->a·,则使用规则3得ACTION[3, a]=r2(因为S->a为文法的第二个产生式)
  • 对于状态4,则需要从I4看,因为S->(S·),则使用规则1得ACTION[4,)]=S5(因为Go[I4,)]=I5)
  • 对于状态5,则需要从I5看,因为S->(S)·,则使用规则3得ACTION[5,)]=r1(因为S->(S)为文法的第一个产生式)

5.3.3.3 LR(0) 分析流程

设输入串为w,ip指向输入串w的首符号a,i指向符号栈顶;状态栈的初始栈顶为0,符号栈初始栈顶为#。

根据上述得到的LR(0) 分析表以及这个流程分析输入串((a))的动作如下:

5.4 SLR(1)分析法

5.4.1 基本概念

(1)"SLR"表示 "Simple LR",即简单LR分析法。构造出的ACTION与GOTO表如果不含多重入口,则称该文法为SLR(1)文法。SLR(1)分析方法中的L代表最左推导。

(2)使用SLR表的分析器叫做一个SLR分析器。

(3)每个SLR(1)文法都是无二义的。但也存在许多无二义文法不是SLR(1)的。

(4)LR(0)∈ SLR(1)∈无二义文法

判断是否是SLR(1)文法:

5.4.2 SLR(1)分析表的构造

5.4.2.1 构造方法概述

(1)把G拓广为G'

(2)对G'构造:       

  1. LR(O)项目集规范族C         

  2. 活前缀识别自动机的状态转换函数GO

(3)使用C和GO,构造SLR分析表:

  1. 令每个项目集&的下标k作为分析器的状态,包含项目S'→·S的集合I的下标k为分析器的初态。        

  2. 构造分析表的ACTION和GOTO子表

在前面中,带大家构造了LR(0)分析表,现在构造SLR(1)分析表。他们两者的过程有很多的相同点,但是也有不同点,下面的表就具体的说明了他们的构造过程。

大家可以看出,他们的第二步有所不同,在专业术语中,SLR(1)多了一步解决冲突的步骤,具体是怎么来解决冲突的,后面会和大家讲解。

5.4.2.2 冲突解决办法

  • 假定一个LR(0)规范族含有如下的一个项目集(状态):I={x->α·bβ,A->α·,B->α·}
  • FOLLOW(A)和FOLLOW(B)的交集为∅,且不包含b
  • 当状态I面临任何输入符号a时,可以:
    • 若a=b,则移进;
    • 若a∈FOLLOW(A),用产生式A->α进行规约;
    • 若a∈FOLLOW(B),用产生式B->α进行规约;
    • 此外,报错。

大家现在看了可能有点懵,下面结合一道例题和大家讲解。

5.4.2.3 构造例子

怎么根据文法G来画出识别活前缀的DFA和LR(0)项目集规范族是怎么求出来的,我们现在不做过多阐述,现在我们主要来看如何去构造SLR(0)分析表,我们第一步就是要解决冲突,我们分析已经求出来的LR(0)项目集规范族可以看出I1,I2,I9的第一个项目都是规约项目,而第二个项目都是移进项目,产生了规约-移进冲突,需要解决冲突。采取SLR(1)冲突消解,步骤如下:

1、首先根据前面讲过的方法构造该文法的First集和Follow集,此处不赘述。

First(S')={(,i};Follow(S')={#};

First(S')={(,i};Follow(E)={#,+,)};

First(S')={(,i};Follow(T)={#,+,),*};

First(S')={(,i};Follow(F)={#,+,),*};

2、这是初始的SLR(1)分析表,

3、按照下面的方法根据状态0填充表的第一行:

ACTION 表项和 GOTO表项可按如下方法构造:(与LR(0)分析表的构造方法一致)

  • 若项目A ->α • aβ属于 Ik 且 GO (Ik, a)= Ij, 期望字符a 为终结符,则置ACTION[k, a] =sj (j表示新状态Ij);
  • 若项目A ->α • Aβ属于 Ik,且GO (Ik, A)= Ij,期望字符 A为非终结符,则置GOTO(k, A)=j (j表示文法中第j个产生式);1
  • 若项目A ->α •属于Ik, 那么对任何终结符a, 置ACTION[k, a]=rj;其中,假定A->α为文法G 的第j个产生式;
  • 若项目S’ ->S • 属于Ik, 则置ACTION[k, #]为“acc”;
  • 分析表中凡不能用上述规则填入信息的空白格均置上“出错标志”

4、下面我们来看看状态1的情况,状态里面有两个项目,一个是归约项目,一个是移进项目,因此有冲突,我们检查一下要归约生成的Follow(S'),里面有#号,所以说这个归约项目告诉我们在状态1面临#的时候应该选择归约,而这个归约呢又是个接受项目。所以就是整个分析结束的接受标志。而第二个项目告诉我们,这是个移进项目,从I1出发识别+之后,DFA告诉我们应该转入6,所以1这一行+这一列选的应该是移入,转入6,虽然状态1里面有一个移进项目,一个规约项目,但是我们通过检查Follow集合,我时把这两个动作给它区分来了。没有造成冲突。

5、下面我们接着看状态2,状态2也有一个规约项目,一个移进项目,E的FOLLOW集合有三个元素,+,),#,所以说第一个项目告诉我们,当状态2处于栈顶,面临的输入符号是+,),#,你就可以按照第一个项目规约,我们可以发现它是第二个产生式,所以说在2的这一行,+,),#这些列都写上用第二个产生式做规约,填上r2(意思说出现A->B·且A不为文法的起始符号时,就无法通过常规的构造ACTION 表项和 GOTO表项的四种情况进行填充表格,此时应该根据Follow(A)进行填充,然后观察A->B这条产生式在原文法的编号x,填充rx到相应方框中。),再看第二个项目,当状态2处于栈顶面临*的时候,就应该是移入了,它正好与E的FOLLOW集元素不重叠,可以移入。

6、后面的步骤都与之前的是一样的,这里直接提供答案:

第六章 语法制导的翻译

6.1 语法制导概述

6.1.1 什么是语法制导翻译?

语法制导翻译使用上下文无关文法CFG来引导对语言的翻译,是一种面向文法的翻译技术。

6.1.2 语法制导翻译的基本思想

  • 如何表示语义信息?为CFG中的文法符号设置语义属性,用来表示语法成分对应的语义信息
  • 如何计算语义属性?文法符号的语义属性值是用与文法符号所在产生式(语法规则)相关联的语义规则来计算的
  • 对于给定的输入串x ,构建x的语法分析树,并利用与产生式(语法规则)相关联的语义规则来计算分析树中各结点对应的语义属性值
  • 将语义规则同语法规则(产生式)联系起来要涉及两个概念:
    • 语法制导定义(Syntax-Directed Definitions, SDD )
    • 语法制导翻译方案(Syntax-Directed Translation Scheme , SDT )

6.1.3 语法制导定义(SDD)

SDD是对CFG的推广:

  • 将每个文法符号和一个语义属性集合相关联
  • 将每个产生式和一组语义规则相关联,这些规则用于计算该产生式中各文法符号的属性值

如果X是一个文法符号,a是X的一个属性,则用X.a表示属性a在某个标号为X的分析树结点上的值

如上,我们为 L 设置了一个属性 inh ,为 T 定义了一个属性 type ;由第一个产生式可以看出,L 的 inh 属性是由 T 的 type属性定义的。对于第四个产生式,用下角标区分两个规则。

6.1.4 语法制导翻译方案(SDT)

SDT是在产生式右部嵌入了程序片段的CFG,这些程序片段称为语义动作。按照惯例,语义动作放在花括号内。

例如:

语义动作可以嵌入在产生式体中的任何位置:

6.1.5 SDD与SDT

SDD

  • 是关于语言翻译的高层次规格说明
  • 隐蔽了许多具体实现细节,使用户不必显式地说明翻译发生的顺序

SDT

  • 可以看作是对SDD的一种补充,是SDD的具体实施方案
  • 显式地指明了语义规则的计算顺序,以便说明某些实现细节

6.2 语法制导定义(SDD)

  • 语法制导定义 SDD 是对 CFG 的推广:

    • 将每个文法符号和一个语义属性集合相关联
    • 将每个产生式和一组语义规则相关联,用来计算该产生式中各文法符号的属性值
  • 文法符号的属性

    • 综合属性(synthesized attribute)
    • 继承属性(inherited attribute)
  • 在属性文法中,终结符只具有综合属性。综合属性是指终结符的属性值在语法树上从叶子节点向根节点进行传递,并最终决定整个句子的属性值。而抽象属性、继承属性和传递属性则是非终结符的属性,它们的取值可以依赖于其他非终结符的属性,或者自己的子节点的属性值。

6.2.1 综合属性(synthesized attribute)

在分析树结点N上的非终结符A的综合属性只能通过N的子结点或N本身的属性值来定义。

如上,E的val属性值是由其两个子结点来定义的,因此,val是E的一个综合属性。

终结符可以具有综合属性。终结符的综合属性值是由词法分析器提供的词法值,因此在SDD中没有计算终结符属性值的语义规则。

这里的 digit 是一个终结符,其属性由词法分析给出F.val = digit.lexval。

6.2.2 继承属性(inherited attribute)

在分析树结点N上的非终结符A的继承属性只能通过N的父结点、N的兄弟结点或N本身的属性值来定义。

如上,L的inh是其兄弟结点T的type属性定义的,因此inh是一个继承属性。

6.2.3 属性文法(Attribute Grammar)

一个没有副作用的SDD有时也称为属性文法。

  • 属性文法的规则仅仅通过其它属性值和常量来定义一个属性值

对之前的用于算术表达式求值的SDD进行修改,如上为L设置了属性值val,并消去了副作用,变为属性文法。

6.3 SDD的求值顺序

SDD为CFG中的文法符号设置语义属性。对于给定的输入串x,应用语义规则计算分析树中各结点对应的属性值。

按照什么顺序计算属性值?

  • 语义规则建立了属性之间的依赖关系,在对语法分析树节点的一个属性求值之前,必须首先求出这个属性值所依赖的所有属性值。

6.3.1 依赖图(Dependency Graph)

  • 依赖图是一个描述了分析树中结点属性间依赖关系的有向图;
  • 分析树中每个标号为X的结点的每个属性a都对应着依赖图中的一个结点;
  • 如果属性X.a的值依赖于属性Y.b的值,则依赖图中有一条从Y.b的结点指向X.a的结点的有向边。

举例如下:

习惯上,将综合节点放在左边,继承属性放在右边。

此外,为了方便表示,途中最下面的两个 L 为虚属性结点。

6.3.2 属性值的计算顺序

可行的求值顺序是满足下列条件的结点序列N1, N2, … , Nk :如果依赖图中有一条从结点Ni到Nj的边(Ni→Nj), 那么i < j(即:在节点序列中,Ni 排在 Nj 前面)。

  • 这样的排序将一个有向图变成了一个线性排序,这个排序称为这个图的·拓扑排序(topological sort)·。

对于依赖图的拓扑排序举例如上。此外,还存在其他形式的拓扑排序。比如1,2,3,4互不依赖,因此其顺序可以随意对调。

  • 对于只具有综合属性的SDD ,可以按照任何自底向上的顺序计算它们的值。
  • 对于同时具有继承属性和综合属性的SDD,不能保证存在一个顺序来对各个节点上的属性进行求值。

如果图中没有环,那么至少存在一个拓扑排序。

从计算的角度看,给定一个SDD,很难确定是否存在某棵语法分析树,使得SDD的属性之间存在循环依赖关系。

幸运的是,存在一个SDD的有用子类,它们能够保证对每棵语法分析树都存在一个求值顺序,因为它们不允许产生带有环的依赖图。

不仅如此,接下来介绍的两类SDD可以和自顶向下及自底向上的语法分析过程一起高效地实现:

  • S-属性定义(S-Attributed Definitions, S-SDD)
  • L-属性定义(L-Attributed Definitions, L-SDD)

6.4 S-属性定义与L-属性定义

6.4.1 S-属性定义

仅仅使用综合属性的SDD称为S属性的SDD,或S-属性定义、S-SDD。

如果一个SDD是S属性的,可以按照语法分析树节点的任何自底向上顺序来计算它的各个属性值。

S-属性定义可以在自底向上的语法分析过程中实现。

例子:

6.4.2 L-属性定义

L-属性定义(也称为L属性的SDD或L-SDD)的直观含义:在一个产生式所关联的各属性之间,依赖图的边可以从左到右,但不能从右到左(因此称为L属性的,L是Left的首字母)。L-属性文法支持从上到下、从下到上、从左到右的边。

其正式定义如下:

其中,为什么右部符号的继承属性只能依赖于A的继承属性,而不能是综合属性呢?

这是因为,父节点的综合属性A.s可以依赖与字节的的属性;当然就包括子节点的继承属性X.i;如果子节点的继承属性再依赖于父节点的综合属性,就会造成循环依赖。

6.4.3 例子

如上,用于定义所在产生式 右部属性的,是继承属性;左部属性的,是综合属性。

SDD是不是L属性定义,主要通过继承属性所依赖的属性值看出。

对于Q.i = q(R.s)中,Q依赖于右边的兄弟的属性值(Q依赖于R,其它式子就没有类似这种的情况);因此,整个SDD不是L-SDD。

第七章 中间代码的生成

7.1 概述

在语法制导翻译过程中,将生成中间代码的代码(抽象语法树的代码)嵌入到语义动作中,即可完成中间代码(抽象语法树)的生成。

经典的中间代码通常包括以下几种:

  • 树和有向无环图(DAG):是比较 high level 的表示形式。例如抽象语法树。
  • 三地址码(3-address code):是比较 low level 的表示形式,接近目标机器代码。
  • 控制流图(CFG):是更精细的三地址码,程序的图状表示,图中的每个节点是一个基本快(BB),基本块内的代码是三地址码。适合做程序分析。
  • 静态单赋值形式(SSA):更精细的CFG,同时包含控制流和数据流的信息。可以简化程序分析算法。

7.2 DAG

表达式的无环有向图DAG是语法树的一种变体,可以用于处理表达式中的公共表达式。

例1:

对于串a+a*(b-c)+(b-c)*d,它的DAG如下:

例2:

为串a+b+(a+b)*b构造DAG:

7.3 三地址代码

7.3.1 三地址码概述

  • 三地址代码是语法树的线性表达形式,每条代码包含一个运算和三个地址,两个地址用于存放运算对象,一个用于存放运算结果。也就是说在三地址代码中不允许出现组合运算。

  • 例如:表达式x+y*z会被翻译为:

    t1=y*z

    t2=x+t1

    其中,t1和t2是编译器产生的临时名字,用于存储程序计算中得到的中间结果。

7.3.2 常见三地址指令形式

  • 形如x = y op z的赋值指令。op是双目运算。
  • 形如x = op y的赋值指令。op是单目运算。
  • 形如x = y的赋值指令。无条件转移指令goto L。L是要跳转语句的标号。
  • 形如if x goto L或if x relop y goto L的条件转移指令。
  • 形如x = y[i]或x[i] = y的带下标赋值指令。
  • 形如x=&y、x=y或x=y的地址及指针赋值指令。
  • 形如param x、call p,n、y=call p,n等函数调用指令。

7.3.3 if语句和while语句的三地址码

7.3.3.1 if语句的三地址码

if语句的三地址码通常包含条件语句和跳转指令,具体可以分为以下几步:

  1. 把条件判断语句的运算结果存到一个临时变量中,比如T1。
  2. 判断T1是否满足条件,如果满足则跳转到目标地址(比如label1),否则执行下一行代码。
  3. 目标地址(label1)是一个标号,可以在程序中被识别,并继续执行下去。

下面是一个if语句的伪代码,以及对应的三地址码:

    if (a > b) {
      c = a;
    }
    t1 = a > b
    if t1 goto label1
    goto label2
    label1:
    c = a
    label2:

7.3.3.2 while语句的三地址码

while语句的三地址码通常包含条件语句、跳转指令和循环控制指令,具体可以分为以下几步:

  1. 把条件判断语句的运算结果存到一个临时变量中,比如T1。
  2. 判断T1是否满足条件,如果不满足则跳转到目标地址(比如label2),否则继续执行循环体内的代码。
  3. 循环体内的代码执行完成后,再次执行1-2步骤,直到判断条件不再满足为止。
  4. 如果条件不再满足,则跳转到目标地址(比如label1),否则执行下一行代码。

下面是一个while语句的伪代码,以及对应的三地址码:

    while (a > b) {
      c = a;
      a = a - 1;
    }
    label1:
    t1 = a > b
    if not t1 goto label2
    c = a
    a = a - 1
    goto label1
    label2: