【編譯原理】語法分析(三)
常用的語法分析方法包括自頂向下和自底向上的方法,在上一篇文章中已經介紹了自頂向下的語法分析方法,本文將介紹自底向上的語法分析方法。
文法&約定
按照慣例,我們給出一個貫穿全文的表示式文法G:
E→E+T|T
T→T*F|F
F→(E)|id
以及使用的符號約定:
- 大寫字母:表示非終結符號,如A、B、C等;
- 小寫字母:表示終結符號,如a、b、c等;
- 希臘字母:表示由終結符號和非終結符號組成的串或者空串,如α、β、γ、ω等;
- 開始符號:用S表示文法的開始符號;
- 結束符號:用$表示結束標記,如輸入結束、棧為空等;
- 空串:用ε表示長度為0的串,即空串。
自底向上的語法分析
顧名思義,自底向上的語法分析過程對應於為一個輸入串構造語法分析樹的過程,它從葉子結點開始逐漸向上到達根結點。和自頂向下的語法分析過程相反,自底向上的語法分析過程把幾個葉子結點或中間結點歸約成一箇中間結點,每次歸約是一次最右推導的逆過程,當完成一次自底向上的語法分析後,對相應的輸入串可以得到一個反向的最右推導。
控制程式碼
對文法G和輸入串id*id
,我們使用自底向上的方法構造它的語法分析樹:
如果從最右邊的語法分析樹開始,到最左邊的語法分析樹為止,把每棵語法分析樹的根結點用符號連線起來,那麼就可以得到串id*id
的一個最右推導:
E
T
T*F
T*id
F*id
id*id
也就是說,對串id*id
的自底向上的語法分析過程就是從id*id
開始,經過一次次歸約,最終得到E
的過程。其中,每次歸約是某個最右推導的逆過程。
自底向上的語法分析的關鍵問題是確定每次對哪一個串進行歸約,我們把這個串叫作控制程式碼。正式地,如果S…αAγαβγ,那麼產生式A→β是αβγ的一個控制程式碼,可以把A→β化簡為β,即說β是αβγ的一個控制程式碼。
對上面得到的串id*id
的最右推導,因為有F*id
id*id
,所以第一個id是id*id
的一個控制程式碼;又因為有T*id
F*id
,所以F是F*id
的一個控制程式碼;依此類推。
這樣的話,對一個輸入串ω,假設它的一個最右推導為Sα1α2…αnω,如果我們能知道所有αi(1<=i<=n)和ω的一個控制程式碼,就能使用自底向上的方法構造ω的語法分析樹。
注意,對某個串可能有不止一個控制程式碼,例如二義性文法。
移入-歸約語法分析技術
移入-歸約語法分析是一種通用的自底向上的語法分析技術。它使用一個棧來儲存文法符號,並用一個輸入緩衝區存放剩餘的輸入符號,使用這種方法時,控制程式碼在被識別之前都出現在棧頂。
一個移入-歸約語法分析器可以執行四種動作:
- 移入:將下一個輸入符號壓入棧頂;
- 歸約:被歸約的符號串的右端必然是棧頂,語法分析器在棧中確定這個串的左端,並決定用哪個非終結符號來替換這個串;
- 接受:宣佈語法分析過程成功完成;
- 報錯:發現一個語法錯誤,並呼叫一個錯誤恢復子例程。
對輸入串id*id
,它的一個移入-歸約語法分析過程如下:
在上圖中,棧的頂部在右邊,並且每次控制程式碼出現時都在棧頂。這裡我們並沒有介紹什麼時候執行移入,什麼時候執行歸約,即如何識別一個控制程式碼。下一節內容將介紹一種發現控制程式碼的方法。
另外,還需要注意的是,在移入-歸約語法分析過程中,可能會產生衝突,包括移入/歸約衝突和歸約/歸約衝突。移入/歸約衝突是指在移入-歸約語法分析的某一步中,既可以執行移入動作,也可以執行歸約動作,從而發生的衝突。歸約/歸約衝突是指在移入-歸約語法分析的某一步中,棧頂的控制程式碼可以選擇歸約成兩個或多個產生式頭,從而發生的衝突。
簡單LR技術:SLR
目前最流行的自底向上的語法分析器都基於所謂的LR(k)語法分析的概念。其中,“L”表示對輸入進行從左到右的掃描,“R”表示反向構造一個最右推導序列,“k”表示在做出語法分析決定時向前看k個輸入符號。當省略(k)時,假設k=1。
本節將介紹簡單LR技術(簡稱為SLR),它是一種最簡單的構造移入-歸約語法分析器的方法。SLR依賴於一張語法分析表,該語法分析表包括ACTION和GOTO集合,它們是根據LR(0)自動機得到的,一個LR(0)自動機由一個狀態集合和一個轉移函式組成。
規範LR(0)項和LR(0)自動機
一個LR語法分析器通過維護一些狀態,用這些狀態來表明在語法分析過程中所處的位置,從而做出移入-歸約決定。
一個文法的一個LR(0)項(簡稱為項)是該文法的一個產生式加上一個位於它的體中某處的點。舉個例子,對產生式A→αβγ來說,它有四個項,分別為A→·αβγ、A→α·βγ、A→αβ·γ和A→αβγ·。項A→·αβγ表明我們希望在接下來的輸入中看到一個可以從αβγ推導得到的串;項A→α·βγ表明我們剛剛在輸入中看到了一個可以從α推導得到的串,並且我們希望在接下來的輸入中看到一個可以從βγ推導得到的串;項A→αβγ·表明我們已經在輸入中看到了一個可以從αβγ推導得到的串,並且是時候把這個串歸約為A了。
一個或多個項可以組成一個項集,而一組項集提供了構建一個DFA的基礎,這個DFA可用於做出語法分析決定,這樣的DFA稱為LR(0)自動機。
LR(0)自動機的每個狀態代表一個項集。為了確定LR(0)自動機的每個狀態代表的項集中包含哪些項,我們需要用到兩個函式CLOSURE和GOTO,這兩個函式的作用有點類似於DFA的ε-closure和move函式。
對文法的一個項集I,CLOSURE(I)的構造規則如下:
- 把I中的所有項加入CLOSURE(I)中;
- 如果A→α·Bβ在CLOSURE(I)中,B→γ是一個產生式,並且B→·γ不在不在CLOSURE(I)中,就把項B→·γ加入CLOSURE(I)中。不斷應用這個規則,直到沒有新項可以加入CLOSURE(I)中為止。
對文法G,計算項集{ E→·E+T
}的CLOSURE集合的過程如下:
- 把項
E→·E+T
加入CLOSURE集合中; - 由於
E→T
是一個產生式,且E→·T
不在CLOSURE集合中,因此把它加入CLOSURE集合中; - 由於
T→T*F|F
是一個產生式,且T→·T*F
和T→·F
都不在CLOSURE集合中,因此把它們加入CLOSURE集合中; - 由於
F→(E)|id
是一個產生式,且F→·(E)
和F→·id
都不在CLOSURE集合中,因此把它們加入CLOSURE集合中。到此,已經沒有新的項可以加入CLOSURE集合中,最終的CLOSURE集合為:
E→·E+T
E→·T
T→·T*F
T→·F
F→·(E)
F→·id
對文法的一個項集I和一個文法符號X,GOTO(I, X)的構造規則如下:
- 如果A→α·Xβ在I中,就把項A→αX·β加入GOTO(I, X)中;
- 把GOTO(I, X)作為CLOSURE函式的引數,計算GOTO(I, X)的閉包。
對上面得到的項集{ E→·E+T
}的CLOSURE集合和符號“(”,計算它的GOTO集合的過程如下:
- 把項
F→(·E)
加入GOTO集合中; - 由於
E→E+T|T
是一個產生式,且E→·E+T
和E→·T
都不在GOTO集合中,因此把它們加入GOTO集合中; - 由於
T→T*F|F
是一個產生式,且T→·T*F
和T→·F
都不在GOTO集合中,因此把它們加入GOTO集合中; - 由於
F→(E)|id
是一個產生式,且F→·(E)
和F→·id
都不在GOTO集合中,因此把它們加入GOTO集合中。到此,已經沒有新的項可以加入GOTO集合中,最終的GOTO集合為:
F→(·E)
E→·E+T
E→·T
T→·T*F
T→·F
F→·(E)
F→·id
到此,我們已經知道如何確定LR(0)自動機的每個狀態(CLOSURE函式)和轉移函式(GOTO函式)。另外,為了規範一個文法的LR(0)自動機,我們把該文法表示為一個增廣文法,即在該文法中加入新的開始符號S’和產生式S’→S得到的文法。引入這個新的開始符號和產生式的目的是告訴語法分析器何時應該停止語法分析並宣稱接受輸入符號串。也就是說,當且僅當語法分析器使用S’→S進行歸約時,輸入符號串被接受。
文法G的增廣文法的LR(0)自動機如下:
LR(0)自動機是如何幫助做出移入-歸約決定的呢?假設串γ使LR(0)自動機從開始狀態0執行到某個狀態j,如果下一個輸入符號為a且狀態j有一個在a上的轉換,就移入a,否則進行歸約操作,狀態j的項將告訴我們使用哪個產生式進行歸約。
對文法G和輸入串id*id,使用上面給出的文法G的LR(0)自動機對它進行移入-歸約語法分析的過程如下:
實際上,在一個LR語法分析器中,LR(0)自動機會被轉換成一張LR語法分析表,在下一小節中,我們繼續介紹如何從LR(0)自動機構建LR語法分析表。
LR語法分析表
一個LR語法分析器的語法分析表由一個語法分析動作函式ACTION和一個轉換函式GOTO組成:
- ACTION函式:ACTION函式有兩個引數,一個是狀態i,一個是終結符號(包括輸入結束標記$)a,ACTION[i, a]有四種取值:
- 移入。如果狀態i上有一個轉移a到達狀態j,那麼ACTION[i, a]=移入j;
- 歸約。如果狀態i上沒有一個轉移a,那麼ACTION[i, a]=按照狀態i上的產生式進行歸約;
- 接受。語法分析器接受輸入並完成語法分析過程;
- 報錯。語法分析器在它的輸入中發現一個錯誤並執行某個糾正動作。
- GOTO函式:實質上和項集的GOTO函式一樣,只不過把項集替換成了狀態。即:如果對項集的GOTO函式,有GOTO[Ii, A]=Ij,那麼對LR語法分析表的GOTO函式,有GOTO[i, A]=j。
根據一個LR(0)自動機,我們可以立馬得出LR語法分析表的GOTO函式,但是,對ACTION函式,應用下面的規則計算:
- 在LR(0)自動機中,如果項A→α·aβ在項集Ii中並且GOTO[Ii, a]=Ij,那麼將ACTION[i, a]設為“移入j”;
- 在LR(0)自動機中,如果項A→α·在項集Ii中,那麼對於FOLLOW(A)中的所有a,將ACTION[i, a]設為“按照A→α歸約”,這裡A不等於S’;
- 在LR(0)自動機中,如果項S’→S·在項集Ii中,那麼將ACTION[i, $]設為“接受”。
除此之外,將所有空白的ACTION和GOTO設為“報錯”。
PS:如果不清楚如何計算FOLLOW函式,可以瀏覽上一篇文章【編譯原理】語法分析(二)。
現在嘗試對文法G構建LR語法分析表,為了方便,我們對文法G中的每個產生式進行編號:
(1) E→E+T (2) E→T
(3) T→T*F (4) T→F
(5) F→(E) (6) F→id
並約定ACTION函式中的每個符號的意義:
- si表示移入並將狀態i壓棧;
- rj表示按照序號為j的產生式進行歸約;
- acc表示接受;
- 空白表示報錯。
得到的LR語法分析表如下:
PS:對每個終結符號的ACTION函式,如果ACTION函式取值為移入,那麼其實質就是GOTO函式。
為了說明LR語法分析表的使用方法,這裡舉一個例子:維護一個狀態棧,初始時狀態0位於棧頂,如果下一個輸入符號是“id”,則將狀態5壓棧;位於狀態5時,如果下一個輸入符號是“*”,則使用產生式F→id
進行歸約,把id替換成F並將狀態5從棧頂彈出,此時位於狀態0(狀態0在棧頂),由於狀態0經過符號F的轉移到達狀態3,因此將狀態3壓棧;依次類推。對LR語法分析表完整系統的使用方法將在下一小節介紹。
LR語法分析演算法
一個LR語法分析器由一個輸入緩衝區、一個狀態棧、一個語法分析表和一個結果輸出組成,如下圖所示:
語法分析表在上一小節中已經介紹過了,這裡重點說一下狀態棧。狀態棧維護一個狀態序列s0s1…sn,其中sn位於棧頂,每個狀態si對應於LR(0)狀態機中的某個狀態,並且除了初始狀態之外,每個狀態都有一個唯一的相關聯的文法符號。也就是說,在LR(0)狀態機中,如果從Ii經過符號α轉移到Ij,那麼狀態j的關聯符號為α。
用狀態棧和剩餘的輸入符號串可以完整的表示語法分析器在某一刻的狀態,這個狀態本質上是反向最右推導中的某個句型。我們用(s0s1…sm, a1a2…an$)表示語法分析器的狀態,並稱其為語法分析器的格局。其中,第一個分量是狀態棧中的狀態序列(sm是棧頂),第二個分量是剩餘的輸入符號串,如果把第一個分量中的每個狀態替換為其關聯的文法符號,再與第二個分量連線,就能得到反向最右推導中的一個句型。
假定LR語法分析器當前的格局為(s0s1…sm, aiai+1…an$),在根據當前格局決定下一個動作時,首先讀入下一個輸入的符號ai和棧頂的狀態sm,然後查詢LR語法分析表中的條目ACTION[sm, ai],執行相應的動作:
- 如果ACTION[sm, ai]=移入s,那麼將狀態s壓入棧頂,格局變為(s0s1…sms, ai+1ai+2…an$);
- 如果ACTION[sm, ai]=按照A→β歸約,那麼將r(r是β的長度)個狀態從棧頂彈出,並將狀態s(s=GOTO[sm-r, A])壓入棧頂,格局變為(s0s1…sm-rs, aiai+1…an$)。注意,在執行歸約動作時,當前的輸入符號不會改變;
- 如果ACTION[sm, ai]=接受,那麼語法分析過程完成;
- 如果ACTION[sm, ai]=報錯,那麼語法分析器發現了一個語法錯誤,並呼叫一個錯誤恢復例程。
綜上所述,一個LR語法分析器和LL語法分析器一樣,也是表驅動的。兩個LR語法分析器的唯一不同之處在於它們的語法分析表不同。
現在對文法G和輸入符號串id*id+id,相應的LR語法分析表已經在上面得到了,分析其LR語法分析過程:
其中,狀態棧的棧頂在右側,符號是棧中每個狀態關聯的文法符號(初始狀態0沒有相關聯的文法符號),並且,從最後一行開始到第一行,把每行的符號和輸入連線起來得到文法G的一個句型,把這些句型用符號連線起來(除去重複的句型),就得到一個最右推導。
歡迎關注微信公眾號fightingZh٩(๑❛ᴗ❛๑)۶
相關文章
- 編譯原理之語法分析-自下而上分析(三)編譯原理語法分析
- 編譯原理之語法分析-自下而上分析(四)編譯原理語法分析
- Go編譯原理系列4(語法分析)Go編譯原理語法分析
- 編譯原理實驗2:語法分析編譯原理語法分析
- 【編譯原理】手工打造語法分析器編譯原理語法分析
- Go編譯原理系列2(詞法分析&語法分析基礎)Go編譯原理詞法分析語法分析
- 【編譯原理複習Part_2】語法分析編譯原理語法分析
- 編譯原理上機作業2——LL(1)語法分析編譯原理語法分析
- 跟vczh看例項學編譯原理——三:Tinymoe與無歧義語法分析編譯原理語法分析
- 小C語言--詞法分析程式(編譯原理實驗一)C語言詞法分析編譯原理
- Go編譯原理系列3(詞法分析)Go編譯原理詞法分析
- 編譯原理實驗1:詞法分析編譯原理詞法分析
- 0920編譯原理詞法分析編譯原理詞法分析
- 【編譯原理】手工打造詞法分析器編譯原理詞法分析
- Go編譯原理系列5(抽象語法樹構建)Go編譯原理抽象語法樹
- 用Java寫編譯器(1)- 詞法和語法分析Java編譯語法分析
- Java 實現《編譯原理》簡單詞法分析功能Java編譯原理詞法分析
- 精讀《手寫 SQL 編譯器 - 語法分析》SQL編譯語法分析
- JAVA語法糖和語法糖編譯Java編譯
- 編譯原理上機作業1——詞法分析器編譯原理詞法分析
- 深入分析 Javac 編譯原理Java編譯原理
- 【水汐の編譯原理】 詞法分析器 課題1編譯原理詞法分析
- 大話css預編譯處理(三):基礎語法篇CSS編譯
- Android反編譯:smali語法Android編譯
- 深入淺出JVM(六)之前端編譯過程與語法糖原理JVM前端編譯
- 程式的編譯和連結原理分析編譯
- 編譯原理編譯原理
- 編譯器有關的Makefile語法編譯
- 關於Basic程式直譯器及編譯原理的簡單化(1)---Basic器的語法分析及主要程式碼 (轉)C程式編譯原理語法分析
- JavaScript的工作原理:解析、抽象語法樹(AST)+ 提升編譯速度5個技巧JavaScript抽象語法樹AST編譯
- 現代編譯原理C語言描述pdf編譯原理C語言
- 跟vczh看例項學編譯原理——二:實現Tinymoe的詞法分析編譯原理詞法分析
- 《編譯原理》LR 分析法與構造 LR(1) 分析表的步驟 - 例題解析編譯原理
- C語言編譯器開發之旅(一):詞法分析掃描器C語言編譯詞法分析
- Flutter 編譯原理Flutter編譯原理
- 編譯原理概述編譯原理
- Swift語法對編譯速度的影響Swift編譯
- Android-NDK-11-C語言編譯原理AndroidC語言編譯原理