原址:http://www.aliensoft.cn/Chapter1.mht
第一章Hello World
JVM |
.NET |
ANTLR文法 |
ANTLR Runtime |
C# |
生成 |
Java |
csc.exe |
語法分析器 |
編譯 |
javac.exee |
語法分析器 |
編譯 |
C |
C++ |
Python |
… … |
嵌入C#,java…代碼片段 |
ANTLR是ANother Tool for Language Recognition的縮寫“又一個語言識別工具”,讀[ 'æntlə ]。從名字上可以看出在ANTLR出現之前已經存在其它語言識別工具了(如LEX[1],YACC[2])。ANTLR的官方定義爲:根據一種可以嵌入如Java, C++或C#等輔助代碼段的文法,來構築出相對該文法的識別器,編譯器或翻譯器的一種語言工具框架。這個定義說明了ANTLR的功能是根據給定文法自動生成編譯器,其過程爲先編寫相應語言的文法然後生成相應語言編譯器。定義提到的語言識別器,編譯器和翻譯器我們以後統稱爲語法分析器。事實上ANTLR是生成相應語言編譯器的源代碼,我們還需要編譯它。那麼ANTLR可以生成哪些方語言的語法分析器源代碼語言的代碼呢?這是程序員很關心的問題。幸運的是ANTLR現在已經支持了多種當前流行的開發語言,包括Java、C#、C、C++、Objective-C、Python和 Ruby.1等。你可以根據需要生成其中任何一種語言的語法分析器。本書主要介紹java,C#兩種語言,有詳細的操作步驟包括如何編譯、執行和如何使用ANTLRWorks開發環境編寫文法等。讀者可以順利上手,避免實際操作的障礙。後面章節還會指出在Java和C#開發中應注意的細微差別,確保程序的順利運行。
1.1開發Hello World示例
本章將開發一個簡單示例讓讀者對ANTLR有一個初步的認識,並搭建開發環境以便後續的學習。讀者在示例中遇到不懂的地方也不必擔心,我們的目的是搭建開發環境學會編譯運行語法分析器。用ANTLR開發一個語法分析器大致分三步,第一步:寫出要分析內容的文法。第二步:用ANTLR生成相對該文法的語法分析器的代碼。第三步:編譯運行語法分析器。
和多數編譯書籍一樣,本章也用解析簡單的表達式作爲示例。要解析的表達式中有二種數據類型:整數 如“23”, “5” 和字符串 如“Hello World”。表達式中以算術表達式爲主也包括賦值表達式。我們列舉兩個表達式語句:
23+4*(5+1); str=“Hello World”;
第一條語句是一個算術表達式,括號改變了運算順序,計算結果不賦給任何變量。第二條是一個賦值表達式,將字符串賦給一個變量。後面我們要開發一個語法分析器來分析這兩條語句。在開發之前先簡單提一下語法樹的概念,在語法分析中一般用樹來表示語法結構,表達式的語法樹是以操作符爲根節點操作數爲子節點的樹形結構,23+4*(5+1)的語法樹根據圖1.1所示的操作符的優先級如下。
+ |
23 |
* |
4 |
+ |
5 |
1 |
圖1.1
算術表達式先計算5+1,5+1在括號中操作符的優先級最高在語法樹中的深度最大,然後是4*(5+1),最後是23+4*(5+1)。可以看出語法樹的求值順序是從下向上的,先計算深度大的操作符5+1結果爲6,然後是4*6結果爲24,然後是23+24表達式的結果爲47。下面再看一下賦值表達式的語法樹結構:
= |
str |
“Hello World” |
圖1.2
賦值操作符“=”做爲根節點變量str作爲左子樹,而字符串表達式“Hello World” 作爲右子樹。瞭解了語法樹後我們開始錄入文法源文件。ANTLR中文法文件是擴展名爲“.g”的文本文件,“.g”文件就是我們的源文件。這裏新建一個叫“E.g”的文法文件,在文件中輸入如下文法定義:
grammar E;
options{ output=AST;}
program : statement + ;
statement: (expression | VAR '=' expression) ';' ;
expression : (multExpr (('+' |'-' ) multExpr)*) | STRING;
multExpr : atom ('*' atom)*;
atom : INT | '(' expression ')';
VAR : ('a'..'z' |'A'..'Z' )+ ;
INT : '0'..'9' + ;
STRING : '"' (('A'..'Z' | 'a'..'z' | ' ') +) '"' ;
WS : (' ' |'/t' |'/n' |'/r' )+ {skip();} ;
文件的第一行grammar E的E爲文法的名稱它與文件名一致。第二行是文法的設置部分options{ output=AST;},output=AST表示讓語法分析器返回包含語法樹的信息。第三行開始是文法定義部分,文法是用EBNF1推導式來描述的(有關EBNF會在後面章節中講解),文法定義中分兩大部分以小寫字母開頭的語法描述和全大寫的詞法描述。其中每一行都是一個規則(rule)或叫做推導式、產生式,每個規則的左邊是文法中的一個名字,代表文法中的一個抽象概念。中間用一個“:”表示推導關係,右邊是該名字推導出的文法形式。下面逐行介紹文法的規則定義:
statement : (expression | VAR '=' expression) ';'
statement代表表達式語句,前面說了語句有兩種,在推導式中以“|”分隔代表並列可選的關係。表達式本身是合法的語句,表達式也可以出現在賦值表達式中組成賦值語句,兩種語句都以“;”字符結束。
expression: (multExpr (('+' |'-' ) multExpr)*) | STRING
expression代表表達式,第二個“|”的左邊是算術表達式的形式,第二個“|”的右邊是字符串表達式。我們通過規則的推導順序可以看出,規則按操作符的優先級首先推導優先級最低的運算“+”,“-”運算。
multExpr : atom ('*' atom)*;
然後是'*'的運算。表達式中沒有除法運算。
atom : '(' expression ')' | INT;
最後是“()”運算,括號中又可以是一個表達式,這樣也就實現了括號的嵌套關係。以“|” 分隔與括號並列的可以出現參與運算的整型數。
VAR : ('a'..'z' |'A'..'Z' )+ ;
INT : '0'..'9' + ;
STRING : '"' (('A'..'Z' | 'a'..'z' | ' ') +) '"' ;
WS : (' ' |'/t' |'/n' |'/r' )+ {skip();} ;
以大寫形式表達的是詞法描述部分,VAR表示變量由一個或多個大小寫字母組成,INT表示整型,整型由一個或多個0到9的數字組成,STRING表示字符串,和變量類似一個或多個大小寫字母組成但要用“"”括起來。WS表示空白,它的作用是可以濾掉空格、TAB、回車換行這樣的無意義字符。{skip();}的作用是跳過這些字符。
1.2下載ANTLR
本章我們的目的是運行第一個示例,後面章節會詳細介紹文法定義的寫法,所以暫時有不清楚的地方不必擔心。這裏應該注意的是:文法中單詞的大小寫,ANTLR文法是大小寫敏感的,文法名稱要和文件名一致。下面我們用ANTLR生成該文法的java分析器代碼。我們先下載ANTLR的Runtime和開發環境,到http://www.antlr.org/download.html ANTLR的下載頁,www.antlr.org爲ANTLR的官方網站,ANTLR是一個開源項目可以免費下載。圖1.1爲開發環境ANTLRWorks的下載頁面,圖1.2爲開發環境ANTLR Runtime3.01的下載頁面。您的機器上需要安裝JDK1.5或更高版本的java SDK。其中ANTLRWorks的下載文件名叫antlrworks-1.1.7.jar安裝JDK後可以直接雙擊運行打開ANTLRWorks開發環境。
(圖1.3)ANTLRWorks下載頁面
(圖1.4)ANTLR Runtime下載頁面
(圖1.5)ANTLRWorks IDE
1.3 Java環境的運行
下面錄入文法文件,運行ANTLRWorks點擊“File – New”菜單新建文法文件,在新文件中將前面的文法錄入。(我的網站中有本書所有示例源代碼,但我建議您還是手工錄入一遍。這樣您會有更好的學習效果。)錄入文法後點擊“File – Save” 菜單文件名爲“E.g”。然後點擊“Generate–Generate Code”,如果ANTLRWorks提示“The grammar has been successfully generated in path…”說明ANTLRWorks成功生成了語法分析器的代碼。會在“E.g”的當前目錄中生成“ELexer.java”、“EParser.java”、“E.tokens”和“E__.g”四個文件,其中有兩個java源文件。“ELexer.java”爲詞法分析部分的代碼,“EParser.java”爲語法分析部分的代碼。那麼爲什麼會生成java代碼呢?ANTLR在不指定目標語言的情況下默認是java語言。我們也可以在文法文件中加入options項指定目標語言。
grammar E;
options { output=AST; language=Java; }
program : statement + ;
……
生成了代碼後,我們來編譯運行語法分析器。剛纔生成的是java代碼,所以先來看看java如何編譯運行。首先要有一個執行程序的main方法,這個類如下:
import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;
public class run
{
public static void main(String[] args) throws Exception
{
ANTLRInputStream input = new ANTLRInputStream(System.in);
ELexer lexer = new ELexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
EParser parser = new EParser(tokens);
EParser.program_return r = parser.program();
System.out.println(((BaseTree)r.getTree()).toStringTree());
}
}
把這段代碼存入run.java文件中,main方法功能是從命令行接收輸入的表達式,通過詞法分析和語法分析兩個步驟來獲得這個表達式的語法樹,並以字符串的形式輸出語法樹的內容。
ANTLRInputStream input = new ANTLRInputStream(System.in);
ELexer lexer = new ELexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
詞法分析步驟是從命令行接收輸入的表達式,通過ANTLR內部ANTLRInputStream類,生成一個ANTLRInputStream類的實例input,再將input傳給ELexer類。ELexer類是詞法分析類,把input中的輸入內容進行詞法分析,詞法分析後會產生記號流(token stream)交給語法分析類,爲語法分析提拱前提。
EParser parser = new EParser(tokens);
EParser.program_return r = parser.program();//此處進行了語法分析
System.out.println(((BaseTree)r.getTree()).toStringTree());
語法分析步驟是根據詞法分析產生的記號流生成語法分析類的實例parser。然後調用parser的方法program()。這個方法名和我們文法中的第一條規則program : statement + ;的名字是一致的,這說明我們要用整個文法進行分析,program是文法的啓點。調用program()方法後就進行了語法分析,program()方法返回語法分析的信息其中包括語法樹。r.getTree()可以返回語法樹,getTree()返回的是Object類型所以這裏做一個類型轉換(BaseTree)r.getTree()並調用其toStringTree()方法獲得語法樹的字符串形式輸出。
到現在完成了源代碼的錄入工作,接下來編譯所有的代碼。編譯的命令行字符串爲:
javac -classpath .;...../antlr-3.0.1/lib/antlr-3.0.1.jar *.java
run.java中的import org.antlr.runtime.*;import org.antlr.runtime.tree.*;所引用的類包含在antlr-3.0.1.jar中,解壓我們之前下載的antlr-3.0.1.tar.gz文件,在其中的lib目錄中可以找到antlr-3.0.1.jar。編譯字符串中的-classpath參數中給出...../antlr-3.0.1/lib/antlr-3.0.1.jar的實現路徑。運行程序的命令行字符串與編譯字符串相似:
java -classpath .;...../antlr-3.0.1/lib/antlr-3.0.1.jar run
好的我們來執行這兩個字符串來編譯並執行程序,執行程序後命令行光標會等待輸入,把之前準備分析的兩個表達式輸入,然後按Ctrl+Z(Windows系統Ctrl+Z,UNIX系統Ctrl+D)表示輸入結束,然後回車。
23+4*(5+1); str="hello world"; ^Z 23 + 4 * ( 5 + 1 ) ; str = "hello world" ;
|
程序輸出23 + 4 * ( 5 + 1 ) ; str = "hello world" ;這表示程序執行成功了。我們的語法分析器已經正確的解析了這兩個表達式。您可以試着用不規則的格式輸入兩個表達式會得到相同的輸出,因爲已經正確分析了表達式的語法,所以輸出格式化的字符串對我們的分析器來說是很簡單的事情了。
23 +4*( 5+ 1 );str = " hello world " ; ^Z 23 + 4 * ( 5 + 1 ) ; str = " hello world " ;
|
1.4 .NET環境的運行
我們再說說.NET的編譯執行。首先生成C#的語法分析器代碼,在文法中的options設置中修改目標語言爲CSharp,還要把WS中的skip()改成Skip()。Java版和.NET版的ANTLR Runtime都使用了各自語言的命名規範,所以名稱上略微有些區別。
options { output=AST; language=CSharp; }
……
WS : (' ' |'/t' |'/n' |'/r' )+ {Skip();} ;
然後用ANTLRWorks中的Generate菜單生成代碼,這時目錄中會生成四個文件“ELexer.cs”、“EParser.cs”、“E.tokens”和“E__.g”。E.tokens和E__.g文件與之前Java開發中生成的兩個同名文件是一樣的,另外ELexer.cs爲詞法分析器,EParser.cs爲語法分析器。在.NET開發中我們採用Visual Studio.NET2005做爲開發環境,讓我們的示例和真正的開發貼近一些。在Visual Studio.NET2005中先新建一個名稱爲“HelloWorld”的C# WindowApplication項目,將生成的ELexer.cs、EParser.cs文件拷貝並加入到項目中。再將.NET版的ANTLR Runtime的DLL引用到項目中來。本示例需要Antlr3.Runtime.dll和antlr.runtime.dll,這兩個文件在antlr-3.0.1.tar.gz解壓後的antlr-3.0.1/antlr-3.0.1/ runtime/CSharp/bin/net-2.0目錄中可以找到。這些操作都完成之後,我們在程序窗體上放一個多行文本框,一個按鈕和一個Label。在窗體的代碼文件Form1.cs中加入:
using Antlr.Runtime;
using Antlr.Runtime.Tree;
我們要實現在文本框中輸入表達式語句,點擊按鈕語法樹結果顯示在Label控件中。在按鈕的事件中加入如下代碼:
ICharStream input = new ANTLRStringStream(textBox1.Text);
ELexer lex = new ELexer(input);
CommonTokenStream tokens = new CommonTokenStream(lex);
EParser parser = new EParser(tokens);
EParser.program_return progReturn = parser.program();
label1.Text = ((BaseTree)progReturn.Tree).ToStringTree();
這裏由於表達式是從界面文本框中獲得,所以第一行代碼和上面java的示例有些不同使用ANTLRStringStream類來接收錄入的內容。後面的代碼和java版本中的幾乎一致,只是有一些java與.NET在命名規則方面的區別。Java方法名首字母爲小寫而.NET是大寫。
(圖1.6).NET版HelloWord的運行結果
1.5 改進示例
到此我們已經學習了java和.NET開發語法分析器的全過程。讀者可能覺得做完這個示例成就感不大,因爲輸入和輸出是一樣的,並沒有看到前面提到的語法樹結構。下面我們修進一下示例在文法中添加一些構造語法樹的符號,使程序構造出如圖1.1、圖1.2的語法樹。文法修改如下:
grammar E;
options{ output=AST;}
program : statement + ;
statement: (expression | VAR '=' ^ expression) ';' ;
expression : (multExpr (('+' ^ |'-' ^ ) multExpr)*) | STRING;
multExpr : atom ('*' ^ atom)*;
atom : INT | '(' ! expression ')' !;
VAR : ('a'..'z' |'A'..'Z' )+ ;
INT : '0'..'9' + ;
STRING : '"' (('A'..'Z' | 'a'..'z' | ' ') +) '"' ;
WS : (' ' |'/t' |'/n' |'/r' )+ {skip();} ;
修改後的文法中所有操作符的後面都添加了一個“^”符號,這表示操作符會在構造語法樹時作爲根節點。“statement: (expression | VAR '=' ^ expression) ';' ! ;”一行中的“;”字符與“atom : INT | '(' ! expression ')' !;”的“( )”字符後面添加了“!”符號,表示不讓“( )”和“;”出現在語法樹中,因爲語法樹已經體現了操作求值順序,所以括號沒有必要出現在語法樹中。代表語句結束的“;”是爲語法分析服務的,語法分析後語法樹中的也沒有必要加入“;”。我們會在以後的章節中更詳細講解如何構造語法樹。現在先用修改後的文法來生成代碼,編譯運行程序,輸入同樣的表達式我們會得到如下結果:
23+4*(5+1); str="hello world"; ^Z (+ 23 (* 4 ( + 5 1 ))) (= str "hello world" ) |
程序的輸出結果了發生了變化:算術表達式的語法樹輸出字符串形式爲:(+ 23 (* 4 (+ 5 1))) 我們已經使“()”不出現在語法樹中了,所以輸出字符串中的括號並不是我們輸入的表達式括號,這些括號表示語法樹的結構。由於我們讓操作符爲根節點,所以這裏“+”、“*”操作符出現在前面,其後是它的左子樹,再往後是它的右子樹,內層括號是外號括號子樹。按照這個規則我們可以繪出語法樹:
+
23 *
4 +
5 1
繪出語法樹和前面圖1.1中的語法樹是一樣的,這說明我們已經正確的生成了語法樹。賦值表達式亦然。我們可以在Visual Studio.NET 2005中運行。在程序中設置斷點並查看progReturn.Tree的對象內存情況。
圖1.7
語法樹是BaseTree類型,BaseTree有一個children集合用來存放子語法樹,子語法樹也是BaseTree類型,可以利用VS.NET內存監視功能一級一級展開,看一看ANTLR的語法樹的對象表現形式。BaseTree類有一個GetChild(int i) 方法可以獲取第N個子樹,還有一個ChildCount屬性代表子樹的個數。結合這兩個屬性和方法可以用如下的代碼遍歷子語法樹。
for (int i = 0; i < tree.ChildCount; i++) {
BaseTree currTree = (BaseTree)tree.GetChild(i);
}
1.6 本書結構(以後補充)
好,完成Hello World示例的開發後,介紹一下本書的結構。本書後面章節大體順序是:先學習文法、推導式等編譯原理基礎知識,使沒有編譯原理知識基礎的讀者鋪平道路。然後全面學習ANTLR開發技術,主要包括詞法、語法、語法樹以及字符模板的內容。在ANTLR全學習之後再強化學習一下編譯原理的知識(如DFA)然後學習如何解決ANTLR開發中的較難的問題。
第二章 編譯原理基礎知識
第三章 詞法分析
第四章 語法分析
第五章 嵌入文法的Actions
第六章 構造語法樹
第七章 遍歷語法樹及語義分析
第八章 使用字符串模板
第九章 編譯錯誤處理
第十章:文法編寫中的錯誤和解決方法
第十一章:ANTLR API
第十二章:一個總體的開發實例。
1.7本章小結
本章開發表達式語法分析器示例詳細地向讀者介紹了ANTLR的開發過程,如何建立ANTLR的開發環境以及如何在Java和.NET環境中編譯和運行程序。本書後面出現的示例希望讀者都要親手完成,只有親手做出正確運行的程序纔算是真正領悟了書中的內容。
從文件加載
TestE10Lexer lexer = new TestE10Lexer(new ANTLRFileStream("TestE10.txt"));
CommonTokenStream tokens = new CommonTokenStream(lexer);
TestE10Parser parser = new TestE10Parser(tokens);