1 解析的整體流程
首先是詞法分析器處理字符序列(對應CharStream
類),生成Token流(對應TokenStream
類,這是連接詞法分析和語法分析過程的橋樑)傳給語法分析器,語法分析器再用它檢查語法正確性,然後解析得到語法樹(葉子結點對應TerminalNode
類,非葉子結點對應RuleNode
類)。
在詞法分析之後,不僅要標記一個個Token,還需要記錄這些Token對應的具體內容(比如知道是一個變量,也要記錄變量名是什麼)。ANTLR的做法是不去記錄這個字符串,而是像上圖一樣,首先爲字符流記錄一個個位置號,然後在TokenStream
裏記錄這個Token對應的字符開始和結束的索引。
在生成的語法樹中,葉子結點TerminalNode
就是要記錄這些Token的具體值(的起止索引)了,而對於非葉子結點RuleNode
,則會根據不同的語法規則生成不同的子類,即上篇說到的形如*Context
的類,*
處是.g4
文件中語法規則的名字首字母大寫。
例如,下圖是上面的賦值語句解析出的語法分析樹上各個結點的類。根節點是stat
語法規則(表示語句)生成的StatContxt
類,它的子結點是assign
語法規則(表示賦值)生成的AssignContext
類,它的四個孩子對應了assign
所匹配的四個詞法符號:
對於標識符ID
和具體符號=
和;
都無法繼續展開,因此會作爲樹的葉子,並使用TerminalNode
記錄。表達式expr
可以再展開,在這裏爲了解析100
,它選擇了其中一個與之匹配的分支(整形數值),因此它生成唯一孩子TerminalNode
記錄這個100
。
這些生成的*Context
類(作爲生成的語法Parser的靜態內部類)可以訪問它所對應的詞組中的所有元素(圖中它的子樹)。例如,上圖的AssignContext
類就可以通過ID()
方法訪問標識符子結點(返回值是TerminalNode
類型),通過expr()
方法訪問表達式子樹(返回值是ExprContext
類型)。
2 語法分析器的工作過程
ANTLR根據給出的語法規則,生成一個遞歸下降的語法分析器,當待解析的語法規則有多條分支時,語法分析器會去前瞻詞法符號(不必是LL(1),可以前瞻若干個詞法符號),這個過程和手寫的Parser是類似的,對於:
stat: assign
| ifstat
| whilestat
...
;
這表示語句(stat
)可以是賦值語句(assign
),可以是IF語句(ifstat
),可以是WHILE語句(whilestat
)或者其它語句。僅就這三條而言,它們的第一個詞法符號分別是標識符、IF關鍵字、WHILE關鍵字,因此可以前瞻一個詞法符號解決,解析stat
的邏輯是:
void stat() {
switch(/*當前輸入的詞法符號*/) {
case ID : assign(); break;
case IF : ifstat(); break;
case WHILE : whilestat(); break;
...
default: /*全都匹配不上,拋出異常*/
}
}
3 歧義處理方法
如果可以通過多條分支解析輸入的文本,那麼就說明輸入文本是有歧義的,可以有多種語義去解釋。用戶提供的.g4
文件中不論是Token的匹配還是語法的描述都可能存在歧義。
3.1 語法描述上的歧義
例如:
stat : expr ';' // 表達式語句
| ID '(' ')' ';' // 函數調用語句
;
expr : ID '(' ')'
| INT
;
對於輸入f();
,可以走stat
的第一條分支,將f()
當作一個表達式expr
來解析,此時f();
被認爲是一個表達式語句。也可以走stat
的第二條分支,此時f();
被認爲是一個函數調用語句,其中標識符f
被視爲函數名。
ANTLR解決語法歧義的方法是,匹配所有可匹配分支的第一條。因此對於剛剛的例子,會將f();
作爲表達式解析。
3.2 Token匹配時的歧義
最常見的是語法關鍵字和標識符之間的歧義,例如:
BEGIN : 'begin';
ID : [a-z]+;
這表示BEGIN
關鍵字匹配begin
序列,標識符匹配一個至多個小寫字母序列。ANTLR解決Token歧義的方法是,匹配定義最靠前的語法規則。利用這一點可以自然的保證begin
不能作爲標識符的問題,因爲BEGIN
的聲明就在ID
的前面。
詞法分析器在匹配Token時是貪婪模式的,即會儘可能匹配一個最長的字符串來生成Token,因此beginner
會匹配爲ID
,而不是BEGIN
後面接名爲ner
的標識符。
3.3 語言語法本身的歧義
這裏書中舉了兩個例子。其一是表達式優先級的歧義,如
在Smalltalk裏就是自左向右處理(因此計算出來是9),在其它語言裏*
優先級高於+
(因此計算出來是7)。因此如何隱式指定表達式運算符優先級是一個問題。
另一種是C語言裏的,如i*j;
中*
是乘號還是指針符號,取決於i
的Token是一個表達式還是一個類型(比如int*j;
就是定義指針變量,8*j;
則是一個表達式語句)。也就是說這類歧義要通過檢查上下文信息解決。