基于LLVM的編譯原理簡明教程 (2) - 詞法與語法分析初步

遞歸 - 詞法分析與語法分析的分界

一般來說,決定詞法分析和語法分析的界限是是否需要遞歸。
詞法分析是將輸入的符號流轉換成一個個獨立的token。比如說,996是個數值型或者更精確一些整型的token。
這個token解析的過程,它前面是什么符號,后面是什么符號,完全沒有關系。
token也不存在遞歸的可能性,token之間相互獨立,不可能是嵌套的關系。
所以,詞法分析可以用正則表達式來實現。只要一個串符合[0-9]+,我們就可以確定地認為,這是一個整數。
詞法分析可以從左到右,完全線性的方式實現。它不需要樹的結構,自然沒有遞歸的需求。

而語法分析就有不同,比如一個表達式,它可能是"7 * 24",也可能是"(8+1) * 5",或者更復雜的組合。這樣的表達式就需要用一棵樹的結構來表示。
比如我們這樣定義表達式:

表達式 = 數字
表達式 = 表達式 + 表達式
表達式 = 表達式 - 表達式

這樣,表達式"1+2+3+4"就可以表示成下圖這樣的一棵樹:

statement

自頂向下和自底向上

針對上面的圖,我們有兩種分析的辦法,一種是自頂向下,一種是自底向上。

為了大家看起來方便,我們不妨把上面的式子改成前綴表達式:

表達式 = 數字
表達式 = + 表達式 表達式
表達式 = - 表達式 表達式

自頂向下就是,先掃描到一個"+"的token,然后分別對它后面的兩個token進行分析。第一個token是數字,不需要遞歸了。然后去看第二個表達式,發現還是一個"+",于是遞歸去分析+,以此類推。

總結起來,自頂向下的思想是,先預測是個什么,然后按照期待去找下一個token。

而自底向上的思想不是這樣,它是從左到右把token都讀進來,然后去嘗試找目前已經讀進來的token們符合哪個式子的定義。
以上面的例子為例:
第一步,讀到"+" 不符合上面三個式子的任何一個,繼續向右讀token。這個過程叫做shift,中文譯成“移進”。
第二步,"+ 1",數字1匹配了第一條,變成+ 表達式,不能繼續匹配了,繼續讀token
第三步,"+ 表達式 +",什么鬼,不匹配,繼續讀。
...
第五步,讀到"+ 表達式 + 表達式 +",還是找不到可以匹配的式子,繼續向右讀。
...
第七步,讀到"+ 表達式 + 表達式 + 表達式 4",4匹配了第一條,變成表達式, "+ 表達式 表達式"匹配了第二條,也變成表達式。這種操作叫做“歸約”-reduce。這一步歸約了"+ 3 4"
第八步,歸約"+2 表達式"
第九步,歸約"+1 表達式"

LR分析器

自底向上的方法的重要方法是LR方法,LR分析器的構造一般如下圖所示:

LR分析

LR分析器是以一個狀態機的方向來運作的。
這其中有兩個重要的表:

  • 一個是主要處理移進的Action表
  • 另一個是主要處理歸約的Goto表

Action表的輸入有兩項:

  • 一是當前的狀態,從狀態棧頂可以取到;
  • 另一個是輸入符號,可以從輸入串中取得。

Action表的輸出有4種情況:

  • 移進:這時候輸出一個移進后的新狀態。輸入符號和新狀態壓入棧
  • 歸約:這時候輸出一個歸約的表達式。棧中的符號串,包括輸入符號和狀態,被替換成歸約后的新符號串。這時候的要變成的狀態就要去Goto表中去查詢。輸入為歸約后的新符號串,也就是產生式的左端,與這個符號串左邊的上一個狀態,查出來之后,就是最新的狀態
  • 接受:說明一次文法解析已經完成,可以輸出語法分析樹了
  • 出錯:走到了兩個表中查不到的狀態

我們看一個龍書上的例子,構造含有二元運算符+和*的算術表達式的文法:

1. E -> E + T
2. E -> T
3. T -> T * F
4. T -> F
5. F -> (E)
6. F -> id

我把龍書上用符號表示的表用文字標注上顏色,使大家更加容易記憶和理解。
對應的action表如下:

action table

goto表如下:

goto table

下面我們嘗試分析一下"id * id $".

  1. 初始狀態是0. 輸入為id. 我們查0行id列的action表,是將id移進棧,同時,狀態棧頂轉為5. 完成這一步后,棧中內容[0 id 5]
  2. 狀態5,輸入為。查action表5行列,是使用公式6(F->id)進行歸約。此時,狀態5和id輸入,都被從棧中歸約掉,變成F。這時的棧為[0 F],因為產生了歸約,所以要再去查goto表,根據目前棧中的值,去查0行F列,查到操作是轉到狀態3. 于是將3壓入棧中,現在棧中的值是[0 F 3]
  3. 狀態3下,還是遇到剛才的輸入“*”。查action表,要做的是使用公式4(T->F)來歸約。同樣,F和狀態3出棧,T入棧。現在棧中的內容是[0 T],又產生了歸約,于是再查goto表,0行T列是轉到狀態2. 現在棧中的值是[0 T 2]
  4. 狀態2下,剛才輸入的還在,繼續查表。action表的2行列是移進,下一狀態是7。終于把這個*移進去了。現在的棧中的內容是:[0 T 2 * 7]
  5. 狀態7下,遇到id。查action表,7行id列,移進,下一狀態是5. 現在棧中內容為:[0 T 2 * 7 id 5]
  6. 狀態5下,遇到結束符$。查action表,5行$列,歸約,使用公式6(F->id). id和狀態5出棧,F入棧。現在的棧是[0 T 2 * 7 F],再去goto表中查7行F列,狀態為10。這一步的最終棧是:[0 T 2 * 7 F 10]
  7. 狀態10下,輸入還是$。查action表,10行$列:使用公式3(T->T*F)歸約。請注意,除去狀態不計的話,[0 T 2 * 7 F 10]的值正是[T * F],于是將[T 2 * 7 F 10]全部出棧,將T入棧。歸約之后再查goto表,[0 T],查0行T列,狀態是2. 這一步最終棧結果:[0 T 2]
  8. 狀態2下,輸入還是$。查action表,歸約,使用公式2(E -> T). T和2出棧,E入棧。現在的棧為[0 E],再查goto表,0行E列,狀態為1。這一步最終結果是[0 E 1]
  9. 狀態1下,輸入仍然是$沒變。查action表,1行$列,接受,解析成功!

下面的問題就變成如何能夠構造action表和goto表。LR下面的不同方法,就是如何生成這兩張表的過程。

子集構造算法

子集構造算法是將不確定的有窮自動機NFA轉換成確定的有窮自動機的算法。

從不確定的有窮自動機轉換成確定的有窮自動機的基本思想是將確定有窮自動機的一個狀態對應于不確定有窮自動機的一個狀態集合。

子集構造算法

狀態集合初值為初始狀態的空閉包(ε-closure),且不作標記
while (狀態集合中還有未標記的狀態T){
    標記這個狀態T;
    for 每個輸入符號a in 輸入集合 {
        U = 空閉包(move(T,a));
        if(U不在狀態集合中){
            U添加到狀態集合中;
            U的狀態為未標記;
        }
        Dtran[T,a]=U;
    }
}

其中,構造子集算法使用到了求空閉包(ε-closure)的算法。

求ε-closure的算法用人話講就是,從起點或者起點的集合,計算出走ε路徑可以到達的所有狀態。我們可以把a,b這些值理解為大于0的權值,而ε為權值為0. 求ε閉包的算法就是求從指定起點的權值之和為0的所有路徑的集合。

求ε-closure空閉包的算法

將T中所有的狀態壓入棧中; //這是所有的起點的集合
空閉包集合初始化為T; //清空棧
while (棧不空){
    棧頂元素t彈出棧; //取一個起點出來
    for 狀態u in 從t到u有一條標記為ε的邊{ //起點和狀態之間有ε的邊
        if (u不在空閉包集合中){
            將u添加到空閉包集合中; //u是符合條件的值
            將u壓入棧中; //如果u下面還可以繼續傳導,后面還可以有ε的邊
        }
    }
}

我們看下面的龍書上的例子:

epsilon-closure

ε-closure(0)就是從0開始距離為ε的所有狀態,直接跟0相連的有1和7。1又可以通達2,4. 所以ε-closure(0)為{0,1,2,4,7}

下面我們開始應用到子集構造算法的例子中:
初始狀態0的空閉包集合為{0,1,2,4,7},我們用A來表示。
move({0,1,2,4,7},a) = {3,8}。這一步是從{0,1,2,4,7},指定輸入為a時可以到達的狀態,move(2,a)=3, move(7,a)=8,其他都不能到達。
ε-closure({3,8})= {1,2,3,4,6,7,8},用B來表示.
move({1,2,3,4,6,7,8},a)={3,8},跟B重復
于是a的情況完成了,我們再遍歷輸入為b的情況。
move({0,1,2,4,7},b)={5}
ε-closure({5})={1,2,4,5,6,7} = C
ε-closure(move(B,b))=ε-closure({5,9})={1,2,4,5,6,7,9} = D
ε-closure(move(C,a))=ε-closure({3,8}) = B
ε-closure(move(C,b))=ε-closure({5}) = C
ε-closure(move(D,a))=ε-closure({3,8}) = B
ε-closure(move(D,b))=ε-closure({5}) = C

最后生成的是這樣的狀態轉換圖:

state machine

SLR方法

拓廣文法

如果文法G的開始符號是S,那么文法G的拓廣文法G'是在G的基礎上增加一個新的開始符號S'和產生式S->S'。新產生式的目的是用做歸約的終點。

閉包運算

閉包是:

  • 初始項目集都是閉包的成員
  • 如果A->α.Bβ在閉包中,且存在產生式B->γ, 若B->.γ不在閉包中,則將其加入閉包。重復直至所有的產生式都加入到閉包中。

例:

E' -> E
E -> E+T | T
T -> T*F | F
F -> (E) | id

closure{[E' -> .E]}包含:

根據規則1,E' -> .E 本身在閉包里
根據規則2,
E -> .E+T
E -> .T
T -> .T*F
T -> .F
F -> .(E)
F -> .id

goto函數

我們終于開始看到如何生成goto函數了。
goto(I,X)函數的定義為A->αX.β的閉包。

例:若I是兩個項目的集合{[E'->.E],[E->E.+T]},則goto(I,+)包括:

E -> E + .T
T -> .T + F
T -> .F
F -> .(E)
F -> .id

項目集的構造算法

算法:

C = {closure([S'->.S])};
do{
    for 項目集I in C, 文法符號X in C {
        if(goto(I,X)!=nullptr && inC(goto(I,X))
        C.add(goto(I,X));
    }
}while(還有更多的項目可以加入C);

SLR語法分析表的構造

算法:

  1. 構造G'的LR(0)項目集規范族。采用上面介紹的項目集構造算法。C={I0,I1,I2,...}
  2. 從Ii構造狀態i,它的分析動作確定如下:
    2.1. 如果[A->α.aβ]在Ii中,并且 goto(Ii,a)=Ij,則置action[i,a]為"移進j",這里的a必須是終結符
    2.2. 如果[A->α.]在Ii中,則對FOLLOW(A)中的所有a,置action[i,a]="歸約A->α",這里的A不能是S'.
    2.3. 如果[S'->.S]在Ii中,則置action[i,$]為“接受”
  3. 對所有的非終結符A,使用下面的規則構造狀態i的goto函數:如果goto(Ii,A)=Ij,則goto[i,A]=j
  4. 不能由2和3構造出來的表項都置為出錯。

構造規范LR語法分析表

SLR對于某些情況是無法歸約的,我們可以通過重新定義項目,把更多的信息并入狀態中,變成[A->α.β,a], 其中A->α.β是產生式,a是終結符或$。
這樣的對象叫做LR(1)項目。

構造LALR語法分析表

LALR是(look-ahead-LR)的縮寫。它的優點是比LR(1)的分析表要小得多。

喬姆斯基的文法分類

我們先看個喬姆斯基文法分類的示例圖:


chmosky

類似于我們上面所講的生成式,可以對應到喬姆斯基的文法分類上。
關于終結符和非終結符,我們就不需要做嚴謹的數學定義了吧。像數字一樣不能推導出其他式子的,就是終結符。像表達式這樣可以繼續推導的就是非終結符。

喬姆斯基將文法分成4類:

  • 0型文法:這種文法只有一種要求,就是左邊的式子里有一個非終結符。直觀理解就是,總要有一個能推導的式子啊。
  • 1型文法:在0型的基礎上,要求右部的長度比左式長。這樣,推導的話可以越推越長,歸約的話可以越歸約越短。
  • 2型文法:在1型的基礎上,要求左部必須為非終結符,不能有終結符。
  • 3型文法:在2型的基礎上,左部只能有一個單獨的非終結符。而右部更有嚴格的限制,必須全部是終結符,或者終結符只能連接一個非連接符。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容