在之前的六篇文章中,我比較詳細的介紹了與詞法分析器相關的算法。它們都比較關注於實現的細節,感覺上可能比較凌亂,本篇就從整體上介紹一下如何定義詞法分析器,以及如何實現自己的詞法分析器。 第二節完整的介紹了如何定義詞法分析器,可以當作一個詞法分析器使用指南。如果不關心詞法分析器的具體實現的話,可以只看第二節。 一、類庫的改變 首先需要說明一下我對類庫做的一些修改。詞法分析部分的接口,與當初寫《C# 詞法分析器》系列時相比,已經發生了不小的改變,有必要做一下說明。 1. 詞法單元的標識符 詞法單元(token)最初的定義是一個 Token 結構,使用一個 int 屬性作為詞法單元的標識符,這也是很多詞法分析器的通用做法。 但後來做語法分析的時候,感覺這樣非常不方便。因為目前還不支持從定義文件生成詞法分析器代碼,只能在程序裡面定義詞法分析器。而 int 本身是不具有語義的,作為詞法單元的標識符來使用,不但不方便還容易出錯。 後來嘗試過使用字符串作為標識符,雖然解決了語義的問題,但仍然容易出錯,實現上也會復雜些(需要保存字符串字典)。 而既簡單,又具有語義的解決方案,就是使用枚舉了。枚舉名稱提供了語義,枚舉值又可以轉換為整數,而且還能夠提供編譯期檢查,完全避免了拼寫錯誤,所以現在的詞法單元便定義為 Token<T> 類,與之相關的很多類也同樣帶上了泛型參數 T。 2. 命名空間 之前的命名空間是 Cyjb.Compiler 和 Cyjb.Compiler.Lexer,現在被改成了 Cyjb.Compilers 和 Cyjb.Compilers.Lexers,畢竟命名空間名稱還是比較適合使用復數。 3. 詞法分析器上下文 之前對詞法分析器上下文的切換,可以使用上下文的索引、標簽或 LexerContext 實例本身。但現在只能夠通過標簽進行切換,這樣實現起來更簡單些,使用上也不會受到過多影響。 4. DFA 的表示 原先 LexerRule 類中對 DFA 的表示有些簡單粗暴,對於不了解具體實現的人來說,很難理解 DFA 的表示。現在重新規劃了 LexerRule<T> 類中的接口,理解起來會更容易些。 二、定義詞法分析器 這一節是 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 結束,表示文件結束。 三、自定義詞法分析器 Cyjb.Compilers 項目中,提供了完整的詞法分析器實現。但是,在實際的使用中,難免會遇到各種各樣的需求,可能已實現的詞法分析器是無法滿足的,此時就必須自己完成詞法分析器了。 在完成定義詞法分析器後,可以從 Grammar<T>.LexerRule 屬性獲取到一個 Cyjb.Compilers.Lexers.LexerRule<T> 對象,該實例中存儲了一個詞法分析器所需的全部信息,並且不會依賴於原始的 Grammar<T> 對象。它就是自定義詞法分析器的核心。 下圖是與 LexerRule<T> 對象相關的類圖。這四個類表示了詞法分析器的核心信息,即生成的 DFA 的數據。