Antlr 簡介

Antlr 是一個基於 Java 開發的功能強大的語言識別工具,Antlr 以其簡介的語法和高速的運行效率在這類工具中出類拔萃。當你需要開發一種領域語言時,語言可能像 Excel 中的公式一樣複雜,也可能像本文中的例子一樣簡單(只有算術運算),這時你可以考慮使用 Antlr 來處理你的語言。

  1. ANTLR 語言識別的一個工具 (ANother Tool for Language Recognition ) 是一種語言工具,它提供了一個框架,可以通過包含 Java, C++, 或 C# 動作(action)的語法描述來構造語言識別器,編譯器和解釋器。 計算機語言的解析已經變成了一種非常普遍的工作,在這方面的理論和工具經過近 40 年的發展已經相當成熟,使用 Antlr 等識別工具來識別,解析,構造編譯器比手工編程更加容易,同時開發的程序也更易於維護。
  2. 語言識別的工具有很多種,比如大名鼎鼎的 Lex 和 YACC,Linux 中有他們的開源版本,分別是 Flex 和 Bison。在 Java 社區裏,除了 Antlr 外,語言識別工具還有 JavaCC 和 SableCC 等。
  3. 和大多數語言識別工具一樣,Antlr 使用上下文無關文法描述語言。最新的 Antlr 是一個基於 LL(*) 的語言識別器。在 Antlr 中通過解析用戶自定義的上下文無關文法,自動生成詞法分析器 (Lexer)、語法分析器 (Parser) 和樹分析器 (Tree Parser)。

Antlr 能做什麼

編程語言處理

識別和處理編程語言是 Antlr 的首要任務,編程語言的處理是一項繁重複雜的任務,爲了簡化處理,一般的編譯技術都將語言處理工作分爲前端和後端兩個部分。其中前端包括詞法分析、語法分析、語義分析、中間代碼生成等若干步驟,後端包括目標代碼生成和代碼優化等步驟。

Antlr 致力於解決編譯前端的所有工作。使用 Anltr 的語法可以定義目標語言的詞法記號和語法規則,Antlr 自動生成目標語言的詞法分析器和語法分析器;此外,如果在語法規則中指定抽象語法樹的規則,在生成語法分析器的同時,Antlr 還能夠生成抽象語法樹;最終使用樹分析器遍歷抽象語法樹,完成語義分析和中間代碼生成。整個工作在 Anltr 強大的支持下,將變得非常輕鬆和愉快。

文本處理

當需要文本處理時,首先想到的是正則表達式,使用 Anltr 的詞法分析器生成器,可以很容易的完成正則表達式能夠完成的所有工作;除此之外使用 Anltr 還可以完成一些正則表達式難以完成的工作,比如識別左括號和右括號的成對匹配等。

Antlr 的安裝

  1. Antlr 本身是使用 Java 開發的,在使用 Antlr 之前必須先安裝 JRE(Java Runtime Environment )。Antlr 需要 Java 1.4 或者 1.4 以上版本。然後我們在 Antlr 的主頁上下載 Antlr 的 Jar 包 antlr-3.2.jar,最新的 Jar 包可能已經升級。把 Jar 添加到 CLASSPATH 環境變量之後,即可使用 Anltr。在 window 中,修改 CLASSPATH 爲
     CLASSPATH = %CLASSPATH%; C:/ antlr-3.2.jar
  2. 至此已經可以運行 Anltr, 使用的方法爲 java org.antlr.Tool XXX.g, 其中 XXX.g 是我們依據 Antlr 的語法規則編寫的文法文件。
  3. 除了 Anltr 運行環境之外,還有一個輔助 Antlr 開發的工具 Antlrworks,在 Antlr 的主頁上下載另一個 Jar 包 Antlrworks-1.4.jar。在 window 中,修改 CLASSPATH 爲
 CLASSPATH = %CLASSPATH%; C:/ Antlrworks-1.4.jar

運行 java org.antlr.works.IDE,然後在 Antlrworks 的 GUI 中新建或者打開文法文件。使用 Antlrworks 可以可視化顯示文法,並可以對語法分析樹和抽象語法樹可視化。

表達式定義

文法定義

我們定義一個最簡單的領域語言,從一個簡單的完成算術運算的例子出發,詳細說明 Antlr 的使用。首先我們需要創建一個 Antlr 的文法文件, 一般以 .g 爲文件名後綴,命名爲 Expr.g 。

在這個文法文件中根據 Antlr 的語法規則來定義算術表達式的文法,文件的頭部是 grammar 關鍵字,定義文法的名字:

 grammar Expr;

爲了簡單起見,假設我們的自定義語言只能輸入一個算術表達式。從而整個程序有一個語句構成,語句有表達式或者換行符構成。如清單 1 所示:

清單 1. 程序和語句
 prog: stat 
 ; 
 stat: expr 
	  |NEWLINE 
 ;

在 Anltr 中,算法的優先級需要通過文法規則的嵌套定義來體現,加減法的優先級低於乘除法,表達式 expr 的定義由乘除法表達式 multExpr 和加減法算符 ('+'|'-') 構成;同理,括號的優先級高於乘除法,乘除法表達式 multExpr 通過原子操作數 atom 和乘除法算符 ('*'|'/') 構成。整個表達的定義如清單 2 所示:

清單 2. 表達式
 Expr : multExpr (('+'|'-') multExpr)* 
 ; 
 multExpr : atom (('*'|'/') atom)* 
 ; 
 atom:  '(' expr ')' 
       | INT  
    | ID  
 ;

最後需要考慮的詞法的定義,在 Antlr 中語法定義和詞法定義通過規則的第一個字符來區別, 規定語法定義符號的第一個字母小寫,而詞法定義符號的第一個字母大寫。算術表達式中用到了 4 類記號 ( 在 Antlr 中被稱爲 Token),分別是標識符 ID,表示一個變量;常量 INT,表示一個常數;換行符 NEWLINE 和空格 WS,空格字符在語言處理時將被跳過,skip() 是詞法分析器類的一個方法。如清單 3 所示:

清單 3. 記號定義
 ID : ('a'..'z' |'A'..'Z')+ ; 
 INT : '0'..'9' + ; 
 NEWLINE:'\r' ? '\n' ; 
 WS : (' ' |'\t' |'\n' |'\r' )+ {skip();} ;

Antlr 支持多種目標語言,可以把生成的分析器生成爲 Java,C#,C,Python,JavaScript 等多種語言,默認目標語言爲 Java,通過 options {language=?;} 來改變目標語言。我們的例子中目標語言爲 Java。

運行 Antlr

完成文法定義之後,即可以運行 Antlr,爲我們生成需要的詞法分析器和語法分析器。在命令行運行以下下命令,如清單 4 所示:

清單 4. 運行 Antlr
java  org.antlr.Tool  c:/			 
antlr_intro\src\expr\Expr.g

成功運行Antlr之後,將爲我們生成 3 個文件,Expr.tokensExprLexer.javaExprParser.java。其中Expr.tokens爲文法中用到的各種符號做了數字化編號,我們可以不關注這個文件。ExprLexerAntlr生成的詞法分析器,ExprParserAntlr 生成的語法分析器,如圖 1 所示。

圖 1. Antlr 生成結果
圖 1 Antlr 生成結果

表達式驗證

基於 Antlr 生成的詞法分析器和語法分析器後,可以基於它們來驗證我們的輸入的表達式是否合法。我們需要調用 Antlr 的 API 完成以下 Java 程序,如清單 5 所示:

清單 5. 調用分析器
 public static void run(String expr) throws Exception { 
 ANTLRStringStream in = new ANTLRStringStream(expr); 
 ExprLexer lexer = new ExprLexer(in); 
	 CommonTokenStream tokens = new CommonTokenStream(lexer); 
	 ExprParser parser = new ExprParser(tokens); 
	 parser.prog(); 	
 }

對每一個輸入的字符串,我們構造一個 ANTLRStringStream 流 in,用 in 構造詞法分析器 lexer,詞法分析的作用是產生記號,用詞法分析器 lexer 構造一個記號流 tokens,然後再使用 tokens 構造語法分析器 parser,至此已經完成詞法分析和語法分析的準備工作。最終調用語法分析器的規則 prog,完成對表達式的驗證。詳細的 Java 程序參考樣例代碼中的 Test.java。

當輸入合法的的表達式時,分析器沒有任何輸出,表示語言被分析器接受;當輸入的表達式違反文法規則時,比如“a + (b * 3”,分析器輸出 line 0:-1 mismatched input '<EOF>' expecting ')';提示期待一個右括號卻遇到了結束符號。如圖 2 所示:

圖 2. 表達式驗證結果
圖 2. 表達式驗證結果

文法可視化

使用 Antlrworks 打開 Expr.g,Antlrworks 對每一個文法定義都做了可視化顯示。整體的文法定義如圖 3:

圖 3. 文法定義的可視化
圖 3. 文法定義的可視化

其中語法規則和詞法記號的定義都有對應的圖形表示方式。比如語法規則 atom 的圖示形式如圖 4 所示:

圖 4. 語法規則 atom 的可視化
圖 4. 語法規則 atom 的可視化

詞法記號 ID 的圖示形式如圖 5 所示:

圖 5. 詞法記號 ID 的可視化
圖 5. 詞法記號 ID 的可視化

使用 Antlrworks 還可以對語法分析樹可視化,在 Antlrworks 的 GUI 窗口中,點擊 Run ->Debug, 在 Input Text 窗口中輸入 a+(2 + b),Start Rule 選擇 prog, 然後完成調試,可以看到 a+(2 + b) 時的語法分析樹,如圖 6 所示:

圖 6. a+(2+b) 的語法分析樹
圖 6. a+(2+b) 的語法分析樹

表達式求值

抽象語法樹

截至目前使用 Anltr 生成的詞法分析器和語法分析器,除了校驗表述式輸入合法性之外,沒有更多的用處。如果需要對表達式做進一步的處理,對表達式的運算結果求值,使用 Antlr 可以有兩種選擇,第一,直接在我們之前的 Expr 文法中嵌入動作,加入 Java 代碼片段;第二,使用 Antlr 的抽象語法樹語法,在語法分析的同時將用戶輸入轉換成中間表示方式:抽象語法樹,後續在遍歷語法樹的同時完成計算。

第二種方法在結構上更爲清晰,便於開發和維護,我們使用第二種方法完成表達式的求值。首先來建立抽象語法樹,Antlr 中建立抽象語法樹只需在原來文法的基礎上加上建樹語法即可。改寫我們的 Expr 文法,在每一個語法規則後,加上相應的抽象語法樹語法。清單 6,展示了程序和語句規則對應的抽象語法樹節點。其 ^ 符用於指示樹的根節點,PROG 和 STAT 是我們引入的佔位符號,僅僅是一個字符串,用於區別不同的節點。

清單 6. 程序和語句的抽象語法樹節點
 prog : stat -> ^(PROG stat); 
 stat : expr EOF  -> ^(STAT expr)

除了可以使用佔位符做根節點外,算符也可以直接作爲根節點,如清單 7 所示,加減乘除 4 個算符分別作爲抽象語法樹的根節點來建立樹。

清單 7. 表達式的抽象語法樹節點
 expr : multExpr (('+'|'-')^ multExpr)* 
 ; 
 multExpr : atom (('*'|'/')^ atom)* 
 ; 
 atom :   '(' expr ')' -> expr 
	   | INT -> ^(NUM INT) 
	   | ID  -> ^(VAR ID) 	
 ;

再次使用 Antlrworks 打開 Expr.g,在調試窗口輸入表達式 a+(2 + b),完成調試可以看到 a+(2 + b) 對應的抽象語法樹如圖 7 所示。整個表達式是一個 PROG,PROG 中包含了一個 STAT,而 STAT 是由一棵表達式構成的。

圖 7. a+(2+b) 的抽象語法樹
圖 7. a+(2+b) 的抽象語法樹

解釋器

抽象語法樹建立之後,可以使用 Antlr 的樹分析器來構造表達式的解釋器。樹分析器的語法和前面的表達式文法有所區別,創建一個 Eval.g 文件,文件的頭部通過 tree grammar 來標識這是一個樹分析器。

 tree grammar Eval;

之後對抽象語法樹節點逐一加入語義動作,完成最終的解釋執行。樹分析器會深度優先遍歷抽象語法樹,當 PROG 節點返回時,完成整個計算,輸出計算結果。STAT 擁有一個返回值,它的值取決於表達式的值。如清單 8 所示:

清單 8. 程序和語句的解釋
 prog : ^(PROG s=stat) {System.out.println("Compute result : " + s.value);}; 
 stat returns[Integer value] 
 : ^(STAT e=expr) {$value = e.value;} 
 ;

表達式同樣擁有返回值,算術運算的求值只需用左子節點的值和右子節點的值完成對應的運算即可;葉子節點 atom,如果輸入是一個常量,直接求出常量代表的值;如果輸入是一個變量,簡單起見,我們用一個隨機數來爲其賦值,如清單 9 所示。實際應用中,可以替換爲從數據庫中或者從文件中讀入變量的值。

清單 9. 表達式的解釋
 expr returns[Integer value] 
 :    ^('+' e1=expr e2=expr) {$value = e1.value + e2.value;} 
	 | ^('-' e1=expr e2=expr) {$value = e1.value - e2.value;} 
   | ^('*' e1=expr e2=expr) {$value = e1.value * e2.value;} 
 | ^('/' e1=expr e2=expr) {$value = e1.value / e2.value;} 
	 | a=atom {$value = a.value;} 
 ; 
 atom returns[Integer value] 
 :     ^(NUM i=INT) {$value = Integer.parseInt(i.getText());} 
 	 |  ^(VAR v=ID){ Random rand = new Random(); $value = rand.nextInt(10);} 
 ;

完成 Eval.g 的編輯之後,再次運行 Antlr.

java org.antlr.Tool c:/ antlr_intro\src\intepreter\Eval.g

Antlr 生成了樹分析器 Eval.java。使用 Antlr 的 API 完成以下 java 代碼,如清單 10 所示。至此完成了對輸入表達式的解釋求值。

清單 10. 調用解釋器
 public static void run(String expr) throws Exception { 
	 ANTLRStringStream in = new ANTLRStringStream(expr); 
	 ExprLexer lexer = new ExprLexer(in); 
	 CommonTokenStream tokens = new CommonTokenStream(lexer); 
	 ExprParser parser = new ExprParser(tokens); 
	 ExprParser.prog_return ret = parser.prog(); 
	 CommonTree t = (CommonTree)ret.getTree(); 
	 CommonTreeNodeStream nodes = new CommonTreeNodeStream(t); 
	 nodes.setTokenStream(tokens); 		
	 Eval e_walker = new Eval(nodes); 
	 e_walker.prog(); 
 }

解釋器執行結果如圖 8 所示:

圖 8. 解釋的輸出結果
圖 8. 解釋的輸出結果

編譯器

編譯執行和解釋執行相比,需要依賴於特定的目標機器,而解釋執行不需要。表達式求值的語義不是十分複雜,在這裏我們假設有一臺這樣機器,它用堆棧進行運算,支持以下 7 種指令,如表 1 所示:

表 1. 抽象機的 7 條指令
指令 說明 操作數個數 語義
LDV Load Variable 1 變量入棧
LDC Load Constant 1 常量入棧
ADD Add 0 棧頂兩個元素出棧,求和後入棧
SUB Subtract 0 棧頂兩個元素出棧,求差後入棧
MUL Multiply 0 棧頂兩個元素出棧,求積後入棧
DIV Divide 0 棧頂兩個元素出棧,求商後入棧
RET Return 0 棧頂一個元素出棧,計算結束

和之前的解釋器類似,創建一個 Compiler.g 樹分析器文件,其中各個表達式的編譯方案如清單 11 所示:

清單 11. 表達式的編譯
 prog : ^(PROG s=stat) {System.out.println("RET");}; 
 stat 
 : ^(STAT e=expr)  
 ; 
 expr 
 :     ^('+' e1=expr e2=expr) {System.out.println("ADD");} 
	 | ^('-' e1=expr e2=expr) {System.out.println("SUB");} 
    | ^('*' e1=expr e2=expr) {System.out.println("MUL");} 
	 | ^('/' e1=expr e2=expr) {System.out.println("DIV");} 
	 | a=atom  
 ; 
 atom  
 :      ^(NUM i=INT) {System.out.println("LDC "+i.getText());} 
 	 |  ^(VAR v=ID)  {System.out.println("LDV "+v.getText());} 
 ;

完成 Compiler.g 的編輯之後,再次運行 Antlr.

java org.antlr.Tool c:/antlr_intro\src\Compiler\Compiler.g

Antlr 生成了樹分析器 Compiler.java。使用Antlr的 API 完成以下java代碼,如清單 12 所示。至此完成了把表達式編譯爲抽象機的指令。

清單 12. 調用編譯器
 public static void run(String expr) throws Exception { 
	 ANTLRStringStream in = new ANTLRStringStream(expr); 
	 ExprLexer lexer = new ExprLexer(in); 
	 CommonTokenStream tokens = new CommonTokenStream(lexer); 
	 ExprParser parser = new ExprParser(tokens); 
	 ExprParser.prog_return ret = parser.prog(); 
	 CommonTree t = (CommonTree)ret.getTree(); 
	 CommonTreeNodeStream nodes = new CommonTreeNodeStream(t); 
	 nodes.setTokenStream(tokens); 		
	 Compiler c_walker = new Compiler(nodes); 
	 c_walker.prog(); 
 }

編譯的輸出結果如圖 9 所示:

圖 9. 編譯器的輸出結果
圖 9. 編譯器的輸出結果

結束語

本文用算術表達式作爲例子,全面展示 Antlr 的使用方法,Antlrworks 的使用方法,以及 Antlr 三大主要功能,詞法分析器、語法分析器和樹分析器。當你需要開發一種語言時,可以考慮使用 Antlr 作爲你的助手。

http://www.ibm.com/developerworks/cn/java/j-lo-antlr/


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