編譯原理(四)語法分析之自頂向下分析


語法分析之自頂向下分析

說明:以老師PPT為標准,借鑒部分教材內容,AlvinZH學習筆記。

基本過程分析

1. 一般方法:對任一字符串,試圖用一切可能的方法,從樹根節點(開始符號)出發,根據文法自上而下地為輸入符號串建立一棵語法樹。直觀理解為從開始符號出發,依據規則建立推導序列,最后推至目標字符串。

2. 特點:分析過程是帶有預測的,是一種試探過程。試探失敗就會出現回溯問題,降低了分析的效率。

3. 存在問題:左遞歸問題、回溯問題。

問題一:左遞歸問題

1. 左遞歸文法:文法規則中有形如 \(U::=U···\) 或 $ U=+>U···$。

為什么自頂向下分析不能處理左遞歸?
答:試想,若遇到非終結符U時,嘗試用該規則右部 "U···" 去匹配字符串,下一步又要匹配U,又要嘗試用用該規則右部 "U···" 右部去匹配,無限循環無終止。

2. 消除左遞歸

(1)直接消除左遞歸:令擴充的BNF表示法改寫文法規則。例如對於規則 \(E∷= E + T | T\),改寫為 \(E∷= T { + T }\)。(提醒一下,這里是正則文法表達,不是正則表達式,重復別寫成了(+T)*)。

如何改寫?方法如下:

  • 規則一:提(前綴)公因子。對於 \(U∷= x y | x w |….| x z\),改寫為U∷= x ( y | w |….| z )。
  • 規則二:對於 \(U∷= x | y |…| z | U v\),改寫為 $ ( x | y |…| z ) { v }$。具有一個直接左遞歸的右部,可直接改寫為重復。

注意:

①對於規則 \(U::=x|xy\), 應該改寫成 \(U ::= x (y | ε)\),而不是 \(U ::= x (ε | y)\),因為這個神奇的 \(ε\) 總是作為最后的選擇,約定其可以與任何字符相匹配(還沒懂?如果放前面,那它可以和U匹配,豈不還是左遞歸?)。

②作用:不僅有助於消除直接左遞歸,而且有助於壓縮文法的長度,使我們更加有效地分析句子。

通過以上兩條規則,就能消除文法的直接左遞歸,並且保證文法的等價性。

(2)將左遞歸改寫成右遞歸

  • 規則三:對於 \(P∷= P α | β\),可改寫為 \(P ::= βP' , P' ::= αP' | ε\)

(3)教材中提及一個算法,可以消除一般的左遞歸(不存在產生式形如 \(A::=A\)\(A::= ε\)),看偽代碼理解起來還真有點難,不過理解一下跟着的拿到例題,其實不過是對於不存在直接左遞歸的產生式直接代入其他產生式化簡罷了,有一點點像把正則文法轉換成正則表達式的感覺(依然得記得擴展BNF與正則式的區別)。

【例題】消除規則 \(E∷= E + T | T\) 的左遞歸。

解:方法一:利用規則二,直接改寫成 \(E ::= T {+T}\)

方法二:利用規則三,改為右遞歸,\(E ::=ET' , E' ::= +TE' | ε\)

問題二:回溯問題

1. 原因:某個非終結符號的規則其右部有多個選擇,並根據所面臨的輸入符號不能准確地確定所要的產生式,就可能出現回溯。回溯的后果是效率極低且代價極高,具體一點就是寫不出代碼。效率低的原因是語法分析要重做,精確定位,嘗試的語法處理工作需要推倒重來。

2. 如何避免回溯?大家注意了,第一個重要的集合就要出現了,它就是——FIRST()集合

FIRST集合定義:在不具有左遞歸的文法G中,U∈Vn,有規則形如 U::= α1 | α2 | ... | αn,則有 FIRST(αi) = {a | αi =*> a... , a∈Vt}。

如何理解這個FIRST集合呢?中文名叫做首字符集,意思是如果選擇從 \(α_i\) 出發,最后可以推到很多終結符號串(個人認為寫成句子好理解一些),取得這些符號串的第一個字符組成FIRST集。對於之后LL分析法中非終結符的FIRST集合也是這樣理解的,不過在這里我們求得是每個右部選擇的FIRST集。

3. 好了,問題來了那怎么用上這個FIRST集呢?結合定義以及回溯出現的原因,為何避免回溯,必須保證每一步的選擇是對的(如果之后匹配不上,可直接判定不是文法的句子二不需要猶豫是不是當初選擇錯了),因此文法需保證:

對於文法中的任意一個非終結符,若其規則右部存在多個選擇,各個選擇推出的終結符號串的頭符號集兩兩不相交。(必要條件)

4. 新的問題,萬一文法不符合上述條件,我非要用自頂向下怎么辦?不是不可以,兩種方法如下:

(1)對文法再次進行改寫加工。注意文法的先行條件:不具有左遞歸。在此基礎上,只需要對不滿足條件的產生式的多個規則右部反復提取左因子,最后總是可以做到符合上述條件的。

舉個栗子,\(U ::= xV | xW\),明顯FIRST(xV)與FIRST(xW)相交,改寫成 \(U ::= x(V |W)\)就可以了(當然非終結符 V 和 W 也要重點關注一下)。

(2)超前掃描:對於不滿足上述條件的文法,采用超前掃描的方法,即向前偵察各輸入符號串的第二個、第三個符號來確定要選擇的目標。

本質上來講也有回溯的味道,因此比第一種方法費時,但是讀的僅僅是向前偵察情況,不作任何語義處理工作。

問題總結

在不采取超前掃描的前提下實現不帶回溯的自頂向下分析,對文法需要滿足兩個條件:

  • 文法是非左遞歸的;
  • 對文法的任一非終結符,若其規則右部有多個選擇時,各選擇所推出的終結符號串的首符號集合要兩兩不相交。

注意:書中也提到了滿足這兩個條件有可能構造,意味着上述兩個條件是必要條件,不是充要條件。為什么?個人認為在LL分析法中再次提到了這個問題,與另一個重要的集合 FOLLOW集 有關,但是在那里說的是判斷LL(1)文法的充要條件,不知道有沒有什么區別。

遞歸子程序法(遞歸下降分析法)

1. 方法:對文法的每一個非終結符都編一個分析子程序,完成對該非終結符語法成分的分析與識別任務。當根據文法和當時的輸入符號預測到要用某個非終結符去匹配輸入串時,就調用該非終結符的分析程序。

為何名為遞歸下降?由於這是自頂向下分析方法,且子程序的調用過程中允許(直接或間接的)遞歸調用。

2. 要求:消除左遞歸。允許存在右遞歸和自嵌入式遞歸。

3. 遞歸子程序法對應的是最左推導過程。方法比較簡單,UI大作業中也要求用到了,這里不再多說了。

LL(1)分析法

1. 為何名為LL(1)?

  • LL:自左向右掃描、自左向右地分析和匹配輸入串。分析過程表現為最左推導的性質。
  • (1):分析過程中,美進行一步推導,只要向前查看一個輸入符號,便能確定當前應選擇的產生式。

2. LL分析程序構造及分析過程

(1)LL(1)分析器組成:分析表、執行程序 (總控程序)、符號棧 (分析棧)。

  • 分析表是一個二維矩陣M,M[A,a] = A::=αi 或者 error,表示非終結符A遇到當前字符為a時,應該使用哪一個右部去匹配,或者出錯。
  • 符號棧:開始狀態、工作狀態、出錯狀態、結束狀態。
  • 執行程序:算法暫不描述。

注意: 對於M[A,a] = A::=αi,此時應該將A退出符號棧,並將 αi 逆序壓入棧中(為了符合最左推導的要求)。

(2)舉個例子,算了,直接說如何構造分析表吧!

3. LL(1)分析表的構造

(1)\(α\) 的頭符號集 FIRST(α)。假定 \(α\) 是文法G的任意符號串,即 \(α∈V^*\),則有 \(FIRST(α) = {a | α =*> a···, a∈V_t}\)。特別的,若有 \(α =*> ε\),則規定 \(ε ∈ FIRST(α)\)

如何理解FIRST(α)?簡單來說,FIRST(α)就是由 \(α\) 可能推導出的所有符號串的第一個終結符號或可能的 \(ε\) 組成的集合。

(2)\(A\) 的后繼符號集 FOLLOW(A)。對於文法G的任何非終結符A,開始符號為S,定義:\(FOLLOW(A) = {a | S =*> ···Aa···, a∈V_t}\)。特別的,若 \(S =*> ···A\),則 #∈FOLLOW(S)。

如何理解FOLLOW(A)?單詞英文意思就是跟隨,簡單來說,FOLLOW(A)是所有句型中緊跟在A之后的非終結符或#組成的集合。

(3)構造FIRST集合

算法說明:對於文法G中的每一個字符X∈V,計算FIRST(X),反復應用如下規則,直至FIRST集不再增大。

  • \(X∈V_t\),則 \(FIRST(X) = {X}\)
  • \(X∈V_n\),且存在 \(X→aα\) 產生式(a∈Vt),則 \(a∈FIRST(X)\)。若有 \(X→ε\),則 \(ε ∈ FIRST(X)\)
  • 若存在 \(X→Y1Y2...Yk\) 產生式,若 \(Y1∈V_n\),則FIRST(Y1)中所有非 \(ε\) 符號加進FIRST(X)。對於一切 \(2≤ i ≤k\),若 \(Y1\)\(Y_2\)、...、\(Y_{i-1}\) 均為非終結符,且 \(ε ∈ FIRST(Y_{1~i-1})\),則FIRST(Yi)中的非 \(ε\) 符號加進FIRST(X)。特別地,若對於所有 \(Y_{1~k}\),都有 \(ε ∈ Y_{1~k}\),則 $ε ∈ FIRST(X)。

理解與應用:

  • 一般來說,求解FIRST集合是從最后一個產生式開始往上推。當然,最后還得正序檢查一遍,防止遞歸造成不完整。
  • 算法第三點看起來很長,其實很好理解,記住FIRST(X)集合的定義——頭符號集,盡可能找到所有可能出現在X推出符號串的非終結符或 \(ε\) 集合。

(4)構造FOLLOW集合

算法說明:對於文法G中每一個非終結符A,計算FOLLOW(A),反復應用如下規則,直至FOLLOw集不再增大。

  • 若A為開始符號S,則#∈FOLLOW(A)。
  • \(A ::= αBβ\),且\(β ≠ ε\),則將FIRST(β)中一切非 \(ε\) 符號加進FOLLOW(A)。
  • \(A ::= αBβ\),若有 \(ε ∈ FIRST(β)\),則FOLLOW(A)中一切符號加進FOLLOW(B)。

理解與應用:

  • 注意到 \(ε\) 是不會出現在FOLLOW集合中的。
  • 還是一句話,記住定義。FOLLOW(A)表示A的后繼符號集,意思是A后面可能會出現的所有終結符或#集合。

(5)構建LL(1)分析表

在求出了每一個產生式的個候選式的FIRST集以及每一個非終結符的FOLLOW集之后,文法G的LL(1)分析表構造算法如下:對於文法G的每一個產生式A→α

  • 對FIRST(α)中的每個終結符a,置 \(M[A,a] = A→α\)
  • \(ε ∈ FIRST(α)\),則對FOLLOW(A)中的每一個符號b,置 \(M[A,b] = A→ε\)
  • M中所有不滿足上述兩條的位置置為空,即error。

(6)實例分析

例:已知文法G[E]:①\(E∷= T E’\);②\(E’∷= + T E’ | ε\);③\(T∷= F T’\);④\(T’∷= * F T’ | ε\);⑤\(F∷= ( E ) | i\)。構造LL(1)分析表。

解:(1)求FIRST集:(從下往上)

FIRST((E)) = { ( } ,FIRST( i ) = { i };
FIRST(*FT’) = { * },FIRST( ε ) = { ε };
FIRST(FT’) = { ( , i };
FIRST(+TE’) = { + };
FIRST(TE’) = { ( , i };

(2)求FOLLOW集:(從上往下)

FOLLOW(E) = { # , ) };
FOLLOW(E’) = { # , ) };
FOLLOW(T) = { + , ) , # };
FOLLOW(T’) = FOLLOW(T) = { + , ) , # };
FOLLOW(F) = { * , + , ) , # };

(3)構造分析表

(4)分析句子

(只分析了一半,看懂了就行)

4. LL(1)文法

(1)若文法G構造出的LL(1)分析表M不含有多重定義元素,稱之為LL(1)文法。LL(1)文法是無二義的。由某LL(1)文法產生的語言稱為LL(1)語言。

可以證明,如果G是左遞歸或二義的,那么M至少含有一個多重定義入口。

(2)充要條件:一個文法G是LL(1)文法,當且僅當對於G的每一個非終結符A的任意兩條不同規則 \(A ::= α | β\),下了條件成立:

  • FIRST(α) ∩ FIRST(β) = Ф;
  • \(β =*> ε\),則FIRST(α) ∩ FOLLOW(A) = Ф。

(3)重要結論

  • 任何LL(1)文法都是無二義性的。
  • 若文法中含有左遞歸規則,則必然是非LL(1)文法。
  • 存在一種算法,它能判斷一個文法是否是LL(1)文法。(利用充要條件)
  • 存在一種算法,它能判定兩個LL(1)文法是否產生相同語言(即等價)。(比較LL(1)分析表)
  • 不存在算法,它能判定任意的上下文無關語言是否由LL(1)文法產生。
  • 非LL(1)語言是存在的。
  • 有些文法可以從非LL(1)文法改寫為LL(1)文法,但並不是所有的非LL(1)文法都能改寫為LL(1)文法。

證明一下?同學你這是要我的命,快去看看自底向上分析吧!

引用說明

- 邵老師課堂PDF
- 《編譯原理級編譯程序構造》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM