精讀《手寫 SQL 編譯器 - 文法介紹》

1 引言

文法用來描述語言的語法規則,因此不只能夠用在編程語言上,也可用在漢語、英語上。mysql

2 精讀

咱們將一塊語法規則稱爲 產生式,使用 「Left → Right」 表示任意產生式,用 「Left => Right」 表示產生式的推導過程,好比對於產生式:git

E → i
E → E + E
複製代碼

咱們進行推導時,能夠這樣表示:E => E + E => i + E => i + i + E => i + i + igithub

也有使用 Left : Right 表示產生式的例子,好比 ANTLR。BNF 範式經過 Left ::= Right 表示產生式。sql

舉個例子,好比 SELECT * FROM table 能夠被表達爲:編程

S → SELECT * FROM table
複製代碼

固然這是最固定的語法,真實場景中,* 可能被替換爲其餘單詞,而 table 不但可能有其餘名字,還多是個子表達式。bash

通常用大寫的 S 表示文法的開頭,稱爲開始符號。編程語言

終結符與非終結符

下面爲了方便書寫,使用 BNF 範式表示文法。函數

終結符就是語句的終結,讀到它表示產生式分析結束,相反,非終結符就是一個新產生式的開始,好比:性能

<selectStatement> ::= SELECT <selectList> FROM <tableName>

<selectList> ::= <selectField> [ , <selectList> ]

<tableName> ::= <tableName> [ , <tableList> ]
複製代碼

全部 ::= 號左邊的都是非終結符,因此 selectList 是非終結符,解析 selectStatement 時遇到了 selectList 將會進入 selectList 產生式,而解析到普通 SELECT 單詞就不會繼續解析。spa

對於有二義性的文法,能夠經過 上下文相關文法 方式描述,也就是在產生式左側補全條件,解決二義性:

aBc -> a1c | a2c
dBe -> d3e
複製代碼

通常產生式左側都是非終結符,大寫字母是非終結符,小寫字母是終結符。

上面表示,非終結符 Bac 之間時,能夠解析爲 12,而在 de 之間時,解析爲 3。但咱們能夠增長一個非終結符讓產生式可讀性更好:

B -> 1 | 2
C -> 3
複製代碼

這樣就將上下文相關文法轉換爲了上下文無關文法。

上下文無關文法

根據是否依賴上下文,文法分爲 上下文相關文法上下文無關文法,通常來講 上下文相關文法 均可以轉換爲一堆 上下文無關文法 來處理,而用程序處理 上下文無關文法 相對輕鬆。

SQL 的文法就是上下文相關文法,在正式介紹 SQL 文法以前,舉一個簡單的例子,好比咱們描述等號(=)的文法:

SELECT
  CASE
    WHEN bee = 'red' THEN 'ANGRY'
    ELSE 'NEUTRAL'
  END AS BeeState
FROM bees;

SELECT * from bees WHERE bee = 'red';
複製代碼

上面兩個 SQL 中,等號先後的關鍵字取決於當前是在 CASE WHEN 語句裏,仍是在 WHERE 語句裏,因此咱們認爲等號所在位置的文法是上下文相關的。

可是當咱們將文法粒度變細,將 CASE WHENWHERE 區塊分別交由兩塊文法解決,將等號這個通用的表達式抽離出來,就能夠不關心上下文了,這種方式稱爲 上下文無關文法

附上一個 mysql 上下文無關文法集合

左推導與右推導

上面提到的推導符號 => 在實際運行過程當中,顯然有兩種方向左和右:

E + E => ?
複製代碼

從最左邊的 E 開始分析,稱爲左推導,對語法解析來講是自頂向下的方式,經常使用方法是遞歸降低。

從最右邊的 E 開始分析,稱爲右推導,對語法解析來講是自底向上的方式,經常使用方法是移進、規約。

右推導過程比左推導過程複雜,因此若是考慮手寫,最好使用左推導的方式。

左推導的分支預測

好比 select <selectList>selectList 產生式,它能夠表示爲:

<SelectList> ::= <SelectList> , <SelectField>
               | <SelectField>
複製代碼

因爲它能夠展開:SelectList => SelectList , a => SelectList , b, a => c, b, a。

但程序執行時,讀到這裏會進入死循環,由於 SelectList 能夠被無限展開,這就是左遞歸問題。

消除左遞歸

消除左遞歸通常經過轉化爲右遞歸的方式,由於左遞歸徹底不消耗 Token,而右遞歸能夠經過消耗 Token 的方式跳出死循環。

Token 見上一期精讀 精讀《手寫 SQL 編譯器 - 詞法分析》

<SelectList> ::= <SelectField> <G>

<G> ::= , <SelectList>
      | null
複製代碼

這實際上是一個通用處理,能夠抽象出來:

E → E + F
E → F
複製代碼
E → FG
G → + FG
G → null
複製代碼

不過咱們也不難發現,經過通用方式消除左遞歸後的文法更難以閱讀,這是由於用死循環的方式解釋問題更容易讓人理解,但會致使機器崩潰。

筆者建議此處不要生硬的套公式,在套了公式後,再對產生式作一些修飾,讓其更具備語義:

<SelectList> ::= <SelectField>
               | , <SelectList>
複製代碼

提取左公因式

即使是上下文無關的文法,經過遞歸降低方式,許多時候也必須從左向右超前查看 K 個字符才能肯定使用哪一個產生式,這種文法稱爲 LL(k)。

但若是每次超前查看的內容都有許多字符相同,會致使第二次開始的超前查看重複解析字符串,影響性能。最理想的狀況是,每次超前查看都不會對已肯定的字符重複查看,解決方法是提取左公因式。

設想以下的 sql 文法:

<Field> ::= <Text> as <Text>
          | <Text> as<String>
          | <Text> <Text>
          | <Text>
複製代碼

其實 Text 自己也是比較複雜的產生式,最壞的狀況須要對 Text 連續匹配六遍。咱們將 Text 公因式提取出來就能夠僅匹配一遍,由於不管是何種 Field 產生式,都一定先遇到 Text:

<Field> ::= <Text> <F>

<F> ::= <G>
      | <Text>

<G> ::= as <H>

<H> ::= <space> <Text>
      | <String>
複製代碼

和消除左遞歸同樣,提取左公因式也會下降文法的可讀性,須要進行人爲修復。不過提取左公因式的修復沒辦法在文法中處理,在後面的 「函數式」 處理環節是有辦法處理的,敬請期待。

結合優先級

對 SQL 的文法來講不存在優先級的概念,因此從某種程度來講,SQL 的語法複雜度還不如基本的加減乘除。

3 總結

在實現語法解析前,須要使用文法描述 SQL 的語法,文法描述就是語法分析的主幹業務代碼。

下一篇將介紹語法分析相關知識,幫助你一步步打造本身的 SQL 編譯器。

4 更多討論

討論地址是:精讀《手寫 SQL 編譯器 - 文法介紹》 · Issue #94 · dt-fe/weekly

若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。

相關文章
相關標籤/搜索