国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
JavaCC 簡介

JavaCC 簡介

簡介

Theodore S. Norvell 寫的一本《The JavaCC Tutorial》的第一章Introduction to JavaCC

JavaCC 入門

1.      JavaCC 和分析器生成程序

JavaCC 是一個能生成語法和詞法分析器的生成程序。語法和詞法分析器是字符串處理軟件的重要組件。編譯器和解釋器集成了詞法和語法分析器來解釋那些含有程序的文件,不管怎樣,詞法和預防分析器被廣泛用于各種應用,所以我希望這本書中的示例能闡述清楚。

那么什么是詞法和語法分析器呢?詞法分析器能把一串字符切分成一溜叫做token 的子串并把它們歸類??匆粋€用C 語言寫的小程序。

 

int main() {

return 0 ;

}

 

C 編譯器的詞法分析器會把上面的程序切割成下面的一串token

“int”, “ ”, “main”, “(”, “)”,

“ ”, “{”, “\n”, “\t”, “return”

“ ”, “0”, “ ”, “;”, “\n”,

}”, “\n”, “” .

詞法分析器還會識別每個token 的類型;在這個例子中這個token 串的類型可能是

KWINT, SPACE, ID, OPAR, CPAR,

SPACE, OBRACE, SPACE, SPACE, KWRETURN,

SPACE, OCTALCONST, SPACE, SEMICOLON, SPACE,

CBRACE, SPACE, EOF .

EOF 這種token 表示輸入文件結束。Token 流將會傳給分析器。在這個C 語言的例子中,分析器并不需要所有的token;本例那些被歸為SPACE token 不會傳給分析器。分析器將會分析token 流以決定程序的結構。通常在編譯器中,分析器輸出一棵代表程序結構的樹。這棵樹將會作為編譯器語法分析和代碼生成的輸入的一部分??紤]某個程序里的一個語句:

fahrenheit = 32.0 + 9.0 * celcius / 5.0 ;

分析器根據(jù)語法規(guī)則分析這個表達式并生成一棵樹:

 

1fahrenheit = 32.0 + 9.0 * celcius / 5.0 (譯注:原文缺失該圖,此圖為譯者根據(jù)前后語境所畫)

 

如果輸入不遵循語言的詞法或句法規(guī)則,詞法分析器同時也負責產(chǎn)生出錯信息。

JavaCC 本身并不是一個詞法分析器或是語法分析器,而是一個分析程序生成器。這就是說它可以根據(jù)輸入的語言規(guī)范輸出一個詞法和語法分析器。JavaCC 生成的是用Java 寫成的詞法和語法分析器。見下圖TBD

DIAGRAM TBD

詞法和語法分析器本身就是一個冗長而又復雜的組件。手工編寫一個這樣的程序需要仔細考慮各種語法規(guī)則的相互影響。比如在一個C 的詞法分析器中,處理整數(shù)的代碼和處理浮點常量的代碼不能相互獨立,因為整數(shù)和浮點數(shù)的開頭都是數(shù)字。而使用像JavaCC 這一點分析程序生成器時,處理整數(shù)的規(guī)則和處理浮點數(shù)的是分開書寫的,而它們之間公共的代碼在生成器處理時被抽取出來了。模塊性的增加意味著語言規(guī)范比手寫的Java 程序更容易編寫、閱讀和修改。通過使用JavaCC 這樣的分析程序生成器,使軟件工程師節(jié)約了不少時間,同時也增強了所編寫軟件的質(zhì)量。

2.      第一個例子——整數(shù)相加

作為第一個例子,我們把一串數(shù)字加起來,像這樣

99 + 42 + 0 + 15

我們忽略所有數(shù)字和符號間的空格和換行符,除此之外,我們不接受除了10個數(shù)字和加號之外的其他字符。

本節(jié)后面的代碼都出自一個叫“adder.jj”的文件。這個文件含有符合JavaCC 規(guī)范的詞法和語法說明,用來作為JavaCC 的輸入。   

2.1.  選項和類聲明

文件的第一部分

 

/* 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)

 

在第一個注釋之后的是選項段;除了STATIC 這一項(缺省為true),所有其他的JavaCC選都為默認值。關于JavaCC 選項的更多信息,請參考JavaCC 的文檔、本書的以后的章節(jié)和FAQ。接下來定義了一個叫做Adder Java 類,但在這你所看到的不是Adder 類的全部;JavaCC 會在處理時為這個類添加其他代碼。main 方法宣稱可能在運行時隱式的拋出兩個異常:ParseException  TokenMgrError;這些類都會由JavaCC生成。

2.2.  詳述詞法分析器

我們待會兒再看那個main函數(shù),現(xiàn)在我們首先來定義一個詞法分析器。在這個簡單的例子中,詞法分析器的定義只有4行:

 

SKIP :  { ” ” }

SKIP :  { ”\n” | ”\r” | ”\r\n” }

TOKEN : { < PLUS : ”+” > }

TOKEN : { < NUMBER : ([”0”-”9”])+ > }

 

第一行說明了空格是一個token,但是會被忽略。所以解析器并不會收到任何單獨的空格。

第二行也說了差不多的事情,只不過被忽略的是換行符,換行符會因操作系統(tǒng)而不同。Unix/Linux 采用LF (linefeed)字符;DOS Windows 則用CR+LF (carriage + linefeed),在老的Macintoshes 機子上,就用一個回車表示。我們要告之JavaCC 所有的可能,就如上面用一個小豎線”|” 把不同的匹配模式隔開。

第三行告訴JavaCC一個單獨的加號是一個token,而且給這個Token取了一個名字:PLUS。

最后一行告訴JavaCC數(shù)字的語法并為它們?nèi)∶麨?/span>NUMBER。如果你熟悉Perl或者Java的正則表達式包,就不難明白這些式子的含義。讓我們仔細看一下這個表達式([“0”-“9”])+。圓括號中的 [“0”-“9”] 是一個匹配任意數(shù)字的正則表達式,這表明unicode編碼中的0-9之間的字符都能被匹配。一個形如 (x)+ 的正則式可以匹配任意重復的x 串。所以表達式 ([“0”-“9”])+ 就可以匹配任意連續(xù)數(shù)字串。這四行每一行都是一個正則表達式實例(regular expression production )

還有一種由詞法分析器生成的token,它的名字是EOF,正如其名,它代表了輸入的終止。不能,也不需要任何對EOF的匹配,JavaCC會自動生成它們。

考慮一個包含如下字符串的輸入文件:

123 + 456\n

我們定義的詞法分析器將會找到7token: NUMBER, 空格, PLUS, 又一個空格, 另一個數(shù)字,一個換行, 然后是EOF。當然,標記了SKIPtoken不會被傳到解析器。所以,解析器只會看到這些東西:

NUMBER, PLUS, NUMBER, EOF

設想一個包含未定義字符的輸入文件,例如:

“123 – 456\n”

在處理完第一個空格之后,我們的可愛的詞法分析器將遇到一個不認識的字符:減號。由于沒有任何token的定義是以減號打頭,詞法分析器會扔出一個TokenMgrError 異常。

現(xiàn)在我們看看另一種情況:

“123++456\n”

我們的詞法分析器會提交一個這樣的串:

NUMBER,PLUS,PLUS,NUMBER,EOF

詞法分析器還沒有智能到判斷一個token 序列是否有意義,這通常是語法分析器的工作。我們接下來要討論的解析器會在詞法分析器提交第二個PLUS 之后發(fā)覺這個錯誤,然后拒絕處理之后的任何token。所以解析器實際上處理的只有:

NUMBER,PLUS,PLUS

同時,跳過(skip )一個token并不代表忽略(ignore )它??紤]下列輸入:

“123 456\n”

詞法分析器會識別出3token:兩個NUMBER和夾在它們中間的空格;然后報錯。 

2.3.  詳述語法分析器

語法分析器的定義使用了一種叫BNF范式的東西,這看起來有點像Java的方法定義:

 

void Start() :

{}

{

<NUMBER>

(

<PLUS>

<NUMBER>

)*

<EOF>

}

 

這個BNF范式聲明了一個正確的輸入序列的模式。我們解釋一下它的意思:它以NUMBER開頭的序列,以EOF結束,中間存在零個或多個由一個PLUS 后面跟一個NUMBER 組成的子序列。

正如所見,語法分析器只會檢查一個輸入序列是否合法,而并沒有真的把數(shù)字加起來。待會兒我們還會修改這個語法分析器,但現(xiàn)在我們先讓它生成Java 組件,然后run 起來。

2.4.  生成一個解析器和一個詞法分析器

我們現(xiàn)在用JavaCC根據(jù)我們寫好的adder.jj 文件生成分析器。具體怎么做依賴于操作系統(tǒng)。下面是在Windows NT, 2000 XP 上完成的。首先使用“命令提示符”程序(CMD.EXE)運行JavaCC

 

D:\home\JavaCC-Book\adder>javacc adder.jj

Java Compiler Compiler Version 2.1 (Parser Generator)

Copyright (c) 1996-2001 Sun Microsystems, Inc.

Copyright (c) 1997-2001 WebGain, Inc.

(type "javacc" with no arguments for help)

Reading from file adder.jj . . .

File "TokenMgrError.java" does not exist. Will create one.

File "ParseException.java" does not exist. Will create one.

File "Token.java" does not exist. Will create one.

File "SimpleCharStream.java" does not exist. Will create one.

Parser generated successfully.

 

這個操作生成了七個Java 類,每一個在獨立的文件中:

l  TokenMgrError 是一個簡單的錯誤類;詞法分析器用它來偵測錯誤,父類是Throwable.

l  ParserException 是另一個錯誤類;解析器用它偵測錯誤,父類是Exception,因此也是Throwable 的子類。

l  Token 是一個表示token 的類。每個Token 對象都有一個整數(shù)域kind 表示token的類型(PLUS, NUMBER, 或者EOF),和一個String image,存儲token 所代表的內(nèi)容。

l  SimpleCharStream 是一個把字符串提交給詞法分析器的接口轉換類。

l  AdderConstants 是一個接口,定義了一組在詞法分析器和解析器中都要用到的類。

l  AdderTokenManager 就是詞法分析器。

l  Adder 是解析器。

現(xiàn)在我們可以用一個Java 編譯器編譯這些類了:

D:\home\JavaCC-Book\adder>javac *.java

2.5.  讓它跑起來

現(xiàn)在我們換個角度來看Adder類的main 方法。

 

static void main( String[] args )

throws ParseException, TokenMgrError {

Adder parser = new Adder( System.in ) ;

parser.Start() ;

}

 

最先注意到main 可能會拋出繼承自Throwable 的兩個子類(譯注:TokenMgrError ParserException)中的任意一個。這風格不是很好,我們應該捕捉這些異常。但是為了保持第一個例子簡潔(譯注:為了讓讀者能迅速把握要點,而不是陷入無窮的細節(jié)之中),我們忽略了這些東西。

第一個語句創(chuàng)建了一個解析器實例,構建函數(shù)使用了自動生成的接受一個java.io.InputStream的重載。其實還有一個(更好的)接受Reader實例的重載(java建議在處理字符串時盡量使用Reader(Writer)而不是InputStream(OutputStream),這樣能更好的避免字符編碼帶來的問題——譯者如是說)。這個構建函數(shù)創(chuàng)建了一個SimpleCharStream對象和一個詞法分析器AdderTokenManager的實例。這樣,詞法分析器通過SimpleCharStream順利地獲取到了我們的輸入。

第二句調(diào)用了一個由JavaCC生成的方法Start()。對語法規(guī)范中的每個BNF產(chǎn)生式,JavaCC都會生成一個對應的方法。這個方法負責嘗試在輸入序列中尋找符合模式的輸入。例如,調(diào)用Start時會使解析器試圖尋找一個匹配下面模式的輸入序列:

<NUMBER>(<PLUS><NUMBER>)*<EOF>

我們可以準備一個合適的輸入然后運行這條命令

 

D:\home\JavaCC-Book\adder>java Adder <input.txt

 

我們運行程序,輸入表達式以后,會出現(xiàn)以下三種不同的情況:

1.         出現(xiàn)詞法錯誤。本例中,詞法錯誤只出現(xiàn)在遇到未知字符時。我們可以通過下面的輸入引發(fā)一個詞法錯誤:

“123-456\n”

這種情況下,程序會拋出一個TokenMrgError異常。這個異常的message域是:Exception in thread “main” TokenMgrError: Lexical error at line 1,column 5. Encountered: “-“ (45), after : “”

2.         出現(xiàn)一個解析錯誤。這發(fā)生在輸入序列不符合StartBNF范式時。例如

“123++456\n”

或者

“123 456\n”

或者

“\n”

這時,程序會扔出一個ParseException異常。這種異常的第一條信息分別是:

Exception in thread “main” ParseException: Encountered + at line 1, column 6.

Was expecting:

<NUMBER> ... 

3.         輸入串符合Start的定義。這時,程序不拋出任何異常,只會默默的停止。

由于解析器除了挑錯什么都不做,所有現(xiàn)在這個程序除了檢查輸入合法性以外什么都做不了。在下一節(jié),我們將會做一些改變讓它更有用。

2.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與期望的類型不符,則拋出一個異常。表達式

(jj_ntk==-1)?jj_ntk():jj_ntk

計算下一個未讀token的類型。而最后一行則要求匹配一個類型0tokenJavaCC 總是用0 來編碼EOF類型。

2.7.  增強解析器

像上文中提到的start方法一樣的,由JavaCC根據(jù)BNF文法生成的方法,在默認情況下僅僅是檢查了輸入是否符合規(guī)則。但是我們可以在BNF中間夾雜Java代碼,這些代碼將來會被包含在生成的方法中。JavaCC為我們提供了一個骨架,而我們要讓它有血有肉。

下面我們改變adder.jj中的BNF規(guī)范,為Start 添加一些聲明和Java 代碼。新的文件叫做adder1.jj。添加或改變的部分用黑體標出:

 

 int start() throws NumberFormatException:

{

Token t;

int i;

int value;

}

{

t = <NUMBER>

{ i = Integer.parseInt(t.image); }

{ value = i; }

(

<PLUS>

t = <NUMBER>

{ i = Integer.parseInt(t.image);}

{ value += i; }

)*

<EOF>

{ return value; }

}

 

首先,我們定義了BNF產(chǎn)生式的返回類型,這樣生成的方法就從void 變?yōu)?/span>int。然后還聲明了NumberFormatException可能會在運行時拋出。我們定義了三個變量。變量t 是一個TokenToken 是一個生成的類用來表示token;Token 類的image 域記錄了匹配的字符串。當一個token 匹配上了一個BNF 產(chǎn)生式,我們就能通過賦上一個引用來記下這個Token 對象。像這樣

t = <NUMBER>

我們可以在BNF產(chǎn)生式的大括號里添加任意的Java語句,這些語句會原封不動的copy到生產(chǎn)的代碼里面。

由于更改了Start的返回類型,我們有必要更改一下我們的main函數(shù):

static void main( String[] args )

throws ParseException, TokenMgrError, NumberFormatException {

Adder parser = new Adder( System.in ) ;

int val = parser.Start() ;

System.out.println(val);

}

 

在結束這個例子前,我們再做一點小小的改進。下面的代碼在start中出現(xiàn)了兩次:

t = <NUMBER>

{ i = Integer.parseInt( t.image ) ; } 

雖然在這個例子中不會引起太大的差異,僅僅涉及兩行代碼,但這種重復會導致維護的問題。所以我們把這兩行提出來作為另一個BNF 產(chǎn)生式,叫做Primary。最新的修改依舊用黑體標出。

 

int start() throws NumberFormatException:

{

int i;

int value;

}

{

value = Primary()

(

   <PLUS>

   i = Primary()

   { value += i; }

)*

<EOF>

{ return value; }

}

int Primary() throws NumberFormatException :

{

Token t ;

}

{

t=<NUMBER>

{ return Integer.parseInt( t.image ) ; }

}

 

這時我們再來看看JavaCC所生成的代碼:

 

final public int Start() throws ParseException, NumberFormatException {

int i ;

int value ;

value = Primary();

label 1:

while (true) {

switch ((jj ntk==-1)?jj ntk():jj ntk) {

case PLUS:

;

break;

default:

jj la1[0] = jj gen;

break label 1;

}

jj consume token(PLUS);

i = Primary();

value += i ;

}

jj consume token(0);

{ if (true) return value ; }

throw new Error(Missing return statement in function);

}

 

final public int Primary() throws ParseException, NumberFormatException {

Token t ;

t = jj consume token(NUMBER);

{ if (true) return Integer.parseInt( t.image ); }

throw new Error(Missing return statement in function);

}

 

待會兒我們還能看到如何向BNF產(chǎn)生式傳遞參數(shù)。

3.      第二個例子:運算器

接下來,我們繼續(xù)改進我們的adder,使它成為一個簡易的四則運算計算器。

第一步,我們讓它能夠和我們進行交互,把每行作為一個單獨的表達式,并計算輸出。稍后,我們會考慮加法之外的其他操作,減法,乘法和除法。

3.1.  選項和類定義

calculator0.jj的開頭如下:

 

 /* calculator0.jj An interactive calculator. */

options {

STATIC = false ;

}

 

PARSER BEGIN(Calculator)

import java.io.PrintStream ;

class Calculator {

static void main( String[] args )

throws ParseException, TokenMgrError, NumberFormatException {

Calculator parser = new Calculator( System.in ) ;

parser.Start( System.out ) ;

}

double previousValue = 0.0 ;

}

PARSER END(Calculator)

 

Calculator previousValue 域用于保存前一行的計算結果,我們的下一版本將允許在表達式中使用美元符號($)表示這個值。import語句可以寫在PARSER_BEGINPARSER_END之間,他們將被復制到生成的類文件中,包定義同樣也在這時聲明。

3.2.  詞法定義

詞法定義的改變不大,首先,換行符不再被忽略,而聲明成一個token,這使得換行符可以被解析器處理。

SKIP:{" "}

TOKEN:{< EOL : "\n"|"\r"|"\r\n" >}

TOKEN:{< PLUS : "+">}

第二,我們將允許小數(shù)參與運算,所以我們要更改NUMBER的定義使得它允許小數(shù)點被匹配。一共有4種形式(用豎線隔開):沒有小數(shù)部分,既有小數(shù)部分又有整數(shù)部分,只有小數(shù)點和小數(shù)部分,只有整數(shù)部分和小數(shù)點。于是我們聲明如下:

TOKEN:{< NUMBER :

(["0"-"9"])+|

(["0"-"9"])+”.” (["0"-"9"])+ |

(["0"-"9"])+”.”|

”.” (["0"-"9"])+

>}

我們又發(fā)現(xiàn)相同的正則表達式出現(xiàn)了好多次,這顯然不是個好現(xiàn)象,所有我們可以給這部分重復的表達式起一個名字。這個名字僅僅在這個詞法分析器中有效,而且不代表任何token類型,這樣的正則表達式在定義中用# 標記。前一表達式等價于以下代碼:

TOKEN:{< NUMBER : <DIGITS>| <DIGITS>”.” <DIGITS> | <DIGITS>”.”|”.” <DIGITS> >}

TOKEN : {< #DIGITS : (["0"-"9"])+ >}

3.3.  解析器定義

解析器的輸入包括了若干行序列,每行都包含一個表達式。用BNF(下一章我們還會討論(譯注:沒有下一章了))表示這種結構就是:

Start ->(Expression EOL)* EOF

下面給出Start BNF 產(chǎn)生式的框架:

 

void Start():

{}

{

(

Expression()

<EOL>

)*

<EOF>

}

 

我們會在這個框架之上添加了一些Java代碼,讓它能記錄并打印出每行表達式的值:

 

void Start(PrintStream printStream) throws NumberFormatException :

{}

{

(

previousValue = Expression()

<EOL>

{ printStream.println(previousValue); }

)*

<EOF>

}

 

每個表達式都包括了一個或者多個由加號(目前它還只認加號)分隔的數(shù)字序列。BNF表示如下:

expression -> primary (PLUS primary)*

這里的primary現(xiàn)在只表示數(shù)字。這個BNF翻譯成JavaCC 的記法就是(增加的用粗體顯示):

 

double Expression() throws NumberFormatException :

{

double i;

double value;

}

{

value = primary()

(

<PLUS>

i = primary()

{ value += i;}

)*

{ return value; }

}

 

這個和adder1.jjStart的定義驚人的相似啊,不過我們吧int改成了double。

primary的定義也和adder1.jj中的差不多,用BNF表示非常簡單:

Primary -> NUMBER

除了它現(xiàn)在能計算雙精度數(shù)字外一切如前:

 

double primary() throws NumberFormatException:

{

Token t;

}

{

t = <NUMBER>

{ return Double.parseDouble(t.image); }

}

 

總結一下我們用到的BNF

Start ->(Expression EOL)* EOF

expression -> primary (PLUS primary)*

Primary -> NUMBER

至此,我們已經(jīng)完成了calculator.jj。下面我們要測試一下它。

 

3.4.  增加減法操作

為了得到一個功能豐富的計算器,我們需要能執(zhí)行更多的操作,比如減法、乘法和除法。我們先從減法開始。

在詞法定義里添加一個新的產(chǎn)生式:

TOKEN :{ < MINUS : “-“ > }

在定義EOLNUMBER時我們使用了小豎線分割不同選項,現(xiàn)在我們要使用同樣的方法吧減號添加進EXPRESSION的定義中,我們的BNF如下:

Expression -> Primary((PLUS|MINUS) Primary)*

還有另外一種等價形式:

Expression -> Primary(PLUS Primary |MINUS Primary)*

因為第二種形式處理起來更簡單些,所有我們用第二種形式。這樣我們就得到了新的JavaCC代碼:

 

double Expression() throws NumberFormatException :

{

double i;

double value;

}

{

value= primary()

(

<PLUS>

i = primary()

{ value+=I;}

|

<MINUS>

i = Primary()

{ value -= i; }

)*

{ return value;}

}

 

3.5.  增加乘法和除法操作

要增加乘除運算是件很簡單是事情,我們只需要添加兩個產(chǎn)生式

TOKEN:{< TIMES : "*" > }

TOKEN:{< DIVIDE : "/" > }

就像我們增加減法操作時所作的,我們還應該更改Expression的定義,現(xiàn)在它的BNF是:

Expression -> Primary(PLUS Primary | MINUS Primary | TIMES Primary| DIVIDE Primary)*

從純粹的句法角度看,這個產(chǎn)生式一點問題都沒有,但是它并不能正確的表達我們的意思,因為沒有考慮運算符的優(yōu)先級。例如我們輸入

2 * 3 + 4 * 5

我們希望得到的是(2*3)+(4*5)但是我們卻得到了((2*3)+4)*5!所有我們不得不使用另外一種表達方式:

Expression -> Term (PLUS Term |MINUS Term)*

Term -> Primary(TIMES Primary |DIVIDE Primary)*

這樣表達式被分成了一連串的加減運算,加減的元素是Term.

2 * 3 + 4 * 5

我們要做的僅僅是把Expression中所有對Primary 的引用改為對Term 的引用:

 

double Expression() throws NumberFormatException :

{

double i;

double value;

}

{

value = Term()

(

<PLUS>

i = Term()

{ value += i;}

|

<MINUS>

i = Term()

{ value -= i; }

)*

{ return value; }

}

 

Term 的產(chǎn)生式類似:

 

double Term() throws NumberFormatException :

{

double i;

double value;

}

{

value = Primary()

(

<TIMES>

i = Primary ()

{ value *= i;}

|

<DIVIDE>

i = Primary ()

{ value /= i; }

)*

{ return value; }

}

 

3.6.  增加括號,單目操作符和歷史記錄

現(xiàn)在我們還需要添加少許其他功能使它變成一個真正的有用的計算器,我們需要括號支持,負數(shù)支持,還要允許使用美元符$表示上一次表達式計算的值。

更改詞法定義變得水到渠成,我們只需增加幾個產(chǎn)生式:

TOKEN:{< OPEN_PAR : "(" > }

TOKEN:{< CLOSE_PAR : ")" > }

TOKEN:{< PREVIOUS : "$" > }

我們不需要為取負做任何詞法更改,因為我們只需要用到減號(MINUS)而已。

改變之后的Primary的一共有4種可能性:一個數(shù),一個$,一個括號包起來的表達式,或者一個帶負號的任意可能

使用BNF表示就是:

Primary -> NUMBER

| PERIVOUS

| OPEN_PAR Expression CLOSE_PAR

| MINUS Primary

這個BNF有兩路遞歸,最后一個是直接遞歸,倒是第二個是間接遞歸。在BNF中使用遞歸是允許的,但是有若干限制??紤]下列表達式:

- - 22

這將會被理解成:

 

在解析表達式時,每個小盒子被當成一個Primary。例如

12 * ( 42 + 19 )

將會被理解成

 

通過這個我們可以看到,Primary這個BNF是如何被遞歸調(diào)用的。

現(xiàn)在我們給出PrimaryJavaCC 實現(xiàn):

 

double Primary() throws NumberFormatException :

{

Token t;

double d;

}

{

t = <NUMBER>

{ return Double.parseDouble( t.image ); }

|

<PREVIOUS>

{ return previousValue; }

   |

<OPEN_PAR> d = Expression() <CLOSE_PAR>

{ return d; }

|

<MINUS> d = Primary()

{ return -d; }

}

 

至此,我們終于完成了我們的計算器。完整的計算器聲明見calculator1.jj。當然,我們能做的改進仍然很多,比如添加新的操作符,這些工作就留給各位讀者了。

這種計算表達式的方式被稱作直接解釋,也就是說解析器自己把輸入解析成數(shù)值然后計算出結果。對于簡單的表達式來講,這工作的很好,但是對于復雜的表達式來講遠遠不夠,比如當我們需要引入某種循環(huán)時??紤]下面的表達式:

sum i : 1..10 of i*i

這就是一個典型的數(shù)學上的求和運算

 

這時直接求值就不好使了,因為沒有任何數(shù)字對于子表達式

i*i

對于這種情況,最好的辦法就是讓解析器把表達式表示成其他什么形式,比如樹,或者某種字節(jié)碼,在解析完成后再計算。

 

4.      文本處理

本章的最后一個例子有點不同。加法器和計算器的例子展示的是如何處理人工語言,這些語言將來完全可以拓展為一門完整的編程語言。而本小節(jié)的例子將說明JavaCC 在處理無結構的文本時也是非常有用的。  

4.1.  一個濾詞器

任務是用某些文本替換輸入中特定模式的文本。我們根據(jù)聲明逐字地搜索并替換四字母詞,不管它是否合適。

選項和類聲明

聲明文件的初始部分聲明了一個將String 映射到String 的靜態(tài)方法。

 

/* four-letter-words.jj A simple report writer. */

options {

STATIC = false ;

}

 

PARSER BEGIN(FLW)

import java.io.Reader ;

import java.io.StringReader ;

class FLW {

static String substitute( String inString ) {

Reader reader = new StringReader( inString ) ;

FLW parser = new FLW( reader ) ;

StringBuffer buffer = new StringBuffer() ;

try {

parser.Start( buffer ) ; }

catch( TokenMgrError e ) {

throw new IllegalStateException() ; }

catch( ParseException e ) {

throw new IllegalStateException() ; }

return buffer.toString() ; }

}

PARSER END(FLW)

 

try 語句的要點是它把ParseException 轉變?yōu)槟切┎灰欢暶髁说漠惓?。原因是這個解析器絕對不會扔出ParseException(或者TokenMgrError);任何一個字符串都是一個合法的輸入。(如果Java 編譯器支持assert 語句,我們可以用assert false; 語句替代throw 語句。)

詞法分析器

詞法分析器的聲明部分是至關重要的一部分。我們把文件分成三種token:四字母詞、多于四個字母的詞和少于四個字母的詞。我們一步步的來。

四字母詞用正則表達式中的量詞就很好聲明。(x){n} 表示表達式x 恰好重復n 次。

TOKEN : { < FOUR LETTER WORD : (<LETTER>){4} > }

我們已經(jīng)看到過(x)+ 表示表達式x至少重復一次。類似地,(x)* 表示表達式x重復若干次。這樣,五字母詞(譯注:作者意指含至少五個字母的單詞)可以這樣聲明:

TOKEN : { < FIVE OR MORE LETTER WORD : (<LETTER>){5} (<LETTER>)* > }

我們像這樣聲明數(shù)字[“0”-“9”]。我們可以用不止一種方法寫出一個匹配任一單個字母的正則表達式,比如[“a”-“z”, “A”-“Z”](注:為了簡單起見,我們把字母表限定為52個大小寫羅馬字母。JavaCC 能完美地處理任意Unicode 字符,也就是說它能輕易地處理標音字母和其他字母表的字母)。

TOKEN : { < #LETTER : [”a”-”z”,”A”-”Z”] > }

我們也可以把所有的數(shù)字一個個列出來,[“0”-”9”] 就相當于

["0","1","2","3","4","5","6","7","8","9"]

更普遍地,我們能列出各種單獨的字符或字符范圍,比如

["0"-"9","a"-"z","A"-"Z","’","-"]

它將匹配任一數(shù)字、字母、撇號或者連字符。還可以寫出字符集的補。比如

~["0"-"9","a"-"z","A"-"Z","’","-"]

匹配任意的非數(shù)字字符。一個極端的例子是空集。正則表達式[] 匹配任意空集中的任意字符,就是說,它什么都不匹配,或它匹配的字符序列的集合是空集。正則表達式[] 沒什么用,但它的補~[] 能匹配任意不在空集中的單個字符,也就是說,它能匹配任一單字符。這正是我們捕獲那些既不是四字母單詞也不是長單詞時所需要的。

TOKEN : { < OTHER : ~[] > }

最長匹配

考慮輸入串”sinister”。我們有好幾種辦法把它打斷成幾部分,每一部分都匹配上我們那三種之一的正則表達產(chǎn)生式。例如,我們可以把它看成八個單獨的字符。這時,它被斷成八個OTHER 類型的token?;蛘甙阉闯蓛蓚€OTHER,一個FOUR_LETTER_WORD,以及另外兩個OTHER。還可以把它看成包含一個FIVE_OR_MORE_LETTER_WORD,跟著零個、一個、兩個、三個OTHER。(一共有17 種可能。)

我們想要的當然是它被當作一個FIVE_OR_MORE_LETTER_WORD匹配上,事實上也是如此,但重點是理解為什么。詞法分析器總是試圖盡可能多地把剩下的字符塞進下一個產(chǎn)生的token里面。這就是”maximal munch”(最長匹配)規(guī)則。假設輸入”sinister cats”。三個產(chǎn)生式都能匹配上輸入的開頭部分:OTHER 捕獲一個字符序列”s” FIVE _LETTER_WORD 捕獲頭四個字符”sini”;而FIVE_OR_MORE_LETTER_WORD 可以捕獲”sinis”,”sinist”,”siniste”, ”sinister”中的任意一個。最長的可能匹配是頭八個字符,留下” cats”。由于下一個字符不是一個字母,唯一能匹配上的就是OTHER。剩下的序列是”cats”。OTHER FIVE _LETTER_WORD 都可以匹配上,但由于最長匹配原則,FIVE _LETTER_WORD 勝出。剩下空串,產(chǎn)生一個EOF。

你可能會想如果兩個產(chǎn)生式都能匹配上一個最長的可能匹配時最長匹配原則不就不適用了?在本例中是不會發(fā)生這樣的情況,因為三個產(chǎn)生式分別匹配長度為一、四、五甚至更多的輸入。但考慮一個Java 編程語言的詞法分析器。我們寫出如下產(chǎn)生式規(guī)則。

TOKEN : { < KWINT : int > }

TOKEN : { < IDENTIFIER : (<LETTER> | ) (<LETTER> | <DIGIT> | )* > }

當余下的輸入為”int0 = 0; …” 時,由于最長匹配規(guī)則”int0”匹配上INDENTIFIER。而,當輸入 “int i; …”時,兩條規(guī)則都能捕捉到最大數(shù)目(3個)的字符。在這個例子中,最先出現(xiàn)的規(guī)則擁有優(yōu)先權。所以,”int” KWINT 匹配上。

在我們的規(guī)范中OTHER 的存在保證了詞法解析器總會產(chǎn)生一些token。如果輸入不為空,就會產(chǎn)生一個OTHER token(雖然可能其他產(chǎn)生式實際上更適合),而如果剩余輸入為空串,將會產(chǎn)生一個EOF。所以我們的詞法分析器永遠不會產(chǎn)生TokenMgrError 異常。(注:在以后的章節(jié)中,我們會看到MORE 關鍵字的使用。當詞法分析規(guī)范中用到MORE時,一個能匹配所有情況的產(chǎn)生式,比如我們的OTHER,不足以保證TokenMgrErrors 不會拋出。見JavaCC FAQ 了解更多。)

濾詞器的解析

濾詞器的解析器規(guī)范就直截了當了。這三種token以任意數(shù)目出現(xiàn)在任意位置。對FIVE _LETTER_WORD,我們在輸出上以四個星號標識。而對于其他token,我們只是簡單地回顯出來。

 

void Start( StringBuffer buffer ) :

{

Token t ;

}

{

(

<FOUR LETTER WORD>

{ buffer.append(****); }

|

( t=<FIVE OR MORE LETTER WORD> | t=<OTHER> )

{ buffer.append( t.image ) ; }

)*

<EOF>

}

 

既然解析器接受任意詞法分析器產(chǎn)生的token 串,它就不會拋出ParseException 異常。我們的詞法分析器和解析器都是“全局的”:它們接受任意的輸入串。

5.      總結

我們已經(jīng)看到JavaCC 允許用正則表達式和BNF 產(chǎn)生式書寫出簡明的詞法分析器和解析器規(guī)范。

詞法分析器的輸入是一串字符——用Java InputStream 對象或者Java Reader 對象表示。詞法分析器的輸出由JavaCC 確定:一串Token 對象序列。解析器的輸入也是固定的,就是詞法分析器產(chǎn)生的那串Token 對象序列。詞法分析器和解析器之間的關系如下圖所示

TBD

解析器的輸出并不是由JavaCC 規(guī)定的,程序員想讓它輸出什么它就什么樣,只要能用Java 表示出來。輸入一般是一些抽象的表示。在加法器和計算器的例子中,輸出是一個Java int double 類型的數(shù)字。不同的輸入可能產(chǎn)生相同的數(shù)字。編譯器中,解析器的輸出可能是機器碼或匯編碼的形式。大多數(shù)編譯器的解析器會生成輸入程序的某種中間代碼,這些中間表示日后會被編譯器的其他部分用到。不同的應用會有不同的輸出形式。例如,輸出可能是一個字串,可能是輸入串的修改版本,就像我們那個濾詞器例子;如果輸入是一個配置文件,輸出也可能是表示配置的Java 對象;等等等等。

一種特別常見的情況是解析器的輸出是一棵完全遵照方法調(diào)用的樹。這種情況下,有一些額外的工具用來自動增強JavaCC 的輸出。這些工具包括JJTreeJTB。

注意詞法分析器的工作完全獨立于解析器,這一點很重要。它把輸入流切割成什么樣的token不受解析器期望的影響,而是由它的規(guī)范完全確定。

 

最后,感謝那個爛尾而且喜歡亂吐槽的譯者:

https://sites.google.com/site/beariceshome/the-javacc-tutorial

本站僅提供存儲服務,所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
javacc例子:加法器
使用Eclipse/Antlr解析簡單文本[CowNew開源社區(qū)]
手把手教你做一個 C 語言編譯器(1):設計
“挑戰(zhàn)用 500 行 Python 寫一個 C 編譯器”
正則表達式引擎的構建---- 構造抽象語法樹
自己動手開發(fā)編譯器(四)利用DFA轉換表建立掃描器
更多類似文章 >>
生活服務
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服