程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#詞法分析器(六)構造詞法分析器

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

編輯:關於C#

現在最核心的 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/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved