第五章 嵌入文法的Actions
在ANTLR中詞法規則和語法規則都是一些上下文無關的規則,它們不能滿足語法分析中的一些高級需求或特殊需求。如:我們可能要判斷一個表達式中的變量之前是否定義了,沒有定義要給出編譯錯誤信息,或者我們要在語法分析時記錄一些信息以後使用。這時我們就要在文法中嵌入Actions。Actions就是嵌入文法中的程序代碼,它與我們在asp或jsp頁面html代碼中嵌入VB和java的代碼非常類似,在生成代碼後actions會在恰當的位置起到恰到好處的作用。在ANTLR文法中嵌入的程序代碼要用Options{ language= }的設置項的目標語言編寫,ANTLR並不會去過多分析actions中的代碼這全靠編寫文法的人自己控制。
5.1嵌入簡單的代碼
ANTLR中Action必須寫在“{}”花括號中,其中可以有一行或多行代碼。我們首先在文法中加入簡單的Action編寫一個示例,下面有一個變量定義的文法,在variable規則的結束符“;”之前加入了{System.out.println("a action of variable");}用於輸出一行字符。
grammar SimpleAction;
variable : type ID ';' {System.out.println("a action of variable");};
type : 'int' | 'float';
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
運行代碼如下:
ANTLRInputStream input = new ANTLRInputStream(System.in);
SimpleActionLexer lexer = new SimpleActionLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
SimpleActionParser parser = new SimpleActionParser(tokens);
parser.variable();
程序運行後輸入:int x; 輸出:a action of variable
本示例沒有讓語法分析程序生成語法樹,System.out.println方法是在分析程序執行到該處時被執行的,所以我們可以直接看到效果。
5.2 Actions中的符號引用
在Action中可以引用規則符號來獲得符號當前的信息,如:我們將上例中的Action修改一下來獲得變量的具體和變量名。
variable : type ID ';' {System.out.println($type.text + " " + $ID.text);};
程序運行後輸入:int x; 輸出:int x。
引用規則符號時使用字符“$”加上符號名來表示,“$”是一個標識,表明了它是一個符號引用而不是普通的嵌入式代碼。
5.3生成的分析器代碼
爲了保證本章後面的學習,現在有必要講解一下ANTLR生成的代碼與文法之間的大概關係。第一章的示例中就涉及到了語法樹,第二章也講了自頂向下推導樹。ANTLR是一個自頂向下的分析工具,從樹的根節點向下,按照左子樹到右子樹的深度遍歷的順序進行分析。下面看一個簡化的表達式文法。
expr : subExpr '+' subExpr | subExpr;
subExpr : ATOM '*' ATOM | ATOM;
ATOM : '0'.. '9';
(左) |
expr |
subExpr |
subExpr |
+ |
atom |
atom |
atom |
3 |
4 |
5 |
* |
(右) |
atom() |
subExpr() |
subExpr() |
3 |
atom() |
atom() |
+ |
expr() |
* |
5 |
4 |
我們用這個文法來分析一個表達式:3 * 4 + 5,分析其推導樹看圖4.1(左)。
圖4.1
ANTLR用自頂向下的分析方法從語法樹的根節通過輸入的字符判斷生成推導樹,向推導樹的子節點分析伸展,直到匹配終結字符。匹配終結字符後會向樹的父節點返回,如圖4.1(左)。這種下推回代的過程在數據結構中要用棧來保存臨時信息,下推時壓棧返回時出棧。ANTLR使用了函數調用的方式來簡潔地實現了這個過程,因爲計算機語言中函數調用時系統會自動壓棧,函數執行結束返回時系統會自動出棧。這樣ANTLR就沒有必要自己去實現這樣的一個棧的構造。如圖4.1(右)讓我們把生成推導樹的過程想象成一些函數之間調用的過程。
ANTLR在生成分析器代碼時把每一個規則(包括語法規則和詞法規則)生成同名的函數,我們可以方便地叫它規則函數。例如這個簡單的expr示例生成的代碼大致爲:
public final void expr() throws RecognitionException {
try {
……//經過詞法分析程序對輸入字符的判斷,決定分支
case 1 :// subExpr '+' subExpr
subExpr();
match(input,5,FOLLOW_5_in_expr12); //匹配 ‘+’
subExpr();
break;
case 2 : // subExpr
subExpr();
break;
} catch (RecognitionException re) {…… }
finally { }
}
public final void subExpr() throws RecognitionException {
try {
……//經過詞法分析程序對輸入字符的判斷,決定分支
case 1 : // ATOM '*' ATOM
{
match(input,ATOM,FOLLOW_ATOM_in_subExpr25);
match(input,6,FOLLOW_6_in_subExpr27); //匹配‘*’
match(input,ATOM,FOLLOW_ATOM_in_subExpr29);
} break;
case 2 :// ATOM
{
match(input,ATOM,FOLLOW_ATOM_in_subExpr33);
} break;
}
}
catch (RecognitionException re) {…… }
finally { }
}
public final void mATOM() throws RecognitionException {
try {
int _type = ATOM;
{ matchRange('0','9'); }
this.type = _type;
}
finally { }
}
expr()函數和subExpr()函數存在於SimpleExprParser類中,mATOM()函數存在於SimpleExprLexer類中,詞法規則生成的方法名前增加了一個“m”字符。
ANTLR生成代碼時我們加入的Actions也就嵌入了生成的代碼之中。前面的示例variable : type ID ';' {System.out.println("a action of variable");}生成的代碼爲:
public final void variable() throws RecognitionException{
try {
……
match(input,ID,FOLLOW_ID_in_variable12);
match(input,6,FOLLOW_6_in_variable14);
System.out.println("a action of variable");
}
} catch
……
}
Action中引用規則符號的示例中variable : type ID ';' {System.out.println ($type.text + " " + $ID.text);};生成的代碼爲:
public final void variable() throws RecognitionException {
Token ID2=null;
type_return type1 = null;
try {
pushFollow(FOLLOW_type_in_variable10);
type1=type();
_fsp--;
ID2=(Token)input.LT(1);
match(input,ID,FOLLOW_ID_in_variable12);
match(input,6,FOLLOW_6_in_variable14);
System.out.println(input.toString(type1.start,type1.stop) + " " + ID2.getText());
}
catch (RecognitionException re) {
reportError(re);
recover(input,re);
}
finally { }
return ;
}
原action中的$type.text使生成的代碼與第一個示例相比生成了變化,在上面的的代碼中已用黑體字提示出來。type()函數返回了一個type_return類型的對象,對象名爲叫type1,而原來Action中的$type.text變成了input.toString(type1.start,type1.stop)。關於$ID.text在生成的代碼中首先用(Token)input.LT(1)返回一個Token對象名爲ID2,原來Action中的$ID.text變成了ID2.getText()。可以看出語法規則type和詞法規則ID生成的代碼是不同的。
abstract public class Token { abstract String getText();//符號內容。 abstract void setText(String text); abstract int getType();//符號在分析程序中相對應的整數值。 abstract void setType(int ttype); abstract int getLine();//符號所在的行數。 abstract void setLine(int line); abstract int getCharPositionInLine();//符號所在的列數。 abstract void setCharPositionInLine(int pos); abstract int getChannel();//符號所在的頻道,對應CHANNEL設置。 abstract void setChannel(int channel); abstract int getTokenIndex();//符號在輸入中的位置。 abstract void setTokenIndex(int index); } |
我們先解釋一個語法規則中$ID.text對應的代碼,代碼中的input就是運行示例的main函數中CommonTokenStream tokens = new CommonTokenStream(lexer);語句中的tokens對象。它是詞法分析後生成的記號流,記號流曾經在第二章開始時講過,我們這裏不再重複。input.LT(1)功能是從記號流中獲得下一個詞法符號,由於variable規則第一個要匹配符號是type類型標識符,第二個要匹配的是ID標識符,所以分析程序先調用type(),並認爲type()返回的對象是type類型標識符(我們後面再講type()函數中的情況)。在typ1=type()之後分析程序再次調用input.LT(1),認爲其返回的對象是ID標識符並交給ID2變量。接下來分析程序調用match()方法檢驗這兩個符號是不是要匹配的符號,如果不是會報出語法分析錯誤。我們在Action中所寫的$ID就是ID2是Taken類型的對象,ID2調用了它的getText()方法來取得匹配ID標識符名。下面給出Taken類和它的屬性。
在C#中Token是一個接口。
public interface IToken
{
int Channel {get; set;}
int CharPositionInLine {get; set;}
int Line {get; set;}
string Text {get; set;}
int TokenIndex {get; set;}
int Type {get; set;}
}
有關ANTLR類的說明請參見http://www.antlr.org/api/Java官方網址。
要想弄清楚語法規則$type.text是對應的代碼,我們還要看一下生成的type()函數代碼。
public type_return type()
{
type_return retval = new type_return();
retval.start = input.LT(1);
try {
if ( (input.LA(1) >= 7 && input.LA(1) <= 8) )
{ input.Consume();
errorRecovery = false;
} else {
MismatchedSetException mse = new
MismatchedSetException(null,input);
RecoverFromMismatchedSet(input,mse,FOLLOW_set_in_type0);
throw mse;
}
retval.stop = input.LT(-1);
} catch (RecognitionException re) {
ReportError(re);
Recover(input,re);
} finally { }
return retval;
}
public class type_return : ParserRuleReturnScope { };
type()函數中與$type.text有關的代碼已經用黑體字標記出來。函數中先創建了type()函數的返回值實例type_return retval = new type_return(),retval對象作爲返回值賦給了variable()函數中的type1對象。type_return類型也在生成的分析程序代碼中下面是type_return類與其基類的代碼。type_return類繼承了ParserRuleReturnScope類,而ParserRuleReturnScope類又繼承了RuleReturnScope類。其中ParserRuleReturnScope類與RuleReturnScope 類是ANTLR中定義的類(下面看一下C#語言的定義)。
public class ParserRuleReturnScope : RuleReturnScope
{ public IToken start; //規則匹配的第一個詞法符號
public IToken stop; //規則匹配的第一個詞法符號
public ParserRuleReturnScope();
}
public class RuleReturnScope
{ public RuleReturnScope();
public virtual object Start { get; }
public virtual object Stop { get; }
public virtual object Template { get; }
public virtual object Tree { get; }//規則生成的語法樹
}
在variable()函數中$type.text變成了input.toString(type1.start,type1.stop),toString(Token, Token)方法是將一個語法規則匹配的第一個詞法符號和最後一個詞法記號之間所有匹配的輸入內容轉換成一個字符串。語法分析程序利用這個方法可以獲得type規則所匹配的類型信息。
5.4 使用變量
除了直接使用“$”符號加規則名的方法引用規則以外,ANTLR文法可以將規則符號賦值到一個變量中,然後引用變量就等於引用規則符號。修改SimpleAction文法:
variable : t=type id=ID ';'
{System.out.println("type: " + $t.text + " ID: " + $id.text);};
對變量的引用是使用“$”符號再加上變量名的形式。下面看一下生成的代碼:
public void variable() // throws RecognitionException [1]
{ IToken id = null;
type_return t = null;
……
t = type();
id = (IToken)input.LT(1);
System.out.println(input.ToString(t.start,t.stop) + " " + id.Text);
……
5.5 +=的用法
修改SimpleAction文法,使能夠在類型標識符後可以定義多個變量。這文法中加入了“+=”操作符功能是將所有定義的變量收集到一個集合當中。
grammar SimpleAction;
variable : type ids+=ID (',' ids+=ID)* ';'
{ System.out.println($type.text);
for(Object t : $ids)
System.out.print(" " + ((Token)t).getText()); }
;
運行此示例輸入:int x, y, z; 輸出:int x y z 我們來看一下生成的代碼。
public final void variable() throws RecognitionException {
Token ids=null;
List list_ids=null;
type_return type1 = null;
try {
pushFollow(FOLLOW_type_in_variable9);
type1=type();
_fsp--;
ids=(Token)input.LT(1);
match(input,ID,FOLLOW_ID_in_variable13);
if (list_ids==null) list_ids=new ArrayList();
list_ids.add(ids);
loop1:
do{ int alt1=2;
int LA1_0 = input.LA(1); if ( (LA1_0==6) ) { alt1=1; }
switch (alt1) {
case 1 : {
match(input,6,FOLLOW_6_in_variable16);
ids=(Token)input.LT(1);
match(input,ID,FOLLOW_ID_in_variable20);
if (list_ids==null) list_ids=new ArrayList();
list_ids.add(ids);
} break;
default : break loop1;
}
} while (true);
match(input,7,FOLLOW_7_in_variable24);
System.out.println(input.toString(type1.start,type1.stop));
for(Object t : list_ids) System.out.print(" " + ((Token)t).getText());
}
catch (RecognitionException re) ……
}
代碼中添加了一個List類型的list_ids對象,每匹配一個ID符號時就加入到list_ids中。List中存放的是Object類型所以我們輸出時要加類型轉換(ANTLR下個版本是否加入泛型?)
5.6規則的參數
規則被翻譯成函數自然可以加參數。ANTLR中利用規則的參數可以實現上下文信息的傳遞來彌補上下文無關文法的缺點。下面用一個簡單的示例來演示參數的用法。
grammar SimpleParam;
variable : type idList[$type.text] ';';
idList[String typeName]
: ID '=' (con=INT | con=FLOAT)
{ if(typeName.equals("int") && $con.text.indexOf(".") != -1) {
System.out.println("error: value of float cannot assgin to var of int.");
}
};
type : 'int' | 'float';
INT : ('0'..'9')+;
FLOAT : ('0'..'9')+ ('.' ('0'..'9')+)?;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
分析器運行後輸入:int x = 5.5; 輸出:error: float value cannot assgin to int type var。variable規則中將type匹配的類型名傳遞給idList規則使在idList規則中可以判斷是否將浮點型付給了整型,如果是則提示錯誤信息。下面是生成的分析程序代碼:
public final void variable() throws RecognitionException {
……
idList(input.toString(type1.start,type1.stop));
}
public final void idList(String typeName) throws RecognitionException {
……
if(typeName.equals("int") && con.getText().indexOf(".") != -1) {
System.out.println("error: value of float cannot assgin to var of int.");
}
……
}
規則參數也可以寫成“$”本規則名爲前綴的寫法。$idList.typeName與typeName是等價的。
5.7規則的返回值
規則也可以有返回值,規則只有單個返回值時規則函數會從void類型變成有返回值的函數。規則可以返回多個值,實際上返回多值的功能是通過返回一個具有多個屬性的對象來實現。不管返回值是單個值對象還是由對象攜帶多個返回值,在文法中的寫法都是一樣的,以“ruleRetObj.RetValue”帶有規則名前綴的形式來引用。返回對象就是我們前講過的 ParserRuleReturnScope類的派生類對象。我們用一個示例來看一下返回值的用法。
grammar SimpleReturn;
variable : type idList[$type.text]
{ System.out.println($idList.retList + "\r\n" + $idList.count); }';';
idList[String typeName] returns [List retList, int count]
: ids+=ID (',' ids+=ID)*
{ $retList = $ids;
$count = $ids.size();
};
type : 'int' | 'float';
INT : ('0'..'9')+;
FLOAT : ('0'..'9')+ ('.' ('0'..'9')+)?;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
運行後輸入:int x, y, z
輸出:[[@2,4:4='x',<4>,1:4], [@5,7:7='y',<4>,1:7], [@8,10:10='z',<4>,1:10]]
3
variable規則從idList規則返回定義的變量名集合和變量的個數,集合中保存的是Token對象。下面看一下生成的程序代碼。
public static class idList_return extends ParserRuleReturnScope {
public List retList;
public int count;
};
public final void variable() throws RecognitionException {
type_return type1 = null;
idList_return idList2 = null;
……
type1=type();
……
idList2=idList(input.toString(type1.start,type1.stop));
……
System.out.println(idList2.retList + "\r\n" + idList2.count);
……
}
public final idList_return idList(String typeName)
throws RecognitionException {
idList_return retval = new idList_return();
……
list_ids.add(ids);
……
if (list_ids==null) list_ids=new ArrayList();
list_ids.add(ids);
……
retval.retList = list_ids;
retval.count = list_ids.size();
return retval;
}
規則idList的兩個返值放到了idList_return的對象中返回,idList_return類加入了retList和count兩個屬性保存了返回值。
5.8 @header用法
ANTLR中每一個文法規則都生成對應的函數,這些函數是分析器類的方法。第一章已經介紹過語法分析器和詞法分析器都分別生成XXXParser類和XXXLexer類。當需要向類的頂端加入代碼時可以用@header定義。一般情況下(如java)用@header來加入包名和導入imports其它java類。
grammar SimpleMember;
@options { language=Java; }
@header { package Simple; }
……
@header在默認的情況下會設置XXXParser類代碼,也就是語法分析類。我們也可以顯示的指明設置詞法類還是設置語法類。
@parser::header { package Simple.Parser; }
@lexer::header { package Simple.Lexer; }
在C#中命名空間由於有“{}”的存在所以無法用來@header定義,@header中不用輸入不閉合的花括號。可以用如下方法來定義:
@lexer::namespace {
Simple.Lexer
}
@parser::namespace {
Simple.Parser
}
::namespace只能用於C#。還要注意一點我們在後面還會講到樹文法(tree grammar)在樹文法中也可以使用@namespace {OKProject}來設置命名空間。
5.9 @members用法
在文法中可以用@members定義分析器類的成員,和@header、Actions一樣並沒有具體的限制,你可以在其中任何內容如屬、方法、事件、內部類和註釋等,還有一個重要的應用是可以用@members重寫基類的方法實現一些高級功能。
下面請看示例:
grammar SimpleMember;
options {
language=CSharp;
}
@header {
using System.Collections.Generic;
}
@members {
private List<IToken> RetList = new List<IToken>();
private int Count = 0;
}
variable : type idList
{
foreach(IToken t in RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine(Count);
}';';
idList
: ids+=ID (',' ids+=ID)*
{
RetList = new List<IToken>();
foreach(object t in $ids)
RetList.Add((IToken)t);
Count = $ids.Count;
};
type : 'int' | 'float';
INT : ('0'..'9')+;
FLOAT : ('0'..'9')+ ('.' ('0'..'9')+)?;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
這個示例實現了與之前的示例相同的功能,但沒有使用返回值,idList()函數中利用分析器類的成員RetList和Count來保存變量的信息,然後在variable()函數中輸出。類成員是在不同的成員函數間傳遞信息的一種方法,但要注意對類成員賦值和取值的先後順序,不然會在分析過程中出現異常。該文法生成了如下的代碼:
using System.Collections.Generic;
......
public class SimpleMemberParser : Parser
{
private List<IToken> RetList = new List<IToken>();
private int Count = 0;
public void variable()
{
......
foreach(IToken t in RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine(Count);
......
}
public void idList()
{
IToken ids = null;
IList list_ids = null;
......
if (list_ids == null) list_ids = new ArrayList();
list_ids.Add(ids);
......
RetList = new List<IToken>();
foreach(object t in list_ids)
RetList.Add((IToken)t);
Count = list_ids.Count;
......
}
5.10 scope屬性
規則的參數和返回值可以實現在規則之間傳遞信息,但要跨越多個規則傳遞時,每一個規則都要定義同樣的參數和返回值一級一級的傳遞,這樣比較麻煩也不太現實。@member定義的類成員也可以實現信息傳遞,但是類的成員在類的範圍內是全局可見的,大量地使用類成員代替參數對程序結構帶來的壞處是顯然的。
我們可以把規則看成是象類一樣的程序單位,如果把variable、idList和type看成是三個類,我們可以定義這些類的屬性在文法中使用scope{…}來定義。看下面的示例:
grammar SimpleScope;
options { language=CSharp; }
@header {
using System.Collections.Generic;
}
variable
scope {
List<IToken> RetList;
int Count;
}
: type {$variable::RetList = new List<IToken>();} idList
{
foreach(IToken t in $variable::RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine($variable::Count);
}';';
idList : ids+=ID (',' ids+=ID)*
{
foreach(object t in $ids)
$variable::RetList.Add((IToken)t);
$variable::Count = $ids.Count;
};
type : 'int' | 'float';
INT : ('0'..'9')+;
FLOAT : ('0'..'9')+ ('.' ('0'..'9')+)?;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
這個示例使用scope屬性代替之前的類成員和返回值,實現了規則之間的信息傳遞。在使用scope屬性時在屬性名前要加“$”符號規則名和“::”,下面看一下生成的代碼。
protected class variable_scope
{
protected internal List<IToken> RetList;
protected internal int Count;
}
protected Stack variable_stack = new Stack();
public void variable() // throws RecognitionException [1]
{
variable_stack.Push(new variable_scope());
......
((variable_scope)variable_stack.Peek()).RetList = new List<IToken>();
idList();
......
foreach(IToken t in ((variable_scope)variable_stack.Peek()).RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine(((variable_scope)variable_stack.Peek()).Count);
......
finally
{
variable_stack.Pop();
}
}
public void idList()
{
......
foreach(object t in list_ids)
((variable_scope)variable_stack.Peek()).RetList.Add((IToken)t);
((variable_scope)variable_stack.Peek()).Count = list_ids.Count;
.....
}
從生成的代碼中可以看出scope屬性的實現有三部分,首先定義了一個新的內部類variable_scope,類中定義了RetList和 Count兩個屬性,然後定義了一個棧對象variable_stack,最在在variable()函數開始時創建一個variable_scope的對象並壓棧,在variable()結束時彈出variable_scope的對象。對scope屬性的操作則是操作棧頂對象的屬性。
@scope{}中定義的屬性可能直接初始化對象,原因我想大家也可以明白。所以我們只能在idList子規則執行前創建RetList對象的實例。還要注意的是這個版本的ANTLR以C#爲目標語言時@scope{}中不能加入註釋。
5.10.1 scope屬性的生命期和作用域
因爲擁有scope屬性的對象實例是在規則函數開始執行時創建,結束執行時刪除。所以scope屬性的生命期覆蓋了其子規則函數的執行時間。本示例中variable規則的RetList和Count屬性的生命期跨越了type()和idList()函數。
$variable::RetList $variable::Count的作用域
|
type() |
idList() |
variable() |
scope屬性只能在語法規則中定義,所以它的作用域是整個語法分析類。
關於scope屬性的功能還有重要的一點,就是當規則出現遞歸時scope屬性可以很好處理嵌套結構中屬性生命期和作用域。下面給出一個關於分析SELECT語句的極度精簡的示例。我們都很熟悉SQL SELECT語句,SELECT語句可以嵌套定義主查詢可以有子查詢,子查詢還可以有子查詢。當我們解析SELECT語句時我們要主查詢和子查詢分別都查詢了哪些表。無論是主查詢還是子查詢都是嵌套定義的一套規則,規則中的action代碼要處理自己在不同嵌套層中的數據。在這種情況下可以用scope屬性所具有的特點來輕鬆處理作用域的問題。
grammar SimpleSelect;
options { language=CSharp; }
@header {
using System.Collections.Generic;
}
selectStatement
scope {Dictionary<string, string> tableSourceList;}
:
{$selectStatement::tableSourceList = new Dictionary<string, string>();}
selectClause (fromClause)?
{ foreach(KeyValuePair<string, string> keyValue in $selectStatement::tableSourceList)
System.Console.WriteLine($selectStatement.text +
":( " + keyValue.Key + " " + keyValue.Value + " )");
}
;
selectClause
: 'SELECT' ('ALL' | 'DISTINCT')? '*'
;
fromClause
: 'FROM' tableSource (',' tableSource)*
;
tableSource
: tn=TableName
{$selectStatement::tableSourceList.Add($tn.text, $tn.text);}
| '(' ss=selectStatement ')' 'AS' tn=TableName {$selectStatement::tableSourceList.Add($tn.text, $ss.text);}
;
TableName : Identifier;
Identifier : ('a'..'z' | 'A'..'Z' | '_')
('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*;
WS : ( ' ' | '\t' | '\r' | '\n' )+ {Skip(); } ;
示例中selectStatement規則定義了一個tableSourceList屬性用來存放當前查詢所查詢的別名和表名,分析完當前查詢後輸出所有查詢表的信息。FROM子句的tableSource規則可以匹配表名和子查詢,子查詢又是selectStatement規則形成了查詢的嵌套定義。(.net中的Dictionary<T, T>相當於java中的Map<T,T>)
在子查詢分析期間子查詢創建了tableSourceList屬性對象併入棧,這時主查詢創建的tableSourceList屬性對象已經不是棧頂的元素。所以在子查詢分析期間主查詢的屬性被隱藏,當前所分析規則的信息被很好的保存。
selectStatement () |
tableSource() tableSourceList.Add |
….. |
selectStatement() |
….. |
tableSource() tableSourceList.Add |
創建 tableSource List |
刪除 tableSource List |
刪除tableSourceList
|
創建tableSourceList
|
圖5.1
ANTLR允許直接操作棧中的任何scope屬性對象,在屬性名後加方括號指定屬性對象在棧中的位置。如果索引爲負數,代表從棧頂向棧底方向的第N個元素。
如:$selectStatement[-2]::tableSourceList;
生成的代碼爲:
((selectStatement_scope)selectStatement_stack[selectStatement_stack.Count-2-1]).tableSourceList
如果索引爲正數,代表從棧底向棧頂方向的第N個元素。
如:$selectStatement[1]::tableSourceList;
生成的代碼爲:
((selectStatement_scope)selectStatement_stack[1]).tableSourceList
其中如果索引爲0,代表的是棧底的元素。生成的代碼爲:
((selectStatement_scope)selectStatement_stack[0]).tableSourceList
5.11 @init
ANTLR規則中可以使用@init定義規則函數的初始化代碼,@init定義的代碼將出現在在其它actions的代碼之前,我們可以將一些局部變量定義、屬性初始化等代碼寫在@init中。如我們在上一節scope屬性代替返回值的示例SimpleScope中使用了action去創建對象,有@init可以把文法改寫爲:
variable
scope {
List<IToken> RetList;
int Count;
}
@init {
$variable::RetList = new List<IToken>();
}
: type idList
{
foreach(IToken t in $variable::RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine($variable::Count);
}';';
在生成的代碼中可以看到,創建對象的代碼放到了合適的位置。
public void variable()
{
variable_stack.Push(new variable_scope());
((variable_scope)variable_stack.Peek()).RetList = new List<IToken>();
……
}
5.12 @after
與@init相對ANTLR文法中可以用@after定義規則函數最後執行的代碼。可以在@after{}中做一些刪除對象輸出結果等收尾工作。我們再修改SimpleScope示例使用了@after{}輸出結果,文法如下:
variable
scope {
List<IToken> RetList;
int Count;
}
@init {
$variable::RetList = new List<IToken>();
}
@after{
foreach(IToken t in $variable::RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine($variable::Count);
}
: type idList ';';
@after和@init的位置誰先誰後沒有關係都可以正確生成代碼,@after中的代碼被放到了try{}的最後,在異常處理範圍之內。下面看一下生成後的代碼:
public void variable()
{ ……
try
{
……
foreach(IToken t in ((variable_scope)variable_stack.Peek()).RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine(((variable_scope)variable_stack.Peek()).Count);
}
catch (RecognitionException re) {……}
finally {……}
return ;
}
5.13全局scope屬性
scope屬性受規則的生命期的限制,規則開始時創建規則結束時刪除。規則可以引用本規則的scope屬性,在規則生命期內只有其子規則可以引用父規則的scope屬性。上一節的嵌套示例中子查詢統計自己查詢表tableSourceList屬性時,父查詢的tableSourceList被屏蔽,雖然可以用tableSourceList[-1]訪問,但是是分開的列表。
但是有時我們需要用一個屬性來統計某個規則下的所有規則(包括子規則)的信息。ANTLR中可以定義一種全局scope屬性,能夠在多個規則中共享中使用。全局scope屬性是使用scope關鍵字加上一個全局scope名稱,其後與scope屬性一樣在{}中可以定義一到多個屬性。請看下面的文法:
grammar SimpleGlobalScopes;
scope CScope {
String name;
List<String> symbols;
}
prog
scope CScope;
@init {
$CScope::symbols = new ArrayList<String>();
$CScope::name = "global";
}
@after {
System.out.println("global symbols = "+$CScope::symbols);
}
: variable* func*;
func : 'void' ID '(' ')' '{' variable* stat+ '}';
block :
'{' variable* {$CScope::symbols.add($variable.text);}
stat+ '}';
stat: ID '=' INT ';' | block;
variable : type ID (ID)* ';';
type : 'int' | 'float';
INT : '0'..'9' + ;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
本文法定義了簡單的編程語言的結構,啓始規則prog代表程序,程序下有變量和函數的定義,函數中可以有變量定義和語句,而語句中有語句塊和賦值語句。語句塊是包括在花括號“{}”內的一組語句,語句塊又可以包含語句塊它們是遞歸的關係。
文法中定義了兩個全局scope屬性name和symbols,全局scope屬性名爲CScope。Name用來保存當前域的名稱,symbols用來保存當前域的所有變量的類型和名稱,全局scope屬性要寫在所有規則之前。
在prog規則名後面加入了 scope CScope定義;,scope CScope;代表prog規則擁有一個CScop全局scope屬性的對象。分析程序會在規則開始時創建一個CScope屬性對象,規則結束時刪除CScope屬性對象。我們看一下scope CScope;對應生成的代碼:
public final void prog() throws RecognitionException {
CScope_stack.push(new CScope_scope());
......
finally {
CScope_stack.pop();
}
}
擁有全局scope屬性對象的規則可以在自己的@init、@after中對全局scope屬性進行初始化和收尾工作。本例中prog規則在@init中創建了symbols集合的對象,並將name設置爲”global”說明這是整個程序的全局變量集合。與scope屬性一樣對全局scope屬性的引用也是使用“$”加全局scope名加“::”符號和屬性名的形式,本例中$CScope::symbols.add($variable.text);語句將變量類型與標識符加到symbols列表中。編譯運行後輸入:“int x, y,z;”輸出:“global symbols = [int x, int y, int z]”。下面有一個應用全局scope屬性的實例,來說明何時使用全局scope屬性。
許多編程語言的語句塊中可以定義變量,語句塊之間可以是嵌套關係。如果外層語句塊定義了變量x,這時如果內層語句塊也定義變量x分析器應該提示變量名重複的錯誤信息。這就要求我們在分析內層語句塊時在列表中也要有上屋語句塊的變量信息,這樣才能知道是否與上層變量重句,這時可以使用全局scope屬性來實現這個功能。下面我們修改文法使分析器可以統計出二類變量,第一類是函數外的全局變量,第二類是函數中最前面定義的變量我們管這樣的變量叫函數變量,作用域在函數之內。函數內變量還可以在函數內的語句塊中定義其作用域在當前的語句塊之內。對這兩類變量的統計我們使用了全局scope屬性來實現。請看下面文法:
grammar SimpleGlobalScopes;
scope CScope {
String name;
List<String> symbols;
}
prog
scope CScope;
@init {
$CScope::symbols = new ArrayList<String>();
$CScope::name = "global";
}
@after {
System.out.println("global var = "+$CScope::symbols);
}
: variable* func*
;
func
scope CScope;
@init {
$CScope::symbols = new ArrayList<String>();
$CScope::name = "func";
}
@after {
System.out.println("func var = "+$CScope::symbols);
}
: 'void' ID '(' ')' '{' variable* stat+ '}'
;
block
scope CScope;
@init {
$CScope::symbols = new ArrayList<String>();
$CScope::name = "block";
}
@after {
System.out.println("block var = "+$CScope::symbols);
}
: '{' variable* stat+ '}'
;
stat: ID '=' INT ';' | block;
variable
: type a=ID {$CScope::symbols.add($type.text + " " + $a.text);}
(',' b=ID {$CScope::symbols.add($type.text + " " + $b.text);} )* ';'
;
type : 'int' | 'float';
INT : '0'..'9' + ;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
分析下面內容:
int a,b;
void f() {
int x;
x = 1;
{
int y;
y = 2;
{
int z;
z = 3;
}
}
}
分析器輸入輸出:
func var = [int x, int y, int z]
global var = [int a, int b]
5.14 catch[]與finally
從前面的例中已經看到規則函數中使用try..catch()..finally將代碼包括到異常處理之中。ANTLR允許我們修改默認的異常處理代碼,可以在規則定義的後面使用catch[]和finally定義自己的異常處理程序。下面我們還是修改SimpleScope示例請看下面的文法:
variable
scope {
List<IToken> RetList;
int Count;
}
@after{
foreach(IToken t in $variable::RetList)
System.Console.WriteLine(t.Text);
System.Console.WriteLine($variable::Count);
}
@init {
$variable::RetList = new List<IToken>();
}
: type idList ';';
catch[RecognitionException re] {
System.Console.WriteLine("variable rule error: " + re.Message);
}
finally { System.Console.WriteLine("parsing variable rule finished."); }
異常處理的寫法與java,C#語言很類似只是catch後面使用的是方括號。但有兩點需要注意,一是catch和finally的位置是在規則結束符號“;”的後面。二是finally中無需寫出scope屬性的刪除代碼,因爲ANTLR已經自動生成了刪除scope屬性對象的代碼。
到此我們學到了ANTLR文法中的各種嵌入代碼的方式,下面我們總結一下:
parser grammar T;
@header {……}
@members {……}
scope scopeName {……}
rule1[int x] returns [int y]
scope{string Y;}
@init {……}
@after {……}
: {……} a=Rule2 {……};
catch[……] {
……
}
finally { …… }
其中要注意的是如果我們要在形如classdef : method*這樣的規則中method後加入Actions時不能寫成method{…}*,這時要寫成(method{…})*的形式讓method和Actions在子規則中。
5.14.1 @rulecatch
如果每一個規則的catch代碼都是相同的,ANTLR有一個比較簡單的寫法。在文法中與@member同樣級別可以加入一個@rulecatch塊加入關於catch的程序代碼。
@rulecatch {
catch (RecognitionException e) {
throw e;
}
}
5.15 嵌入規則的Actions
規則中的Actions也可以看成是文法規則中的一項內容。它在規則中的位置與生成的代碼執行的順序是一致的。a : b Acion1 c規則中Action1會在a之後c之前執行。下面我們研究一下在選擇結構、重複和嵌套的規則定義中Action出現的位置對代碼的影響。
grammar ActionInRule1;
a : A {System.out.println($A.text);} B {System.out.println($B.text);};
A : 'A';
B : 'B';
運行後輸入: AB
輸出:A
B
生成代碼爲:
Token A1=null;
Token B2=null;
try {
A1=(Token)input.LT(1);
match(input,A,FOLLOW_A_in_a10);
System.out.println(A1.getText());
B2=(Token)input.LT(1);
match(input,B,FOLLOW_B_in_a14);
System.out.println(B2.getText());
}
從代碼中看到Action與規則是順序的,所以如果將 a 規則修改成:
a : A {System.out.println($A.text); System.out.println($B.text);} B ;
對$B的引用是沒有意義的,所以ANTLR給出錯誤提示“illegal forward reference: $B.text”。
繼續修改文法使A和B成選擇關係。
grammar ActionInRule2;
a : A {System.out.println($A.text);}
| A {System.out.println($A.text);};
生成的代碼爲:
a : A {System.out.println($A.text);}
public final void a() throws RecognitionException {
Token A1=null;
Token A2=null;
try {
……
switch (alt1) {
case 1 : {
A1=(Token)input.LT(1);
match(input,A,FOLLOW_A_in_a9);
System.out.println(A1.getText());
} break;
case 2 : {
A2=(Token)input.LT(1);
match(input,A,FOLLOW_A_in_a16);
System.out.println(A2.getText());
} break;
}……
}
選擇關係的規則中生成的代碼大體是一個分支結構。在這個示例中我們會在生成的代碼中興奮地發現ANTLR智能的區別了兩個分支中的$A引用,分別用A1和A2來代替。A1和A2是在函數一開始定義的,所以它們的作用域還是整個函數。這個示例說明在並列的選擇結構中同樣的$A引用,是分別引用了不同的變量。
grammar ActionInRule2;
a : v=A {System.out.println($v.text);}
| v=A {System.out.println($v.text);};
如果將兩個子規則A賦給同一個變量,這時的v則是同一個變量。這裏我們不列出生成代碼。根據之前的講述讀者應該可以理解。
grammar ActionInRule2;
a : A {System.out.println($A.text);}
| B {System.out.println($A.text);};
如果選擇的兩個並列項是A和B,而B的一端要引用$A。這時B分支生成的代碼中$A.text沒能被ANTLR處理,生成的代碼還是System.out.println($A.text);原樣,這當然是無法編譯的。這說明一個選擇項中無法用規則名直接引用別一個選擇項的規則符號(這種情況只能使用變量)。
下面看一下規則中重複的文法:
grammar ActionInRule3;
variable : type ID {System.out.println($ID.text);}
(',' ID {System.out.println($ID.text);} )* ';';
type : 'int' | 'float';
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
此文件生成代碼時ANTLR提示錯誤信息“ID is a non-unique reference”,說明ANTLR不能區分兩個$ID的引用。這時我們只能把兩個$ID賦給兩個變量然後再引用,請看下面對variable規則的修改:
variable : type a=ID {System.out.println($a.text);}
(',' b=ID {System.out.println($b.text);} )* ';';
修改後的文法解決了這個問題,下面看一下生成的代碼:
public final void variable() throws RecognitionException {
Token a=null;
Token b=null;
try {
……
a=(Token)input.LT(1);
match(input,ID,FOLLOW_ID_in_variable14);
System.out.println(a.getText());
do {
……
b=(Token)input.LT(1);
match(input,ID,FOLLOW_ID_in_variable23);
System.out.println(b.getText());
} while (true);
……
}……
}
生成代碼中對應重複的是循環語句,“*”的作用是使用do..while語句實現的。那麼爲什麼ANTLR對於此文法不能智能的區分兩個$ID引用呢?原因是ID(',' ID)* 規則與之前的選擇結構不同,第二個到第N個ID也就是子規則(',' ID)位置在第一個ID之後,所以在此子規則中引用第一個ID的需求是可能的,因此無法區分是要引用哪一個ID,只能將其賦給兩不變量來實現。
瞭解了規則中的Actions在生成代碼中的位置後,我們可以更好的使用Actions。因爲java和C#語言允許在任何語句塊中定義新的對象,所以在Actions中我們也可以定義對象(當目標語言爲C時會有限制)。根據Actions在生成代碼中的位置我們就可以判斷出定義的對象的作用域和生命期,這對正確做Actions有很大幫助。
variable : type a=ID {String theFirstID = $a.text;}
(',' {List<String> moreIDs = new ArrayList<String>();}
b=ID { moreIDs.add($b.text);} )* ';';
5.16 詞法規則中的Actions
詞法規則中使用Actions和語法規則中不完全相同,前面我們講解的都是語法規則中的Actions。下面我們學習一下詞法規則中如何使用Actions。詞法規則中的Actions用法和語法規中的用法大致相似,如語法規則相同詞法規則也可以用規則符號名直接引用規則符號信息。
grammar ActionInTokenRule1;
variable : QID;
QID : ID {System.out.println($ID.text);} ;
ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
詞法規則中也可以使用變量:
QID : id=ID {System.out.println($id.text);} ;
下面看一下生成的代碼:
public final void mQID() throws RecognitionException {
try {
int _type = QID;
Token id=null;
int idStart15 = getCharIndex();
mID();
id = new CommonToken(input, Token.INVALID_TOKEN_TYPE, Token.DEFAULT_CHANNEL,
idStart15, getCharIndex()-1);
System.out.println(id.getText());
this.type = _type;
} finally { }
}
詞法分析生成的規則函數中定義了一個Token類型的id對象,本章前面已經講過詞法規則返回的是Token類型,詞法規則的信息是使用Token的對象來獲得的。id = new CommonToken(input, Token.INVALID_TOKEN_TYPE, Token.DEFAULT_CHANNEL, idStart15, getCharIndex()-1);這條語句用來獲得輸入中與ID規則匹配的信息。這個處在詞法分析器代碼中的input對象是ICharStream對象的實例,是main函數中的第一條語句ANTLRInputStream input = new ANTLRInputStream(System.in);的input。(語法分析中的input是main函數中的第三條語句是CommonTokenStream的對象。)通過調用CommonToken的構造函數將輸入流input中的DEFAULT_CHANNEL頻道的idStart15開始到getCharIndex()-1位置的字符生成CommonToken對象。CommonToken對象是Token的派生類,這樣就可以使用Token的屬性text來獲得詞法符號的內容。下面是main函數:
public static void main(String[] args) throws Exception
{
ANTLRInputStream input = new ANTLRInputStream(System.in);
ActionInTokenRule2Lexer lexer = new ActionInTokenRule2Lexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ActionInTokenRule2Parser parser = new ActionInTokenRule2Parser(tokens);
parser.variable();
}
分析了詞法分析的代碼之後,我們來學習一下Action在詞法規則中與語法規則的不同。下面我們象語法規則中那樣直接引用本規則符號來輸出本規則的匹配內容。
QID : ID {System.out.println($QID.text);} ;
但是ANTLR提示:“rule QID has no defined paramters.”錯誤無法生成代碼。爲什麼ANTLR會提示規則QID沒有定義參數呢?在詞法規則中$QID.text代表QID規則的參數text。語法規則中的Actions中不能直接引用本規則符號。
那麼詞法規則的參數據如何定義呢?在第三章詞法分析中的fragment用法中曾經講到過只有用fragment定義的詞法規則纔可以加入參數,但fragment詞法規則不能被語法規則直接引用也不會出現在語法樹中。爲了能加入詞法規則參數我們將ID詞法規則加上fragment關鍵字並添加兩個參數。
grammar ActionInTokenRule3;
variable : QID;
QID : ID["x", "y"] {System.out.println($ID.text);} ;
fragment ID[String X, String Y] :
('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
{System.out.println("X:" + $ID.X + " Y:" + $ID.Y);};
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; } ;
$ID.X用於引用參數X,也可以直接使用X。下面是生成的代碼:
public final void mQID() throws RecognitionException {
try {
……
mID("x", "y");
……
}
finally {
}
}
public final void mID(String X, String Y) throws RecognitionException {
……
System.out.println("X:" + X + " Y:" + Y);
}
詞法規則不能定義返回值,因爲語法分析程序是通過類似(IToken)input.LT(1)的語句獲得的,所以對於語法規則直接引用的語法規則程序沒有對辦法傳遞自定義參數。而fragment語法規則不能讓語法規則直接引用只能在詞法規則之間引用,所以可以使用參數。每一個詞法規則的信息都是用一個Token類來表示。
編譯器在提示錯誤信息時都會提示錯誤的行號和列號,我們可以利用Token類的Line和CharPositionInLine屬性輸出字符在輸入中的位置的。我們修改5.13全局scope屬性的SimpleGlobalScopes示例,使能夠輸入變量的位置。
grammar SimpleGlobalScopes2;
……
variable
: type a=ID
{$CScope::symbols.add($type.text + " " + $a.text + " " + $a.line + ":" + $a.pos);}
(',' b=ID
{$CScope::symbols.add($type.text + " " + $b.text + " " + $b.line + ":" + $b.pos);}
)* ';'
;
與SimpleGlobalScopes使用相同的輸入,輸出爲:
func var = [int x 3:8, int y 6:12, int z 10:16]
global var = [int a 1:4, int b 1:6]
在語法規則和詞法規則中出現的字符串常量定義,與普通的詞法規則是一樣的,只需定義變量,賦值後即可獲得相關信息。
type : 'int' | 'float' ;
block
: begin='{' variable* stat+ '}' { System.out.println($begin.line);};
……
WS @init{ a=0; }
: ( ' ' | '\t' | a='\r' | '\n' )+
{ System.out.println(a); $channel = HIDDEN; } ;
詞法規則中也可以使用變量注意有些變量要初始化:
type : 'int' | 'float' ;
5.17 Actions中變量引用總結
下面總結一下各種Actions中引用變量的寫法:
parser grammar T;
@header {……}
@members {……}
scope scopeName {string X;}
rule1[int x] returns [int y]
scope{string Y;}
@init { }
@after {……}
: a=Rule2
{ scopeName::X; rule1::Y; $rule1.text; $Rule2.text; $a.text;
scopeName[-i]::X; scopeName[i]::X;rule1::Y[-i]; rule1::Y[i];
};
catch[……] {
……
}
finally { …… }
Rule2 : Rule3 {$Rule3.text;};
fragment Rule3[string P] : b='Rule2' {$b.text; $Rule3.P;}
5.18 表達式求值示例
下面是一個有加減乘除四種運算的表達式文法,我們使用Actions讓這個文法具有表示式求值的能力:
grammar E;
options { output=AST; }
expression : multExpr (('+' |'-' ) multExpr)*;
multExpr : atom (('*' | '/') atom)*;
atom : INT | '(' expression ')';
INT : '0'..'9' + ;
WS : (' ' |'\t' |'\n' |'\r' )+ {skip();};
下面是對表達式文法使用Actions加入求值功能後的文法:
grammar EEvalue;
options{language=Java;}
expression returns[int v]
: val1=multExpr {$v=$val1.v;}
((op='+' |op='-' ) val2=multExpr
{ if($op.text.equals("+")) {
$v = $val1.v + $val2.v;
} else if($op.text.equals("-")) {
$v = $val1.v - $val2.v;
}
}
)*
;
multExpr returns[int v]
: val1=atom {$v=$val1.v;}
((op='*' | op='/') val2=atom
{ if($op.text.equals("*")) {
$v = $val1.v * $val2.v;
} else if($op.text.equals("/")) {
$v = $val1.v / $val2.v;
}
}
)*
;
atom returns[int v]
: INT {$v=Integer.parseInt($INT.text); System.out.println("v:"+$v);}
| '(' expression {$v=$expression.v;} ')';
INT : '0'..'9' + ;
WS : (' ' |'\t' |'\n' |'\r' )+ {skip();};
修改後的文法中expression、multExpr、atom規則都具有返回值,規則中的Actions計算並返回本規則所表示的運算的結果值,下面的運行代碼中最後輸出啓始expression規則的返回值(因爲是單個返回值,所以規則函數直接返回整型值。
EEvalueLexer lexer = new EEvalueLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
EEvalueParser parser = new EEvalueParser(tokens);
System.out.println(parser.expression());
5.19 本章小結
本章講述了嵌入到規則中的Actions,分析器如何相對規則生成代碼。全面地介紹了有關語法規則中Actions的用法。相關代碼在生成代碼後的位置,讀都學習本章後也應該對ANTLR生成的代碼結構有所瞭解。有了嵌入代碼的輔助,大大增強了語法分析器的能力,分析器也變得靈活可定義。由於ANTLR還沒有達到世界一流開發工具的嚴謹程度,所以有些時候查看ANTLR生成的代碼對理解ANTLR是很有用的。