系列導航html
在以前的六篇文章中,我比較詳細的介紹了與詞法分析器相關的算法。它們都比較關注於實現的細節,感受上可能比較凌亂,本篇就從總體上介紹一下如何定義詞法分析器,以及如何實現本身的詞法分析器。git
第二節完整的介紹瞭如何定義詞法分析器,能夠看成一個詞法分析器使用指南。若是不關心詞法分析器的具體實現的話,能夠只看第二節。github
1、類庫的改變
首先須要說明一下我對類庫作的一些修改。詞法分析部分的接口,與當初寫《C# 詞法分析器》系列時相比,已經發生了不小的改變,有必要作一下說明。正則表達式
1. 詞法單元的標識符
詞法單元(token)最初的定義是一個 Token 結構,使用一個 int 屬性做爲詞法單元的標識符,這也是不少詞法分析器的通用作法。算法
但後來作語法分析的時候,感受這樣很是不方便。由於目前還不支持從定義文件生成詞法分析器代碼,只能在程序裏面定義詞法分析器。而 int 自己是不具備語義的,做爲詞法單元的標識符來使用,不但不方便還容易出錯。數組
後來嘗試過使用字符串做爲標識符,雖然解決了語義的問題,但仍然容易出錯,實現上也會複雜些(須要保存字符串字典)。緩存
而既簡單,又具備語義的解決方案,就是使用枚舉了。枚舉名稱提供了語義,枚舉值又能夠轉換爲整數,並且還可以提供編譯期檢查,徹底避免了拼寫錯誤,因此如今的詞法單元便定義爲 Token<T> 類,與之相關的不少類也一樣帶上了泛型參數 T。post
2. 命名空間
以前的命名空間是 Cyjb.Compiler 和 Cyjb.Compiler.Lexer,如今被改爲了 Cyjb.Compilers 和 Cyjb.Compilers.Lexers,畢竟命名空間名稱仍是比較適合使用複數。htm
3. 詞法分析器上下文
以前對詞法分析器上下文的切換,可使用上下文的索引、標籤或 LexerContext 實例自己。但如今只可以經過標籤進行切換,這樣實現起來更簡單些,使用上也不會受到過多影響。對象
4. DFA 的表示
原先 LexerRule 類中對 DFA 的表示有些簡單粗暴,對於不瞭解具體實現的人來講,很難理解 DFA 的表示。如今從新規劃了 LexerRule<T> 類中的接口,理解起來會更容易些。
2、定義詞法分析器
這一節是 Cyjb.Compilers 類庫中詞法分析器的使用指南,包含了完整的文檔、實例以及相關注意事項。類庫的源碼能夠從 Cyjb.Compilers 項目找到,類庫文檔請參見 wiki。
1. 定義詞法單元的標識符
前面說到,目前是使用枚舉類型做爲詞法單元的標識符,這個枚舉類型中的字段能夠任意定義,沒有任何限制。不過,爲了方便以後的語法分析部分,特別要求枚舉值必須是從 0 開始的整數,枚舉值最好是連續的,由於不連續的枚舉值會致使語法分析部分浪費更多的空間。
使用特殊的值 -1 來表示文件結束(EndOfFile),該值能夠從 Token<T>.EndOfFile 字段獲得,也能夠經過 Token<T>.IsEndOfFile 屬性獲取詞法單元是否表示文件結束。
這裏仍然使用計算器做爲示例,如下代碼便定義了做爲標識符的枚舉:
在使用的時候,顯然會比整數更加方便。
2. 定義詞法分析器的上下文
詞法分析器的全部定義都是從 Cyjb.Compilers.Grammar<T> 類開始的,所以首先須要實例化一個 Grammar<T> 類的實例:
詞法分析器的上下文,能夠用來控制規則是否生效。上下文有兩種類型:包含型或者排除型。
- 若是當前是包含型上下文,那麼會激活當前上下文的全部規則,同時會激活全部沒有指定任何上下文的規則。
- 若是當前是排除型上下文,那麼只會激活當前上下文的全部規則,其它任何規則都不會被激活。
使用如下的方法來分別定義排除型和包含型的詞法分析器上下文,label 參數即爲上下文的標籤:
默認的詞法分析器上下文是 "Initial",經過該標籤能夠切換到默認的上下文中。須要特別注意的是,因爲實現上的緣由,上下文必須先於全部終結符定義。
例如,如下的代碼定義了一個包含型上下文 Inc,以及一個排除型上下文 Exc。
3. 定義正則表達式
使用如下的方法來定義正則表達式:
正則表達式能夠經過 Cyjb.Compilers.RegularExpressions.Regex 類的相關方法構造獲得,也能夠直接使用表示正則表達式的字符串,相關定義的規則能夠參考 《C# 詞法分析器(三)正則表達式》。
注意,這裏定義的正則表達式僅僅用於簡化終結符定義,方便重複使用一些通用或複雜的正則表達式,並無其它的做用。這裏定義的正則表達式也不能夠包含向前看符號(/)、行首限定符(^)、行尾限定符($)或者上下文(<context>)。
例如,如下代碼定義了一個名爲 digit 的正則表達式,之後須要表示數字的時候,就能夠直接經過 「{digit}」 來引入,而不須要每次都寫 「[0-9]+」。
4. 定義終結符
使用 Grammar<T>.DefineSymbol 方法的相關重載來定義終結符,如如下代碼所示:
這些重載被分紅了三組。第一組重載,接受 T id 做爲與詞法單元對應的標識符,和相應的正則表達式及其上下文。當相應的終結符被匹配後,自動返回標識符爲 id 的 Token<T>實例。
第二組重載,具備額外的參數 action,這是隻包含一個 ReaderController<T> 參數的委託,當匹配了相應的終結符時,就會被調用。經過 ReaderController<T> 的相應屬性和方法,能夠對詞法分析過程進行一些控制。
最後一組重載,缺乏了標識符 id,也就沒法自動返回 Token<T> 實例,所以必須指定匹配到相應終結符時要執行的方法。
終結符的動做
在成功匹配某個終結符時,就會執行相應的動做,該動做是一個 Action<ReaderController<T>> 類型的委託。
ReaderController<T> 類包含了與當前匹配的終結符相關的信息,包括上下文、標識符、源文件和文本。主要的方法有 Accept、More、Reject 以及操縱上下文的方法 BeginContext、PopContext 和 PushContext。
Accept 方法會接受當前的匹配,詞法分析器會返回表示當前匹配的 Token<T> 實例。
More 方法會通知詞法分析器,保留本次匹配的文本。假設本次匹配的文本是 "foo",下次匹配的文本是 "bar",若是本次匹配時調用了 More 方法,下次匹配的文本就會變成 "foobar"。
Reject 方法會拒絕當前的匹配,轉而使用次優的規則繼續嘗試匹配。詳細信息請參考《C# 詞法分析器(六)構造詞法分析器》的 2.4 節「支持 Reject 動過的詞法分析器」。
Accept 方法和 Reject 方法不可以在一次匹配中同時調用,由於它們是互斥的動做。若是在一次匹配中兩個方法都沒有調用,那麼詞法分析器會什麼都不作——丟棄本次匹配的結果,直接進行下一次匹配。
對於詞法分析器上下文的控制,簡單的用法就是利用上下文來切換匹配的規則集,以實現一些「次級語法」,能夠參考《C# 詞法分析器(六)構造詞法分析器》的 3.3 節給出的示例「轉義的字符串」。
下面給出計算器的終結符定義。其中,Id 的定義是經過引入正則表達式 digit 來完成的,並且它定義了本身的動做,會將本身對應的文本轉換爲 double 類型,並保存到 Token<T>.Value 屬性中。最後一條語句,經過定義空的動做,使得匹配到的空白被丟棄。
5. 構造詞法分析器
以上四步便完成了詞法分析器的定義,接下來就是構造詞法分析器。使用如下四個方法,就能夠直接構造出相應的詞法單元讀取器(TokenReader<T> 的子類的實例):
若是調用的是 GetReader 方法重載,則認爲動做中不包含拒絕(Reject),會返回比較簡單但更高效的詞法分析器實現。若是調用的是 GetRejectableReader 方法重載,則認爲動過中包含拒絕(Reject),會返回功能更強大但效率略低的詞法分析器實現。
其規則是:
- 若是不包含向前看和拒絕動做,則返回 SimpleReader<T> 的實例。
- 若是隻包含定長的向前看(不包含變長的向前看或拒絕動做),則返回 FixedTrailingReader<T> 的實例。
- 若是隻包含拒絕動做(不包含向前看),則返回 RejectableReader<T> 的實例。
- 若是包含變長的向前看,或者同時包含拒絕動做和向前看(不管是否變長),則返回 RejectableTrailingReader<T> 的實例。
關於其中實現的細節,請參考《C# 詞法分析器(六)構造詞法分析器》。
全部的詞法單元讀取器,都繼承自 TokenReader<T> 類,主要包含兩個方法:PeekToken 和 ReadToken,與字面意義相同,就是讀取輸入流中的下一個詞法單元,不更改(Peek)或提高(Read)輸入流的字符位置。
TokenReader<T> 類還實現了 IEnumerable<T> 接口,所以可使用 foreach 語句從中讀取詞法單元。可是,TokenReader<T> 自己並不會儲存以前讀取過的詞法單元,在被枚舉的時候,實際上仍是會調用 ReadToken 方法,所以只能在一個位置枚舉 TokenReader<T>,並且只能枚舉一次,枚舉完畢後,TokenReader<T> 也一樣到達了流的結尾。若是但願屢次枚舉,還請緩存到數組中再進行操做。
如下是構造出計算器的詞法單元讀取器,並輸出全部讀取到的詞法單元的代碼:
最後是完整的構造計算器的代碼:
代碼的輸出結果以下圖所示:
能夠看到,最後老是會以特殊值 -1 結束,表示文件結束。
3、自定義詞法分析器
Cyjb.Compilers 項目中,提供了完整的詞法分析器實現。可是,在實際的使用中,不免會遇到各類各樣的需求,可能已實現的詞法分析器是沒法知足的,此時就必須本身完成詞法分析器了。
在完成定義詞法分析器後,能夠從 Grammar<T>.LexerRule 屬性獲取到一個 Cyjb.Compilers.Lexers.LexerRule<T> 對象,該實例中存儲了一個詞法分析器所需的所有信息,而且不會依賴於原始的 Grammar<T> 對象。它就是自定義詞法分析器的核心。
下圖是與 LexerRule<T> 對象相關的類圖。這四個類表示了詞法分析器的核心信息,即生成的 DFA 的數據。
圖 1 與 LexerRule<T> 相關的類圖
LexerRule<T>.CharClass 屬性保存了與字符類相關的數據,這是一個長度爲 65536 的數組,保存了每一個字符所屬的字符類。使用從 0 開始的連續整數表示不一樣的字符類,全部包含的字符類的數量可從 LexerRule<T>.CharClassCount 屬性獲取。關於字符類的詳細信息,請參考《C# 詞法分析器(四)構造 NFA》的第三節「劃分字符類」。
LexerRule<T>.Contexts 屬性保存了與詞法分析器的上下文相關的數據,這是一個字典,其鍵爲上下文的標籤,值爲相應的 DFA 頭節點索引。LexerRule<T>.ContextCount 屬性表示了上下文的數量。
LexerRule<T>.Symbols 屬性是定義的終結符的列表,列表的每一項都是一個 SymbolData<T> 結構,包含終結符的標識符、動做和向前看信息。
LexerRule<T>.States 屬性是詞法分析器的 DFA 狀態的列表,列表的每一項都是一個 StateData 結構,包含相應 DFA 狀態的轉移和對應的終結符索引。這個列表中實際上包含 ContextCount×2 個 DFA,這些 DFA 的首節點索引是從 0 到 ContextCount×2-1,其中每一個上下文對應 2 個 DFA,前一個 DFA 對應於當前上下文中的全部非行首規則,用於從非行首位置進行匹配;後一個 DFA 對應於當前上下文中的全部規則,用於從行首位置進行匹配。索引爲 i 的上下文,對應的兩個 DFA 就是 i*2 和 i*2+1。關於行首和非行首規則的詳細信息,請參考《C# 詞法分析器(四)構造 NFA》的第四節「多條正則表達式、限定符和上下文」。
以上就是詞法分析器所需的信息,只要獲取了這些信息,就能夠根據須要,構造本身的詞法分析器。詳細的實現,請參考《C# 詞法分析器(六)構造詞法分析器》中提供的算法,甚至能夠將數據寫入 .cs 文件中(甚至可使用其它語言實現,由於數據自己是不影響實現的),實現詞法分析器的生成(雖然如今我還仍未實現這點)。
以上的數據,所有是以比較容易理解的形式存儲的,未進行壓縮,因此可能會佔用比較多的空間。在具體的實現中,能夠根據須要改變數據存儲格式,或選用一些壓縮算法(如使用四數組壓縮 DFA 狀態)。