VinSong's Blog

Back

Formal Grammars and Parsing#

不管是 Top-down 還是 Bottom-up 的 parsing 我們都需要用到一些共同的理論,或是定義,這裡主要介紹分析文法用的理論、名詞

Context Free Grammar#

一個 CFG G=(Vt,Vn,S,P)G = (V_t, V_n, S, P)

  • VtV_t:A set of finite terminal
  • VnV_n:A finite set of nonterminal
  • SS:start symbol, S∈VnS \in V_n
  • PP:A finite set of production 也就是 rules
    • 形式會是 A→X1X2…XmA \to X_1 X_2 \ldots X_m for A∈VnA \in V_n and Xi∈(Vn∪Vt)X_i \in (V_n \cup V_t)

Derivation 的意思就是把 production 左邊的東西換成右邊的東西

  • ⇒∗\Rightarrow^* 是 0 或更多步驟 derivation 可以推得
  • ⇒+\Rightarrow^+ 是 1 或更多步驟 derivation 可以推得

我們延伸定義兩個東西

  • sentential form 從 start symbol derive 出的可以包含 non-terminal 的 string,也就是
sentential form={β∣S⇒∗β}\text{sentential form} = \{\beta \mid S \Rightarrow^* \beta \}
  • sentence 從 start symbol derive 出的全都是 terminal 的 string
sentence={w∈Vt∗∣S⇒∗w}\text{sentence} = \{w \in V_t^* \mid S \Rightarrow^* w \}

一個 Language 則是

L(G)={w∈Vt∗∣S⇒+w}L(G) = \{ w \in V^*_t \mid S \Rightarrow^+ w \}

這裡的 ww 就是一個 sentence,L(G)L(G) 就是可以藉由 GG 推出來的所有 sentence 的集合

Left-most/Right-most Derivation#

Left-most 會從最左邊開始一個一個把 non-terminal 換掉,會用

⇒lm, ⇒lm∗, ⇒lm+\Rightarrow_{\mathrm{lm}},\ \Rightarrow^*_{\mathrm{lm}},\ \Rightarrow^+_{\mathrm{lm}}

是 Top-down Parsing 用的方法,而 right-most derivation 則是用

⇒rm, ⇒rm∗, ⇒rm+\Rightarrow_{\mathrm{rm}},\ \Rightarrow^*_{\mathrm{rm}},\ \Rightarrow^+_{\mathrm{rm}}

是 Bottom-up Parsing 用的方法的反過來(從尾到頭),也就是說我們先建立 rm 然後 bottom-up parsing 會從尾巴合成成 start symbol,用 stack 的方法一個一個塞進來,一個一個換,換到 stack 裡面剩下 start symbol

一個 Parse tree 沒有替換順序的資訊,但是 derivation 有,接著我們有了這些東西可以繼續定義新名詞

  • Phrase 就是由一個 parse tree 上 non-terminal derive 出的 sequence,是 sentential form
    • simple phrase, prime form 是沒有繼續包含更小的 phrase (也就是全部 symbol 都在 parse tree 上的同一層)
  • Handle 是一個 left-most 的 simple phrase,也就是下一步要被 reduce 的那個 phrase,這個是 bottom-up parsing 會用到的概念

Reduce of CFG#

有些時候我們會在 rule 裡面發現一些可以消掉的

  • Unreachable: 沒有 symbol 可以被任何其他 rule 從 start symbol parse 出來
  • Derive no terminal string: 無法得到一個全是 terminal 的 string

Ambiguous#

前面章節提到的 ambiguous 通常會導致我們 operator 的 precedence 出現問題,但他其實還分成很多種

Dangling Else#

if ((k >= 0) && (k < table_size))
    if (table[k] >= 0)
        printf("Entry %d is %d\n", k, table[k]);
else printf("Error: index %d out of range \n", k);
c

在 C language 裡,這樣的話我們的 else 應該要跟著裡面那個 if,如果想改跟著外面那個,會需要

if ((k >= 0) && (k < table_size)) {
    if (table[k] >= 0)
        printf("Entry %d is %d\n", k, table[k]);
} else printf("Error: index %d out of range \n", k);
c

但即便他有這種規則,在 parsing 的階段還是會有 ambiguous,我們還是需要設計一個消除方法,最常會出現的三種情況就是

  • Operator Associativity
  • Operator Precedence
  • Dangling Else

邏輯上我們只需要保證最早要做的要出現在最左邊,最常見的解決方法就是 Left Factoring

但是這種方法只能解決不知道要選哪條 rule 的問題,但無法真正解決 Ambiguity

Parser and Recognizer#

  • Recognizer 只需要看看這個 program 對不對
  • Parser 除了做到這件事,還得建立起一個 Parse tree

Parsing Technique Naming#

有三個欄位

{L/R} {L/R} {(number)}
plaintext
  1. 最左邊這個是我們從左 parse 到右,還是右 parse 到左
  2. 中間這個是說 left-most derivation 還是 right-most derivation
  3. 這個數字是說要 look ahead 幾個 token 的

e.g. LL(1)LL(1) 代表一個 top-down approach 要偷看一個 token

Grammar Analysis Algorithms#

在進行 parsing 之前,我們會先對 Grammar 做一些分析,這邊主要會計算三個東西:

  • Nullable:哪些 non-terminal 可以 derive λ\lambda
  • FIRST:一個 sentential form 最前面可能出現哪些 terminal
  • FOLLOW:一個 non-terminal 後面可能出現哪些 terminal

Nullable Symbols#

如果一個 non-terminal AA 可以經過若干次 derivation 變成 empty string:

A⇒∗λA \Rightarrow^* \lambda

那我們稱 AA 是 nullable symbol,也可以把所有 nullable symbols 定義成:

Nullable={A∈Vn∣A⇒∗λ}\mathrm{Nullable}=\{A\in V_n\mid A\Rightarrow^*\lambda\}

Computing Nullable Symbols#

我們可以用一個 iterative marking algorithm 找出所有 nullable non-terminal。

對每一條 production 維護一個 count:

count 代表 RHS 還有多少個 symbol 尚未被確認為 nullable。

比如說

A→BCDA\to BCD

一開始 count=3,如果後來知道 B,DB,D 都 nullable,那就只剩下 CC count=1,如果最後 CC 也是 nullable 那 count=0,代表 RHS 所有 symbols 都可以消失,因此 AA 也是 nullable

假設:

A→BCD,B→BX,B→λC→EFG,C→BDD,D→ED→λ\begin{align} &A\to BCD,\quad B\to BX,\quad B\to\lambda \\ &C\to EFG,\quad C\to BDD,\quad D\to E \\ &D\to\lambda \end{align}

一開始直接看到

B→λ,D→λB\to\lambda,\qquad D\to\lambda

所以:

Worklist = [B, D]
txt

接著更新包含 B,DB,D 的 production:

  • A→BCDA\to BCD:count 從 33 變成 11
  • C→BDDC\to BDD:count 從 33 變成 00

所以 CC 也是 nullable:

Worklist = [C]
txt

再處理 CC:

  • A→BCDA\to BCD:count 從 11 變成 00

所以 AA 也是 nullable,最後得到

Nullable={A,B,C,D}\mathrm{Nullable}=\{A,B,C,D\}

FIRST set#

FIRST(α)\mathrm{FIRST}(\alpha) 的正式定義是:

FIRST(α)={a∈Vt∣α⇒∗aβ}∪{{λ},α⇒∗λ∅,otherwise\mathrm{FIRST}(\alpha)=\{a\in V_t\mid\alpha\Rightarrow^*a\beta\}\cup\begin{cases}\{\lambda\},&\alpha\Rightarrow^*\lambda\\\emptyset,&\text{otherwise}\end{cases}

也就是:從 α\alpha 可以 derive 出來的 sentential form 中,第一個可能出現的 terminal 的集合;如果 α\alpha 本身可以 derive λ\lambda,那 λ\lambda 也會放進 FIRST。

例如

A→aB∣bCA\to aB\mid bC

則

FIRST(A)={a,b}\mathrm{FIRST}(A)=\{a,b\}

Computing FIRST#

如果 ZZ 是一個 terminal:

FIRST(Z)={Z}\mathrm{FIRST}(Z)=\{Z\}

如果有 production

Z→Y1Y2⋯YkZ\to Y_1Y_2\cdots Y_k

就從左往右檢查:

  1. 把 FIRST(Y1)−{λ}\mathrm{FIRST}(Y_1)-\{\lambda\} 加入 FIRST(Z)\mathrm{FIRST}(Z)
  2. 如果 λ∈FIRST(Y1)\lambda\in\mathrm{FIRST}(Y_1),繼續看 Y2Y_2
  3. 如果 Y1,Y2Y_1,Y_2 都 nullable,繼續看 Y3Y_3
  4. 一直做到第一個不能 derive λ\lambda 的 symbol
  5. 如果全部 Y1,…,YkY_1,\ldots,Y_k 都 nullable,則把 λ\lambda 加入 FIRST(Z)\mathrm{FIRST}(Z)

比如說

A→BCA\to BC

而

FIRST(B)={a,λ},FIRST(C)={b}\mathrm{FIRST}(B)=\{a,\lambda\},\qquad\mathrm{FIRST}(C)=\{b\}

因為 BB 可以消失,所以第一個 terminal 除了可能是 aa,也可能直接來自 CC:

FIRST(A)={a,b}\mathrm{FIRST}(A)=\{a,b\}

簡單來講就是:從左往右找第一個可能出現的 terminal;前面的 symbol 如果可以消失,就繼續往右看。

FOLLOW set#

FOLLOW(A)\mathrm{FOLLOW}(A) 的正式定義是:

FOLLOW(A)={a∈Vt∣S⇒∗⋯Aa⋯ }\mathrm{FOLLOW}(A)=\{a\in V_t\mid S\Rightarrow^*\cdots Aa\cdots\}

在某個由 start symbol derive 出來的 sentential form 中,可能直接出現在 AA 後面的 terminal 的集合。

例如:

S→A+bS\to A+b

則

+∈FOLLOW(A)+\in\mathrm{FOLLOW}(A)

FOLLOW 的其中一個用途是幫助 parser 判斷 什麼時候應該 reduce。

例如:

A→b,B→bA\to b,\qquad B\to b

parser 只看到 b 的時候,不知道應該 reduce 成 AA 還是 BB。

如果下一個 lookahead 是 a,而

a∈FOLLOW(A),a∉FOLLOW(B)a\in\mathrm{FOLLOW}(A),\qquad a\notin\mathrm{FOLLOW}(B)

就可以知道這個 b 應該 reduce 成 AA。

Computing FOLLOW#

有三個主要規則。

  1. 如果 SS 是 start symbol:
$∈FOLLOW(S)\$\in\mathrm{FOLLOW}(S)

其中 $ 表示 input 結束。

  1. 如果有 production
B→αAβB\to\alpha A\beta

則

FIRST(β)−{λ}⊆FOLLOW(A)\mathrm{FIRST}(\beta)-\{\lambda\}\subseteq\mathrm{FOLLOW}(A)

因為 AA 後面就是 β\beta,所以 β\beta 最前面可能出現的 terminal,也可能直接出現在 AA 後面。

  1. 如果有
B→αAB\to\alpha A

或

B→αAβB\to\alpha A\beta

而且

λ∈FIRST(β)\lambda\in\mathrm{FIRST}(\beta)

則

FOLLOW(B)⊆FOLLOW(A)\mathrm{FOLLOW}(B)\subseteq\mathrm{FOLLOW}(A)

因為 AA 已經在最後,或是 AA 後面的 β\beta 可以全部消失,所以原本可能接在 BB 後面的 terminal,也可能直接接在 AA 後面。

計算 FIRST 時需要知道前面的 symbol 能不能消失,而計算 FOLLOW 時又會使用 FIRST,所以我們會有個計算順序,有了這幾個東西我們就能做 Parsing 了

NTU-Compiler 編譯程式設計 Ch4 Formal Grammars and Parsing
https://vinsong.csie.org/notes/compiler/ch04-grammars.html
Author VinSong
Published at 2026年10月20日
← 回到 NTU-Compiler 編譯程式設計 目錄