继词法分析和文法分析之后,本文将介绍使用上下文无关文法来引导对语言的翻译。
SDD
语法制导定义(Syntax-Directed Definition,SDD)是一个上下文无关文法和属性及语义规则的结合。属性和文法符号相关联,语义规则和产生式相关联,文法符号X的属性a表示为X.a。
非终结符号可以有两种属性:
- 综合属性:如果语法分析树上的结点N的某个属性a只能通过N的子结点和N本身的属性值来定义,那么属性a是结点N的一个综合属性;
- 继承属性:如果语法分析树上的结点N的某个属性b只能通过N的父结点、N本身和N的兄弟结点的属性值来定义,那么属性a是结点N的一个继承属性。
终结符号可以有综合属性,但不能有继承属性,终结符号的属性是由词法分析器提供的词法值。
综合属性
如果语法分析树中的结点N的某个属性a只能通过N的子结点或N本身的属性值来定义,那么属性a是结点N的一个综合属性。
对表达式文法G:
E → E+T | TT → T*F | FF → (E) | id
它对应的SDD如下:
其中,属性val是文法符号的数值,属性lexval是由词法分析器返回的数值。为id1*id2+id3
构建语法分析树,其中,id1、id2、id3的lexval属性分别为3、4、5:
在这棵语法分析树中,所有结点的属性都被显示了出来,这样的语法分析树也称为注释语法分析树。
继承属性
如果语法分析树上的结点N的某个属性b只能通过N的父结点、N本身和N的兄弟结点的属性值来定义,那么属性a是结点N的一个继承属性。
考虑上一小节中的表达式文法的非左递归形式G’:
E → TE'E'→ +TE' | εT → FT'T'→ *FT' | εF → (E) | id
它对应的SDD如下:
其中,属性val是文法符号的数值,属性lexval是由词法分析器返回的数值,属性inh是一个继承属性,属性syn是一个综合属性。为id1*id2+id3
构建语法分析树,其中,id1、id2、id3的lexval属性分别为3、4、5:
在这棵语法分析树中,属性inh“向下地传递参数”,属性syn“向上地返回结果”,关于继承属性和综合属性的求值顺序将在下一小节介绍。
SDD的求值顺序
和终结符号的综合属性直接由词法分析器给出不同,非终结符号的综合属性和继承属性都或多或少地依赖于其他属性,因此确定一棵注释语法分析树中属性的求值顺序是十分重要的,依赖图可以帮助我们完成这个工作。
依赖图描述了某个语法分析树中的属性实例之间的信息流,从一个属性实例到另一个属性实例的边表示计算第二个属性实例需要用到第一个属性实例的值。具体地讲,一个依赖图包括:
- 属性结点:对于语法分析树中的每个结点N,N的每个属性在依赖图中都有一个结点;
- 综合属性的边:如果和产生式p关联的语义规则通过X.a的值定义了综合属性Y.b的值,那么在依赖图中有一条从X.a到Y.b的边;
- 继承属性的边:如果和产生式p关联的语义规则通过X.a的值定义了继承属性Y.b的值,那么在依赖图中有一条从X.a到Y.b的边。
得到依赖图后,如果这个依赖图中没有环,那么这个依赖图至少存在一个拓扑排序。假设依赖图的结点集合为N,边集合为E,计算此依赖图的拓扑排序的步骤为:
- 构建一个序列A,A的长度为len(N),将i初始化为0;
- 从N中选出一个入度为0的结点n,从N中删除n且从E中删除所有以n为起点的边,将A[i]设为n,同时将i加1;
- 如果N不为空,重复步骤2,否则排序结束,得到拓扑序列A[0]、A[1]、…、A[len(N)-1]。
考虑图1的注释语法分析树,它的依赖图如下,结点前的标号是拓扑排序后的结果:
考虑图2的注释语法分析树,它的依赖图如下,结点前的标号是拓扑排序后的结果:
到这里,我们已经知道了如何借助依赖图确定一棵注释语法分析树中属性的求值顺序。但是,不是所有的注释语法分析树都能通过依赖图找到一个拓扑排序,如果依赖图中存在环,那么这个依赖图没有拓扑排序。如果我们能小心地定义SDD中的语义规则使得其依赖图中没有环,那么我们一定能够确定此SDD中属性的求值顺序,下面介绍的S属性的SDD和L属性的SDD一定能确定属性的求值顺序。
S属性的SDD
如果一个SDD的每个属性都是综合属性,那么这个SDD是一个S属性的SDD。
对于一个S属性的SDD,可以按照语法分析树结点的任何自底向上顺序来计算它的各个属性值。S属性的SDD可以在自底向上的语法分析过程中实现。
L属性的SDD
L属性的SDD的思想是在一个产生式体所关联的各个属性之间,依赖图的边总是从左到右,而不能从右到左。也就是说,每个属性必须满足如下条件:
- 要么是一个综合属性;
- 要么是一个继承属性,但是它的语义规则有这些限制:假设有产生式A→X1X2…Xn和继承属性Xi.a,Xi.a的计算规则只能使用A的继承属性以及Xj(1<=j<=i)的综合属性或继承属性,并且Xi的全部属性组成的依赖图中不存在环。
L属性的SDD可以在自顶向下的语法分析过程中实现。
SDT
语法制导的翻译方案(Syntax-Directed Translation scheme,SDT)是SDD的一种补充,它是在其产生式体中嵌入程序片段的一个上下文无关文法,这些程序片段称为语义动作,它们可以出现在产生式体中的任何地方。通常情况下,语义动作的两边要加上花括号,如果花括号作为文法符号出现,则要给它们加上引号。
实现SDT的通用方法
语义动作可以放在产生式体中的任何位置,当一个语义动作左边的所有符号都被处理完后该动作立即执行。更具体的,对产生式A→X{a}Y,有:
- 如果语法分析过程是自底向上的,那么当X出现在语法分析栈的栈顶时立即执行动作a;
- 如果语法分析过程是自顶向下的,那么当试图展开Y(Y是非终结符号)或在输入中检测到Y(Y是终结符号)之前执行动作a。
通用的SDT实现方法如下:
- 忽略语义动作,对输入进行语法分析,并产生一棵语法分析树;
- 然后检查每个内部结点N,假设它的产生式是A→α。将α中的各个动作当作N的附加子结点加入,使得N的子结点从左到右和α中的符号及动作完全一致;
- 对这棵语法分析树进行前序遍历,并且当访问到一个以某个动作为标号的结点时立即执行此动作。
对于表1显示的SDD,把它转换成SDT:
注意到,此SDT和相应的SDD相比,只有前两个产生式的语义动作和原来的语义规则不同。由于这两个产生式的产生式头已经是开始符号,因此相应的语义动作需要把实现结果输出。由此SDT构建的嵌入了动作的语法分析树如下:
对上面的语法分析树进行前序遍历,每遇到一个动作就立即执行它,遍历完成后就实现了此SDT。
在语法分析过程中实现SDT
可以在语法分析过程中实现的SDT包括后缀SDT和L属性定义的SDT,后缀SDT通常在自底向上的语法分析过程中实现,L属性定义的SDT通常在自顶向下的语法分析过程中实现。注意,不是所有的SDT都能在语法分析过程中实现。
后缀SDT
如果一个SDD的基础文法是LR文法并且此SDD是S属性定义的,那么我们可以构造这样一个SDT,其中的每个动作都放在产生式的最后,并且在按照这个产生式将产生式体归约为产生式头的时候执行这个动作。像这样所有动作都在产生式体最右端的SDT也称为后缀翻译方案(后缀SDT)。
将一个S属性定义的且基础文法为LR文法的SDD转换为一个SDT的规则如下:
- 将计算一个产生式头的综合属性的动作放置在这个产生式体的最右端。
注意到,由于表1中的SDD是S属性定义的且其基础文法G是LR文法,因此可以把它转换成后缀SDT,表3中的SDT就是此SDD的一个转换形式。下面我们进一步改进表3中的SDT,使其能够在语法分析过程中实现:
表4中的stack表示LR语法分析栈,top指向栈顶。为了帮助读者理解这个SDT中的语义动作,以产生式F→(E)
为例,考虑将(E)
归约成F
的过程,如图6所示:
图6显示了把(E)
归约成F
的LR语法分析过程。图6(a)是即将把(E)
归约成F
的语法分析栈快照,此时有stack[top]=")"
,stack[top-1]="E"
,stack[top-2]="("
。执行的归约动作是连续3次出栈操作(将”)”、”E”、”(“依次弹出栈顶,如图6(b))和1次入栈操作(将”F”压入栈顶,如图6(c))。注意到,(c)中的top相当于(a)中的top-2,也就是说,F实际上出现在(a)中的stack[top-2]处;另外,由于E.val将被赋值给F.val,并且E出现在(a)中的stack[top-1]处,因此在(a)中有stack[top-2].val=stack[top-1].val
;将(a)变成(c)还需要执行2次出栈操作(实际上是3次出栈操作和1次入栈操作),因此还需要把top指针向下移动2位。
从SDT中消除左递归
在详细介绍如何在语法分析过程中实现L属性定义的SDT之前,我们先来介绍如何从SDT中消除左递归。在介绍语法分析的时候我们知道了带有左递归的文法不能按照自顶向下的方式进行语法分析,并且我们还知道如何从一个左递归的文法中消除左递归。当文法是SDT的一部分时,我们还需要考虑如何处理其中的语义动作。
情况1
最简单的情况是,我们只需要关心一个SDT中动作的执行顺序。在这种情况下,当对文法进行消除左递归的转换时,可以简单地将动作当成终结符号处理。之所以可以这样做,是因为对文法的转换保持了由文法生成的符号串中终结符号的顺序。举个例子,对下面的SDT:
E → E+T { print('+'); }E → T
将动作看作终结符号,消除左递归后得到的SDT为:
E → TRR → +T { print('+'); } R | ε
情况2
另一种比较复杂的情况是,一个SDT的动作包含了对属性值的计算。举个例子,对下面的SDT:
A → A1Y { A.a = g(A1.a, Y.y); }A → X { A.a = f(X.x); }
其中,f和g是两个任意的函数,对基础文法消除左递归得到:
A → XRR → YR | ε
现在尝试在文法中加入语义动作。我们给文法符号R附加上两个属性,一个属性i是继承属性,它用来保存函数f和g的结果,另一个属性s是综合属性,它会在计算结束后返回最终的计算结果。得到的SDT为:
A → X { R.i = f(X.x); } R { A.a = R.s; }R → Y { R1.i = g(R.i, Y.y); } R1 { R.s = R1.s; }R → ε { R.s = R.i; }
我们用注释语法分析树来展示对SDT消除左递归前后的变化(这里省略了对R.s的计算):
L属性定义的SDT
如果一个SDD的基础文法能够以自顶向下的方式进行语法分析,并且此SDD是L属性定义的,那么我们可以把这个SDD转换成一个L属性定义的SDT。
将一个L属性定义的SDD转换为一个SDT的规则如下:
- 将计算某个非终结符号A的继承属性的动作插入到产生式体中紧靠在A的本次出现之前的位置上。如果A的多个继承属性以无环的方式相互依赖,就需要对这些属性的求值动作进行排序,以便先计算需要的属性;
- 将计算一个产生式头的综合属性的动作放置在这个产生式体的最右端。
在语法分析过程中实现L属性定义的SDT的方法包括:
- 使用一个递归下降的语法分析器。它为每个非终结符号都建立一个函数,对应于非终结符号A的函数以参数的形式接受A的继承属性,并返回A的综合属性;
- 使用一个递归下降的语法分析器,以边扫描边生成的方式生成代码;
- 与LL语法分析器结合实现一个SDT。属性的值存放在语法分析栈中,各个规则从栈中的已知位置获取需要的属性值;
- 与LR语法分析器结合实现一个SDT。如果基础文法是LL的,我们总是可以按照自底向上的方式来处理语法分析和翻译过程。
在递归下降语法分析过程中进行翻译
一个递归下降的语法分析器对每个非终结符号A都有一个函数A,我们可以把这个语法分析器扩展为一个翻译器:
- 函数A的参数是非终结符号A的继承属性,返回值是非终结符号A的综合属性;
- 在函数A的函数体中进行语法分析并处理属性。
举个例子,考虑表2中的SDD,非终结符号E对应的函数E如下:
float E() {float Tval = T();float EQUOTEinh = Tval;float EQUOTEsyn = EQUOTE(EQUOTEinh);float Eval = EQUOTEsyn;return Eval;
}
在函数E中,首先,注意到函数E没有任何参数,这是因为非终结符号E没有任何继承属性;其次,在E的函数体中,调用了函数T和函数EQUOTE,前者对应于非终结符号T(T也没有任何继承属性),后者对应于非终结符号E’(E’有一个继承属性);最后,定义了一系列变量来保存继承属性或综合属性(实际上是为了方便读者理解才定义了如此多的变量),并返回了计算结果。
另外,非终结符号E’对于的函数EQUOTE如下:
float EQUOTE(float EQUOTEinh) {if (下一个输入符号为'+') {读取输入return EQUOTE(EQUOTEinh + T());} else if (输入已经结束) {return EQUOTEinh;} else {...}
}
边扫描边生成代码
对上面的例子,如果需要在调用函数EQUOTE的过程中打印出表达式,而不是返回最后的结果,那么可以一边扫描一边生成代码。使用这个方法需要满足的条件是:每个非终结符号存在一个综合属性作为主属性,对主属性的求值是将相关产生式体中的非终结符号的主属性值连接起来得到的(连接时也可能包括非主属性),各个非终结符号的主属性值在连接运算中出现的顺序和这些非终结符号在产生式体中出现的顺序相同。
修改函数EQUOTE,使其打印出表达式:
float EQUOTE(float EQUOTEinh) {if (下一个输入符号为'+') {读取输入print(EQUOTEinh);print('+');float Tval = T();return EQUOTE(EQUOTEinh + Tval);} else if (输入已经结束) {return EQUOTEinh;} else {...}
}
L属性的SDD与LL语法分析
对于一个基础文法是LL文法的L属性的SDD,如果我们已经把它转换成了一个L属性定义的SDT,那么我们可以在LL语法分析过程中完成对它的翻译。
为了在LL语法分析过程中实现L属性定义的SDT,语法分析栈需要被扩展,扩展后的语法分析栈可以存放三种记录:
- 代表终结符号和非终结符号的记录,实际上是状态记录;
- 动作记录。非终结符号A的继承属性放在表示A的栈记录中,对A的继承属性求值的代码放在一个紧靠在A的栈记录之上的动作记录中;
- 综合记录。非终结符号A的综合属性放在一个紧靠在A的栈记录之下的综合记录中。
在语法分析过程中,某些属性值可能会被拷贝到动作记录或综合记录中,这是因为一个分析表驱动的LL语法分析器模拟了一个最左推导过程。举个例子,当语法分析器按照一个产生式A→BC对栈顶的非终结符号A展开时,栈顶的A被替换为BC,假设C有一个继承属性C.i依赖于A的某个属性,但是此时已经无法访问到A,因此我们需要把A的属性临时拷贝到C的动作记录中,这样一来,C在计算其属性C.i时就能找到这个依赖属性了。
下图是用文法G’中的产生式E'→ +TE'
对E’进行展开的语法分析过程:
我们对这个语法分析过程进行分析:
- 图8(a)中展示了即将展开
E'
的语法分析栈,此时栈顶是E'
的栈记录,紧跟其后的是E'
的综合记录,假设E'.inh
已经被拷贝给了E1'
的动作记录中的EQUOTEval
; - 图8(b)用产生式体
+TE1'
替换了E'
。一开始,+
的栈记录在栈顶,它将和输入进行匹配,在匹配完成后,它被弹出栈顶,T
的栈记录变为栈顶; - 由于
T
的栈记录位于栈顶,因此会用某个产生式对符号T
进行展开,当这个展开过程结束后,T
的综合记录将位于栈顶,并且该记录中的val
属性值已经被赋值了; - 由于
E1'
的继承属性依赖于T.val
,因此在将T的综合记录弹出栈顶之前,还需要把T.val
拷贝给E1'
的动作记录中的Tval
; - 此时
E1'
的动作记录位于栈顶,它使用两个拷贝的属性值计算E1'.inh
,计算的结果被放入E1'
的栈记录中; - 由于
E1'
的栈记录位于栈顶,因此会用某个产生式对符号E1'
进行展开,当这个展开过程结束后,E1'
的综合记录将位于栈顶,并且该记录中的syn
属性值已经被赋值了; E1'
的综合记录将E1'.syn
赋值给E'.syn
,到这里,用产生式体+TE1'
展开E'
的过程结束。
L属性的SDD与LR语法分析
我们可以使用自底向上的方法来完成任何可以用自顶向下方式完成的翻译过程,也就是说,对一个基础文法为LL文法的L属性的SDD,我们可以修改这个文法,使得可以在LR语法分析过程中实现这个新文法上的SDD。具体的方法包括:
- 将给定的基础文法为LL文法的L属性的SDD转换成SDT,这样的SDT在每个非终结符号之前放置语义动作计算它的继承属性,并且在产生式最右端放置一个语义动作计算综合属性;
- 对每个内嵌的语义动作,向这个文法中引入一个标记非终结符号来替换它。每个这样的位置都有一个不同的标记,并且对于任意一个标记M都有一个产生式M→ε;
- 如果标记非终结符号M在某个产生式A→α{a}β中替换了语义动作a,对a进行修改得到a’,并且将a’关联到M→ε上。这个动作a’将动作a需要的A或α中符号的任何属性作为M的继承属性进行拷贝,并且按照a中的方法计算各个属性,计算得到的属性将作为M的综合属性。
对表2中的SDD,先将其转换成SDT:
以这个SDT中的产生式E'→+TE1'
为例,它的LR语法分析过程如下:
图9(a)中首先对产生式E'→+TE1'
进行变换,引入了一个标记非终结符号M对嵌入的动作{E1'.inh=E'.inh+T.val}
进行替换,其中,E'.inh
和T.val
将作为M的两个继承属性,E1'.inh
将作为M的综合属性。
图9(b)中是LR语法分析栈,我们假设接下来的输入能够匹配产生式体+TME1'
并且最终被归约成产生式头E'
。在这个语法分析栈中,我们不知道栈顶的记录代表了哪一个文法符号(或者状态),但是我们一定能确定这个记录中的某个字段保存了E’的inh属性。
图9(c)中显示了即将把产生式体+TME1'
归约成产生式头E'
之前的语法分析栈。我们对从图9(b)到图9(c)的LR语法分析过程进行详细的分析:
- 语法分析栈的初始状态如图9(b)所示,此时栈顶的记录保存了E’的inh属性;
- 由于检测到的下一个输入的文法符号是”+”,因此识别出的产生式肯定是
E'→+TME1'
; - 假设接下来的输入被成功归约成T,此时栈顶是代表非终结符号T的记录,该记录中保存了T的val属性。由于我们已经确定了产生式是
E'→+TME1'
,因此我们可以确定下一步是把ε归约成M; - 在把ε归约成M时,栈顶是代表标记非终结符号M的记录,并且会执行一个动作,这个动作把E.inh(位于stack[top-3]的记录中)和T.val(位于stack[top-1]的记录中)分别拷贝给M的i和j属性,同时使用属性i和j计算它的综合属性s,属性s实际上是下一个非终结符号E1’的继承属性;
- 假设接下来的输入被成功归约成E1’,此时栈顶是代表非终结符号E1’的记录,计算E1’的syn属性时用到的E1’的inh属性是从代表M的记录中拿到的,这种情况下,代表M的记录相当于图9(b)中栈顶的记录,唯一的区别是这里我们能够知道保存E1’.inh的记录是代表M的记录;
- 由于句柄
+TME1'
已经位于栈顶,因此我们可以把+TME1'
归约成E'
,此时会执行一个动作,这个动作把E1’.syn拷贝到stack[top-3]记录中,并执行3次出栈操作(实际上是4次出栈操作和1次入栈操作)。到这里,从图9(b)到图9(c)的LR语法分析过程结束。