詞法分析器(scanner), 在第〇章咱們已經說過它的做用: 識別全部標識符並對其標記屬性信息. 這篇內容講解這個過程是如何實現的. git
首先, 先引入三個概念: 串, 單詞與模式. 串就是咱們一般說的字符串, 是一個一維的邏輯結構, 按照某個特定順序存儲了一些符號; 單詞就是符合某一模式的串; 那麼什麼是模式呢? 簡單的說就是字符的造成規則. 好比在 C 中, 咱們知道變量的名字由字母, 數字, 以及下劃線組成, 且數字不能夠出如今首位. 除此以外, 變量名不能夠與保留字相同(好比不能夠"int for = 3;"). 這就是一條規則. 根據這個模式(規則)咱們能夠識別出給定字符串的若干字串, 這些字串即是知足這一模式的單詞. 正則表達式
有了上面的概念, 咱們即可以進一步描述詞法分析器的任務: 根據約定的模式, 識別出串中的單詞, 並標記其屬性信息(好比識別出一個數字常量, 它的屬性多是它的存儲類型(可能涉及類型轉換)以及數字的值, 識別出一個操做符; 它的屬性多是它表明的操做含義;).編程
舉個例子, 咱們有串"float aaa = bb 12.23". 咱們有這樣的幾個規則(並不是C標準), "float 是類型名, 指的是浮點數", "變量名由字母組成", " 是操做符, 指的是乘法", "數字常量由數字(+ "." + 數字)". 通過詞法分析器, 咱們應該能夠獲得這樣的記號流: "<TYPE, FLOAT> <ID, "aaa"> <OP, SET> <ID, "bbb"> <OP, MUL> <NUM, FLOAT, "12.23">". 編程語言
一般在識別單詞的時候, 可能會發現有些串有二義性. 也就是說同一個串可能符合多個模式. 好比, "while"是 C 的保留字, 可是個人變量名能夠定義爲"while_count_ge_0". 順便說一句, 詞法分析器在分析串的時候是一個字符一個字符讀入的. 那麼它就可能在讀取到"while"後立刻標記它爲"保留字,循環語句標誌"; 亦或是在識別串中的">="時, 可能在識別">"後立刻斷定這個是"操做符, 大於"而不是"大於或等於". 這個時候就須要超前搜索, 即知足某一模式後, 仍繼續讀入字符, 直到不知足任何模式才作標記, 而不是在第一次知足模式後就立刻標記.函數
讀到這, 你可能以爲詞法分析器很強大. 不知你有沒有"debug 彈出的 warning 和 error 都是詞法分析器給出的" 這樣的錯覺. 事實上詞法分析器只能區分不多的錯誤. 好比在識別某一模式時, 不能準確地落在某一標記上. 好比在 C 中寫了這樣的東西 "a = 12.ab3". 在詞法分析器讀取小數點"."後, 它期待下一個字符要麼是數字, 要麼是空格分號之類的斷句符號. 但接下來讀入的字母令它始料未及, 它無法繼續作標記, 這個時候就會拋出錯誤. 詞法分析器也只能給出這樣的錯誤信息. 至於函數未聲明, 變量未定義等其餘語法文法的錯誤, 詞法分析器是不會明白的. 工具
邏輯上,咱們要實現的詞法分析器其實是對咱們對於語言模式要求的實現. 這句話看着有點繞, 簡單的解釋就是, 咱們想要根據某些模式對串中的單詞作出區別與標記, 詞法分析器就是要幫咱們完成這項工做. 爲了形式化地描述模式, 咱們須要瞭解正規式(正則表達式).spa
坊間有個段子: "沒有人能學會正則表達式". 咱們在這裏介紹的也只是正規式最基本的語法成分, 不過用來實現詞法分析器是足夠的.翻譯
首先咱們有一張字母表, 所謂字母表, 就是咱們須要匹配的符號的集合, 用 Σ 表示. 下面介紹一些必要的正規式知識. debug
ε 表示空串, 匹配的結果什麼都沒有的空串.
a (假設 a ∈ Σ, a 在字母表中) 表示匹配 a 這個字母
a? 表示匹配 a 或不匹配 a
a* 表示匹配 a 0次或若干次, 等價於{ε, a, aa, aaa, aaaa, ...}
a+ 表示匹配 a 1次或屢次, 和 a* 惟一的區別在於它不接受空串 ε
a|b 表示這一位匹配的字符是 a 或 b
ab 表示按順序匹配兩個字符 a 和 b, 有時候也不省略中間的點乘符號 a·b設計
有了這些知識, 咱們就能夠用正規式構造邏輯上的詞法分析器了. 好比, 咱們要描述 C 語言中對數字的表示模式.
這個表達式定義了 C 中對於十進制數字常量的模式要求(假定不會有空串). digit 是數字符號集合, dot 是小數點. e 就是字母 e. 先看錶達式前半部分的含義: (有或沒有(正號或者負號))(0個或多個數字)(有或沒有(小數點及(0個或多個數字))). 用人話來講, 就是能夠匹配多位數字, 也能夠匹配帶小數點的浮點數. 能夠沒符號, 也能夠有正負號. 因爲 C 中容許將 "0.123" 寫成 ".123", 因此前面的 digit 用的是 "*" 而不是 "+". 同理, C 中一樣容許 "123." 表示 "123", 故小數點後面也能夠沒有數字.
後半部分就是科學計數法的匹配, 要匹配一個字母 e, 剩下的和前面同樣. 一樣是由於數字常量能夠不使用科學計數法, 所以這部分也無關緊要.
若是要匹配變量名:
若是咱們不考慮保留字的狀況, 這就是變量名的模式. underscore 是下劃線, letter 是英文字母集合, unl 就是帶有下劃線的字母集. digit 一樣表示數字集. 這樣的話, 變量名的模式就是, 首字母必定不能是數字(至少匹配一個 unl 中的字母), 後面的字母既能夠來自 unl, 又能夠來自 digit. 且數目能夠爲 0, 1, 也能夠不少(不考慮變量名長度的限制).
前文說過, 詞法分析器每次僅處理串中的一個字母. 這不由讓咱們聯想到狀態自動機模型: 由初態出發, 每次根據讀入的一個字符判斷下一個狀態位置, 以此類推, 直到停機, 落在的那個狀態表示什麼就意味着這個串序列表示的是什麼. 若是沒法正常停在某個終態, 則發生錯誤. (好比前面的 "12.ab3", 讀到字母 a, 自動機會不知道下一個狀態是什麼, 形成非正常停機.)
爲了進一步用更具體的手段實現詞法分析器, 咱們須要將正規式表示成自動機. 所幸正規式能夠機械化地翻譯成不肯定狀態自動機(NFA). 具體實現方法叫作 "Thopmson 方法". 翻譯的規則以下:
咱們用單圈表示狀態, 雙圈表示終態. 初始狀態只能有一個, 通常標記爲 S, 而終態可能有不少.
對於第一個, 空串的模式表示爲"什麼都沒有就能夠從初態到終態", 由於初態即終態. 第二個表示識別一個字母 a, 在初態接受這個字母就能夠到達終態, 完成識別.
a|b 就意味着不管是 a 仍是 b 都能到達終態; a* 就是能夠不少 a 或者沒有. 同理 a+ 指的是至少有一個 a. (沒有寫 a?, 由於 a? 等價於 a|ε)
舉個例子, 好比對於正規式 0(0|1)1+, 意思是, 首位必須是 0, 第二位必須是 0 或 1, 後面至少有一個 1 的單詞. 畫成 NFA 是這樣:
不知道你對 NFA 的名字是否感受很困惑, 爲何叫"不肯定狀態"呢? 下面舉這樣的例子.
正規式 0+(0|1)1 的 NFA :
經過觀察這個 NFA, 你會發現一個很是詭異的現象: 假設你在狀態 1, 接下來讀進一個數字符號'0', 你是繼續停留在狀態 1, 仍是轉移到狀態 2 ? 若是再下一個符號是 1, 你可能知道須要轉移到狀態 2. 但別忘了, 咱們的詞法分析器每次僅讀進一個字符, 若是這個例子中的(0+)匹配特別長的0串, 咱們是否是須要使用緩衝區, 預先讀取特別多的字符, 僅僅用來判斷第二步跳轉! 這顯然是很是不現實的. 還有就是NFA 中可能涉及空串匹配, 也就是說一個狀態可能平白無故就跳到其餘狀態. 固然你能夠作出約定, 要求你們不要寫這麼奇怪的正規式. 但咱們有更好的解決方案: 肯定狀態自動機(DFA).
(注: 真正的 NFA 比這個複雜, 由於他要自行添加不少空串匹配, 爲了避免影響閱讀, 這裏的 NFA 作了適當的簡化.)
知道了 NFA 的缺陷, 咱們但願咱們設計的 DFA, 在每讀取一個字符後就能夠找到本身的下一個狀態. 所以, 咱們須要將 NFA 翻譯成 DFA. 這裏用的方法叫作 "子集構造法".
簡單來說, 咱們先將初態以及它用空串匹配就能夠到達的一切狀態, 做爲初始狀態集. 將這個狀態集看作一個獨立的狀態, 它根據本身集合中元素的全部狀態的匹配規則找到規則和目標狀態, 獲得的目標狀態再根據"可由空串匹配到達的一切狀態", 找到這樣的狀態集合. 以此類推, 作出新的狀態遷移圖. 新的狀態圖中, 若某狀態表示的老狀態集合中包含終態, 那麼這個狀態也是終態(顯然能夠有不少).
上面的文字可能不是很好理解, 咱們舉個例子.
如今咱們有這樣的 NFA :
咱們能夠看出, 這張圖表示的正規式是 ((a(a|b)b)?a)+
先構造初態集合, 從初態 S 咱們能夠用空串匹配走到狀態 1, 4, 因此初態集合是{S, 1, 4}.由初態集合的每一個頂的轉移規則, 咱們先看初態集合根據匹配 a, 能夠到達狀態 {1, 5}, 再用空串匹配, 找到初態集合匹配 a 後到達的狀態集合 {1, 5, F}. 以此類推, {S, 1, 4} 根據匹配 b 能夠獲得空集. 因而咱們再從 {1, 5, F} 出發, 使用 a 匹配獲得 {2}, 使用 b 匹配獲得空集. 再從 {2} 出發, 經過 a 或 b 獲得 {3}; 再從 {3} 出發經過 b 獲得 {4}; 再從 {4} 經過 a 獲得 {1, 5, F}.
畫成 DFA :
固然, 你能夠將狀態集合更名, 改爲新的狀態, 而不是一個個集合.
到這裏, 咱們由正規式拿到了肯定狀態自動機 DFA. DFA 用編程語言實現起來就簡單多了.
(後續工做還有 DFA 的簡化, 將 DFA 用相似於子集構造法查找能夠合併的狀態, 減小複雜度)
2017.9.13Osinovsky