語法分析的作用是處理詞法分析得到的記號流建立語法樹(又稱分析樹), 並且建立符號表處理語法錯誤。
本文約定大寫英文字母A、B、C等表示非終結符;小寫英文字母a、b、c等表示終結符;小寫希臘字母α、β、δ等表示任意記號序列
上下文無關文法
上下文無關文法(Context Free Grammar,CFG)可以表示大多數程序設計語言的語法,又足夠簡單讓我們實現相應的分析器。
文法由四元組定義:
-
非終結符(Nonterminals)集合
-
終結符(Terminals)集合
-
開始符號(Start Symbol)
-
產生式(productions)
所謂產生式是替換規則, 在進行推導時產生式左側的記號序列可以由右側記號替換。如產生式E=>E+E中表示表達式可以表示為兩個表達式相加。
產生式的推導具有自反性和傳遞性。若產生式右側為空串則稱為空產生式。
所謂上下文無關文法是指所有產生式左側僅有一個非終結符, 上下文有關文法中存在形如aAb=>α的產生式。
上下文無關文法允許我們任意情況下將左側記號替換為右側序列,讓我們更容易構建分析器。BNF(巴克斯-諾爾范式)經常用來表達上下文無關文法。
定義算術表達式中的產生式:
-
E=>(E)
-
E => E+E
-
E => -E
展示一下算術表達式-(a+b)的推導過程:
E => -E => -(E) => -(E+E) => -(E+b) => -(a+b)
或者以分析樹的形式表示:

自底向上看分析樹我們對原表達式采用了從右到左的分析順序,這種順序稱為最右推導或正規推導。對應地, 有最左推導的定義。
我們嘗試對算術表達式a*b+c按照不同優先級不同推導方式不同分別建立語法樹:
-
最左推導乘法優先:
E => E+E => E*E+E => a*E+E => a*b+E => a*b+c -
最左推導加法優先:
E => E*E => E*(E+E) => a*(E+E) => a*(b+E) => a*(b+c) -
最右推導乘法優先:
E => E+E => E*E+E => E*E+c => E*b+C => a*b+c -
最右推導加法優先:
E => E*E => E*(E+E) => E*(E+c) => E*(b+c) => a*(b+c)
可以看出推導的結果僅與文法和句子有關與推導方法無關。自底向上看語法樹, 高優先級的生成式會被先處理。
若某一文法對一個句子有多棵分析樹則稱文法是二義的。避免二義性需要規定產生式的優先級和結合性或者修改文法。
作為表達能力更強的上下文無關文法一定可以表示正規式, 但是正規式不一定可以表示上下文無關文法。
將正規式轉換為上下文無關文法的流程如下:
-
構造正規式的NFA;
-
若0為初態,則A0為開始符號;
-
對於move(i,a)=j,引入產生式Ai => aAj;
-
對於move(i,ε)=j,引入產生式 A i=> Aj;
-
若i是終態,則引入產生式Ai => ε。
示例, 從正規式r=(a|b)*abb的NFA構造CFG:

A0 => aA0|bA0|aA1
A1 => bA2
A2 => bA3
A3 => ε

自頂向下語法分析
語法分析是對輸入序列進行最左推導,嘗試建立它的語法樹,最終得到一條合法句子或發現錯誤的過程。
自上而下分析是一個試探回溯的過程, 嘗試一切可能建立與輸入序列匹配的語法樹。
消除左遞歸
若存在形如A=>Aα|β的生成式時我們稱文法存在直接左遞歸, 直接左遞歸會使最左推導語法分析陷入死循環中必須加以消除。
若A=>Aα|β中A不是β的前綴(即β不以A開頭)則可以將產生式用兩個產生式替換:
A => βA'
A' => αA' | ε
這樣原來的左遞歸被替換為右遞歸, 而右遞歸不會導致最左推導陷入死循環。
文法E => E+T | T 存在直接左遞歸, 根據代換規則替換為兩個產生式:
E => TE'
E'=> +TE' | ε
用原文法分析a+b: E => E+T => a+T => a+b, 當然我們根據經驗避免了左遞歸。
使用消除左遞歸后的文法分析a+b: E => TE' => T+TE' => T+T => a+T => a+b
有些文法不含直接左遞歸,但通過推導可以得到左遞歸產生式:
- S => Qc | c
- Q => Rb | b
- R => Sa | a
經過推導可以得到左遞歸產生式: S => Qc => Rbc => Sabc。
消除間接左遞歸的基本思想是將間接左遞歸轉換為直接左遞歸, 再將直接左遞歸轉換為右遞歸。
首先產生式將產生式重新排列為可以迭代的順序: A[i] => A[i+1]α | β; A[i+1] => A[i+2]α | β; ...。上文中的示例已經滿足該條件不必調整。
將3.代入2.中得到4.: Q => (Sa | a)b | b => Sab | ab | b
將4.代入1.中得到5.: S => (Sab | ab | b)c | c => Sabc | abc | bc | c
消除5.中的直接左遞歸得到6.: S => abcS’| bcS'| cS'; S' => abcS' | ε
從開始符號S出發不會經過QR, 可以將1,2,3, 4刪去僅保留5,6作為新文法。
遞歸下降分析
遞歸下降分析法是指為記號流中特定非終結符編寫一個子程序, 當發現該終結符時即調用該子程序進行分析。
我們規定*優先從左向右處理記號流, 嘗試分析1*1+1*1。
自左向右掃描記號流, 發現相應運算符即交由子程序處理。注意因為*優先,則+更靠近分析樹根部, 所以應優先調用+的子程序, 其次為*子程序, 最后對的1處理完成后返回。
調用棧即代表語法樹:

LL文法分析
LL分析器是一種處理上下文無關文法的自頂向下的語法分析器, 它從左到右處理輸入,再對句型執行最左推導出語法樹故稱為LL分析器。
LL分析器是一種預測分析器, 與普通遞歸下降分析器不同,預測分析器會向后探查來決定當前語法樹結構從而避免回溯。
若一個LL分析器向前探查k個記號(token), 那我們稱該分析器為LL(k)分析器, 其中最常用是設計簡單且可以處理大多數情況的LL(1)分析器。
給定上下文無關文法:
(1) S => E
(2) S => (S + E)
(3) E => i
試圖對((i+i)+i)進行最左推導:
S => (S+E) => ((S+E)+E) => ((E+E)+E) => ((i+E)+E) => ((i+i)+E) => ((i+i)+i)
從狀態2(S+E)推導狀態3時我們有兩個選擇S=>(S+E)或者S=>E。在示例中選擇任何一種推導都無關緊要, 但在某些情況下做出錯誤選擇后不得不進行回溯。
LL分析法可以通過探查后面的記號來做出決定, 理論上說對任何上下文無關語言總存在k,使得LL(k)無法識別的記號流可以被LL(k+1)識別。
LL分析器依賴分析表來進行決策, 分析表根據后面的記號和當前狀態決定下一步狀態轉移:
| - | + | ( | ) | i |
|---|---|---|---|---|
| S | - | 2 | - | 1 |
| E | - | - | - | 3 |
LL分析器維護一個記號棧, 用於保存待處理的記號. LL分析器首先在棧中壓入語法樹的根元素, 然后根據記號流中的下一個記號對棧頂元素進行推導, 當棧頂元素無需繼續處理時彈出棧頂。當棧為空時語法分析結束。
我們嘗試使用LL(1)分析器對(i+i)進行分析:
-
記號棧首先壓入語法樹的根元素
S, 記號棧為:[S] -
從記號流讀入
(,讀取棧頂S, 根據分析表應用規則(2)將S改寫為(S+E), 記號棧為:[(,S,+,E,)] -
從記號流讀入
i, 棧頂(無需推導彈出, 處理新棧頂S: 根據分析表應用規則(1)將S改寫為E,記號棧為:
[E,+,E,)] -
處理棧頂
E, 應用規則(3)將記號棧改寫為:[i, +, E, ) ] -
根據讀入的
i和+依次丟棄棧頂i和+。 -
根據棧頂
E和讀入的i, 改寫記號棧:[i,)] -
讀入
), 彈出棧頂分析完畢。
自底向上語法分析
實際應用中語法分析器更多地從記號流開始建立分析樹, 而非從根元素開始建立語法樹。加上自底向上分析法更強大, 大多數實用編譯器均采用自底向上分析法。
LR分析器是典型的自底向上分析法, 它從左向右處理記號流並嘗試進行最右推導。
給定上下文無關文法:
(1) S => E
(2) S => S + E
(3) E => i
LR分析器可以執行幾種特定動作:
-
r: reduce應用語法規則化簡
-
p: push將下一個記號入棧
-
a: accept分析完成, 記號流被文法接受
-
e: error發現語法錯誤
若記號流分析完畢沒有發現錯誤,則認為記號流被文法接受。
LR分析器同樣需要一個記號棧和一個分析表。分析表根據當前狀態和下一個記號決定動作, 為了便於描述, 我們根據記號棧來描述狀態:
| - | i | + |
|---|---|---|
| [] | r(3),r(1) | p |
| [ S ] | e | p |
| [S, +] | r(3) | e |
| [S, +, E] | r(2) | e |
嘗試對i+i進行LR分析
-
讀入
i壓棧,依次逆用(3), (1)此時記號棧為:[S] -
讀入
+壓棧, 此時記號棧為:[S, +] -
讀入
i壓棧, 逆用(3)此時記號棧為:[S,+, E] -
逆用(2), 此時記號棧為
[S]
至此分析結束, 得到語法樹:
S => S + E => S + i => E + i => i + i
