現在最核心的 DFA 已經成功構造出來了,最後一步就是根據 DFA 得到完整的詞法分析器。
由於目前還不能像 Flex 那樣支持詞法定義文件,所以仍然需要在程序中定義規則,而且也不能非常靈活的自定義詞法分析器,不過基本的東西完全夠用了。
一、詞法規則的定義
詞法分析器用到的所有規則都在 Grammar<T> 類中定義,這裡的泛型參數 T 表示詞法分析器的標識符的類型(必須是一個枚舉類型)。定義規則方法包括:定義上下文的 DefineContext 方法、定義正則表達式的 DefineRegex 方法和定義終結符的 DefineSymbol 方法。
調用 DefineContext 方法定義的詞法分析器上下文,會使用 LexerContext 類的實例表示,它的基本定義如下所示:
// 當前上下文的索引。 int Index; // 當前上下文的標簽。 string Label; // 當前上下文的類型。 LexerContextType ContextType;
在詞法分析器中,僅可以通過標簽來切換上下文,因此 LexerContext 類本身被設置為 internal。
上下文的類型就是包含型或者排除型,等價於 Flex 中的 %s 和 %x 定義(可參見 Flex 的 Start Conditions)。這裡簡單的解釋下,在進行詞法分析時,如果當前上下文是排除型的,那麼僅在當前上下文中定義的規則會被激活,其它的(非當前上下文中定義的)規則都會失效。如果當前上下文是包含型的,那麼沒有指定任何上下文的規則也會被激活。
默認上下文標簽為 "Initial"。
Grammar<T> 中定義正則表達式的 DefineRegex 方法,就等價於 Flex 中的定義段(Definitions Section),可以定義一些常見的正則表達式以簡化規則的定義,例如可以定義
grammar.DefineRegex("digit", "[0-9]");
在正則表達式的定義中,就可以直接使用 "{digit}" 來引用預先定義的正則表達式。
最後是定義終結符的 DefineSymbol 方法,就對應於 Flex 中的規則段(Rules Section),可以定義終結符的正則表達式和相應的動作。
終結符的動作使用 Action<ReaderController<T>> 來表示,由 ReaderController<T> 類來提供 Accept,Reject,More 等方法。
其中,Accept 方法會接受當前詞法單元,並返回 Token 對象。Reject 方法會拒絕當前匹配,轉而尋找次優的規則,這個操作會使詞法分析器的所有匹配變慢,需要謹慎使用。More 方法通知詞法分析器,下次匹配成功時,不替換當前的文本,而是把新的匹配追加在後面。
Accept 方法和 Reject 方法是相互沖突的,每次匹配成功只能調用其中的一個。如果兩個都未調用,那麼詞法分析器會認為當前匹配是成功的,但不會返回 Token,而是繼續匹配下一個詞法單元。
二、詞法分析器的實現
2.1 基本的詞法分析器
由於多個規則間是可能產生沖突的,例如字符串可以與多個正則表達式匹配,因此在說明詞法分析器之前,首先需要定義一個解決沖突的規則。這裡采用與 Flex 相同的規則:
總是選擇最長的匹配。
如果最長的匹配與多個正則表達式匹配,總是選擇先被定義的正則表達式。
基本的詞法分析器非常簡單,它只能實現最基礎的詞法分析器功能,不能支持向前看符號和 Reject 動作,但是大部分情況下,這就足夠了。
這樣的詞法分析器幾乎相當於一個 DFA 執行器,只要不斷從輸入流中讀取字符送入 DFA 引擎,並記錄下來最後一次出現的接受狀態就可以了。當 DFA 引擎到達了死狀態,找到的詞素就是最後一次出現的接受狀態對應的符號(這樣就能保證找到的詞素是最長的),對應多個符號的時候只取第一個(之前已經將符號索引從小到大進行了排序,因此第一個符號就是最先定義的符號)。
簡單的算法如下:
輸入:DFA D
s=s0
while (c != eof) {
s=D[c]
if (s∈FinalStates) {
slast=s
}
c = nextChar();
}
slast 即為匹配的詞素
實現該算法的代碼可見 SimpleReader<T> 類,核心代碼如下:
// 最後一次匹配的符號和文本索引。 int lastAccept = -1, lastIndex = Source.Index; while (true) { int ch = Source.Read(); if (ch == -1) { // End Of File。 break; } state = base.LexerRule.Transitions[state, base.LexerRule.CharClass[ch]]; if (state == LexerRule.DeadState) { // 沒有合適的轉移,退出。 break; } int[] symbolIndex = base.LexerRule.SymbolIndex[state]; if (symbolIndex.Length > 0) { lastAccept = symbolIndex[0]; lastIndex = Source.Index; } } if (lastAccept >= 0) { // 將流調整到與接受狀態匹配的狀態。 Source.Unget(Source.Index - lastIndex); DoAction(base.LexerRule.Actions[lastAccept], lastAccept, Source.Accept()); }
2.2 支持定長的向前看符號的詞法分析器
接下來,將上面的基本的詞法分析器進行擴展,讓它支持定長的向前看符號。
向前看符號的規則形式為 r=s/t,如果 s 或 t 可以匹配的字符串長度是固定的,就稱作定長的向前看符號;如果都不是固定的,則稱為變長的向前看符號。
例如正則表達式 abcd 或者 [a-z]{2},它們可以匹配的字符串長度是固定的,分別為 4 和 2;而正則表達式 [0-9]+ 可以匹配的字符串長度就是不固定的,只要是大於等於一都是可能的。
區分定長和變長的向前看符號,是因為定長的向前看符號匹配起來更容易。例如正則表達式 a\*/bcd,識別出該模式後,直接回退三個字符,就找到了 a* 的結束位置。
對於規則 abc/d*,識別出該模式後,直接回退到只剩下三個字符,就找到了 abc 的結束位置。
我將向前看符號可以匹配的字符串長度預先計算出來,存儲在 int?[] Trailing 數組中,其中 null 表示不是向前看符號,正數表示前面(s)的長度固定,負數表示後面(t)的長度固定,0 表示長度都不固定。
所以,只需要在正常的匹配之後,判斷 Trailing 的值。如果為 null,不是向前看符號,不用做任何操作;如果是正數 n,則把當前匹配的字符串的前 n 位取出來作為實際匹配的字符串;如果是負數 -n,則把後 n 位取出來作為實際匹配的字符串。實現的代碼可見 FixedTrailingReader<T> 類。
2.3 支持變長的向前看符號的詞法分析器
對於變長的向前看符號,處理起來則要更復雜些。因為不能確定向前看的頭是在哪裡(並沒有一個確定的長度),所以必須使用堆棧保存所有遇到的接受狀態,並沿著堆棧向下找,直到找到包含 int.MaxValue - symbolIndex 的狀態(我就是這麼區分向前看的頭狀態的,可參見上一篇《C# 詞法分析器(五)轉換 DFA》的 2.4 節 DFA 狀態的符號索引)。
需要注意的是,變長的向前看符號是有限制的,例如正則表達式 ab\*/bcd\*,這時無法准確的找到 ab\* 的結束位置,而是會找到最後一個 b 的位置,導致最終匹配的詞素不是想要的那個。出現這種情況的原因是使用 DFA 進行字符串匹配的限制,只要是前一部分的結尾與後一部分的開頭匹配,就會出現這種問題,所以要盡量避免定義這樣的正則表達式。
實現的代碼可見 RejectableTrailingReader<T> 類,沿著狀態堆棧尋找目標向前看的頭狀態的代碼如下:
// stateStack 是狀態堆棧 int target = int.MaxValue - acceptState; while (true) { astate = stateStack.Pop(); if (ContainsTrailingHead(astate.SymbolIndex, target)) { // 找到了目標狀態。 break; } } // ContainsTrailingHead 方法利用符號索引的有序,避免了很多不必要的比較。 bool ContainsTrailingHead(int[] symbolIndex, int target) { // 在當前狀態中查找,從後向前找。 for (int i = symbolIndex.Length - 1; i >= 0; i--) { int idx = symbolIndex[i]; // 前面的狀態已經不可能是向前看頭狀態了,所以直接退出。 if (idx < base.LexerRule.SymbolCount) { break; } if (idx == target) { return true; } } return false; }
在沿著堆棧尋找向前看頭狀態的時候,不必擔心找不到這樣的狀態,DFA 執行時,向前看的頭狀態一定會在向前看狀態之前出現。
2.4 支持 Reject 動過的詞法分析器
Reject 動作會指示詞法分析器跳過當前匹配規則,而去尋找同樣匹配輸入(或者是輸入的前綴)的次優規則。
比如下面的例子:
g.DefineSymbol("a", c => { Console.WriteLine(c.Text); c.Reject(); }); g.DefineSymbol("ab", c => { Console.WriteLine(c.Text); c.Reject(); }); g.DefineSymbol("abc", c => { Console.WriteLine(c.Text); c.Reject(); }); g.DefineSymbol("abcd", c => { Console.WriteLine(c.Text); c.Reject(); }); g.DefineSymbol("bcd", c => { Console.WriteLine(c.Text); }); g.DefineSymbol(".", c => { });
對字符串 "abcd" 進行匹配,最後輸出的結果是:
abcd
abc
ab
a
bcd
查看本欄目
三、一些詞法分析的例子
接下來,我會給出一些詞法分析器的實際用法,可以作為參考。
3.1 計算器
我首先給出一個計算器詞法分析程序的完整代碼,之後的示例就只包含規則的定義了。
Grammar g = new Grammar(); g.DefineSymbol("[0-9]+"); g.DefineSymbol("+"); g.DefineSymbol("-"); g.DefineSymbol("*"); g.DefineSymbol("/"); g.DefineSymbol("^"); g.DefineSymbol("("); g.DefineSymbol(")"); // 吃掉所有空白。 g.DefineSymbol("s", c => { }); LexerRule lexer = g.CreateLexer(); string text = "456 + (98 - 56) * 89 / -845 + 2^3"; TokenReader reader = lexer.GetReader(text); while (true) { try { Token token = reader.ReadToken(); if (token.Index == Token.EndOfFileIndex) { break; } else { Console.WriteLine(token); } } catch (SourceException se) { Console.WriteLine(se.Message); } } // 輸出為: Token #0 456 Token #1 + Token #6 ( Token #0 98 Token #2 - Token #0 56 Token #7 ) Token #3 * Token #0 89 Token #4 / Token #2 - Token #0 845 Token #1 + Token #0 2 Token #5 ^ Token #0 3
3.2 字符串
下面的例子可以匹配任意的字符串,包括普通字符串和逐字字符串(@"" 這樣的字符串)。由於代碼中的字符串用的都是逐字字符串,所以雙引號比較多,一定要數清楚個數。
g.DefineRegex("regular_string_character", @"[^""\\\n\r\u0085\u2028\u2029]|(\\.)");
g.DefineRegex("regular_string_literal", @"\""{regular_string_character}*\""");
g.DefineRegex("verbatim_string_characters", @"[^""]|\""\""");
g.DefineRegex("verbatim_string_literal", @"@\""{verbatim_string_characters}*\""");
g.DefineSymbol("{regular_string_literal}|{verbatim_string_literal}");
string text = @"""abcd\n\r""""aabb\""ccd\u0045\x47""@""abcd\n\r""@""aabb\""""ccd\u0045\x47""";
// 輸出為:
Token #0 "abcd\n\r"
Token #0 "aabb\"ccd\u0045\x47"
Token #0 @"abcd\n\r"
Token #0 @"aabb\""ccd\u0045\x47"
3.3 轉義的字符串
下面的例子利用了上下文,不但可以匹配任意的字符串,同時還可以對字符串進行轉義。
g.DefineContext("str");
g.DefineContext("vstr");
g.DefineSymbol(@"\""", c => { c.PushContext("str"); textBuilder.Clear(); });
g.DefineSymbol(@"@\""", c => { c.PushContext("vstr"); textBuilder.Clear(); });
g.DefineSymbol(@"<str>\""", c => { c.PopContext(); c.Accept(0, textBuilder.ToString(), null); });
g.DefineSymbol(@"<str>\\u[0-9]{4}", c =>
textBuilder.Append((char)int.Parse(c.Text.Substring(2), NumberStyles.HexNumber)));
g.DefineSymbol(@"<str>\\x[0-9]{2}", c =>
textBuilder.Append((char)int.Parse(c.Text.Substring(2), NumberStyles.HexNumber)));
g.DefineSymbol(@"<str>\\n", c => textBuilder.Append('\n'));
g.DefineSymbol(@"<str>\\\""", c => textBuilder.Append('\"'));
g.DefineSymbol(@"<str>\\r", c => textBuilder.Append('\r'));
g.DefineSymbol(@"<str>.", c => textBuilder.Append(c.Text));
g.DefineSymbol(@"<vstr>\""", c => { c.PopContext(); c.Accept(0, textBuilder.ToString(), null); });
g.DefineSymbol(@"<vstr>\""\""", c => textBuilder.Append('"'));
g.DefineSymbol(@"<vstr>.", c => textBuilder.Append(c.Text));
string text = @"""abcd\n\r""""aabb\""ccd\u0045\x47""@""abcd\n\r""@""aabb\""""ccd\u0045\x47""";
// 輸出為:
Token #0 abcd
Token #0 aabb"ccdEG
Token #0 abcd\n\r
Token #0 aabb\"ccd\u0045\x47
可以看到,這裡的輸出結果,恰好是 3.2 節的輸出結果轉義之後的結果。需要注意的是,這裡利用 c.Accept() 方法修改了要返回的詞法單元,而且由於涉及到多重轉義,在設計規則的時候一定要注意雙引號和反斜槓的個數。
現在,完整的詞法分析器已經成功構造出來,本系列暫時就到這裡了。相關代碼都可以在這裡找到,一些基礎類(如輸入緩沖)則在這裡。
作者:CYJB
出處:http://www.cnblogs.com/cyjb/