系列導航html
如今最核心的 DFA 已經成功構造出來了,最後一步就是根據 DFA 獲得完整的詞法分析器。git
因爲目前還不能像 Flex 那樣支持詞法定義文件,因此仍然須要在程序中定義規則,並且也不能很是靈活的自定義詞法分析器,不過基本的東西徹底夠用了。github
1、詞法規則的定義
詞法分析器用到的全部規則都在 Grammar<T> 類中定義,這裏的泛型參數 T 表示詞法分析器的標識符的類型(必須是一個枚舉類型)。定義規則方法包括:定義上下文的 DefineContext 方法、定義正則表達式的 DefineRegex 方法和定義終結符的 DefineSymbol 方法。正則表達式
調用 DefineContext 方法定義的詞法分析器上下文,會使用 LexerContext 類的實例表示,它的基本定義以下所示:算法
1
2
3
4
5
6
|
// 當前上下文的索引。
int
Index;
// 當前上下文的標籤。
string
Label;
// 當前上下文的類型。
LexerContextType ContextType;
|
在詞法分析器中,僅能夠經過標籤來切換上下文,所以 LexerContext 類自己被設置爲 internal。數組
上下文的類型就是包含型或者排除型,等價於 Flex 中的 %s 和 %x 定義(可參見 Flex 的 Start Conditions)。這裏簡單的解釋下,在進行詞法分析時,若是當前上下文是排除型的,那麼僅在當前上下文中定義的規則會被激活,其它的(非當前上下文中定義的)規則都會失效。若是當前上下文是包含型的,那麼沒有指定任何上下文的規則也會被激活。post
默認上下文標籤爲 "Initial"。flex
Grammar<T> 中定義正則表達式的 DefineRegex 方法,就等價於 Flex 中的定義段(Definitions Section),能夠定義一些常見的正則表達式以簡化規則的定義,例如能夠定義ui
1
|
grammar.DefineRegex(
"digit"
,
"[0-9]"
);
|
在正則表達式的定義中,就能夠直接使用 "{digit}" 來引用預先定義的正則表達式。atom
最後是定義終結符的 DefineSymbol 方法,就對應於 Flex 中的規則段(Rules Section),能夠定義終結符的正則表達式和相應的動做。
終結符的動做使用 Action<ReaderController<T>> 來表示,由 ReaderController<T> 類來提供 Accept,Reject,More 等方法。
其中,Accept 方法會接受當前詞法單元,並返回 Token 對象。Reject 方法會拒絕當前匹配,轉而尋找次優的規則,這個操做會使詞法分析器的全部匹配變慢,須要謹慎使用。More 方法通知詞法分析器,下次匹配成功時,不替換當前的文本,而是把新的匹配追加在後面。
Accept 方法和 Reject 方法是相互衝突的,每次匹配成功只能調用其中的一個。若是兩個都未調用,那麼詞法分析器會認爲當前匹配是成功的,但不會返回 Token,而是繼續匹配下一個詞法單元。
2、詞法分析器的實現
2.1 基本的詞法分析器
因爲多個規則間是可能產生衝突的,例如字符串能夠與多個正則表達式匹配,所以在說明詞法分析器以前,首先須要定義一個解決衝突的規則。這裏採用與 Flex 相同的規則:
- 老是選擇最長的匹配。
- 若是最長的匹配與多個正則表達式匹配,老是選擇先被定義的正則表達式。
基本的詞法分析器很是簡單,它只能實現最基礎的詞法分析器功能,不能支持向前看符號和 Reject 動做,可是大部分狀況下,這就足夠了。
這樣的詞法分析器幾乎至關於一個 DFA 執行器,只要不斷從輸入流中讀取字符送入 DFA 引擎,並記錄下來最後一次出現的接受狀態就能夠了。當 DFA 引擎到達了死狀態,找到的詞素就是最後一次出現的接受狀態對應的符號(這樣就能保證找到的詞素是最長的),對應多個符號的時候只取第一個(以前已經將符號索引從小到大進行了排序,所以第一個符號就是最早定義的符號)。
簡單的算法以下:
輸入:DFA DD s=s0s=s0 while (c != eof) { s=D[c]s=D[c] if (s∈FinalStatess∈FinalStates) { slast=sslast=s } c = nextChar(); } slastslast 即爲匹配的詞素
實現該算法的代碼可見 SimpleReader<T> 類,核心代碼以下:
2.2 支持定長的向前看符號的詞法分析器
接下來,將上面的基本的詞法分析器進行擴展,讓它支持定長的向前看符號。
向前看符號的規則形式爲 r=s/tr=s/t,若是 ss 或 tt 能夠匹配的字符串長度是固定的,就稱做定長的向前看符號;若是都不是固定的,則稱爲變長的向前看符號。
例如正則表達式 abcd 或者 [a-z]{2},它們能夠匹配的字符串長度是固定的,分別爲 4 和 2;而正則表達式 [0-9]+ 能夠匹配的字符串長度就是不固定的,只要是大於等於一都是可能的。
區分定長和變長的向前看符號,是由於定長的向前看符號匹配起來更容易。例如正則表達式 a\*/bcd,識別出該模式後,直接回退三個字符,就找到了 a* 的結束位置。
對於規則 abc/d*,識別出該模式後,直接回退到只剩下三個字符,就找到了 abc 的結束位置。
我將向前看符號能夠匹配的字符串長度預先計算出來,存儲在 int?[] Trailing 數組中,其中 null 表示不是向前看符號,正數表示前面(ss)的長度固定,負數表示後面(tt)的長度固定,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> 類,沿着狀態堆棧尋找目標向前看的頭狀態的代碼以下:
在沿着堆棧尋找向前看頭狀態的時候,沒必要擔憂找不到這樣的狀態,DFA 執行時,向前看的頭狀態必定會在向前看狀態以前出現。
2.4 支持 Reject 動過的詞法分析器
Reject 動做會指示詞法分析器跳過當前匹配規則,而去尋找一樣匹配輸入(或者是輸入的前綴)的次優規則。
好比下面的例子:
1
2
3
4
5
6
|
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
具體的匹配過程以下所示:
第一次匹配了第 4 個規則 "abcd",而後輸出字符串 "abcd",並 Reject。
因此詞法分析器會嘗試次優規則,即第 3 個規則 "abc",而後輸出字符串 "abc",並 Reject。
接下來繼續嘗試次優規則,即第 2 個規則 "ab",而後輸出字符串 "ab",並 Reject。
繼續嘗試次優規則,即第 1 個規則 "a",而後輸出字符串 "a",並 Reject。
而後,繼續嘗試次優規則,即第 6 個規則 ".",此時字符串 "a" 被成功匹配。
最後,剩下的字符串 "bcd" 剛好與規則 5 匹配,因此直接輸出 "bcd"。
在實現上,爲了作到這一點,一樣須要使用堆棧來保存全部遇到的接受狀態,若是當前匹配被 Reject,就沿着堆棧找到次優的匹配。實現的代碼可見 RejectableReader<T> 類。
上面這四個小節,說明了詞法分析器的基本結構,和一些功能的實現。實現了全部功能的詞法分析器實現可見 RejectableTrailingReader<T> 類。
3、一些詞法分析的例子
接下來,我會給出一些詞法分析器的實際用法,能夠做爲參考。
3.1 計算器
我首先給出一個計算器詞法分析程序的完整代碼,以後的示例就只包含規則的定義了。
3.2 字符串
下面的例子能夠匹配任意的字符串,包括普通字符串和逐字字符串(@"" 這樣的字符串)。因爲代碼中的字符串用的都是逐字字符串,因此雙引號比較多,必定要數清楚個數。
1
2
3
4
5
6
7
8
9
10
11
|
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 轉義的字符串
下面的例子利用了上下文,不但能夠匹配任意的字符串,同時還能夠對字符串進行轉義。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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() 方法修改了要返回的詞法單元,並且因爲涉及到多重轉義,在設計規則的時候必定要注意雙引號和反斜槓的個數。
如今,完整的詞法分析器已經成功構造出來,本系列暫時就到這裏了。相關代碼均可以在這裏找到,一些基礎類(如輸入緩衝)則在這裏。