在這個例子中,咱們將判斷以下輸入的式子是不是一個合法的加法運算:java
99 + 42 + 0 + 15
而且在輸入上面式子的時候,數字與加號之間的任何位置,都是能夠有空格或者換行符的,也就說,即便咱們輸入的式子是下面這種形式,咱們所編寫的詞法和語法分析器也應該要能判斷出來它是一個合法的加法運算表示形式:正則表達式
99 + 42 + 0 + 15
(注:上面輸入的式子中既有空格,也有製表符,還有換行符)windows
語法描述文件的第一部分是:函數
/* adder.jj Adding up numbers */ options { STATIC = false ; } PARSER_BEGIN(Adder) class Adder { public static void main( String[] args ) throws ParseException, TokenMgrError { Adder parser = new Adder( System.in ); parser.Start(); } } PARSER_END(Adder)
上面的代碼能夠分爲兩個部分,一個是options塊,另外一個是PARSER_BEGIN(XXX)…… PARSER_END(XXX)塊。編碼
咱們在後面還會再講到main方法。這裏讓咱們先看看詞法描述器。當前例子中所需的詞法描述器經過以下的四行來進行描述:3d
SKIP : { " "} SKIP : { "\n" | "\r" | "\r\n" } TOKEN : { < PLUS : "+" > } TOKEN : { < NUMBER : (["0"-"9"])+ > }
「123 + 456\n」
詞法分析器將解析的7個token,依次是NUMBER、空格、PLUS、空格、NUMBER、換行符、EOF。在解析出來的這些token中,被標記爲SKIP的token將不會被往下傳遞給語法分析器。詞法分析器在分析完成以後,只會將如下token傳遞給語法分析器:NUMBER, PLUS, NUMBER, EOF。
再假設一種不合法的輸入,以下:code
「123 - 456\n」
詞法分析器在對上面的輸入進行解析時,解析到的第一個token是NUMBER,第二個token是空格,接下來,它就遇到了一個減號字符——由於在咱們上面的詞法描述器中沒有定義減號這個token,所以就沒法對其進行解析,此時詞法分析器就拋出一個異常:TokenMgrError。
再看另一種輸入狀況:對象
「123 ++ 456\n」
這時詞法分析器能夠對其進行解析,並給語法分析器傳遞以下的token序列:NUMBER, PLUS, PLUS, NUMBER, EOF。
很明顯,這不是一個合法的「加運算」的輸入它連續出現了兩個PLUS token。可是,詞法分析器的任務是將輸入解析成一個個的token,而不是判斷token的順序是否正確。判斷tokens序列的順序是否正確是語法分析器的任務。接下來將會介紹到的語法分析器,它在分析到第二個PLUS token的時候,將會檢測到錯誤,一旦檢測到錯誤,它就中止從詞法分析器請求tokens了,因此,實際上真正傳遞給語法分析器的tokens序列只有NUMBER, PLUS, PLUS。blog
語法分析器的描述由BNF生產式構成。能夠看到,語法分析器的描述看起來跟java的方法定義形式有點類似。token
void Start() : {} { <NUMBER> ( <PLUS> <NUMBER> )* <EOF> }
上面的BNF生產式指定了合法的token序列的規則:必須以NUMBER token開頭,以EOF token結尾,而在NUMBER和EOF中間,能夠是0至多個PLUS和NUMBER的token,並且必須是PLUS後跟着NUMBER。
根據上面的語法描述器語法,解析器將只檢測輸入序列是否無錯誤,它不會把數字加起來。咱們接下來將很快修改解析器描述文件以更正此問題,可是首先,讓咱們生成Java組件並運行它們。
將前面部分提到的幾個部分都合併起來,保存成adder.jj文件:
/* adder.jj Adding up numbers */ options { STATIC = false ; } PARSER_BEGIN(Adder) class Adder { public static void main( String[] args ) throws ParseException, TokenMgrError { Adder parser = new Adder( System.in ); parser.Start(); } } PARSER_END(Adder) SKIP : { " "} SKIP : { "\n" | "\r" | "\r\n" } TOKEN : { < PLUS : "+" > } TOKEN : { < NUMBER : (["0"-"9"])+ > } void Start() : {} { <NUMBER> ( <PLUS> <NUMBER> )* <EOF> }
而後在上面調用javacc命令。下面是在windows系統上的演示。
執行完以後,會生成7個java文件。以下所示:
其中:
接下來咱們對這些java文件進行編譯:
編譯完成以後,可獲得對應的class文件:
如今,讓咱們來看看Adder類中的main方法:
Pubic static void main( String[] args ) throws ParseException, TokenMgrError { Adder parser = new Adder( System.in ); parser.Start(); }
首先注意到main方法有可能拋出兩個異常錯誤類:ParseException和TokenMgrError,它們都是Throwable類的子類。這並非一種好的編碼習慣,理論上咱們應該對着兩異常進行try-catch捕獲,可是在本例中咱們暫且將其拋出,以使得代碼簡潔易懂。
main方法的第一行代碼new了一個parser對象,使用的是Adder類的默認構造器,它接收一個InputStream類型對象做爲輸入。此外Adder類還有一個構造器,這個構造器接收的入參是一個Reader對象。構造函數依次建立一個SimpleCharacterStream類實例和一個AdderTokenManager類的實例(即詞法分析器對象)。所以,最後的效果是,詞法分析器經過SimpleCharacterStream實例對象從System.in中讀取字符,而語法分析器則是從語法分析器中讀取tokens。
第二行代碼則是調用了語法分析器的一個名爲Start()的方法。對於在.jj文件中的每個BNF生產式,javacc在parser類中都會生成相應的方法。此方法負責嘗試在其輸入流中找到與輸入描述匹配的項。好比,在本例中,調用Start()方法將會使得語法分析器嘗試從輸入中符合下面描述的tokens序列:
<NUMBER> (<PLUS> <NUMBER>)* <EOF>
咱們能夠事先準備一個input.txt文件,裏面的內容下面會說到。在準備了輸入文件以後,接下來就能夠用下面的命令來執行程序了。
執行的結果有可能爲如下3中狀況之一:
想要知道JavaCC生成的語法分析器是如何工做的,咱們須要看一些它生成的代碼。
final public void Start() throws ParseException { jj_consume_token(NUMBER); label_1: while (true) { jj_consume_token(PLUS); jj_consume_token(NUMBER); switch ((jj_ntk == -1) ? jj_ntk() : jj_ntk) { case PLUS: ; break; default: jj_la1[0] = jj_gen; break label_1; } } jj_consume_token(0); }
jj_consume_token方法以token類型做爲入參,並試圖從語法分析器中獲取指定類型的token,若是下一個獲取到的token跟語法分析器中定義的不一樣,此時就會拋出一個異常。看下面的表達式:
(jj_ntk == -1) ? jj_ntk() : jj_ntk
該表達式計算下一個未讀的token。 程序的最後一行是要獲取一個爲0的token。JavaCC老是使用0來表示EOF的token。