例1:整數加法運算
在這個例子中,我們將判斷如下輸入的式子是否是一個合法的加法運算:
99 + 42 + 0 + 15
並且在輸入上面式子的時候,數字與加號之間的任何位置,都是可以有空格或者換行符的,也就說,即使我們輸入的式子是下面這種形式,我們所編寫的詞法和語法分析器也應該要能判斷出來它是一個合法的加法運算表示形式:
99 + 42 + 0
+ 15
(注:上面輸入的式子中既有空格,也有制表符,還有換行符)
1.Option塊和class聲明塊
語法描述文件的第一部分是:
/* 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)塊。
- 在options中,幾乎所有配置項的默認值都適用於本例子,除了 STATIC選項,STATIC默認是true,這里要將其修改為false,使得生成的函數不是static 的。
- 接下來是ARSER_BEGIN(XXX)……PARSER_END(XXX)塊,這里定義了一個名為 Adder的類,當然在這個塊中定義的並非是Adder類的全部,JavaCC會根據.jj描述文件的其他部分的描述,來生成Adder的其他聲明信息。另外注意到,在該類的main方法聲明中, 拋出了兩個異常類,分別為ParseException和TokenMgrError,這兩個異常類會在使用javacc 命令編譯當前.jj描述文件的時候生成。)
2.詞法描述器
我們在后面還會再講到main方法。這里讓我們先看看詞法描述器。當前例子中所需的詞法描述器通過如下的四行來進行描述:
SKIP : { " "}
SKIP : { "\n" | "\r" | "\r\n" }
TOKEN : { < PLUS : "+" > }
TOKEN : { < NUMBER : (["0"-"9"])+ > }
- 第一行是SKIP,表示將會被詞法分析器忽略的部分:空格。被忽略掉意味着,它們將不會被傳給語法分析器。
- 第二行也是SKIP,定義了將會被忽略的部分:換行符。之所以會有幾個,是因為在不同的系統中,換行符有不同的表示方式——在Unix/Linux系統中,換行符是"\n";在Windows系統中,換行符是"\r";在mac系統中,換行符則是"\r\n"。這幾個換行符用一個豎杠分隔,表示“或”的意思。
- 第三行定義了一個名為PLUS的token,用它來表示加號"+"。
- 第四行定義了一個名為NUMBET的token,用它來表示([”0”-”9”])+,即所有的正整數序列。可以注意到([”0”-”9”])+是一個正則表達式。
這四行描述都可以被稱為表達生產式。
事實上,詞法分析器中還可以生成一種token,這種token用EOF表示,用來代表輸入序列的末尾。但是沒有必要在詞法分析器部分顯式的定義EOF這個token,因為JavaCC會自動處理文件的結束符了。
假設有如下的輸入:
“123 + 456\n”
詞法分析器將解析的7個token,依次是NUMBER、空格、PLUS、空格、NUMBER、換行符、EOF。在解析出來的這些token中,被標記為SKIP的token將不會被往下傳遞給語法分析器。詞法分析器在分析完成之后,只會將以下token傳遞給語法分析器:NUMBER, PLUS, NUMBER, EOF。
再假設一種不合法的輸入,如下:
“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。
3.語法分析器
語法分析器的描述由BNF生產式構成。可以看到,語法分析器的描述看起來跟java的方法定義形式有點相似。
void Start() :
{}
{
<NUMBER>
(
<PLUS>
<NUMBER>
)*
<EOF>
}
上面的BNF生產式指定了合法的token序列的規則:必須以NUMBER token開頭,以EOF token結尾,而在NUMBER和EOF中間,可以是0至多個PLUS和NUMBER的token,而且必須是PLUS后跟着NUMBER。
根據上面的語法描述器語法,解析器將只檢測輸入序列是否無錯誤,它不會把數字加起來。我們接下來將很快修改解析器描述文件以更正此問題,但是首先,讓我們生成Java組件並運行它們。
4.生成詞法描述器和語法描述器
將前面部分提到的幾個部分都合並起來,保存成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文件。如下所示:
其中:
- TokenMgrError 是一個簡單的定義錯誤的類,它是Throwable類的子類,用於定義在詞法分析階段檢測到的錯誤。
- ParseException是另一個定義錯誤的類。它是Exception 和Throwable的子類,用於定義在語法分析階段檢測到的錯誤。
- Token類是一個用於表示token的類。我們在.jj文件中定義的每一個token(PLUS, NUMBER, or EOF),在Token類中都有對應的一個整數屬性來表示,此外每一個token都有名為image的string類型的屬性,用來表示token所代表的從輸入中獲取到的真實值。
- SimpleCharStream是一個轉接器類,用於把字符傳遞給語法分析器。
- AdderConstants是一個接口,里面定義了一些詞法分析器和語法分析器中都會用到的常量。
- AdderTokenManager 是詞法分析器。
- Adder 是語法分析器。
接下來我們對這些java文件進行編譯:
編譯完成之后,可得到對應的class文件:
5.執行程序
現在,讓我們來看看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中情況之一:
- 程序報一個詞法錯誤。比如,詞法錯誤只有在詞法分析器無法解析出輸入的字符時才會拋出。假設input.txt文件中的內容是123 – 456,在這種情況下,程序就會拋出一個TokenMgrError的錯誤,報錯的信息是:Exception in thread ”main” TokenMgrError: Lexical error at line 1,column 5. Encountered: ”-” (45), after : ”” 。即:詞法分析器不認識“-”減號,因為在.jj文件中,我們並沒有對“-”定義相應的token。
- 程序報一個語法錯誤。當語法分析器接收到的tokens序列不匹配Start()方法中的規范時,就會拋出該錯誤。比如,若input.txt文件中的內容是123 ++ 456或123 456又或者什么都不寫,此時程序就會拋出一個ParseException.異常,對於123 ++ 456來說,拋出的異常就是:Exception in thread ”main” ParseException: Encountered ”+” at line 1, column 6. Was expecting:
... 。 - 若輸入中的tokens序列跟Start()方法中的規范匹配時,將不拋出任何錯誤異常,程序正常結束。
但是從我們的代碼可以看出,當我們的輸入是合法的時候,語法分析器什么也不干,它僅僅用於檢查我們的輸入是否符合相應的表達式規范而已。在下一小節中,我們將對.jj文件進行一些修改,使得生成的語法分析器有更多的用處。
6.生成的代碼解析
想要知道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。