A. LR分析法的SLR(1)分析表的构造
在前面讨论LR(0)分析表的构造算法时,我们曾经指出,仅当一个文法G是LR(0)文法时,才能对它构造出无冲突动作的LR(0)分析表。然而,对于通常的程序设计语言来说,它们一般都不能用LR(0)文法来描述。例如,考虑如下“简单分程序”的文法G[B′]:
0? B′→B3? D→d
1? B→bD;Se4? S→s;S
2? D→D;d5? S→s
相应识别其全部活前缀的DFA及LR(0)分析表如图417及表414所示。由于在项目集I8中,既含有移进项目[S→s·;S],又含有归约项目[S→s·],因而反映到分析表中就出现了具有多重定义的元素ACTION[8,′;′]={s10,r5},前者指明当输入符号为“;”时应将它移进栈中,而后者则要求按第5个产生式S→s进行归约。于是就出现了有“移进归约”冲突的分析动作。又如,对于通常用来描述简单表达式的文法G[E],当构造它的项目集规范族时,也会出现类似的情况。这就表明,这两个文法都不是LR(0)文法。然而,尽管如此,对大多数程序设计语言来说,这种具有冲突项目的项目集,在整个项目集规范族中所占的比例毕竟是很小的。因此,如果我们能设法解决出现在一个项目集中的“移进归约”或“归约归约”冲突,那么,只要对前述构造LR(0)分析表的算法稍加修改,它仍能适用于我们现在讨论的情况。
表414G[B′]的LR(0)分析表
b[]d[];[]s[]e[]#[]B[]D[]S0[]s2[8]11[7]acc2[3]s4[9]33[4]s54[]r3[]r3[]r3[]r3[]r3[]r35[][]s7[][]s8[10]66[6]s97[]r2[]r2[]r2[]r2[]r2[]r28[]r5[]r5[]r5,s10[]r5[]r5[]r59[]r1[]r1[]r1[]r1[]r1[]r110[5]s8[10]1111[]r4[]r4[]r4[]r4[]r4[]r4
仔细分析上述构造LR(0)分析表的算法容易看出,使分析表中出现多重定义分析动作的原因在于其中的规则(2),即对于每一项目集Ii中的归约项目A→α·,不管当前的输入符号是什么,都把ACTION子表相应于Ii那一行 (即第i行)的各个元素指定为rj,其中j是产生式A→α的编号。因此,如果该项目集Ii中同时还含有形如B→α·bβ或C→α·的项目,则在分析表的第i行中,必然会出现多重定义的元素。
由此可见,对于含有冲突的项目集
Ii={B→α·bβ,A→α·,C→α·}
在构造分析表时,如果能根据不同的向前符号a,将Ii中各项目所对应的分析动作加以区分,那么就有可能使冲突得到解决。为此,对于文法中的非终结符号U,我们定义集合
FOLLOW(U)={a|S′#?*…Ua…, a∈VT∪{#}}
即FOLLOW(U)是由所有含U的句型中,直接跟在U后的终结符号或#组成的集合。现对上述项目集Ii,考察FOLLOW(A),FOLLOW(C)及{b},若它们两两不相交,则可采用下面的方法,对Ii中各个项目所对应的分析动作加以区分。
对任何输入符号a:
(1) 当a=b时,置ACTION[i,b]=“移进”;
(2) 当a∈FOLLOW(A)时,置ACTION[i,a]={按产生式A→α归约};
(3) 当a∈FOLLOW(C)时,置ACTION[i,a]={按产生式C→α归约};
(4) 当a不属于上述三种情况之一时,置ACTION[i,a]=“ERROR”。
一般地,若一个项目集I含有多个移进项目和归约项目,例如
I={A1→α·a1β1, A2→α·a2β2,…,Am→α·amβm, B1→α·, B2→α·, …, Bn→α·}
则当集合{a1,a2,…,am},FOLLOW(B1),FOLLOW(B2),…,FOLLOW(Bn)两两不相交时,可类似地根据不同的向前符号,对状态为i时的冲突动作进行区分。
上述用来解决分析动作冲突的方法称为SLR(1)规则。此规则是由F?DeRemer于1971年提出的。
有了SLR(1)规则之后,只须对前述构造LR(0)分析表的算法中的规则(2)作如下的修改:“(2)′若归约项目A→α·属于Ii,设A→α为文法的第j个产生式,则对于任何属于FOLLOW(A)的输入符号a,置ACTION[i,a]=rj”,且其余的规则仍保持不变,就得到了构造SLR(1)分析表的算法。
对于给定的文法G,若按上述算法构造的分析表不含多重定义的元素,则称文法G为SLR(1)文法。例如,对于上面的文法G[B′],它的项目集
I8={S→s·; S,S→s·}
含有冲突的项目,但由于FOLLOW(S)={e}≠{;},故冲突可用SLR(1)规则解决,与上述项目相应的分析动作分别是:
ACTION[8,;]=s10ACTION[8,e]=r5
此外,再注意到FOLLOW(B′)=FOLLOW(B)={#}和FOLLOW(D)={;},则按上述算法为G[B′]所构造的SLR(1)分析表b[]d[];[]s[]e[]#[]B[]D[]S0[]s2[8]11[7]acc2[3]s4[9]33[4]s54[4]r35[3]s7[][]s8[10]66[6]s97[4]r28[4]s10[][]r59[7]r110[5]s8[10]1111[6]r4
B. LR分析法的LR(0)分析表的构造
顾名思义,LR(0)分析就是LR(K)分析当K=0的情况,亦即在分析的每一步,只要根据当前的栈顶状态 (或者说根据当前分析栈中已移进或归约出的全部文法符号)就能确定应采取何种分析动作,而无须向前查看输入符号。
为了给出构造LR分析表的算法,我们首先需要引入一些非常重要的概念和术语。 由例4?6对输入串“a,b,a”的分析过程容易看出,如果所分析的输入串没有语法错误,则在分析的每一步,若将分析栈中已移进和归约出的全部文法符号与余留的输入符号串拼接起来,就形成了所给文法的一个规范句型。换言之,也就是在分析的每一步,如输入串已被扫视的部分无语法错误,则当前分析栈中的全部文法符号应当是某一规范句型的前缀。而且还不难看出,此种规范句型的前缀决不会含有句柄右边的任何符号,这是因为一旦句型的句柄在栈的顶部形成,将会立即被归约之故。以后,我们将把规范句型具有上述性质 (即不含句柄之右的任何符号)的前缀称为它的活前缀。例如,对于文法G[L]的规范句型“E,b,a” (见表412分析过程第5步),其句柄为“b”,栈中的符号串为“E,b”,此句型的活前缀为ε,“E”,“E,”,“E,b”等。
由此可见,一个LR分析器的工作过程,实质上也就是一个逐步产生 (或识别)所给文法的规范句型之活前缀的过程。同时,在分析的每一步,分析栈中的全部文法符号 (如果输入串无语法错误)应是当前规范句型的活前缀,并且与此时的栈顶状态相关联。因此,我们自然会想到,如果能构造一个识别所给文法的所有活前缀的有限自动机,那么就能很方便地构造出相应的LR分析表来。稍后我们将讨论这一问题。 上面我们已经说过,在一个规范句型的活前缀中决不含有句柄右边的任何符号。因此,活前缀与句柄的关系不外下述三种情况:
(1) 其中已含有句柄的全部符号 (句柄的最右符号即为活前缀的最右符号);
(2) 其中只含句柄的一部分符号 (句柄开头的若干符号与活前缀最右的若干个符号一致);
(3) 其中全然不含有句柄的任何符号。
第一种情况表明,此时某一产生式A→β的右部符号串β已出现在栈顶,因此相应的分析动作应是用此产生式进行归约。第二种情况意味着形如A→β1β2的产生式的右部子串β1已出现于栈顶,正期待着从余留输入串中看到能由β2推出的符号串。而第三种情况则意味着期望从余留输入串中能看到由某一产生式A→α的右部,即α所推出的符号串。为了刻画在分析过程中,文法的一个产生式右部符号串已有多大一部分被识别,我们可在该产生式的右部的某处加上一个圆点“·”来指示位置。例如,对于上述三种情况,标有圆点的产生式分别为A→β·,A→β1·β2以及A→·α。我们把右部某位置上标有圆点的产生式称为相应文法的一个LR(0)项目。特别,对形如A→ε的产生式,相应的LR(0)项目为A→·。显然,不同的LR(0)项目反映了分析过程中栈顶的不同情况。下面我们就会看到,文法的全部LR(0)项目,将是构造识别其全部活前缀的有限自动机的基础。 在作出文法的全部LR(0)项目之后,现在用它们来构造识别全部活前缀的DFA。这种DFA的每一个状态由若干个LK(0)项目所组成的集合 (称为项目集)来表示。下面以例4?7所给出的文法为例来说明构造此种DFA的方法。
首先,我们用I0表示这个DFA的初态,它预示着分析过程的开始,并且期待着将给定的输入符号串逐步归约为文法的开始符号S′。或者反过来说,我们所期待的是,从使用产生式S′→S开始,能够逐步推导出所给的输入符号串。因此,我们应将项目S′→·S列入状态I0中。换言之,也就是我们期待着将要扫视的输入串正好就是能由S推出的任何终结符号串。然而,由于不能从输入串中直接读出非终结符号S,因此我们也应当把项目S→·A以及S→·B列入到I0中。由于A和B同样是非终结符号,所以又应当将A→·aAb,A→·c和B→·aBb,B→·d列入I0中。由于最后列入I0的项目中,圆点之后都是终结符号,故I0已经“封闭”,构造项目集I0宣告结束。这样,表示初态的项目集I0由如下的项目组成:
I0: S′→·SS→·AA→·aAb
S→·BB→·aBbB→·d
A→·c
我们将项目S′→·S称为项目集I0的基本项目。上述从项目S′→·S出发构造项目集I0的过程,可用一个对其基本项目集{S′→·S}的闭包运算,即CLOSURE({S′→·S})来表示。一般地,设I为一项目集,则构造I的闭包CLOSURE(I)的算法如下:
(1) I中每一项目都属于CLOSURE(I);
(2) 若形如A→α·Xβ的项目属于CLOSURE(I),且X为非终结符号,则文法中任何X产生式的一切圆点在最左边的项目X→·γ也都属于CLOSURE(I);
(3) 重复上述过程,直至不再有新的项目加入CLOSURE(I)为止。
有了初态I0之后,我们来说明如何确定从I0可能转移到的下一个状态。设X为一个文法符号 (终结符号或非终结符号),若I0中有圆点位于X左边的项目A→α·Xβ (其中α可以为ε),则当分析器从输入串识别出 (即移进或归约出)文法符号X后,分析器将进入它的下一个状态。设此状态为Ii,显然Ii中必含有全部形如A→αX·β的项目,我们将这样的项目称为A→α·Xβ的后继项目。对于每一文法符号X,如果存在这样的后继项目,则可能不止一个,设其组成的集合为J,J中的每个项目也就是项目集Ii的基本项目。因此,按照与上面构造项目集I0相类似的讨论,我们就有
Ii=CLOSURE(J)
为了指明Ii是“I0关于文法符号X的后继状态”这一事实,我们可定义一个状态转移函数
GO(I,X)=CLOSURE(J)
其中,I为当前状态,X为文法符号,J为I中所有形如A→α·Xβ的项目的后继项目所组成的集合,而CLOSURE(J)就是项目集I (即状态I)关于X的后继项目集 (即后继状态)。例如,对于上例,我们有:
I1=GO(I0,S)=CLOSURE({S′→S·})={S′→S·}
I2=GO(I0,A)=CLOSURE({S→A·})={S→A·}
I3=GO(I0,B)=CLOSURE({S→B·})={S→B·}
I4=GO(I0,a)=CLOSURE({A→a·Ab,B→a·Bb})=
{A→a·Ab, B→a·Bb, A→·aAb, B→·aBb, A→·c, B→·d}
I5=GO(I0,c)=CLOSURE({A→c·})={A→c·}
I6=GO(I0,d)=CLOSURE({B→d·})={B→d·}
请注意,由于I0中无圆点在b之前的项目,故GO(I0,b)无定义。这样,我们求出了I0的全部后继项目集I1,I2,I3,I4,I5,I6。容易看出,由于I1,I2,I3,I5,I6诸项目集中的项目均无后继项目,因此它们都没有后继状态。对于项目集I4,我们再仿此求出它的后继状态,这些后继状态是:
I7=GO(I4,A)=CLOSURE({A→aA·b})={A→aA·b}
I9=GO(I4,B)=CLOSURE({B→aB·b})={B→aB·b}
此外,由于
GO(I4,a)=I4GO(I4,c)=I5GO(I4,d)=I6
故它们均不产生新的项目集。最后,再求出I7,I9的后继项目集。它们分别是
I8=GO(I7,b)=CLOSURE({A→aAb·})={A→aAb·}
I10=GO(I9,b)=CLOSURE({B→aBb·})={B→aBb·}
由于I8和I10已无后继项目集,所以至此我们已求出所给文法G[S]的全部项目集I0~I10,通常,我们将这些项目集的全体称为文法G[S]的LR(0)项目集规范族,并记为
C={I0,I1,I2,I3,…,I10}
于是,我们所要构造的识别文法G[S]全部活前缀的DFA为
M=(C,V,GO,I0,C)
其中: M的状态集也就是文法的LR(0)项目集规范族C={I0,I1,…,I10};
M的字母表也就是文法的字汇表V={S′,S,A,B,a,b,c,d};
M的映像也就是如上定义的状态转换函数GO;
M的终态集也是C,这表明M的每一状态都是它的终态。
对于上述文法G[S],如上构造的识别其全部活前缀的DFA的状态转换图如图416所示。
由于状态转换图416中的每一个状态都是终态,因此在上述DFA工作的过程中,从初态I0出发,沿着有向边所指示的方向前进,可以使DFA在所经历的任何状态上中止它的工作。当DFA到达某一状态时,我们把从初态I0出发,到达该状态所经过的全部有向边上的标记符号依次连接起来,就得到了DFA在到达该状态时,所识别出的某规范句型的一个活前缀。例如:当上述DFA处于初态I0时,它所识别的活前缀为ε;当M处于状态I3时,它所识别的活前缀为B;当M处于I4时,它所识别的活前缀为aa*;而达到I9时,它所识别的活前缀为aa*B等等。需要注意的是,对那些只含有归约项目的项目集,即M的I1,I2,I3,I5,I6,I8和I10,当M到达这些状态时,表明活前缀中已含有相应句柄的全部符号 (即句柄已在栈顶形成),因此,我们将这些状态称为句柄识别状态。特别是当M处于状态I1时,M所识别的活前缀为S,这意味着已将所给的输入串归约为S,如果再按产生式S′→S归约一步,就得到了拓广文法G′的开始符号S′。因此,我们将状态I1称为“接受状态”,它预示着对输入串的分析已成功地完成。
对于一个给定文法G的拓广文法G′,当识别它的全部活前缀的DFA作出之后,我们就可据此构造相应的LR(0)分析表了。然而,应当首先注意的是,用前述方法所构造的每一个LR(0)项目集,实质上表征了在分析过程中可能出现的一种分析状态;再根据前面对LR(0)项目的分类,项目集中的每一个项目又与某一种分析动作相关联,因此,我们自然会提出这样的要求,即每一项目集中的诸项目应当是相容的。所谓相容,是指在一个项目集中,不出现这样的情况:
(1) 移进项目和归约项目并存;
(2) 多个归约项目并存。
如果一个文法G满足上述条件,也就是它的每个LR(0)项目集中都不含冲突项目,则称G为LR(0)文法。显然,只有当一个文法是LR(0)文法时,才能构造出不含冲突动作的LR(0)分析表来。
从前面的讨论和分析,我们将不难得出构造LR(0)分析表的算法。为方便起见,我们用整数0,1,2,…表示状态I0,I1,…,而且如表411那样,也将GOTO子表中有关终结符号的各列并入ACTION子表相应的各列中去,此外,算法中形如sj和rj等记号的含义同前,此算法如下:
(1) 对于每个项目集Ii中形如A→α·Xβ的项目,若GO(Ii,X)=Ij,且X为一终结符号a时,置ACTION[i,a]=sj。但若X为非终结符号时,则仅置GOTO[i,X]=j。
(2) 若归约项目A→α·属于Ii,设A→α为文法的第j个产生式,则对文法的任何终结符号或句子的右界符# (将它们统一地记为a),置ACTION[i,a]=rj。
(3) 若接受项目S′→S·属于Ii,则置ACTION[i,#]=acc。
(4) 在分析表中,凡不能按上述规则填入信息的元素,均置为“出错”。
C. LR分析法的LALR(1)分析表的构造
上述每个LR(1)项目均由两部分组成: 第一部分是一个LR(0)项目,称为LR(1)项目的核;第二部分则是一个向前搜索符号集。对于移进项目而言,搜索符号对分析表的构造无影响;但对归约项目而言,则仅在当前输入符号属于该搜索符号集时,才能用相应的产生式进行归约。LR(1)分析表的这种机理,较圆满地解决了SLR(1)分析所难以解决的某些“移进归约”或“归约归约”冲突,从而使LR(1)的分析能力比SLR(1)分析有明显的提高。然而,LR(1)分析的主要缺点在于,对同一个文法而言,LR(1)分析表的规模将远远大于相应的SLR(1)或LR(0)分析表。例如,为一个C语言构造LR(0)分析表,一般大约设置300个状态即可,而构造LR(1)分析表则需上千个状态,即后者将导致时间和内存空间开销的急剧上升。因此,就有必要寻求一种其分析表的规模与SLR(1)相当,但其分析能力又不比LR(1)相差太大的LR分析方法,这就是下面我们要介绍的LALR(1)分析技术。
下面,我们首先对造成LR(1)项目集族规模大幅度上升的原因进行分析,然后再设法从中找出构造高效LR分析表 (即LALR(1)分析表)的方法。为此,试看下面的例子。
再考察文法G[E]:
0?S→E4?T→F
1?E→E+T5?F→(E)
2?E→T6?F→ID
3?T→T*F
利用上面所给算法,为G[E]构造的LR(1)项目集族和识别活前缀的DFA如图420(a),(b)所示 (请注意,由于图幅较大,这里将其划分为(a),(b)两部分)。对比这两幅图我们立即就会发现,除其中的状态0和状态3之外,对于(a)中的每一状态 (LR(1)项目集),在(b)中都有一个状态 (LR(1)项目集)与其相似。例如,比较状态7和16:在这两个项目集中,除搜索符号集不同外,各个LR(1)项目的核都彼此相同 (即产生式相同,且产生式中圆点的位置也相同),我们把具有这种特点的两个LR(1)项目集称为同心集。所以,在图420(a)和(b)中,7/16,5/12,10/17,4/13,8/18,2/14,11/19,6/20,1/15和9/21都是同心集。显然,在LR(0)分析器中,每个“心”仅对应一个LR(0)项目集;但在LR(1)分析器中,由于向前搜索符号的不同,同一个“心”将会派生出多个同心集。这就是对同一文法而言,LR(1)项目集族远大于LR(0)项目集规范族的原因。
7E→E+·T[]#+T→·T*F
T→·F
F→·(E)
F→·ID〖〗#+*
#+*
#+*
#+*[][]16E→E+·T[]+)T→·T*F
T→·F
F→·(E)
F→·ID〖〗+)*
+)*
+)*
+)*
为解决上述问题,F?DeRemer提出了LALR(1)分析法。这种方法的基本思想是将LR(1)项目集族中的同心项目集加以合并,以削减项目集的个数。所谓合并同心集,实际上也就是将其中的每个LR(1)项目的向前搜索符集对应地合并在一起。例如,对于文法G[E]的同心项目集4和13,设合并后的新项目集为4/13,则有
4E→T·
T→T·*F〖〗#+
#+*[][]13E→T·
T→T·*F〖〗+)
+)*[][]4/13E→T·
T→T·*F〖〗#+)
#+)*
由于同心集的合并,对原来识别活前缀的DFA也须作相应的修改。
对于LALR(1)项目集族,我们须着重指出如下几点:
(1) 合并同心集也就是将同心集中每个LR(1)项目的两个组成部分 (核及向前搜索符号集)分别、对应地合并在一起。设I1,I2,…,Im为同心项目集,J是合并之后的新的项目集,显然J与Ii同心;再设X∈V∪{#},则GO(I1,X),GO(I2,X),…,GO(Im,X)也必然同心,若把这m个同心项目集合并后的新项目集记为K,则有GOTO(J,X)=K。可见前面定义的GOTO函数在这里仍然适用。
(2) 尽管原来各LR(1)项目集均不存在冲突,但合并同心集后就有可能出现冲突。换言之,即LR(1)文法未必总是LALR(1)文法。不过,由此引入的冲突只能是“归约归约”冲突,而决不会是“移进归约”冲突。事实上,设原LR(1)项目集族中有如下两个项目集
Ik:
[A→α·,W1]
[B→β·aγ,b]Ij:
[A→α·,W2]
[B→β·aγ,c]
并设Ik与Ij均无冲突,故有
W1∩{a}=?W2∩{a}=?
从而
(W1∪W2)∩{a}=?
现将Ik与Ij合并,有
Ik/j:
[A→α·,W1∪W2]
[B→β·aγ,{b}∪{c}]
若此时Ik/j有“移进归约”冲突,则必有
(W1∪W2)∩{a}≠?
这就与Ik与Ij无冲突的假设相矛盾。因此,合并同心集不会引入新的“移进归约”冲突。
(3) 对同一个语法上正确的输入符号串而言,不论用LALR(1)分析表还是用LR(1)分析表进行分析,所经历的移进、归约序列总是相同的 (仅状态名可能不同)。然而,当输入符号串有错时,LALR分析器可能会比LR(1)分析器多进行几步归约才能报错,但决不会比LR分析器多移进输入符号。也就是说,LALR分析器虽然可能延迟了发现出错的时间,但对错误的准确定位不产生影响。
(4) LALR(1)项目集族总是与同一文法的SLR(1)项目集族有同样个数的项目集。但是构造LALR项目集族的开销比SLR大。实现LALR分析对文法的要求比LR(1)严、比SLR(1)宽,但开销远小于LR(1)。权衡利弊的结果,LALR堪称为当前实现自底向上语法分析,特别是构造分析器自动生成工具的最为适用的技术。
综上所述,可给出构造LALR(1)分析表的算法如下。
1? 对已给的拓广文法G′,构造相应的LR(1)项目集族C={I0,I1,…,In}。
2? 对于C,将各LR(1)项目集按同心关系进行分组,并将同组的同心集加以合并,设所得的新项目集族为C′={J0,J1,…,Jm},其中含有项目[S′→·S,#]的项目集对应于初态。
3? 若C′中的项目集含有冲突项目,则G′不是LALR(1)文法。否则,可按如下法则构造LALR(1)分析表:
(1) 用构造LR(1)分析表类似的方法构造ACTION表;
(2) 对于某个X∈VN,若有GO(Jk,X)=Jt,则置GOTO(k,X)=t。
上述通过构造LR(1)项目集族和合并同心集来构造LALR分析表的方式仅有理论意义而无实用价值。因为构造完整的LR(1)项目集族的时间和空间开销都很大,故应首先设法予以解决。
迄今已有多种高效构造LALR分析表的算法,其共同的特点都是不从直接构造完整的LR(1)项目集入手,而是通过构造LR(0)项目集并添加相应的向前搜索符号来形成LALR(1)项目集 (请注意,对同一个文法而言,LALR(1)项目集与同心的LR(0)项目集一一对应)。例如,OCCS/YACC构造LALR(1)项目集所采用的策略是,每当创建一新的项目集时,就检查目前是否已存在与之同心的项目集,若有这样的项目集,则只需将向前搜索符号加入其中,而不再建立新的项目集。一种更为有效的方法甚至无需构造完整的LALR(1)项目集,而仅通过各个项目集中的“核心项目”便能构造相应的LALR(1)分析表。这里所说的核心项目是指形如[S′→·S,#]的项目 (其中,S′→S是拓广文法的第1个产生式),或者是形如[A→α·Xβ,a]的项目 (其中,α≠ε,即圆点不出现在产生式右部的最左位置),亦即那些用于构造本项目集闭包的“基本项目”。例如,对于文法G[E],各项目集的核心项目如图422所示。
下面,我们对利用项目集的核心项目构造LALR分析表的原理进行说明。 构造ACTION表的关键在于确定“归约”和“移进”两种动作。
(1) 归约动作的确定
由核心项目的定义可知,任何归约项目都必然会出现在某个项目集的核心项目之中,现设项目集I的核心为K,若[A→α·,a]∈K (其中α≠ε,搜索符号如何配置下面再介绍),我们立即可以确定: 在当前状态下所面临的输入符号为a时,应按产生式A→α进行归约,即有
ACTION[I,a]=rj
若α=ε,则当且仅当
[B→γ·Cδ, b]∈KC?*[]rAη
且a∈FIRST(ηδb)时,才能确定面临输入符号a时用产生式A→ε进行归约。由于对任何C∈VN,满足C?*[]rAη的所有非终结符号A预先能完全确定,故项目集I所引发的归约动作,仅由其核心K即能完全确定。
(2) 移进动作的确定
若
[A→α·Xβ,b]∈KX?*[]raη(a∈VT)
且上述推导的最后一步未使用ε产生式,则可确定: 状态I面临输入符号a时的动作为“移进”。其中,终结符号a可通过预先计算FIRST(X)加以确定。 对于任何项目[B→γ·Xδ,b]∈K,相应的项目[B→γX·δ,b]显然必属于某个项目集J=GO(I,X)的核心L。另外,若
[B→γ·Cδ,b]∈KC?*[]rAη
且A→Xβ是文法中的一个产生式,则对于任何
a∈FIRST(ηδb)[A→X·β,a]∈L
由于对每一对非终结符号(C,A),是否存在关系C?*[]rAη,可采用类似于计算FIRST集的方法预先求出,故仅从I的核心同样可构造出GOTO表。 上面的讨论,是在假定每个核心项目都已配置了搜索符号的情况下进行的。现在,再回头讨论: 如何为每个LR(0)项目集的核心项目配置搜索符号,使之成为LALR项目集的核心项目。为此,我们首先考察搜索符号从项目集I传播到项目集GO(I,X)的规律。
再设项目集I的核心为K,若有
[B→γ·Cδ,b]∈KC?*[]rAη
且A→Xβ是文法中的一个产生式,则根据上面的讨论有
[A→X·β,a]∈La∈FIRST(ηδb)
其中L是项目集J的核心,且J=GO(I,X)。现分如下两种情况讨论搜索符号a和b间的关系。
(1) 当ηδ?*ε时,显然也有[A→X·β,b]∈L。此时,我们就说项目[A→X·β,b]中的搜索符号b是从项目[B→γ·Cδ,b]中传递过来的 (propagate)。
(2) 当ηδ不能推导出ε时,a仅取决于η或δ,而与b无关,此时我们就说搜索符号a是自生的 (spotaneous)。
无论a是传递的还是自生的,它总能根据项目[B→γ·Cδ,b]中的有关信息,通过上述计算获得,这便是搜索符号从项目集I传播到项目集J的规律。
其次,在同一项目集中,核心项目中的搜索符号向非核心项目传播的规律与上述规律极为相似。事实上,设[B→γ·Cδ,b]∈K,而C→α是文法中的一个产生式,则[C→·α,c]是I的一个非核心项目。其中,搜索符c∈FIRST(δb),且按如下方法确定: 若δ不能推出ε,则c是自生的;否则,c=b,即c是从上面的项目传递下来的。
类似地,也可讨论搜索符号在非核心项目间的传播规律。例如,对于文法G[E],从核心项目[S→·E,#]开始,向前搜索符号在I0中的传递和自生的情况如图423所示。
设K是LR(0)项目集I的核心,X是某个文法符号,则对GO(I,X)的核心中的每一项目A→αX·β,通过程序47描述的操作 (请注意,这里使用了一个虚拟搜索符号lookahead),可由I中的项目确定其全部自生的搜索符号,并能确定K中的哪些项目将其搜索符号传递给GO(I,X)中的项目A→αX·β。
程序47确定自生搜索符号和传递搜索符号的项目
for (K中的每个项目B→γ·δ)
{
J′=CLOSURE ([B→γ·δ,lookahead]);
/*计算GO函数之值 */
for (J′中的每一项目[A→α·Xβ,a])
{
if(a!=lookahead)
确定GO(I,X)核心项目[A→αX·β,a]
之搜索符号a是自生的
if(a==lookahead)
确定GO(I,X)核心项目[A→αX·β,a]之搜索符号a是从K中项目
B→γ·δ传递过来的;
}
}
最后,我们再考虑如何给每个LR(0)项目集的核心中的各个项目都配置一个搜索符号集,以获得各个LALR(1)项目集的核心。完成此项任务的大致过程如下。
(1) 为拓广文法G′构造全部LR(0)项目集的核心。
(2) 首先从初始项目集I0惟一的核心项目S′→·S (其搜索符号显然为#)开始,对每个LR(0)项目集的核心和每个文法符号X,利用上面的算法,确定GO(I,X)各核心项目的自生搜索符号集,并确定从I的哪些项目将搜索符号传递到GO(I,X)的核心项目。
(3) 按某种便于操作的结构,建立一张核心项目表,此项目表记录了每个项目集的各个核心项目及其相应的搜索符号集。开始时,这些搜索符号集仅是由第(2)步所确定的自生搜索符号集 (若该核心项目无自生向前搜索符号则为空)。
(4) 传递每个核心项目中的自生搜索符号,直到无法再进行传递为止。即反复扫视各项目集的每个核心项目,每当访问一个核心项目i时,便根据第(2)步所获的信息,将i当前要传递的搜索符号添加到承接它的那个核心项目之中,直至没有新的搜索符号要传递为止。
对一个给定的文法G而言,当它的各个LALR(1)项目集的核心构造出来之后,就能根据上面所描述的原理,为G构造相应的LALR(1)分析表。不过,尽管上述构造LALR分析表的方法效率较高,但对于常见的程序设计语言,企图用手工的方式来建立LALR分析表仍几乎是不可能的。所幸的是,目前已有一些自动生成LALR分析表的工具可资使用(如YACC)。
还应当指出,在构造LR语法分析器时,尚有若干技术问题需予以考虑,如二义性文法的处理,避免按单产生式的归约,等等。前者我们将在第5章介绍语法分析器自动生成工具时再进行讨论;至于后者,由于需涉及一些语义处理及其信息传递的细节,故就不再讨论了。
在结束本章时,我们还要给出如下的结论,这些结论的证明读者可参阅有关的文献(1,2,8,15)。
(1) 任何LR(K),LL(K)及简单优先文法类都是无二义性的;对于算符优先文法,如果不考虑归约所得非终结符号的名字,也可认为是无二义性的。
(2) 任何二义性的文法都不可能是LR(1)(或LL(1))文法,但可借助于其它因素,如算符的优先级和结合规则以及某些语义解释等等,来构造无冲突的分析表。
(3) 每个SLR(K)文法都是LR(K)文法,但却存在这样的LR(1)文法,它对任何K而言均不是SLR(K)文法。
D. 编译原理中,LR(0)文法的项目集规范族的I0,I1,I2,I3…………是怎么求的~
先举个例子:
}
将其命名为I1。
其他可类似推出。
E. 编译器笔记13-语法分析-LR分析法概述
可以用LR分析法分析的文法可以称为LR分析法。LR文法( Knuth ,1963)是最大的、可以构造出相应移入- 归约语法分析器的文法类。
LR(k)分析,需要向前查看k个输入符号的LR分析,k=0 和 k=1 这两种情况具有实践意义,当省略(k)时,表示k=1。而在LR(k)这样的名称中,k代表的是分析时所需前瞻符号(lookahead symbol)的数量,也就是除了当前处理到的输入符号之外,还得再向右引用几个符号之意;省略 (k)时即视为LR(1),而非LR(0)。
作为对比这里列出LL(1)文法的含义:
问:自底向上分析的关键问题是什么?
答:如何正确地识别句柄,句柄是逐步形成的,用“状态”表示句柄识别的进展程度。例如在 自底向上分析概述 中所提及到句柄识别错误的例子,通过状态跟下一个输入符号就可以判断出应该做出哪一个动作,而状态相当于一种记忆功能记录当前句柄识别到什么程度。
与移入分析器不同的是LR分析器多了一个与符号栈平行的状态栈。
之后的分析过程与上图类似,直至到如下状态,分析成功。可见分析时进行什么动作是由栈状态栈栈顶的状态和下一个输入符号决定。
输入:串w和LR语法分析表,该表描述了文法G的ACTION函数和GOTO函数。
输出:如果w在L(G)中,则输出w的自底向上语法分析过程中的归约步骤;否则给出一个错误指示。
方法:初始时,语法分析器栈中的内容为初始状态s0 ,输入缓冲区中的内容为w$。然后,语法分析器执行下面的程序:
先了解LR(0)项目和增广文法这两个概念
右部某位置标有圆点的产生式称为相应文法的一个LR(0)项目(简称为项目):A → α1·α2
文法开始符号S表示的是语言中的最大成分。如下图当b出现时可以将它移入到分析栈中。b移进栈后我们期待归约出B。当归约出B时我们还期待再归约一个B。
如果G是一个以S为开始符号的文法,则G的增广文法G'就是在G中加上新开始符号S'和产生式S'→S而得到的文法
引入这个新的开始产生式的目的是使得文法开始符号仅出现在一个产生式的左边,从而使得分析器只有一个接受状态。
项目可以分为以下几类:
上图中S'对应的第一个项目称为初始项目,而S'对应的最后一个项目称之为接收项目在此状态下文法的开始符号已经被归约出来,因此可以接收了故称为接收项目。红色方框中的项目则被称为归约项目。
项目集闭包(Closure of Item Sets)
可以把等价的项目组成一个项目集(I),称为项目集闭包,每个项目集闭包对应着自动机的一个状态。
先了解CLOSURE和GOTO这两个函数
项目集I的闭包的数学定义:
返回项目集I对应于文法符号X的后继项目集闭包
规范LR(0)项集族(Canonical LR(0) Collection)
说明: 该自动机的初始状态就是文法的初始项目的项目集闭包,其终止状态集合只有一个状态就是文法的接收项目的项目集闭包。
如果LR(0)分析表中没有语法分析动作冲突,那么给定的文法就称为LR(0)。不是所有CFG都能用LR(0)方法进行分析,也就是说,CFG不总是LR(0)文法。
为了解决移进/归约冲突和归约/归约冲突需要使用到 SLR分析法 和 LR(1)分析法 。
问: 为什么没有移进/移进冲突?
答: 首先只有在移进状态和待约状态下的项目才会有使用到移进操作。在0状态时所有项目都是移进状态根据LL文法显然不会产生移进/移进操作,因为每个产生式左部的SELECT集是没有交集的。而在其他具有待约状态项目的状态中,所有集合都是等价的。假若在某状态下输入终结符y时发生移进/移进冲突,即存在两个这样的项目A0→α0·yβ0,A1→α1·yβ1,但显然这两个项目是不等价的显然与同一状态下所有项目等价相矛盾,因此这种移进/移进冲突是不存在的。假若在某状态下输入非终结符X时发生移进/移进冲突,即存在两个这样的项目A0→α0·Xβ0,A1→α1·Xβ1,而A0与A1在同一状态下是等价的则两项目要么是A0→α0·Xβ0与X→.Xβ1(原项目A1变为X,α1变为ε)要么是A1→α1·Xβ1与X→.Xβ0(原项目A0变为X,α0变为ε)。显然X→Xβ0|Xβ1(左递归)是不符合LL文法的因此这种情况也是不可能出现。
综上移进/移进冲突在LR分析下是不存在的。
F. 编译原理笔记17:自下而上语法分析(4)LR(0)、SLR(1) 分析表的构造
(移进项目就是纳凯态指圆点右边是终结符的项目,规约项目指的就是圆点在右部最右端的项目)
LR(0) 文法可以直接通过识别活前缀的 DFA 来构造 LR 分析表
假定 C = {I 0 , I 1 , ... , I n } (aka. LR(0) 项目规范族、DFA 状态集)
首先为文法产生式进行编号,拓广文法的产生式要标记为 0(这里就是后面分析表中 rj 的产生式编号 j 的由来)
然后令每个项目集 I k 的下标 k 作为分析器洞源的状态(行首),包含 S' → .S 的集合下标为分析器的初态(也就是 DFA 的初态孙型,一般都是 0 )。
下面用一个例子来说明 ACTION、GOTO 子表的构造:
SLR(1) 为解决冲突提出了一个简单的方法:通过识别活前缀的 DFA 和【简单向前看一个终结符】构造 SLR(1) 分析表。
如果我们的识别活前缀的 DFA 中存在移进-规约冲突、规约-规约冲突,都可以尝试使用这个方法来解决冲突。(这里说【尝试】,当然是因为 SLR 也只能解决一部分问题,并不是万能的灵丹妙药。。)
这里,我们拿前面那个 LR(0) 解决不了的文法来举例
该文法不是 LR(0) 文法,但是是 SLR(1) 文法。
观察上图 DFA 中的状态2,想象当我们的自动机正处于这个状态:次栈顶已经规约为 T 了,栈顶也是当前的状态 2 ,而当前剩余输入为 *。
如果这个自动机不会【往前多看一步】的话,那么对处于这个状态的自动机来说,看起来状态 2 中的移进项目和规约项目都是可选的。这就是移进-规约冲突。
想要解决这个冲突,就轮到【往前多看一步】上场了——把当前剩余输入考虑进来,辅助进行项目的选择:
对其他的冲突也使用同样的方法进行判断。
这种冲突性动作的解决办法叫做 SLR(1) 解决办法
准备工作部分,与 LR(0) 分析表的构造差不多:同样使用每个项目集的状态编号作为分析器的状态编号,也就同样用作行下标;同样使用拓广文法产生式作为 0 号产生式。
填表也和 LR(0) 类似,唯一的不同体现在对规约项的处理方法上:如果当前状态有项目 A → α.aβ 和 A → α. ,而次栈顶此时是 α 且读写头读到的是 a,那么当且仅当 a∈FOLLOW(A) 时,我们才会用 A → α 对 α 进行规约。
如果构造出来的表的每个入口都不含多重定义(也就是如上图中表格那样的,每个格子里面最多只有一个动作),那么该表就是该文法的 SLR(1) 表,这个文法就是 SLR(1) 文法。使用 SLR(1) 表的分析器叫做一个 SLR(1) 分析器。
任意的二义文法都不能构造出 SLR(1) 分析表
例:悬空 else
例:
这里的 L 可以理解为左值,R 可以理解为右值
经过计算可以确定其 DFA 如下图所示。
在 状态4 中,由于 "=" 同时存在于 FOLLOW(L) 与 FOLLOW(R) 中,因此该状态内存在移进-规约冲突,故该文法不是 SLR(1) 文法。
这样的非二义文法可以通过增加向前看终结符的个数来解决冲突(比如LL(2)、LR(2))但这会让问题更加复杂,故一般不采用。而二义文法无论向前看多少个终结符都无法解决二义性。