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))但這會讓問題更加復雜,故一般不採用。而二義文法無論向前看多少個終結符都無法解決二義性。