C# 詞法分析器(六)構造詞法分析器

系列導航html

  1. (一)詞法分析介紹
  2. (二)輸入緩衝和代碼定位
  3. (三)正則表達式
  4. (四)構造 NFA
  5. (五)轉換 DFA
  6. (六)構造詞法分析器
  7. (七)總結

如今最核心的 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 相同的規則:

  1. 老是選擇最長的匹配。
  2. 若是最長的匹配與多個正則表達式匹配,老是選擇先被定義的正則表達式。

基本的詞法分析器很是簡單,它只能實現最基礎的詞法分析器功能,不能支持向前看符號和 Reject 動做,可是大部分狀況下,這就足夠了。

這樣的詞法分析器幾乎至關於一個 DFA 執行器,只要不斷從輸入流中讀取字符送入 DFA 引擎,並記錄下來最後一次出現的接受狀態就能夠了。當 DFA 引擎到達了死狀態,找到的詞素就是最後一次出現的接受狀態對應的符號(這樣就能保證找到的詞素是最長的),對應多個符號的時候只取第一個(以前已經將符號索引從小到大進行了排序,所以第一個符號就是最早定義的符號)。

簡單的算法以下:

輸入:DFA DD s=s0s=s0 while (c != eof) { s=D[c]s=D[c] if (sFinalStatess∈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() 方法修改了要返回的詞法單元,並且因爲涉及到多重轉義,在設計規則的時候必定要注意雙引號和反斜槓的個數。

如今,完整的詞法分析器已經成功構造出來,本系列暫時就到這裏了。相關代碼均可以在這裏找到,一些基礎類(如輸入緩衝)則在這裏

相關文章
相關標籤/搜索