介绍自下而上语法分析方法。所谓自下而上分析法就是从输入串开始,逐步进行“归约”,直至归约到文法的开始符号;或者说,从语法树的末端开始,步步向上“归约”,直到根结。
我们所讨论的自下而上分析法是一种“移进-归约”法。这种方法的大意是,用一个寄存符号的先进后出栈,把输入符号一个一个地移进到栈里,当栈顶形成某个产生式的一个候选式时,即把栈顶的这一部分替换成(归约为)该产生式的左部符号。
首先考虑下面的例子:
假定文法G为
(1) S→aAcBe
(2) A→b
(3) A→Ab
(4) B→d <1>
我们希望把输入串abbcde归约到S。每实现一步归约都是把栈顶的一串符号用某个产生式的左部符号来代替。后面我们权且把栈顶上的这样一串符号称为“可归约串” ,存在种种不同的方法刻画“可归约串”。对这个概念的不同定义形成了不同的自下而上分析法。在算符优先分析中,用“最左素短语”来刻画“可归约串”,在“规范归约”分析中,则用“句柄”来刻画“可归约串”。
自下而上分析的中心问题是,怎样判断栈顶的符号串的可归约性,以及,如何归约。这是算符优先分析和LR分析将讨论的问题。各种不同的自下而上分析法的一个共同特点是,边输入单词符号(移进符号栈),边归约。也就是在从左到右移进输入串的过程中,一旦发现栈顶呈现可归约串就立即进行归约。这个过程对于编译实现来说是一个十分自然的过程。
令G是一个文法,S是文法的开始符号,假定abd是文法G的一个句型,如果有
则称b是句型abd相对于非终结符A的短语。特别是,如果有
作为“短语”的两个条件均是不可缺少的。仅仅有Ab,未必意味着b就是句型abd的一个短语。因为,还需有SaAd这一条件。
稍为精确的一点说,假定a是文法G的一个句子,我们称序列
是a的一个规范归约,如果此序列满足:
容易看到,规范归约是关于a的一个最右推导的逆过程。因此,规范归约也称最左归约。
在形式语言中,最右推导常被称为规范推导。由规范推导所得的句型称为规范句型。如果文法G是无二义的,那么,规范推导(最右推导)的逆过程必是规范归约(最左归约)。
请注意句柄的“最左”特征,这一点对于移进-归约来说是重要的。因为,句柄的“最左”性和符号栈的栈顶两者是相关的。对于规范句型来说,句柄的后面不会出现非终结符号(即,句柄的后面只能出现终结符)。基于这一点,我们可用句柄来刻画移进-归约过程的“可归约串”。因此,规范归约的实质是,在移进过程中,当发现栈顶呈现句柄时就用相应产生式的左部符号进行替换。
栈是语法分析的一种基本数据结构。在解释“移进-归约”的自下而上分析过程时我们就已经提到了符号栈。一个“移进-归约”分析器使用了这样的一个符号栈和一个输入缓冲区。今后我们将用一个不属于文法符号的特殊符号‘#’作为栈底符,即在分析开始时预先把它推进栈;同时,也用这个符号作为输入串的“结束符”,即无条件地将它置在输入串之后,以示输入串的结束。
分析开始时,栈和输入串的初始情形为:
符号栈 | 输入串 |
---|---|
# | w# |
分析器的工作过程是:自左至右把输入串w的符号一一移进符号栈里,一旦发现栈顶形成一个可归约串时,就把这个串用相应的归约符号(在规范归约的情况下用相应产生规则的左部符号)代替。这种替换可能持续多次,直至栈顶不再呈现可归约串为止。然后,就继续移进符号,重复整个过程,直至最终形成如下格局:
符号栈 | 输入串 |
---|---|
#S | # |
此时,栈里只含#与最终归约符S(在规范归约的情形下S为文法开始符号),而输入串w全被吸收,仅剩下结束符。这种格局表示分析成功。如果达不到这种格局,意味着输入串w(源程序)含有语法错误。
语法分析对符号栈的使用有四类操作:“移进”、“归约”、“接受”和“出错处理”。
对于“归约”而言请留心一个非常重要的事实,任何可归约串的出现必在栈顶,不会在栈的内部。对于规范归约而言,这个事实是明显的。由于规范归约是最右推导的逆过程,因此这种归约具有“最左”性,故可归约串必在栈顶,而不会在栈的内部。正因如此,先进后出栈在归约分析中是一种非常有用的数据结构。
如果要实际表示一棵语法分析树的话,一般来说,使用穿线表是比较方便的。这只须对每个进栈符号配上一个指示器就可以了。
当要从输入串移进一个符号a入栈时,我们就开辟一项代表端末结a的数据结构,让这项数据结构的地址(指示器值)连同a本身一起进栈。端末结的数据结构应包括这样一些内容:(1) 儿子个数:0;(2) 关于a自身的信息(如单词内部值,现在暂且不管)。
当要把栈顶的n个符号,如 X1X2…Xn 归约为 A 时,我们就开辟一项代表新结A的数据结构。这项数据结构应包含这样一些内容:(1) 儿子个数:n;(2) 指向儿结的n个指示器值;(3) 关于A自身的其它信息。归约时,把这项数据结构的地址连同A本身一起进栈。
最终,当要执行“接受”操作时,我们将发现一棵用穿线表表示的语法树业已形成,代表根结的数据结构的地址和文法的开始符号(在规范归约情况下)一起留在栈中。
用这种方法表示语法树是最直截了当的。当然,也可以用别的或许是更加高效的表示方法。
一个文法,如果它的任一产生式的右部都不含两个相继(并列)的非终结符,即不含如下形式的产生式右部:
在后面的定义中,a、b代表任意终结符;P、Q、R代表任意非终结符;‘…’代表由终结符和非终结符组成的任意序列,包括空字。
假定G是一个不含e-产生式的算符文法。对于任何一对终结符a、b,我们说:
如果一个算符文法G中的任何终结符对(a,b)至多只满足下述三关系之一:
现在来研究从算符优先文法G构造优先关系表的算法。
通过检查G的每个产生式的每个候选式,可找出所有满足 a≖b 的终结符对。为了找出所有满足关系 ⋖ 和 ⋗ 的终结符对,我们首先需要对G的每个非终结符P构造两个集合 FIRSTVT(P) 和 LASTVT(P) :
所谓素短语是指这样的一个短语,它至少含有一个终结符,并且,除它自身之外不再含任何更小的素短语。所谓最左素短语是指处于句型最左边的那个素短语。如上例,P*P和i是句型P*P+i的素短语,而P*P是它的最左素短语。
现在考虑算符优先文法,我们把句型(括在两个#之间)的一般形式写成:
aj−1⋖aj
aj≖aj+1,…,ai−1≖ai
ai⋗ai+1
根据这个定理,下面我们讨论算符优先分析算法。为了和定理的叙述相适应,我们现在仅使用一个符号栈S,既用它寄存终结符,也用它寄存非终结符。下面的分析算法是直接根据这个定理构造出来的,其中k代表符号栈S的使用深度。
在实际实现算符优先分析算法时,一般不用表5.1这样的优先表,而是用两个优先函数f和g。我们把每个终结符q与两个自然数 f(q) 和 g(q) 相对应,使得
函数f称为入栈优先函数,g称为比较优先函数。使用优先函数有两方面的优点:便于作比较运算,并且节省存储空间,因为优先关系表占用的存储量比较大。其缺点是,原先不存在优先关系的两个终结符,由于与自然数相对应,变成可比较的了。因而,可能会掩盖输入串的某些错误。但是,我们可以通过检查栈顶符号q和输入符号a的具体内容来发现那些原先不可比较的情形。
如果优先函数存在,那么,从优先表构造优先函数的一个简单方法是:
现在必须证明:若a≖b,则f(a)=g(b);若a⋖b,则f(a)< g(b);若a⋗b,则f(a)> g(b)。第一个关系可从函数的构造直接获得。因为,若a≖b,则既有从fa到gb的弧,又有从gb到fa的弧。所以,fa和gb所能到达的结是全同的。至于a⋗b和a⋖b的情形,只须证明其一。如果a⋗b,则有从fa到gb的弧。也就是,gb能到达的任何结fa也能到达。因此,f(a)³ g(b)。我们所需证明的是,在这种情况下,f(a)=g(b)不应成立。我们将指出,如果f(a)=g(b),则根本不存在优先函数。假若f(a)=g(b),那么必有
a⋗b, a1⋖≖b, a1⋗≖b1,…am⋗≖bm, a⋖≖bm
因为对任何优先函数都必须满足(5.5) 所规定的条件,而上面的关系恰恰表明,对任何优先函数f和g来说,必定有
f(a)> g(b)³ f(a1)³ g(b1)³ … ³ f(am)³ g(bm)³ f(a)
从而导致f(a)> f(a),产生矛盾。因此,不存在优先函数f和g。
使用算符优先分析法时,可在两种情况下,发现语法错误:
若在栈顶终结符号与下一输入符号之间不存在任何优先关系;
若找到某一“句柄”(此处“句柄”指素短语),但不存在任一产生式,其右部为此“句柄”。
针对上述情况,处理错误的子程序也可分成几类。首先,我们考虑处理类似第2种情况错误的子程序。当发现这种情况时,就应该打印错误信息。子程序要确定该“句柄”与哪个产生式的右部最相似。例如,假定从栈中确定的“句柄”是abc,可是,没有一个产生式,其右部包含a,b,c在一起。此时,可考虑是否删除a,b,c中的一个。例如,假若有一产生式,其右部为aAcB,则可给出错误信息:“非法b”;若另有一产生式,其右部为abdc,则可给出错误信息:“缺少d”。
规范归约(最左归约—最右推导的逆过程)的关键问题是寻找句柄。在一般的“移进-归约”过程中,当一串貌似句柄的符号串呈现于栈顶时,我们有什么方法可以确定它是否为相对于某一产生式的句柄呢?LR方法的基本思想是,在规范归约过程中,一方面记住已移进和归约出的整个符号串,即记住“历史”,另一方面根据所用的产生式推测未来可能碰到的输入符号,即对未来进行“展望”。当一串貌似句柄的符号串呈现于分析栈的顶端时,我们希望能够根据所记载的“历史”和“展望”以及“现实”的输入符号等三方面的材料,来确定栈顶的符号串是否构成相对某一产生式的句柄。
LR分析法的这种基本思想是很符合哲理的。因而可以想象,这种分析法也必定是非常通用的。正因如此,实现起来也就非常困难。作为归约过程的“历史”材料的积累虽不困难(实际上,这些材料都保存在分析栈中),但是,“展望”材料的汇集却是一件很不容易的事情。这种困难不是理论上的,而是实际实现上的。因为,根据历史推测未来,即使是推测未来的一个符号,也常常存在着非常多的不同可能性。因此,当把“历史”和“展望”材料综合在一起时,复杂性就大大增加。如果简化对“展望”资料的要求,我们就可能获得实际可行的分析算法。
后面所讨论的LR方法都是带有一定限制的。
一个LR分析器实质上是一个带先进后出存储器(栈)的确定有限状态自动机。我们将把“历史”和“展望”材料综合地抽象成某些“状态”。分析栈(先进后出存储器)用来存放状态。栈里的每个状态概括了从分析开始直到某一归约阶段的全部“历史”和“展望”资料。任何时候,栈顶的状态都代表了整个的历史和已推测出的展望。因此,在任何时候都可从栈顶状态得知你所想了解的一切,而绝对没有必要从底而上翻阅整个栈。LR分析器的每一步工作都是由栈顶状态和现行输入符号所唯一决定的。为了有助于明确归约手续,我们把已归约出的文法符号串也同时放在栈里(显然它们是多余的,因为它们已被概括在“状态”里了)。于是,我们可以把栈的结构看成是:
栈的每一项内容包括状态s和文法符号X两部分。(s0,#)为分析开始前预先放到栈里的初始状态和句子括号。栈顶状态为sm,符号串X1X2…Xm是至今已移进归约出的部分。
LR分析器的核心部分是一张分析表。这张分析表包括两部分,一是“动作”(ACTION)表,另一是“状态转换”(GOTO)表。它们都是二维数组。ACTION[s, a]规定了当状态s面临输入符号a时应采取什么动作。GOTO[s,X]规定了状态s面对文法符号X(终结符或非终结符)时下一状态是什么。显然,GOTO[s,X]定义了一个以文法符号为字母表的DFA。
每一项ACTION[s,a]所规定的动作不外是下述四种可能之一:
移进 把(s,a)的下一状态s¢=GOTO[s,a]和输入符号a推进栈,下一输入符号变成现行输入符号。
归约 指用某一产生式A→b进行归约。假若b的长度为r,归约的动作是A,去除栈顶的r个项,使状态sm-r变成栈顶状态,然后把(sm-r,A)的下一状态s¢=GOTO[sm-r,A]和文法符号A推进栈。归约动作不改变现行输入符号。执行归约动作意味着b(=Xm-r+1…Xm)已呈现于栈顶而且是一个相对于A的句柄。
接受 宣布分析成功,停止分析器的工作。
报错 发现源程序含有错误,调用出错处理程序。
LR分析器的总控程序本身的工作是非常简单的。它的任何一步只需按栈顶状态s和现行输入符号a执行ACTION[s,a]所规定的动作。不管什么分析表,总控程序都是一样地工作。
一个LR分析器的工作过程可看成是栈里的状态序列、已归约串和输入串所构成的三元式的变化过程。分析开始时的初始三元式为:
一个 LR 分析器的工作过程就是一步一步地变换三元式,直至执行“接受”或“报错”为止。
对于一个 LR 分析器来说,栈顶状态提供了所需的一切“历史”和“展望”信息。请注意一个非常重要的事实:如果仅由栈的内容和现实的输入符号就可以识别一个句柄,那么,就可以用一个有限自动机自底向上扫描栈的内容和检查现行输入符号来确定呈现于栈顶的句柄是什么(如果形成一个句柄时)。实际上, LR 分析器就是这样的一个有限自动机。只是,因栈顶的状态已概括了整个栈的内容,因此,无需扫描整个栈。栈顶状态就好象已代替我们进行了这种扫描。
我们主要关心的问题是,如何从文法构造LR分析表。对于一个文法,如果能够构造一张分析表,使得它的每个入口均是唯一确定的,则我们将把这个文法称为LR文法。并非所有上下文无关文法都是LR文法。但对于多数程序语言来说,一般都可用LR文法描述。直观上说,对于一个LR文法,当分析器对输入串进行自左至右扫描时,一旦句柄呈现于栈顶,就能及时对它实行归约。
一个LR分析器有时需要“展望”和实际检查未来的k个输入符号才能决定应采取什么样的“移进-归约”决策。一般而言,一个文法,如果能用一个每步顶多向前检查k个输入符号的LR分析器进行分析,则这个文法就称为LR(k)文法。但对多数的程序语言来说,k=0或1就足够了。因此,我们只考虑k£1的情形。
注意,LR方法关于识别产生式右部的条件远不象预测法那样严峻。预测法要求每个非终结符的所有候选的首符均不同,预测分析程序认为,一旦看到首符之后就看准了该用哪一个产生式进行推导。但LR分析程序只有在看到整个右部所推导的东西之后才认为是看准了归约方向。因此,LR方法比预测法应该更加一般化。
对于一个文法G,我们可以构造一个有限自动机,它能识别G的所有活前缀。在这个基础上,我们将讨论如何把这种自动机转变成LR分析表。
对于一个文法G,我们首先要构造一个 NFA ,它能识别G的所有活前缀。这个 NFA 的每个状态是下面定义的一个“项目”。文法G每一个产生式的右部添加一个圆点称为G的一个 LR(0) 项目(简称项目)。例如,产生式 A→XYZ 对应有四个项目:
A→⋅XYZ
A→X⋅YZ
A→XY⋅Z
A→XYZ⋅
但是,产生式 A→e 只对应一个项目 A→⋅ 。在计算机中,每个项目可用一对整数表示,第一个整数代表产生式编号,第二个整数指出圆点的位置。
直观上说,一个项目指明了在分析过程的某时刻我们看到产生式多大一部分。例如,上面四项的第一个项目意味着,我们希望能从后面输入串中看到可以从XYZ推出的符号串。第二个项目意味着,我们已经从输入串中看到能从X推出的符号串,我们希望能进一步看到可以从YZ推出的符号串。
我们可以使用这些项目状态构造一个NFA,用来识别这个文法的所有活前缀。这个文法的开始符号S¢仅在第一个产生式的左部出现。使用这个事实,我们规定项目1为NFA的唯一初态。任何状态(项目)均认为是NFA的终态(活前缀识别态)。如果状态i和j出自同一产生式,而且状态j的圆点只落后于状态i的圆点一个位置,如状态i为
那么,就从状态i画一条标志为Xi的弧到状态j。假若状态i的圆点之后的那个符号为非终结符,如 i 为 X→a⋅Ab , A 为非终结符,那么,就从状态i画e弧到所有 A→⋅g 状态(即,所有那些圆点出现在最左边的A的项目)。
子集方法,我们能够把识别活前缀的NFA确定化,使之成为一个以项目集合为状态的DFA,这个DFA就是建立LR分析算法的基础。
构成识别一个文法活前缀的DFA的项目集(状态)的全体称为这个文法的LR(0)项目集规范族。这个规范族提供了建立一类LR(0)和SLR(简单LR)分析器的基础。
为了便于叙述,我们用一些专门术语来称呼不同的项目。凡圆点在最右端的项目,如A→a·,称为一个“归约项目”。对文法的开始符号S¢的归约项目,如S¢→a·,称为“接受”项目。显然,“接受”项目是一种特殊的归约项目。形如A→a·ab的项目,其中a为终结符,称为“移进”项目。形如A→a·Bb的项目,其中B为非终结符,称为“待约”项目。
下面所引进的 e−CLOSURE (闭包)的办法来构造一个文法G的LR(0)项目集规范族。
为了使“接受”状态易于识别,我们总把文法G进行拓广。假定文法G是一个以S为开始符号的文法,我们构造一个 G¢ ,它包含了整个G,但它引进了一个不出现在G中的非终结符S¢,并加进一个新产生式 S¢→S ,而这个 S¢ 是 G¢ 的开始符号。那么,我们称G¢是G的拓广文法。这样,便会有一个仅含项目S¢→S的状态,这就是唯一的“接受”态。
假定 I 是文法 G¢ 的任一项目集,定义和构造I的闭包 CLOSURE(I) 的办法是:
在构造 CLOSURE(I) 时,请注意一个重要的事实,那就是,对任何非终结符B,若某个圆点在左边的项目B→·g进入到CLOSURE(I),则B的所有其它圆点在左边的项目B→·b也将进入同一个CLOSURE集。因此,在某种情况下,并不需要真正列出CLOSURE集里的所有项目B→·g,而只须列出非终结符B就可以了。
函数GO是一个状态转换函数。 GO(I,X) 的第一个变元I是一个项目集,第二个变元X是一个文法符号。函数值 GO(I,X) 定义为:
其中: J=任何形如A→aX⋅b的项目|A→aX⋅b属于I 。
直观上说,若I是对某个活前缀g有效的项目集,那么,GO(I,X)便是对gX有效的项目集。通过函数CLOSURE和GO很容易构造一个文法G的拓广文法G¢的LR(0)项目集规范族。构造算法是:
PROCEDURE ITEMSETS(G¢);
BEGIN
C:={CLOSURE({S¢®·S})};
REPEAT
FOR C中的每个项目集I和G¢的每个符号X DO
IF GO(I,X)非空且不属于C THEN
把GO(I,X)放入C族中
UNTIL C 不再增大
END
这个算法的工作结果C就是文法G¢的LR(0)项目集规范族。
我们希望从识别文法的活前缀的DFA建立LR分析器(带栈的确定有限状态自动机)。因此,需要研究这个DFA的每个项目集(状态)中的项目的不同作用。
我们说项目 A→b1⋅b2 对活前缀ab1是有效的,其条件是存在规范推导S¢aAwab1b2w。一般而言,同一项目可能对好几个活前缀都是有效的(当一个项目出现在好几个不同的集合中时便是这种情形)。若归约项目A→b1·对活前缀ab1是有效的,则它告诉我们应把符号串b1归约为A,即把活前缀ab1变成aA。若移进项目A→b1·b2对活前缀ab1是有效的,则它告诉我们,句柄尚未形成,因此,下一步动作应是移进。但是,可能存在这样的情形,对同一活前缀,存在若干项目对它都是有效的。而且它们告诉我们应做的事情各不相同,互相冲突。这种冲突通过向前多看几个输入符号,或许能够获得解决。我们在下一节将讨论这种情形,当然,对于非LR文法,这种冲突有些是绝对无法解决的,不论超前多看几个输入符号也无济于事。
对于每个活前缀,我们可以构造它的有效项目集。实际上,一个活前缀g的有效项目集正是从上述的DFA的初态出发,经读出g后而到达的那个项目集(状态)。换言之,在任何时候,分析栈中的活前缀X1X2…Xm的有效项目集正是栈顶状态Sm所代表的那个集合。这是LR分析理论的一条基本定理。实际上,栈顶的项目集(状态)体现了栈里的一切有用信息—历史。
假若一个文法G的拓广文法G¢的活前缀识别自动机中的每个状态(项目集)不存在下述情况:1) 既含移进项目又含归约项目,或者2) 含有多个归约项目,则称G是一个LR(0)文法。换言之,LR(0)文法规范族的每个项目集不包含任何冲突项目。
对于LR(0)文法,我们可直接从它的项目集规范族C和活前缀识别自动机的状态转换函数GO构造出LR分析表。下面是构造LR(0)分析表的算法。
假定 C=I0,I1,…,In 。前面,我们已习惯用数码表示状态,因此,令每个项目集 Ik 的下标 k 作为分析器的状态。特别是,令那个包含项目 S¢→⋅S 的集合 Ik 的下标k为分析器的初态。分析表的 ACTION 子表和 GOTO 子表可按如下方法构造:
由于假定 LR(0) 文法规范族的每个项目集不含冲突项目,因此,按上法构造的分析表的每个入口都是唯一的(即,不含多重定义)。我们称如此构造的分析表是一张 LR(0) 表。使用 LR(0) 表的分析器叫做一个 LR(0) 分析器。
上面所说的LR(0)文法是一类非常简单的文法。这种文法的活前缀识别自动机的每一个状态(项目集)都不含冲突性的项目。但是,即使是定义算术表达式这样的简单文法也不是LR(0)的。因此,本节我们将要研究一种有点简单“展望”材料的LR分析法,即SLR法。
我们将看到,许多冲突性的动作都可能通过考察有关非终结符的FOLLOW集而获解决。例如,假定一个LR(0)规范族中含有如下的一个项目集(状态)I,
I={X→a·bb,
A→a·,
B→a·}
其中,第一个项目是移进项目,第二、三项目是归约项目。这三个项目告诉我们应做的动作各不相同,互相冲突。第一个项目告诉我们应该把下一个输入符号b(如果是b)移进。第二个项目告诉我们应把栈顶的a归约为A;第三个项目则说应把a归约为B。解决冲突的一种简单办法是,分析所有含A或B的句型,考察句型中可能直接跟在A或B之后的终结符,也就是说,考察集合FOLLOW(A)和FOLLOW(B),如果这两个集合不相交,而且都不包含b,那么,当状态I面临任何输入符号a时,我们就可以采取如下的“移进-归约”决策:
一般而言,假定 LR(0) 规范族的一个项目集 I 中含有 m 个移进目;
A1→a⋅a1b1,A2→a⋅a2b2 ,…, Am→a⋅ambm ;
同时含有 n 个归约项目: B1→a⋅ , B2→a⋅ ,…, Bn→a⋅ ,
如果集合 {a1,…,am}, FOLLOW(B1),…,FOLLOW(Bn) 两两不相交(包括不得有两个 FOLLOW 集合有#),则隐含在I中的动作冲突可通过检查现行输入符号a属于上述 n+1 个集合中的哪个集合而获得解决。这就是:
对任给的一个文法G,我们可用如下的办法构造它的SLR(1)分析表:首先把G拓广为G¢,对G¢构造LR(0)项目集规范族C和活前缀识别自动机的状态转换函数GO。使用C和GO,然后再按下面的算法构造G¢的SLR分析表。
假定C={I0,I1,…,In},令每个项目集Ik的下标k为分析器的一个状态,因此,G¢的SLR分析表含有状态0,1,…,n。令那个含有项目S¢→·S的Ik的下标为初态。函数ACTION和GOTO可按如下方法构造:
按上述算法构造的含有ACTION和GOTO两部分的分析表,如果每个入口不含多重定义,则称它为文法G的一张SLR表。具有SLR表的文法G称为一个SLR(1)文法。数字1的意思是,在分析过程中顶多只要向前看一个符号。使用SLR表的分析器叫做一个SLR分析器。
若按上述算法构造的分析表存在多重定义的入口(即含有动作冲突),则说明文法G不是SLR(1)的。在这种情况下,不能用上述算法构造分析器。
每个SLR(1)文法都是无二义的。但也存在许多无二义文法不是SLR(1)的。
在SLR方法中,若项目集Ik含有A→a·,那么,在状态k时,只要所面临的输入符号aÎFOLLOW(A),就确定采取“用A→a归约”的动作。但是,在某种情况下,当状态k呈现于栈顶时,栈里的符号串所构成的活前缀ba未必允许把a归约为A,因为可能没有一个规范句型含有前缀bAa。因此,在这种情况下,用A→a进行归约未必有效。
可以设想让每个状态含有更多的“展望”信息,这些信息将有助于克服动作冲突和排除那种用A→a所进行的无效归约。我们可以设想,必要时,对状态进行分裂,使得LR分析器的每个状态能够确切地指出,当a后跟哪些终结符时才容许把a归约为A。
我们需要重新定义项目,使得每个项目都附带有k个终结符。现在每个项目的一般形式是 [A→a⋅b,a1a2…ak] ,此处, A→a⋅b 是一个LR(0)项目,每一个a都是终结符。这样的一个项目称为一个LR(k)项目。项目中的 a1a2…ak 称为它的向前搜索符串(或展望串)。向前搜索符串仅对归约项目 [A→a⋅,a1a2…ak] 有意义。对于任何移进或待约项目 [A→a⋅b,a1a2…ak],b¹e ,搜索符串 a1a2…ak 没有作用。归约项目 [A→a⋅,a1a2…ak] 意味着:当它所属的状态呈现在栈顶且后续的k个输入符号为 a1a2…ak 时,才可以把栈顶上的a归约为A。我们只对k£1的情形感兴趣,因为,对多数程序语言的语法来说,向前搜索(展望)一个符号就多半可以确定“移进”或“归约”。
形式上我们说一个 LR(1) 项目 [A→a⋅b,a] 对于活前缀g是有效的,如果存在规范推导
假定I是一个项目集,它的闭包CLOSURE(I)可按如下方式构造:
1. I的任何项目都属于CLOSURE(I)。
2. 若项目[A→a·Bb, a]属于CLOSURE(I),B→x是一个产生式,那么,对于FIRST(ba)中的每个终结符b,如果[B→·x, b]原来不在CLOSURE(I)中,则把它加进去。
3. 重复执行步骤2,直至CLOSURE(I)不再增大为止。
因为,[A→a·Bb, a]属于对活前缀g=da有效的项目集意味着存在一个规范推导
因此,若bac可推导出bw,则对于每个形如B®x的产生式,我们有SgBbwgxbw,也就是说,[B→·x, b]对g也是有效的。注意,b可能是从b推出的第一个符号,或者,若b推出e,则b就是a,把这两种可能性结合在一起,我们说bÎFIRST(ba)。
令I是一个项目集,X是一个文法符号,函数GO(I,X)定义为:
BEGIN
C:={CLOSURE({[S¢→·S,#]})};
REPEAT
FOR C中的每个项目集I和G¢的每个符号X DO
IF GO(I,X)非空且不属于C,THEN 把GO(I,X)加入C中
UNTIL C不再增大
END
现在来讨论从文法的LR(1)项目集族C构造分析表的算法。
假定C={I0, I1,…, In},令每个Ik的下标k为分析表的状态。令那个含有[S¢→·S, #]的Ik的k为分析器的初态。动作ACTION和状态转换GOTO可构造如下:
按上述算法构造的分析表,若不存在多重定义的入口(即,动作冲突)的情形,则称它是文法G的一张规范的LR(1)分析表。使用这种分析表的分析器叫做一个规范的LR分析器。具有规范的LR(1)分析表的文法称为一个LR(1)文法。
每个SLR(1)文法都是LR(1)文法。一个SLR(1)文法规范的LR分析器比其SLR分析器含有更多的状态。
现在来讨论构造分析表的LALR 方法。这本质上是一种折衷方法。LALR 分析表比规范LR分析表要小得多,能力也差一点。但它却能对付一些SLR所不能对付的情形,例如,文法(5.9)的情形。
对于同一个文法,LALR 分析表和SLR分析表永远具有相同数目的状态。对于ALGOL一类语言来说,一般要用几百个状态,但若用规范LR分析表,同一类语言,却要用几千个状态。因此,用SLR或LALR要经济得多。
我们称两个LR(1)项目集具有相同的心,如果除去搜索符之后,这两个集合是相同的。我们将试图把所有同心的LR(1)项目集合并为一。我们还将看到一个心就是一个LR(0)项目集。
由于GO(I,X)的心仅仅依赖于I的心,因此,LR(1)项目集合并后的转换函数GO可通过GO(I,X)自身的合并而得到。即,在合并项目集时用不着同时考虑修改转换函数的问题。动作ACTION应进行修改,使得能够反映各被合并的集合的既定动作。
假定有一个LR(1)文法,即,它的LR(1)项目集不存在动作冲突,如果我们把同心集合并为一,就可能导致存在冲突。但是这种冲突不会是“移进—归约”冲突。因为,如存在这种冲突,则意味着,面对当前的输入符号a,有一个项目[A→a·, a]要求采取归约动作,同时又有另一项目[B→b·ag, b]要求把a移进。这两个项目既然同处在合并之后的一个集合中,则意味着,在合并前,必有某个c使得[A→a·, a]和[B→b·ag, b]同处于(合并前的)某一集合中。然而,这一点又意味着,原来的LR(1)项目集就已存在着“移进—归约”冲突。故同假设不符。因此,同心集的合并不会产生新的“移进-归约”冲突。
但是,同心集的合并有可能产生新的“归约—归约”冲突。例如,考虑文法
(0) S¢→S
(1) S→aAd | bBd | aBe | bAe
(2) A→c
(3) B→c
这个文法只产生四个符号串:acd、bcd、ace和bce。如果我们构造这个文法的LR(1)项目集族,那么,将发现不会存在冲突性动作。因而它是一个LR(1)文法。在它的集族中,对活前缀ac有效的项目集为{[A→c·, d], [B→c·, e]},对bc有效的项目集为{[A→c·, e], [B→c·, d]}。这两个集合都不含冲突,它们是同心的。一经合并就变成:{[A→c·, d/e], [B→c·, d/e]。显然,这是一个含有“归约—归约”冲突的集合。因为,当面临e或d时,我们不知道该用A→c还是用B→c进行归约。
下面,我们将给出构造LALR分析表算法。基本思想是,首先构造LR(1)项目集族,如果它不存在冲突,就把同心集合并在一起。若合并后的集族不存在归约-归约冲突,就按这个集族构造分析表。这个算法的主要步骤是:
经上述步骤构造的分析表若不存在冲突,则称它为文法G的LALR分析表。存在这种分析表的文法称为一个LALR(1)文法。
这个算法的思想虽然简单明确,但实现起来甚费时间和空间。
任何二义文法决不是一个LR文法,因而也不是SLR或LALR文法。这是一条定理。但是,某些二义文法是非常有用的。例如,若用下面的文法来描述含有+、*的算术表达式:
E→E+E|E∗E|(E)|i .......<1>
那么,只要对算符+、*赋予优先级和结合规则,这个文法是再简单不过了。这个文法与文法
E→E+T|T
T→T∗F|F ......<2>
F→(E)|i
相比,有两个明显的好处:首先,如需要改变算符的优先级或结合规则无需去改变文法 <1> 自身。其次,文法 <1> 的分析表所包含的状态肯定比 <2> 所包含的状态要少得多。因为 <2> 中含有单非产生式(右部只含一个单一的非终结符) E→T 和 T→F ,这些旨在定义算符优先级和结合规则的产生式要占用不少状态和消耗不少时间。本节将讨论如何使用LR分析法的基本思想,凭借一些其它条件,来分析二义文法所定义的语言。
在LR分析过程中,当我们处在这样一种状态下,即输入符号既不能移入栈顶,栈内元素又不能归约时,就意味着发现语法错误。发现错误后,便进入相应的出错处理子程序。处理的方法分为两类:第一类多半使用插入、删除或修改的办法。如在语句a[1,2:=3.14;中插入一个]。如果不可能使用这种办法,则采用第二类办法。第二类处理办法包括在检查到某一不合适的短语时,它不能与任一非终结符可能推导出的符号串相匹配。如语句
if x>k+2 then go 10 else k is 2;
由于把保留字goto误写成go,校正程序试图改成goto,但后面还有错误(将‘:=’误为‘is’),故放弃将go换为goto。校正子程序在此种情况下,将go 1跳过,作为非法语句看待。这种方法企图将含有语法错误的短语局部化。分析程序认定含有错误的符号串是由某一非终结符A所推导出的,此时该符号串的一部分已经处理,处理的结果反映在栈顶部一系列状态中,剩下的未处理符号仍在输入串中。分析程序跳过这些剩余符号,直至找到一个符号a,它能合法地跟在A的后面。同时,要把栈顶的内容逐个移去,直至找到某一状态s,该状态与A有一个对应的新状态GOTO[s,A],并将该新状态下推入栈。这样,分析程序就认为它已找到A的某个匹配并已将它局部化,然后恢复正常的分析过程。
利用这种方法,可以以语句为单位进行处理,也可以把跳过的范围缩小。例如,若在‘if’后面的表达式中遇到某一错误,分析程序可跳至下一个输入符号‘then’而不是‘;’或‘end’。
与算符优先分析方法比较,用LR分析方法时,设计特定的出错处理子程序比较容易,因为不会发生不正确的归约。在分析表的每一空项内,可以填入一个指示器,指向特定的出错处理子程序。第一类错误的处理一般采用插入、删除或修改的办法,但要注意,不能从栈内移去任何那种状态,它代表已成功地分析了的程序中的某一成分。
前面讨论的只是很简单的情况。一个可投入实际运行的LR分析程序,需要考虑许多更为复杂的情形。例如,当处在某一状态下遇到各种不合法的符号时,错误诊察子程序需要向前查看几个符号,根据所查看的符号才能确定应采取哪一种处理办法。又如前已述及,分析表中有些状态在遇到不合法的输入符号时,不是立即转到错误诊察子程序,而是进行某些归约,这不仅推迟了发现错误的时间,而且往往会带来一些处理上的困难。试研究下面的一输入符号串:
a:=b?c];
这里以‘?’表示在b与c之间有某个错误。如果分析程序遇到‘a:=b’而不向前多看几个符号,则它就会把‘a:=b’先归约成语句,而后我们就再没有机会通过简单地插入符号‘[’进行修补了。但是,即使采用向前查看的办法,查看的符号也不能太多,否则会使分析表变得过分庞大。应该找出一种切实可行的办法,使得在确定处理出错办法时能够参考一些语义信息,以便在向前查看几个符号时,可以避免作出有时从语法上看是正确的,然而却是无意义的校正这一情况。例如,语句
a[1,2:=3.14;
中,标识符‘a’是一个数组标识符,这一语义信息将导致插入符号‘]’。
例题 什么叫句柄?什么叫素短语?(北京航空航天大学1999年硕士生入学考试)
解答:一个句型的最左直接短语称为该句型的句柄。所谓素短语是指这样的一个短语,它至少含有一个终结符,并且,除它自身之外不再含任何更小的素短语。
例题 文法G(S):
S→bTc
S→a
T→R
R→R/S
R→S
符号串bR/bTc/bSc/ac是不是该文法的一个句型,请证实。若是句型,写出该句型的所有短语、素短语以及句柄。(上海交大1997年试题)
解答: 由于有推导:
SÞbTcÞbRcÞbR/Sc
ÞbR/S/ScÞbR/S/S/ScÞbR/bTc/S/ScÞbR/bTc/bTc/Sc
ÞbR/bTc/bRc/ScÞbR/bTc/bSc/ScÞbR/bTc/bSc/ac
所以, bR/bTc/bSc/ac 是 G 的一个句型。
句型 bR/bTc/bSc/ac 中:
短语: a,S,bSc,bTc,R/bTc,R/bTc/bSc,R/bTc/bSc/a,bR/bTc/bSc/ac
素短语: a,bSc,bTc
句柄: bTc
例题 给出文法 G(S) :
S→SaA|A
A→AbB|B
B→cSd|e
请证实 AacAbcBaAdbed 是文法G的一个句型;请写出该句型的所有短语、素短语以及句柄。(上海交大2000年试题)
解答: 由于有推导:
SÞSaAÞAaAÞAaBÞAacSd
ÞAacAdÞAacAbBd
ÞAacAbBbBdÞAacAbcSdbBdÞAacAbcSaAdbBd
ÞAacAbcAaAdbBdÞAacAbcBaAdbBdÞAacAbcBaAdbed
所以, AacAbcBaAdbed 是 G 的一个句型。
句型 AacAbcBaAdbed 中:
短语:
e,B,A,BaA,cBaAd,AbcBaAd,AbcBaAdbe,cAbcBaAdbed,AacAbcBaAdbed
素短语: e,BaA
句柄: B