探索Antlr(Antlr 3.0更新版) java
簡介
Antlr(ANother Tool for Language Recognition)是一個工具,它爲咱們構造本身的識別器(recognizers)、編譯器(compiler)和轉換器(translators)提供了一個基礎。經過定義本身的語言規則,Antlr能夠爲咱們生成相應的語言解析器,這樣即可以省卻了本身全手工打造的勞苦。
目標
如同程序設計語言入門大多采用「Hello World」同樣,編譯領域的入門每每選擇計算器。而這裏邁出的第一步更爲簡單:一個只能計算兩個數相加的計算器,也就是說,它能夠計算「1+1」。
基礎知識
先來考慮一下如何下手,若是你曾經接受過編譯原理的教育,權當憶苦思甜了。這個計算器工做的前提是有一個須要計算的東西,無論咱們是以文件的形式提供,仍是手工輸入,至少咱們可讓咱們的計算器知道「1+1」的存在。
有了輸入以後,咱們要先檢查輸入的正確性,只有對正確的輸入進行計算纔是有意義的。如同寫文章有形式和內容之分,這裏的檢查也要細分一下,率先完成的檢查固然是面子功夫——形式上的東西,看看是否有錯別字的存在,咱們要作的是數值相加,結果人家給出了一個字母,這確定不是咱們但願獲得的,因此咱們有權力拒絕這個不合法的東西。對於程序員來講,若是在本身的程序裏寫了一個語言不接受的標識符,好比在Java裏用「123r」作標識符,那編譯器確定會罷工,拒絕讓程序經過編譯的。在編譯原理裏面,這個過程叫作詞法分析。在咱們的計算器中,咱們只接受整數和加號,其它的一律不理。這裏咱們說的是「整數」,而非 「1」、「2」……,對咱們來講,它們表明着同一類的東西,編譯原理教導咱們把這這種東西叫作token,那些數字對咱們來講,都是同樣的token,不一樣的僅僅是它們的值而已。
形式說得過去並不表明內容就能夠接受,南北朝時期許多駢體文讓咱們看到了隱藏在華麗的外表下的空虛靈魂。你能夠說 「我吃飯」,若是說「飯吃我」,除非是在練習反正話的場合,不然沒有人會認爲它是有意義的,由於顯然這不是咱們習慣的主謂賓結構。只有在闖過了詞法分析的關口,才能到達這裏,在編譯原理裏面,咱們把這個階段叫作語法分析。若是說詞法分析階段的輸入是字符流的話,那麼語法分析階段的輸入就是token流——詞法分析的輸出。咱們這裏接受的合法語法是「整數 加號 整數」。
編寫語法文件
好了,制訂好本身的語言規則以後,咱們須要以Antlr的語言把它描述出來。下面即是以Antlr的語言描述的語法:
grammar Calculator;
expr: INT PLUS INT;
PLUS : '+' ;
INT : ('0'..'9')+ ;
Antlr的語法文件一般會保存在一個「.g」的文件中,咱們的語法文件叫作「Caculator.g」。
咱們來看看這裏的定義:
expr: INT PLUS INT;
這條語句定義了expr,它等價於「:」右邊的部分,也就是說,
* 一個INT,後面跟着一個PLUS,後面再接着一個INT。
至於INT和PLUS,它來自後面的定義:
PLUS : '+' ;
INT : ('0'..'9')+ ;
* PLUS定義的token,就是一個單一的「+」
* INT定義的token,由從'0'到'9'之間任意的數字組成,後面的加號表示它是能夠重複一次到屢次
若是你曾經與Antlr 2.x有過一面之緣,你會發現,這個語法文件與Antlr 2.x的語法文件有着些許不一樣。首先,咱們沒有區分詞法分析和語法分析,由上面的代碼能夠看出,兩者在形式上是一致的,不一樣的是,對於詞法分析的輸入是字符,而語法分析的輸入是詞法分析的結果,也就是token。Antlr 2.x必須顯式的區分這兩者,而在Antlr 3.0以後,Antlr會替你料理這一切。再有,這裏的語法文件名必須與grammar定義的名字保持一致,對於Java程序員,這是一個順其天然的選擇。
編譯語法文件
如同不編譯的程序是沒法發揮其威力同樣,單單語法文件對咱們來講,並無很大的價值。咱們的工做就是使用Antlr提供工具對咱們的語法文件進行編譯,不一樣於平常的編譯器輸出可執行文件,這裏的輸出是程序語言的源文件。Antlr缺省目標語言是Java語言,它也能夠支持C,C#和Python語言,其餘的語言尚在開發之中,從3.0發佈包結構來看,Ruby的支持很快就會加進來。
將Antlr提供的JAR文件加入到classpath中,其中包括Antlr 2.7.7,Antlr 3.0與其runtime,stringtemplate。你沒看錯,除了3.0,這裏還包含着2.7.7。緣由很簡單,Antlr 3.0是基於以前版本開發的。
而後把語法文件的名稱做爲參數傳給語法編譯器:
java org.antlr.Tool Caculator.g
在確保命令正確執行,且語法文件編寫正確的狀況下,Antlr爲咱們生成了幾個文件:
* CalculatorLexer.java
* CalculatorParser.java
* Calculator__.g
* Calculator.tokens
正如前面說過的,Antlr替咱們料理好了詞法分析和語法分析,其中, CalculatorLexer.java就是咱們的詞法分析器,而CalculatorParser.java中包含了語法分析器,它們是咱們這裏關注的主要對象。至於另外兩個文件,Calculator__.g是一個自動生成的lexer語法文件,而Calculator.tokens則是列出了咱們定義的token,咱們並不會在程序中和它們直接打交道,因此,讓咱們暫時忽略它們的存在。
運行程序
生成代碼以後,就是如何使用這些生成的代碼。下面就是咱們的主程序,它負責將詞法分析部分(Lexer)和語法分析部分(Parser)驅動起來:
public class Main {
public static void main(String[] args) throws Exception {
ANTLRInputStream input = new ANTLRInputStream(System.in);
CalculatorLexer lexer = new CalculatorLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CalculatorParser parser = new CalculatorParser(tokens);
try {
parser.expr();
} catch (RecognitionException e) {
System.err.println(e);
}
}
}
從這段代碼中能夠清晰的看出,Lexer的輸入是一個字符流,而Parser則須要Lexer的協助來完成工做,用Lexer構造出的Token流做爲其輸入。一切就緒,咱們讓它跑起來,嘗試輸入一些內容,看它是否可以經過驗證。事實證實,咱們的程序能夠輕鬆識別「1+1」,而對於不合法的東西,它會產生一些抱怨。
計算結果
還記得咱們的目標嗎?咱們的目標是計算出「1+1」的結果,而如今這個程序剛剛可以識別出「1+1」,咱們還要繼續前進。
熟悉XML解析的朋友對於SAX和DOM必定不陌生,兩者之間差異在於SAX屬於邊解析邊處理,而DOM則是把全部的內容解析所有解析完(在內存中造成一棵樹)以後,再統一處理。Antlr也有與之相似的兩種處理方式,SAX的朋友是在Parser中加入處理動做(Action)處理將隨着解析的過程進行,而DOM的夥伴則是解析造成一棵抽象語法樹(Abstract Syntax Tree,簡稱AST),再對樹進行處理。
加入Action
先來看看SAX的朋友。由於處理動做是加在expr上,其它部分保持不變。下面是修改過的expr:
expr returns [int value=0]
: a = INT PLUS b = INT
{
int aValue = Integer.parseInt($a.text);
int bValue = Integer.parseInt($b.text);
value = aValue + bValue;
}
;
看到經常使用的字符串轉整數的方法,熟悉Java的朋友想必已經露出了會心的微笑。沒錯,這裏定義Action的方法採用就是Java語言,由於咱們生成的目標是Java,若是你期待另闢蹊徑,那這裏的代碼就要用你的目標語言來編寫。
仔細看一下不難發現,action徹底是在原有的規則基礎上改造的來。首先用returns定義了這個Action的返回值,它將返回value這個變量的值,其類型是int,咱們還順便定義這個變量的初始值——「0」。接下來,咱們用a、b拿住了兩個token的值,咱們前面說過,在檢查的過程當中,咱們並不關心每一個token具體的內容,只要token的類型知足須要便可,但在action中,咱們要計算結果,那必須使用token具體的內容,因此,咱們用變量拿住了token。這裏咱們用$a.text獲取這個token的具體值。剩下的動做就很簡單了,把文本轉換爲數字,進行加法運算。
再給舊版本一些憶苦思甜的時間,Antlr 2.x寫法有一些細微差異。首先,Antlr 2.x用「a : INT」將一個Token賦給一個變量,而這裏用的是「a = INT」。再有,咱們用$a.text獲取token的值,而在Antlr 2.x中,咱們會用a.getText(),固然,在Antlr 3.0中,咱們也能夠這麼寫,不過,a.getText()這種寫法顯然太過於Java。
是否是對咱們的計算器有些火燒眉毛了,那就揮動工具生成全新的Parser。不過,在新的體驗以前,咱們還要稍微修改一下主程序,以體現咱們的勞動成果。
public class Main {
public static void main(String[] args) throws Exception {
ANTLRInputStream input = new ANTLRInputStream(System.in);
CalculatorLexer lexer = new CalculatorLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CalculatorParser parser = new CalculatorParser(tokens);
try {
System.out.println(parser.expr());
} catch (RecognitionException e) {
System.err.println(e);
}
}
}
好了,讓這個計算器來爲咱們求證「1+1」吧!
AST
SAX的朋友表演完了,下面就是DOM的夥伴登場了。
創建AST的方式很簡單,只要咱們加上一個AST的選項便可,不過,同DOM的處理方式同樣,前面的解析只是爲了後面的處理作準備,因此,這裏咱們要修改一下以前編寫的語法文件,下面就是咱們的新語法文件:
grammar Calculator;
options {
output=AST;
ASTLabelType=CommonTree;
}
expr : INT PLUS^ INT;
PLUS : '+' ;
INT : ('0'..'9')+ ;
稍微有些不一樣的地方是,咱們加上了兩個選項,告訴Antlr,咱們要輸出的是一個普通的AST。再有,在PLUS上面的「^」,這個符號用來告訴Antlr建立一個節點,以此做爲當前樹的根節點。
你也許會有些疑問,怎麼沒看到計算的加法的地方?正如前面所說,這裏只描述了語法結構,這是爲了後面的處理在作準備,那麼後面如何處理呢?別急,大戲要壓軸。下面登場的是Antlr整個故事最後一個大角,TreeParser:
tree grammar CalculatorTreeParser;
options {
tokenVocab=Calculator;
ASTLabelType=CommonTree;
}
expr returns [int value]
: ^(PLUS a=INT b=INT)
{
int aValue = Integer.parseInt($a.text);
int bValue = Integer.parseInt($b.text);
value = aValue + bValue;
}
;
Antlr 能夠接受三種類型語法規範——Lexer、Parser和Tree-Parser。若是說Lexer處理的是字符流、Parser處理的是Token流,那麼TreeParser處理的則是AST。前面Action的處理方式中,咱們看到,規則同處理放到了一塊兒,顯得有些混亂,而採用了AST的處理方式,規則同處理就徹底分離了:在Parser中定義規則,在TreeParser中定義處理,若是咱們須要對一樣的語法進行另外的處理,咱們只要從新 TreeParser,而沒必要在規則與Action混合的世界中苦苦掙扎。
有了前面Action的基礎,再來看TreeParser也就簡單許多,須要說明的就是:
^(PLUS a=INT b=INT)
除去變量的說明,簡化一下這段代碼
^(PLUS INT INT)
第一個符號PLUS對應了表示着根節點,兩個INT則分別表明了兩棵子樹,這樣恰好與前面生成的語法樹對應上。
再來看看從新打造的主程序:
public class Main {
public static void main(String[] args) throws Exception {
ANTLRInputStream input = new ANTLRInputStream(System.in);
CalculatorLexer lexer = new CalculatorLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CalculatorParser parser = new CalculatorParser(tokens);
try {
CommonTree t = (CommonTree)parser.expr().getTree();
CommonTreeNodeStream nodes = new CommonTreeNodeStream(t);
CalculatorTreeParser walker = new CalculatorTreeParser(nodes);
System.out.println(walker.expr());
} catch (RecognitionException e) {
System.err.println(e);
}
}
}
結語
體驗過最簡單的Antlr程序,咱們就有了讓它更爲豐富的基礎,接下來即是本身動手的時間了。
參考資料
《ANTLR入門》 2004年第三期《程序員》
《ANTLR Reference Manual》
《The Definitive ANTLR Reference》node
說明:程序員
用antlr的eclipse插件,每次修改parser文件,都會自動生成java代碼,但生成的java代碼沒有package信息,增長以下代碼便可:
eclipse
@header { package mypack; } @lexer::header { package mypack; }