探索Antlr(Antlr 3.0更新版)

版權聲明:轉載時請以超鏈接形式標明文章原始出處和作者信息及本聲明
http://dreamhead.blogbus.com/logs/10756716.html

探索Antlr》是兩年前寫的一篇文章,如今,Antlr 3.0已經發布了,有了一些變化,爲了反映這些變化,我決定重寫這篇《探索Antlr》。

探索Antlr(Antlr 3.0更新版) 

簡介
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》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章