# 【Compiler】 Ch4. Syntax Analysis * 在學會了scanner如何實作之後,我們已經得到了一系列的tokens,這代表傳進來的字至少電腦都看得懂。 * 接下來我們要判斷這些tokens語法是否正確,字雖然分開看都看得懂,但合在一起就不一定了。 * 一般做語法解析有三種方式: * Universal parsing methods * 較複雜,這邊不會教。 * Top-down * Bottom-up * 這一章我們就要使用下面兩種方法來完成Syntax Analysis。 ## Context-Free Grammars * 在做語法分析的時候,我們習慣使用Context-Free Grammars來描述語言。 * 使用語法可以精準的對語法規範。 * 添加新的語法也很容易。 * 為什麼不使用和Lexical一樣使用Regular Languages? * Regular Languages雖然很好懂,但它能描述的語言範圍太小。 * 例如$\{^n\}^n$這個語言不在regular language的範圍內。 ### Prove a language is regular or not * 如果一個語言是regular,那就畫出它的NFA或是直接找到regular expressions,就可以證明它是regular。 * 如果不是的話可以使用Pumping Lemma證明。 --- * 如果A是 regular language,那麼任意在符合A的字串s = xyz, |s| $\geq$ n符合: * $xy^iz \in A, i \geq 0$ * |y| > 0 * |xy| $\leq$ n * 範例:證明$L = \{0^n1^n | n ≥ 0\}$ 不是regular。 * y有三種可能: 1. 只有`0` * y變2倍之後`0`和`1`數量就不同了,新的字串不符合L。 2. 只有`1` * 同上 3. 有`0`又有`1` * y變2倍之後,新字串變成`0` `1`交錯,不符合L。 ## Pushdown Automata * Pushdown Automata是一個較複雜的狀態機,除了之前Finite Automata的東西之外,它多了一個stack去儲存上一個輸入。 * 為什麼不使用一般的Finite Automata? * 因為我們的語法改為Context-Free Grammars,變得複雜了,因此狀態機也要變得複雜。 * 可以用NFA、DFA表達就表示有Regular Expressions、反過來說不能用Regular表示就沒有NFA、DFA。 * 數學表達法:$M=(K, \sum , \Gamma , \delta , q_0, Z_0, F)$ * K: 總狀態的集合 * $\sum$: 輸入字母集 * $\Gamma$: stack的字母集 * $\delta$: 狀態轉移關係的集合 * $\delta$(0, a, 0) = {1},表示在狀態0看到a且stack是0的時候往狀態1跑。 * $q_0$: 起始狀態(屬於K) * $Z_0$: 初始stack內的符號(屬於$\Gamma$) * F: 結束狀態的集合 --- 練習題 建立語言$L = \{0^n1^n|n\geq 0\}$ 的Pushdown Automata。 答案 M = ({q0 , q1}, {0, 1}, {0, 1}, δ, q0 , є, ψ) δ(q0 , 0, є) = (q0 , 0) δ(q0 , 0, 0) = (q0 , 00) δ(q0 , 1, 0) = (q1 , є) δ(q1 , 1, 0) = (q1 , є) ![](https://i.imgur.com/EIxTP4y.png) --- 練習題 建立語言$L = \{wcw^R | w ∈ \{0, 1\}^*\}$ 的Pushdown Automata。 (R表示反轉) 答案 M = ({q0, q1}, {0, 1, c}, {R, G, B}, δ, q0, R, ψ) δ(q0 , 0, R) = (q0 , BR) δ(q0 , 0, G) = (q0 , BG) δ(q0 , 0, B) = (q0 , BB) δ(q0 , 1, R) = (q0 , GR) δ(q0 , 1, G) = (q0 , GG) δ(q0 , 1, B) = (q0 , GB) δ(q0 , c, R) = (q1 , R) δ(q0 , c, G) = (q1 , G) δ(q0 , c, B) = (q1 , B) δ(q1 , 0, B) = (q1 , є) δ(q1 , 1, G) = (q1 , є) δ(q1 , є, R) = (q1 , є) ![](https://i.imgur.com/wugn1Py.png) ## Parse Trees * 一棵Parse Tree可以表示我們如何從起始符號一路derives成目標句子。 * 給定Parse Tree或是Derivations都可以得知該文法如何定義語言。 * 範例:E → E + E | E * E | (E) | −E | id * w = − (id + id) * Derivation: E ⇒ −E ⇒ − (E) ⇒ − (E + E ) ⇒ − (id + E ) ⇒ − (id + id) * Parse Tree: ![](https://i.imgur.com/AWYdgzl.png) ### Ambiguity * 句子的Parse Tree在左優先或右優先(leftmost or rightmost)的前提下各自只會有一棵唯一樹。 * 但是在不給定優先度的時候,同一個句子可能有很多棵Parse Tree,這種情形我們稱該文法為Ambiguity的文法。 * 範例:E → E + E | E * E | (E) | −E | id * w = id + id * id * 可以得到兩種過程: * E ⇒ E + E ⇒ id + E ⇒ id + E * E ⇒ id + id * E ⇒ id + id * id * E ⇒ E * E ⇒ E + E * E ⇒ id + E * E ⇒ id + id * E ⇒ id + id * id * 這種情形就是你的compiler不知道要先做加法還是乘法,有兩種解決方法: 1. 給定優先度 2. 重寫文法 ### Eliminating Ambiguity * 範例:if else匹配 * 原文法: stmt → if expr then stmt stmt → if expr then stmt else stmt stmt → other * 輸入:if E1 then if E2 then S1 else S2 * 擁有以下兩種Parse Tree,表示該文法為Ambiguity ![](https://i.imgur.com/BVu2MVG.png) - 改寫為新文法: stmt → matched_stmt    | unmatched_stmt matched_stmt → if expr then matched_stmt else matched_stmt    | other unmatched_stmt → if expr then stmt    | if expr then matched_stmt else unmatched_stmt ### Left Recursion * 另一個文法上的問題是Left Recursion。 * 試想有一文法$A \overset{+}{\Rightarrow} A \alpha$ * 例如:$A\rightarrow A\alpha | \beta$ * 進行Derivation時,我們通常是用DFS的概念,如果第一項不能推導出輸入,則我們再選擇第二項 * 假設現在輸入為$\beta \alpha \alpha$ * 我們從起始符號A開始推導 * $A\rightarrow A\alpha$ * $A\alpha \rightarrow A\alpha \alpha$ * $A\alpha \alpha \rightarrow A\alpha \alpha \alpha$ * ... * 我們知道要該輸入只要選擇$\beta$就能成功,但是因為無限迴圈造成compiler當機。 ### Eliminating Left Recursion * 要解決Left Recursion最簡單的方法就是改寫文法 * 原文法:A → Aα | β * 新文法: * A → β A’ * A’ → α A’ | є * 雖然導致文法看起來變得很不直覺,但可以表示完全一樣的語言且消除Left Recursion的問題。 --- 練習 改寫以下文法以消除Left Recursion E → E + T | T T → T * F | F F → (E ) | id 答案 E → TE’ E’ → +TE’ | є T → FT’ T’ → *FT’ | є F → (E ) | id --- * 有時候Left Recursion要經過多層的derives才會出現。 * 範例: * S → A a | b * A → A c | S d | є * 解決方法是將符號替換代入: * A → A c | A a d | bd | є * 再用一般的方法消除: * S → A a | b * A → bdA’ | A’ * A → cA’ | adA’ | є ## Top-Down Parsing * 前面講了那麼多,就是為了實作出Top-Down Parsing。 * Top-Down Parsing的概念前面其實已經提過了,就是找到一個leftmost derivation可以變成輸入字串。 * 但這樣的方法有個缺點,就是你的文法一旦變得複雜之後,每一種可能都要跑一遍,就可能要即大量的運算時間才能判斷輸入是否符合文法。 * 如果我們可以根據輸入直接判斷要選哪種derivation就可以大幅加快速度。 * 使用Predictive Parser。 ### Predictive Parser * 藉由先偷看下一個輸入來判斷現在應該要選哪個derivation。 * 我們需要先找出每個符號的開頭(FIRST)可能是那些字元。 * 範例: * type → simple | ^id | array [ simple ] of type * simple → integer | char | num dotdot num * 那麼FIRST(simple) = integer和char和num * FIRST(type) = FIRST(simple)和 ^ 和 array * 因此我們的Parser今天如果看到 ^ 就知道要選擇type → ^id去derive。 --- * 其實也可以直接透過文法建出狀態圖來得到Predictive Parser要解釋有點困難,直接看圖應該就很好理解。 * 範例: * E → TE’ * E’ → +TE’ | є * T → FT’ * T’ → *FT’ | є * F → (E ) | id ![](https://i.imgur.com/KfFcxev.png) * 每經過一條nontermial的邊的時候,就要先跳到它的圖,跑到結束狀態再回來。 * 有兩條路走的時候就根據輸入來決定路線。 * 假設輸入w = id + id * id,則過程為: 1. 0到1,跳至T的圖7 2. 7到8,跳至F的圖14 3. 輸入為id,選擇14到17,輸入剩+ id * id 4. 回到T的圖8,8到9,跳至T'的圖10 5. 輸入不是*,選擇10到13 6. 回到T的圖9,回到E的圖1,1到2,跳至E'的圖3 7. 輸入為+,選擇3到4,輸入剩id * id 8. 4到5,跳至T的圖7 9. 7到8,跳至F的圖14 10. 輸入為id,選擇14到17,輸入剩* id 11. 回到T的圖8,8到9,跳至T'的圖10 12. 輸入是*,選擇10到11,輸入剩id 13. 11到12,跳至F的圖14 14. 輸入是id,選擇14到17,輸入為空 15. 回到T'的圖12,12到13,跳到T'的圖10 16. 輸入不是*,選擇10到13 17. 回到T'的圖13,回到T的圖9,回到E'的圖5,5到6,跳到E'的圖3 18. 輸入不是+,選擇3到6 19. 回到E'的圖6,回到E的圖2 20. 確認輸入為空,代表該輸入匹配該語法 ### Nonrecursive Predictive Parser * 上面的Predictive Parser雖然看起來蠻容易完成的,但是可以看到步驟非常多(況且這還是很簡單的語法)。 * 我們必須避免掉parse過程中不停cursive檢查語法的問題,這時候就要使用Nonrecursive Predictive Parser。 * Nonrecursive Predictive Parser的概念很簡單,我們建立一個table和一個stack,我們只要根據現在stack頂端和輸入的字,就可以在table中找到要使用的derivation。 --- * 範例:與剛剛的使用一樣的例子 * E → TE’ * E’ → +TE’ | є * T → FT’ * T’ → *FT’ | є * F → (E ) | id * w = id + id * id * Parsing table: ![](https://i.imgur.com/rSZnerB.png) * 初始化:在stack放入`$`當作結束符號,然後放入起始符號E開始parse;輸入字串的尾端也放入`$`當作輸入結束。 * 過程:![](https://i.imgur.com/u6MXb35.png) --- * 然而,我們到底要怎麼得到這個神奇的Parsing table呢? #### FIRST and FOLLOW * 我們只要得到文法的FIRST和FOLLOW,就可以產生出Parsing Table了,因此我們要先學會怎麼從文法得到FIRST和FOLLOW。 * FIRST(A)指所有可能出現在A開頭的terminals * FOLLOW(A)指所有可能接在A後面的terminals * A是nonterminal,得到的結果都是terminals * 一樣用上面的文法當作例子: * E → TE’ * E’ → +TE’ | є * T → FT’ * T’ → *FT’ | є * F → (E ) | id * FIRST(A)應該比較簡單,就是把A右邊的第一個字x拿出來 * 如果x是terminal就加入x * 例如上面的E’ → +TE’ | є,就將+加入FIRST(E’) * 如果x是nonterminal就加入FIRST(x) * 例如上面的E → TE’,就將FIRST(T)加入FIRST(E) * 則我們得到: * FIRST(E) = FIRST(T) = FIRST(F) = {(, id} * FIRST(E’) = {+, є} * FIRST(T’) = {*, є} --- * FOLLOW比較麻煩一點點,我們必須觀察每一個derivation中的nonterminal後面有接什麼東西。 * 狀況一:A → αBβ,將FIRST(β) − {є} 加入至 FOLLOW(B) * β在B的後面,所以β的FIRST就是B的FOLLOW。 * 狀況二:A → αB,將FOLLOW(A) 加入至 FOLLOW(B) * A可以變成αB(B在A的最後面),因此A後面接的東西也是B後面接的東西。 * 狀況三:A → αBβ 且 є ∈ FIRST(β),將FOLLOW(A) 加入至 FOLLOW(B) * 雖然B不是A的結尾,但因為β可以變成空字串,因此又變成狀況二了。 * 總之就是要注意є的可能,它會讓你要考慮的東西變多。 * 根據上面我們得到: * FOLLOW(E) = FOLLOW(E’) = {), $} * FOLLOW(T) = FOLLOW(T’) = {+, ), $} * FOLLOW(F) = {+, *, ), $} #### Construction of Predictive Parsing Table * 得到FIRST和FOLLOW的之後,我們將Production按照規則放入table中,就可以完成Parsing Table了。 * Table M上面的row是terminal,左邊的column是nonterminal。 * 演算法: 1. 跑所有production P = A → α 2. 如果找到 a ∈ FIRST(α),將P放入M[A, a] 3. 如果找到 є ∈ FIRST(α),將P放入M[A, b],b = FOLLOW(A) 4. 如果找到 є ∈ FIRST(α) and $ ∈ FOLLOW(A),將P放入M[A, $] 5. 其餘在M裡面空白的地方都是error > 建議自己在紙上求一次FIRST、FOLLOW、TABLE,確定自己學會。 --- * 再來一個範例,你可能會發現一些問題。 * 文法: * S → iEtSS’ | a * S’ → eS | є * E → b * 先求FIRST * FIRST(S) = i, a * FIRST(S’) = e, є * FIRST(E) = b * 再求FOLLOW * FOLLOW(S) = FOLLOW(S’) = e, $ * FOLLOW(S’) = e, $ * FOLLOW(E) = t * 得到table: ![](https://i.imgur.com/swOtGDt.png) * 是否有發現什麼地方怪怪的? * 有一格同時出現兩個Production了! ### LL(1) Grammars * 如果想要保證你的parsing table不會出現多個元素在同一格,就必須使用LL(1)的文法,什麼是LL(1)? * 第一個`L`代表input從left開始讀取。 * 第二個`L`代表derivation是leftmost。 * 最後的`1`代表我們將提前看1個輸入字元來判斷。 * 這樣的文法必須擁有以下特性: * 不會發生ambiguous * 不會發生left recursive * 所有productions A → α | β * α 和 β 不能推導出同樣的terminal當作開頭 * α 和 β 最多只能有其中一個推導出є * 如果β可以變空字串,α不能推導出任何是FOLLOW(A)的terminal當作開頭 * 看起來頗複雜,所以就先跳過,不用太在意XD ## Bottom-Up Parsing * 另一種Parsing的方法是Bottom-Up,我們不是將起始符號S推導至input,而是將input做reduce,如果最後可以變成起始符號S,代表該input符合文法。 * 一個productions A → ab中 * 左邊展開為右邊叫做reduce * 右邊縮減為左邊叫做derive * 這種Parsing又稱做Shift-Reduce Parsing,因為我們會不斷使用Shift和Reduce這兩個動作來進行Parse。 * 現在來詳細說明動作。 ### Stack Implementation of Shift-Reduce Parsing * 我們在做parse之前,需要先有一個stack用來存放symbol。 * 然後我們總共有幾個動作: * shift: 把下一個input放入stack * reduce: 把stack裡面的symbol用nonterminal取代 * accept: 宣告成功(該input符合該文法) * error: 宣告失敗(該input不符合該文法) --- * 範例 * (1) E → E + E * (2) E → E * E * (3) E → (E) * (4) E → id * w = id + id * id * 然而,因為沒有規定shift和reduce的優先度,語法可能有好幾種結果:![](https://i.imgur.com/hbQUSlB.png) * 在第`6`步的時候,stack = E + E,這時候可以進行shift也可以進行reduce。 ### LR Parsers * 我們想要更有系統地來完成Shift-Reduce Parsing,稱做LR(k) Parser。 * `L`代表input從left開始讀取。 * `R`代表代表derivation是rightmost。 * `k`代表我們提前看input中的k個字元來判斷。 * 我們先以LR(0)為主,也就是沒有提前看input的部分。 * 優點: * 幾乎可以識別所有context-free grammars。 * 支援predictive parsers用於加速。 * 偵測錯誤的速度很快。 * 缺點: * 建立parser很麻煩。 * 一般實務上使用程式協助產生。 * LR Parser的重點在於有一個Parsing Table,告訴我們什麼狀態遇到什麼輸入時應該執行什麼動作。 * 在學會建立Parsing Table之前,我們先學會如何使用它。 --- * 範例: * (1) E → E + T * (2) E → T * (3) T → T * F * (4) T → F * (5) F → (E ) * (6) F → id * w = id * id + id $ * 我們還需要一個stack存放symbol和狀態,初始為stack s = $ 0 * stack一定是一個符號配一個狀態 * $是結尾符號,0是狀態0 * 給予table: ![](https://i.imgur.com/qERqb9a.png) * 左側為狀態、上面為symbol。 * 使用:查看input和stack頂端的狀態,去對照table的指示。 * 指示會是一個英文字加上一個數字i。 * 如果table的指示是s開頭,表示執行shift。 * 將該input放入stack,並將i放入stack當下一個狀態。 * 如果table的指示是r開頭,代表執行reduce。 * 將stack內的symbol按照第i條production轉換為nonterminal,然後再次查表。 * 查看轉換出來的nonterminal和現在的state,放入新的狀態。 * 結果:![](https://i.imgur.com/tpiMSx2.png) * 可能看文字不太好懂,上圖有附上完整過程,請多練習。 --- * 接下來要學習如何建立table,有以下幾種建立LR parsing table的方法: * Simple LR (SLR) * Canonical LR (LR) * Lookahead LR (LALR) * 不同的方法可以辨識的語法不同,所需的cost也不同。 ### SLR * 將一個點加入production裡面得到的東西稱做item: * 例如:production A → XYZ * A → ⋅XYZ * A → X⋅YZ * A → XY⋅Z * A → XYZ⋅ * 例如:production A → є yields * A → ⋅ * 點就代表現在解析語法的進度,藉由對這些item進行某些運算可以幫助我們得到table,現在請先對item有些概念。 * 我們以下的操作中,會稍微改寫一下文法: * 如果原本的起始符號是S,我們會加入新的符號S’當作起始符號,並且加入production S’ → S以維持文法的正確。 * 這個動作稱作Augmented grammar,能夠幫助accept的檢測。 #### Closure Operation * 第一個要介紹的運算動作是Closure,概念和CH3.的closure有些類似。 * 設I是一個item的集合,則closure(I)也是一個item的集合,它從I建立出來: 1. 將所有I裡面的item加入到closure(I)。 2. 如果A → α⋅Bβ存在於closure(I)之中,且存在B → γ這個production,將B → ⋅γ加入到closure(I)。 3. 重複至沒有新的item可以被加入。 --- * 範例: * E’ → E * E → E + T * E → T * T → T * F * T → F * F → (E) * F → id * 求closure({E’ → E }) 1. 將自己本身先加入 * E’ → ⋅E 2. 將E在左邊的productions也加入 * E → ⋅ E + T * E → ⋅ T 3. E加入過了因此跳過;將T在左邊的productions加入 * T → ⋅ T * F * T → ⋅ F 4. T加入過了因此跳過;將F在左邊的productions加入 * F → ⋅(E) * F → ⋅ id } #### Goto Operation * 第二個是Goto,該運算需要用到Closure。 * 設I是items的集合、X是一個文法中的symbol,則goto(I,X)是在 `A → α⋅Xβ` 存在於I的前提之下,所有item`A → αX⋅β`的closure。 * 簡單來說就是如果輸入符號就是點的下一個符號,點就可以往前。 * 記得最後要做closure。 --- * 範例: * E’ → E * E → E + T * E → T * T → T * F * T → F * F → (E) * F → id * 求goto({E’ → E ⋅, E → E ⋅ + T }, +) * 點的下一步是`+`的是E → E ⋅ + T,因此做closure({E → E + ⋅ T})。 * 記得將點往下移。 * closure ({E → E + ⋅T }) = * E → E + ⋅T * T → ⋅ T * F * T → ⋅ F * F → ⋅(E) * F → ⋅ id #### Set-of-Items Construction * 學會Closure和Goto之後,我們離建立SRL的parser table已經只差一點了。 * 接下來我們要使用Closure和Goto將item們分組,分好之後一個一個的組別就會是之後table的狀態。 * 演算法: 1. 設C = { closure(S’→ ⋅S) } 2. C裡面每個item集合 I 和該文法每個symbol X 做 goto(I, X) 3. 如果goto的結果不在C之中,將其加入C 4. 直到每個items集合都被加入C --- * 警告,接下來雖然看起來很多,但其實不難,耐住性子慢慢看就懂了。 * 範例: * E’→ E * E → E + T * E → T * T → T * F * T → F * F → (E) * F → id * I0 = closure(E’→ E) * E’→ ⋅E * E → ⋅ E + T * E → ⋅ T * T → ⋅ T * F * T → ⋅ F * F → ⋅(E ) * F → ⋅ id * goto(I0, E) = * E’ → E ⋅ * E → E ⋅ + T * 新的item set,設為I1 * goto(I0, T) = * E → T ⋅ * T → T ⋅ * F * 新的item set,設為I2 * goto(I0, F) = * T → F ⋅ * 新的item set,設為I3 * goto(I0, “(“) = * F → (⋅ E ) * E → ⋅ E + T * E → ⋅ T * T → ⋅ T * F * T → ⋅ F * F → ⋅(E ) * F → ⋅ id * 新的item set,設為I4 * goto(I0, id) = * F → id ⋅ * 新的item set,設為I5 * I0跑完了,接著就跑I1。 * 接下來就不打了,直上一圖流:![](https://i.imgur.com/gOxK8P4.png) #### Constructing an SLR Parsing Table * 學了那麼多的運算,我們現在終於可以開始建立Parsing Table了。 * 演算法: 1. 設C = {I0, I1, ..., In} 所有的item集合。 2. 對於每個狀態Ii 1. 如果存在A → α⋅aβ ∈ Ii 且 goto(Ii, a) = Ij,則 action[i, a] = shift j * 如果藉由輸入轉移到其他狀態,做shift 2. 如果存在A → α⋅ ∈ I,則 action[i, a] = reduce A → α ∀a ∈ FOLLOW(A) * 如果點在最後面,在遇到FOLLOW的符號時做reduce 3. 如果存在S’ → S⋅ ∈ I,則action[i, $] = accept * 如果是起始符號的結尾,在遇到結尾符號時宣告accept 3. 如果 goto(Ii , A) = Ij,則goto[i, A] = j * 如果是輸入nonterminal時進行轉移,就放置於goto的地方(table右邊) 4. 找到包含S’ → ⋅S這個item的state當作起始狀態 * 可以的話,請花點時間試試看,自行根據文法推出FOLLOW、Set-of-Items,最後得到Parsing Table。 --- * 再來一個範例,你可能會發現一些問題。 * (0) S’ → S * (1) S → L = R * (2) S → R * (3) L → *R * (4) L → id * (5) R → L * 這邊我們就直接上圖了,不多花時間在解釋如何找狀態:![](https://i.imgur.com/KJWpcuy.png) * 接著,我們關注在`goto(I0, L)裡面的R → L⋅` 以及`goto(I2, "=") = I6`這兩行上面。 * 將這兩行填入table中,你會發現變成這樣:![](https://i.imgur.com/9sz0zg8.png) * 注意到同一個格子出現了兩個元素! * 我們在更仔細觀察I2: * S → L ⋅ = R * R → L ⋅ * 如果輸入是 L = R,會因為`R → L ⋅`而導致reduce成 R = R。 * R應該在右邊,因此這個SLR發生錯誤了。 * 這時候就代表我們需要更多資訊去處理這個問題。 #### LR(1) * 現在我們不使用SLR,因為它無法辨識某些語言(即便它很簡單)。現在我們改成使用LR(1)。 * item的樣子變為`[A →α ⋅ β, a]` * a是我們預先讀入的輸入,稱做lookahead。 * lookahead其實只有在β = є的時候有用。 * 出問題的地方是reduce,而reduce只有點在最後面才會出現。 * [A →α ⋅, a] 只會在symbol為a的地方進行reduce。 * 和前面的方法比較,前面是所有FOLLOW(A)都會reduce。 #### Set-of-Items Construction Ver. LR * 將原本的item們分組加上lookahead。 * 大致上步驟都一樣(使用goto),但是closure的部分需要加上lookahead。 * 原本是看到A → α⋅Bβ且擁有B → γ就將B → ⋅γ加入 * 更改為看到[A → α⋅Bβ, a]且擁有 B → γ 就將 [B → ⋅γ, b]加入。 * 其中b是FIRST(βa) * goto的操作則是直接保留lookahead即可。 * 好像有點難理解,我們實際跑一次範例看看會比較清楚。 --- * 範例: * S’ → S * S → CC * C → cC * C → d * I0 = closure({[S’ → S, $]}) * 一開始lookahead為$。 * S’ → ⋅S, $ * 先將自己加入。 * S → ⋅ CC, $ * 將S開頭的加入,lookahead是FIRST(є$) = $ * є是因為`S’ → ⋅S`中,S後面沒東西。 * C → ⋅ cC, c/d * 將C開頭的加入,lookahead是FIRST(C) = c, d * C → ⋅ d, c/d * 同上 * 簡單來說,lookahead就是點移動之後再後面一個symbol的FIRST,若是空的就放原本的lookhead。 * 剩下的狀態一樣直接一圖流附上:![](https://i.imgur.com/SUGttCR.png) #### Construction of the LR Parsing Table * 再來建表的部分就很簡單啦,因為99%都和前面一樣。 * 差別只有在點在最後面的時候: * 原本是在所有FOLLOW的地方都做reduce。 * 現在是在所有lookahead的地方做reduce。 * 剛剛那題的表格,大家可以建好之後比較看看有沒有錯誤:![](https://i.imgur.com/ghPvYuI.png) ### LALR * 剛剛的SLR有一點點的缺點,就是它的狀態數太多了。 * 仔細觀察剛剛那題,你會發現I3和I6、I4和I7的前面都長得一模一樣,只差在後面的lookahead。 * 前面重複的部分稱呼為core。 * 那麼,我們是不是可以藉著合併這些狀態達到狀態縮減呢? * 使用Lookahead LR (LALR)。 #### Construction of the LALR Parsing Table * 建立LALR的Parsing Table很簡單,我們只需要直接將LR table中所有core相同的狀態合併就好了。 * 怎麼合?就真的直接合:![](https://i.imgur.com/R5LyclC.png) * 不是我要偷懶,這個我也不知道要怎麼講,就真的合在一起而已。 * 合併後的好處是狀態變少;壞處是會變得較晚發現錯誤,但一定還是會發現。 * 壞處的影響不太,因此LALR很常使用。 * 如果LR找不到一樣的core,那該文法還算是LALR嗎? * 算是,只是table和LR長得一模一樣。 * LALR包含LR。 ### Conflicts * 當你合併之後,是有可能出現衝突的。 * 不會出現shift-reduce衝突,因為兩者的core必定不同。 * 但可能出現reduce-reduce衝突。 * 舉個簡單的例子: * S’ → S * S → aAd | bBd | aBe | bAe * A → c * B → c * 建好table之後,合併時你會發現同一個格子出現兩個動作。 * ![](https://i.imgur.com/uY6MUYQ.png) --- * 所有的ambiguous文法都會產生衝突(shift-reduce或是reduce-reduce)導致LR失敗,但是這樣的文法很好用、很直覺。 * 因此,比較好的做法並不是改寫文法,而是想辦法處理衝突。 * 給予優先度是個好做法。 --- * 例子: * (0) E’ → E * (1) E → E + E * (2) E → E * E * (3) E → (E) * (4) E → id * 這是很常見的運算式文法,我們習慣上會希望乘法優先於加法。 * 這是其中出現E + E 和 E * E的部分: * I7: E → E + E ⋅ E → E ⋅ + E E → E ⋅ * E * I8: E → E * E ⋅ E → E ⋅ + E E → E ⋅ * E * 則最後的table應該長這樣:![](https://i.imgur.com/4v30n81.png) * 乘法優先度較高,因此I8總是reduce而I7遇到`*`則是會先shift。 --- * 下一個例子也是很常見的文法: * (0) S’ → S * (1) S → iSeS * (2) S → iS * (3) S → a * 也就是我們程式中的if else文法。 * 這個文法的衝突在於else不知道要和誰匹配,所以同時出現reduce和shift。 * 習慣上我們選擇比較近的if去匹配,因此是shift優先。 * 不知道為什麼是shift可以看下圖的I4,shift才會把else給shift進stack中。 * ![](https://i.imgur.com/Ya382PB.png) ### Operator-Precedence Parsing * 最後介紹的這個parser是一個特別的parser,它專門用於處理operator的文法。 * 在這裡operator grammars定義為:不能有任何production的右側出現є或是兩個鄰近的nonterminals。 * 例如: * E → E + E | E - E | E * E | E / E | (E ) | -E | id * 就是一個operator grammar。 * E → EAE | (E ) | -E | id * A → + | - | * | / * 就不是一個operator grammar。 * 優點是很簡單、容易實作。 * 缺點有以下幾個: * 負號很麻煩,因為減號和負號長得一模一樣。 * 少數明明就符合文法的句子它不會accept。 * 適用的文法很少。 --- * Operator-Precedence Parsing擁有三種優先度關係: * a <⋅ b * b 優先於 a * a ≐ b * a和b優先度相等 * a ⋅> b * a 優先於 b * 我們將所有Operator的優先度畫成一個對應表格:![](https://i.imgur.com/loDAJry.png) * 然後在輸入進來時,我們只須按照表格將符號插入到句子之間就好了。 * 演算法: * 從左邊開始掃描輸入,直到遇到第一個⋅> * 然後往回掃描所有≐,直到遇到<⋅ * 將<⋅ ⋅> 之間的所有內容reduce --- * 範例 w = id + id * id * $ <⋅ id ⋅> + <⋅ id ⋅> * <⋅ if ⋅> $ * 先將箭頭按照表格放入 * $ <⋅ id ⋅> + <⋅ id ⋅> * <⋅ id ⋅> $ * $ <⋅ E + <⋅ id ⋅> * <⋅ id ⋅> $ * $ <⋅ E + <⋅ E * <⋅ id ⋅> $ * $ <⋅ E + <⋅ E * E ⋅> $ * $ <⋅ E + E ⋅> $ * $ E $ ###### tags: `compiler` `note`