继词法分析和文法分析之后,本文将介绍使用上下文无关文法来引导对语言的翻译。
语法制导定义(Syntax-Directed Definition,SDD)是一个上下文无关文法和属性及语义规则的结合。属性和文法符号相关联,语义规则和产生式相关联,文法符号X的属性a表示为X.a。
非终结符号可以有两种属性:
终结符号可以有综合属性,但不能有继承属性,终结符号的属性是由词法分析器提供的词法值。
如果语法分析树中的结点N的某个属性a只能通过N的子结点或N本身的属性值来定义,那么属性a是结点N的一个综合属性。
对表达式文法G:
E → E+T | T
T → T*F | F
F → (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“向上地返回结果”,关于继承属性和综合属性的求值顺序将在下一小节介绍。
和终结符号的综合属性直接由词法分析器给出不同,非终结符号的综合属性和继承属性都或多或少地依赖于其他属性,因此确定一棵注释语法分析树中属性的求值顺序是十分重要的,依赖图可以帮助我们完成这个工作。
依赖图描述了某个语法分析树中的属性实例之间的信息流,从一个属性实例到另一个属性实例的边表示计算第二个属性实例需要用到第一个属性实例的值。具体地讲,一个依赖图包括:
得到依赖图后,如果这个依赖图中没有环,那么这个依赖图至少存在一个拓扑排序。假设依赖图的结点集合为N,边集合为E,计算此依赖图的拓扑排序的步骤为:
考虑图1的注释语法分析树,它的依赖图如下,结点前的标号是拓扑排序后的结果:
考虑图2的注释语法分析树,它的依赖图如下,结点前的标号是拓扑排序后的结果:
到这里,我们已经知道了如何借助依赖图确定一棵注释语法分析树中属性的求值顺序。但是,不是所有的注释语法分析树都能通过依赖图找到一个拓扑排序,如果依赖图中存在环,那么这个依赖图没有拓扑排序。如果我们能小心地定义SDD中的语义规则使得其依赖图中没有环,那么我们一定能够确定此SDD中属性的求值顺序,下面介绍的S属性的SDD和L属性的SDD一定能确定属性的求值顺序。
如果一个SDD的每个属性都是综合属性,那么这个SDD是一个S属性的SDD。
对于一个S属性的SDD,可以按照语法分析树结点的任何自底向上顺序来计算它的各个属性值。S属性的SDD可以在自底向上的语法分析过程中实现。
L属性的SDD的思想是在一个产生式体所关联的各个属性之间,依赖图的边总是从左到右,而不能从右到左。也就是说,每个属性必须满足如下条件:
L属性的SDD可以在自顶向下的语法分析过程中实现。
语法制导的翻译方案(Syntax-Directed Translation scheme,SDT)是SDD的一种补充,它是在其产生式体中嵌入程序片段的一个上下文无关文法,这些程序片段称为语义动作,它们可以出现在产生式体中的任何地方。通常情况下,语义动作的两边要加上花括号,如果花括号作为文法符号出现,则要给它们加上引号。
语义动作可以放在产生式体中的任何位置,当一个语义动作左边的所有符号都被处理完后该动作立即执行。更具体的,对产生式A→X{a}Y,有:
通用的SDT实现方法如下:
对于表1显示的SDD,把它转换成SDT:
注意到,此SDT和相应的SDD相比,只有前两个产生式的语义动作和原来的语义规则不同。由于这两个产生式的产生式头已经是开始符号,因此相应的语义动作需要把实现结果输出。由此SDT构建的嵌入了动作的语法分析树如下:
对上面的语法分析树进行前序遍历,每遇到一个动作就立即执行它,遍历完成后就实现了此SDT。
可以在语法分析过程中实现的SDT包括后缀SDT和L属性定义的SDT,后缀SDT通常在自底向上的语法分析过程中实现,L属性定义的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位。
在详细介绍如何在语法分析过程中实现L属性定义的SDT之前,我们先来介绍如何从SDT中消除左递归。在介绍语法分析的时候我们知道了带有左递归的文法不能按照自顶向下的方式进行语法分析,并且我们还知道如何从一个左递归的文法中消除左递归。当文法是SDT的一部分时,我们还需要考虑如何处理其中的语义动作。
最简单的情况是,我们只需要关心一个SDT中动作的执行顺序。在这种情况下,当对文法进行消除左递归的转换时,可以简单地将动作当成终结符号处理。之所以可以这样做,是因为对文法的转换保持了由文法生成的符号串中终结符号的顺序。举个例子,对下面的SDT:
E → E+T { print('+'); }
E → T
将动作看作终结符号,消除左递归后得到的SDT为:
E → TR
R → +T { print('+'); } R | ε
另一种比较复杂的情况是,一个SDT的动作包含了对属性值的计算。举个例子,对下面的SDT:
A → A1Y { A.a = g(A1.a, Y.y); }
A → X { A.a = f(X.x); }
其中,f和g是两个任意的函数,对基础文法消除左递归得到:
A → XR
R → 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的计算):
如果一个SDD的基础文法能够以自顶向下的方式进行语法分析,并且此SDD是L属性定义的,那么我们可以把这个SDD转换成一个L属性定义的SDT。
将一个L属性定义的SDD转换为一个SDT的规则如下:
在语法分析过程中实现L属性定义的SDT的方法包括:
一个递归下降的语法分析器对每个非终结符号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 {
...
}
}
对于一个基础文法是LL文法的L属性的SDD,如果我们已经把它转换成了一个L属性定义的SDT,那么我们可以在LL语法分析过程中完成对它的翻译。
为了在LL语法分析过程中实现L属性定义的SDT,语法分析栈需要被扩展,扩展后的语法分析栈可以存放三种记录:
在语法分析过程中,某些属性值可能会被拷贝到动作记录或综合记录中,这是因为一个分析表驱动的LL语法分析器模拟了一个最左推导过程。举个例子,当语法分析器按照一个产生式A→BC对栈顶的非终结符号A展开时,栈顶的A被替换为BC,假设C有一个继承属性C.i依赖于A的某个属性,但是此时已经无法访问到A,因此我们需要把A的属性临时拷贝到C的动作记录中,这样一来,C在计算其属性C.i时就能找到这个依赖属性了。
下图是用文法G’中的产生式E'→ +TE'
对E’进行展开的语法分析过程:
我们对这个语法分析过程进行分析:
E'
的语法分析栈,此时栈顶是E'
的栈记录,紧跟其后的是E'
的综合记录,假设E'.inh
已经被拷贝给了E1'
的动作记录中的EQUOTEval
;+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'
的过程结束。我们可以使用自底向上的方法来完成任何可以用自顶向下方式完成的翻译过程,也就是说,对一个基础文法为LL文法的L属性的SDD,我们可以修改这个文法,使得可以在LR语法分析过程中实现这个新文法上的SDD。具体的方法包括:
对表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语法分析过程进行详细的分析:
E'→+TME1'
;E'→+TME1'
,因此我们可以确定下一步是把ε归约成M;+TME1'
已经位于栈顶,因此我们可以把+TME1'
归约成E'
,此时会执行一个动作,这个动作把E1’.syn拷贝到stack[top-3]记录中,并执行3次出栈操作(实际上是4次出栈操作和1次入栈操作)。到这里,从图9(b)到图9(c)的LR语法分析过程结束。