經過前幾篇文章的鋪墊,核心的 DFA(確定有限狀態自動機)已經成功構建。最後一步,就是基於這個 DFA 構建出功能完整、可實際投入使用的詞法分析器。雖然目前尚不支援像 Flex 那樣的詞法定義文件,仍需在代碼中定義規則,但基本功能已完全足夠應對大多數場景。
所有規則都在 Grammar<T> 類中定義,泛型參數 T 代表詞法單元標識符的類型(必須為枚舉)。定義規則主要包含三個方法:
調用 DefineContext 定義的上下文由 LexerContext 類表示,包含索引、標籤和類型。上下文類型分為包含型和排除型,對應 Flex 的 %s 和 %x:
默認上下文標籤為 "Initial"。
DefineRegex 等價於 Flex 的定義段,可預定義常用模式簡化規則:
grammar.DefineRegex("digit", "[0-9]");
後續規則中即可使用 {digit} 引用。
DefineSymbol 對應 Flex 的規則段,定義終結符的正則與動作。動作使用 Action<ReaderController<T>>,由 ReaderController<T> 提供核心方法:
Accept 和 Reject 互斥,每次匹配只能調用其一。若兩者均未調用,視為匹配成功但不返回 Token,繼續匹配下一個單元。
採用與 Flex 相同的衝突解決規則:
基礎分析器相當於 DFA 執行器,不斷讀取字符送入 DFA,記錄最後一次接受狀態。當進入死狀態時,最後記錄的接受狀態即為最長匹配。核心邏輯見 SimpleReader<T>:
int lastAccept = -1, lastIndex = Source.Index;
while (true) {
int ch = Source.Read();
if (ch == -1) break; // EOF
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());
}
向前看規則形式為 r = s/t。若 s 或 t 可匹配字符串長度固定,稱為定長向前看。預先計算長度存入 int?[] Trailing 數組:
null:非向前看n:前部 s 長度固定為 n-n:後部 t 長度固定為 n0:長度均不固定匹配後根據 Trailing 值截取實際匹配文本。實現見 FixedTrailingReader<T>。
變長向前看無法確定回退位置,需使用堆棧保存所有接受狀態,沿堆棧向下尋找包含 int.MaxValue - symbolIndex 的狀態(即向前看頭狀態)。注意:若 s 結尾與 t 開頭可匹配同樣字符,可能導致匹配偏差,應盡量避免此類正則。實現見 RejectableTrailingReader<T>:
int target = int.MaxValue - acceptState;
while (true) {
astate = stateStack.Pop();
if (ContainsTrailingHead(astate.SymbolIndex, target)) break;
}
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;
}
Reject 指示跳過當前規則,尋找同樣匹配輸入(或前綴)的次優規則。例如定義多個前綴規則並全部 Reject,最終會輸出所有匹配的前綴。實現需在匹配邏輯中引入回溯機制。
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;
Console.WriteLine(token);
} catch (SourceException se) {
Console.WriteLine(se.Message);
}
}
輸出 Token 序列包含數字、運算符、括號等。
支援普通字符串與逐字字符串(@""):
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}");
利用上下文機制實現轉義解析:
g.DefineContext("str");
g.DefineContext("vstr");
g.DefineSymbol(@""", c => { c.PushContext("str"); textBuilder.Clear(); });
g.DefineSymbol(@"""", c => { c.PushContext("vstr"); textBuilder.Clear(); });
g.DefineSymbol(@""", c => { c.PopContext(); c.Accept(0, textBuilder.ToString(), null); });
g.DefineSymbol(@"\\u[0-9]{4}", c => textBuilder.Append((char)int.Parse(c.Text.Substring(2), NumberStyles.HexNumber)));
// ... 其他轉義序列處理
g.DefineSymbol(@".", c => textBuilder.Append(c.Text));
// 逐字字符串處理類似
輸出即為轉義後的實際字符串內容。
至此,完整的詞法分析器已構建完成。相關代碼可在項目倉庫中找到,基礎類(如輸入緩衝)亦已提供。本系列暫告一段落。