語法分析生成器 JFlex 使用教程

openkk 12年前發布 | 86K 次閱讀 語法分析 語法/詞法分析器

這個項目中使用 JFlex 的目的是為我們的計算器創建一個詞法分析器。這個詞法
分析器,或者叫掃描器,將為我們計算器檢查輸入,而且確認所有的字符歸類是有
效的。
用于 JFlex 的詞法分析說明文件可以分成三個部分。每個部分由 %% 分開。

用戶代碼段
%%
參數設置和聲明段
%%
詞法規則段

用戶代碼段
這個段中的所有內容將被拷貝到生成的詞法類的類聲明之前。在這個段中,常見的
是 package 和 import 語句。我們的詞法說明在這個段中引入(import)了兩個類
,sym 和 java_cup.runtime.*,如下所示:
import java_cup.runtime.*;
import sym;

在我們的例子中,sym 類(與處理器一起)由CUP產生。

參數設置和聲明段
這個段含有參數,詞法狀態,和宏定義。設置參數將包含額外的代碼,它們將被包
括在產生的掃描器類。參必須另開新行,以 % 開頭。可以包含的參數很多。在隨
JFlex 來的手冊中可以得到一個可以包含的參數列表。在我們的詞法說明中用到
的參數如下:
%class Lexer
%line
%column
%cup

第一個參數,class Lexer,告訴 JFlex 把生成的類命名為Lexer并把代碼寫到名
為Lexer.java的文件。參數 Line打開行計數,使你可以用變量yyline存取輸入的
當前行號。參數column有類似的作用,除了它是通過變量yycolumn存取當前列號。
最后一個參數,cup,把JFlex設置成一個特殊模式,使它與CUP產生的處理器兼容
,我們使用的就是。

然后,你可以聲明掃描器用到的成員變量和函數。可以加入的代碼是Java代碼,并
放在 %{ 和 }%之間。它們將被拷貝到生成的詞法類源代碼中。在我們的詞法說明
中,聲明了兩個成員函數。這些函數創建java_cup.runtime.Symbol對象。第一個
僅僅記錄當前記號的位置信息。第二個還包含了記號的值。以下是到這個聲明的連
接。

Declarations

這個段的最后是宏定義。宏用作正則表達式的縮寫。一個宏定義包含一個宏標識符
,其后為一個=,然后是宏要代表的正則表達式。如下是一個我們的詞法說明中用
到的宏定義的連接。還有一個連接包含了一個列表,列出了創建正則表達式可用的
東西,及每一項的含義。

Macro Declarations

可用于創建正則表達式的列表

詞法規則段
詞法分析說明的最后一段包含正則表達式和當掃描器匹配了相關的正則表達式后所
要執行的動作。掃描器會激活具有最大匹配的表達式。所以如果存在兩個正則表達
式"to"和"too",掃描器會匹配"too",因為它是最長的。如果兩個正則表達式完全
相同,具有相同的長度,那么掃描器將匹配最先列在說明中的表達式。假設掃描器
讀進了字符串"to",它試圖在下面所列的正則表達式中尋找一個來匹配所讀入的。
第二個正則表達式是可選的,因為它包含的字符串類是可以匹配字符串"to"的。但
掃描器會選擇列表中的第一個正則表達式,因為它列在最前面。
"to"
[a-z]*

每個正則表達式可以附帶動作,這樣當掃描器匹配了正則表達式后就可以激活它的
動作。每個正則表達式的動作就是你可以寫的Java代碼片段。你想要的動作可以是
打印出一些東西,或是返回掃描器發現的標識符給處理器。用于打印出掃描器發現
的標識符并且返回給處理器的示例代碼如下所示:

"+" { System.out.print("+"); return symbol(sym.PLUS); }
"-" { System.out.print("-"); return symbol(sym.MINUS); }
"*" { System.out.print("*"); return symbol(sym.TIMES); }
"/" { System.out.print("/"); return symbol(sym.DIVIDE); }

JFlex 允許程序員定義特殊的詞法狀態(lexical states)用作開始條件來細化說明
。YYINITIAL 是一個預定義的詞法狀態,是詞法分析器初始掃描輸入的狀態。它是
我們將用的唯一狀態。所以,我們所有的正則表達式都將從這個詞法狀態開始識別
。然而,可以定義其它這樣的狀態,本質上說將建立一個狀態機的新的分支的開始
。在下面的例子中,從 YYINITIAL 通過一個變換到達詞法狀態。在這個狀態段定
義的正則表達式將只能在這個分支中被識別。

{
\" {string.setLength(0); yybegin(STRING); }
"=" { return symbol(sym.EQ); }
"==" { return symbol(sym.EQEQ);}
"+" {return symbol(sym.PLUS); }

{
\" { yybegin(YYINITIAL);
return symbol(sym.STRINGLITERAL, string.toString());}
[^\n\r"\]+ {string.append(yytext());}
}

在如上的代碼中,掃描器將從狀態 YYINITIAL 開始。當它匹配了正則表達式\",
也就是找到了一個雙引號,它將把狀態轉換到 STRING 狀態。那么現在能夠匹配的
正則表達式只有這個狀態下列出的狀態。所以,掃描器將呆在這個狀態中直到它匹
配了另一個雙引號 - 在這兒它將再次回到 YYINITIAL 狀態。再說一次,在我們的
計算器中從不使用這樣的初始條件,除了最早的YYINITIAL狀態。下面是我們使用
的詞法規則的一個連接:

到詞法規則的連接

------------------------------------------------------------------------

到 JFlex 文件 lcalc.flex 的連接。它是我們的計算器的詞法說明文件。它里面
有很多的注釋解釋為什么。它可以被拷貝,并且本文提供的CUP文件和Main文件都
可以被拷貝,這樣你就可以運行示例項目。它們包括如何準備每個文件和運行計算
器的指南。做這些需要Jdk,JFlex,和CUP,它們可以從本文中所列的 Web 站點中
免費下載。

更多的有關JFlex的信息請參考JFlex手冊,從本文中所列的 Web 站點免費下載
JFlex時就可以得到。

用 CUP
在這個項目中CUP的目的是為我們的計算器建立一個句法的分析器。這個句法的分
析器,或叫處理器,將為我們的計算器檢查輸入,并且確保句法的正確性。
這就是說根據我們的語法說明把輸入中的語句安排為一個有效的順序。
一個CUP文件的說明語法分為 4個部分:

預先聲明
終結符和非終結符的說明
終結符的優先級和結合性
文法
預先聲明
這一段提供預先和各種各樣的聲明來指定如何產生處理器,并提供一部分運行時的
代碼。這個段是可選的,并不需要被包括在一個 CUP 說明文件中。對于我們的計
算器,我們在這個段中將有三個項。
第一項是一個輸入聲明。我們將輸入類 java_cup.runtime.* 。
import java_cup.runtime.* ;

我們加的第二項是處理器代碼。這個處理器代碼將被直接放在產生的處理器的類定
義中。它以 parser code {: 開始,以 :} 結束,所有的代碼都寫在中間。在處理
器代碼中我們將改變兩種方法。我們將改變 report_error 和
report_fatal_error 方法。我們將修改這些方法,使得如果在輸入中發生一個語
法錯誤或一個致命錯誤,將打印出一個錯誤消息,其中包括輸入中發生錯誤的行號
,列號。這樣的在錯誤消息中的額外信息在定位輸入中的錯誤是非常有用。

到處理器代碼的連接

我們在這個段加的最后一項指出處理器該如何從掃描器那里請求下一個標識符,格
式是 scan with {: ... :}。我們將用這個來告訴處理器去調用 JFlex 創建的掃
描器。

scan with {: return lexer.yylex(); :};

終結符和非終結符的聲明
這個段包括符號表,及負責命名的聲明,并為每個終結符和非終結符提供類型。這
個段在 CUP 說明中是需要的。終結符聲明的語法是:terminal classname name1,
name2, ...; 。Classname 是對象的類型,如 整數(Integer)。如果不寫
classname,終結符將無內容為詞法器來傳遞給處理器。在classname 后面所列的
終結符的名字是你想要聲明為這種類型的,例如下面的:
terminal PLUS, MINUS, TIMES, DIVIDE, SEMI;
terminal Integer NUMBER;

注意,這里只有 NUMBER 有一個伴隨的 classname。在我們的例子里,它是有值的
唯一終結符。例如,當詞法器識別了一個 PLUS,它把相關的代碼傳給處理器;但
是當它識別了一個 NUMBER,它不僅要傳NUMBER 相關的代碼,還有它的包含在類型
類中的值(Integer)。

非終結符以同樣的方式聲明。唯一的區別是聲明的開頭反映了它是一個非終結符,
而不是終結符,如下所示:

non terminal expr_list, expr_part;
non terminal Integer expr;

終結符的優先級和結合性
這個段指明終結符的優先級和結合性,它是可選的段,不必須包括。這個段在處理
二義的終結符時會用上。如果不用這個段,你可以調整語法的結構使它沒有二義。
例如,TIMES 應該有比 PLUS 高的優先級。當處理器遇到一個語句比如 5+4*3,它
不知道這個表達式應該按照 5+(4*3) 計算還是按照(5+4)*3 計算。為了用這個段
消除這種二義性,你需要如下來聲明優先級。最高的優先級從列表的底部開始,向
上遞減。left 表示在那個優先級中終結符的結合性是從左到右。
precedence left PLUS,MINUS;
precedence left TIMES,DIVIDE;

要調整語法的結構消除二義性,你需要如下創建一個結構。這個結構能夠消除二義
性是因為 TIMES 在語法中比PLUS更靠下。這將導致在你向上回溯時 TIMES在PLUS
前被應用。

語法結構的例子

文法
在說明的語法中的最后一段,包含處理器的文法。在文法中的每一個產生式有一個
非終結符在左邊,然后是 ::=,其后是零個或多個動作,終結符,非終結符等,再
后面是一個分號。在等號右邊的每一個符號可以標有一個名字,它能用于在處理樹
上包含內容(即數值)。標號名由符號名后面跟一個冒號給出,如下面所示的e1,
e2定義為expr的標號。等號左邊的自動賦給標號 RESULT。使用標號RESULT 的一個
例子將出現在本段的后面。
expr::=expr:e1 PLUS expr:e2

標號的名字必須在產生式中保持唯一。如果同一個非終結符有幾個產生式,它們可
以在一起聲明,用 |隔開。在最后一個產生式的末尾需要一個分號,如下所示:


expr::=expr PLUS expr
| expr MINUS expr
;

動作定義也可以寫在產生式中。動作就是 Java 代碼,當產生式被識別時將被執行
。動作定義放在一對定界符{:和:}之間。下面是一個使用這些選項的語法片斷的例
子。下面還有連接指到一個名為ycalc.cup 的文件,它包括用于 CUP語法說明,可
以研究以下它的語法。

expr ::= factor:f PLUS expr:e
{: RESULT = new Integer(f.intvalue() + e.intvalue()); :}
|
factor:f MINUS expr:e
{: RESULT = new Integer(f.intvalue() - e.intvalue()); :}
|
factor:f
{: RESULT = new Integer(f.intvalue()); :}
;

------------------------------------------------------------------------

到CUP文件的連接 ycalc.cup。它是我們的計算器的語法說明文件。它里面有很多
的注釋解釋為什么。它可以被拷貝,并且本文提供的CUP文件和Main文件都可以被
拷貝,這樣你就可以運行示例項目。它們包括如何準備每個文件和運行計算器的指
南。做這些需要Jdk,JFlex,和CUP,它們可以從本文中所列的 Web 站點中免費下
載。

更多的有關JFlex的信息請參考JFlex手冊,從本文中所列的 Web 站點免費下載
JFlex時就可以得到。

------------------------------------------------------------------------

我們的計算器的主體
有不止一種方法來寫我們項目的主體。有一種是在程序運行時要求用戶的輸入。另
外一種是在啟動程序時要求提供它一個輸入文件的名字。這里所描述的主體是采用
以上的后一種方法來獲得輸入。我們所做的第一件事是引入我們將用到的三個類。
第一個類是為了我們的處理器,第二個是類java_cup.runtime.Symbol ,最后一個
是類 java.io.* 。然后我們聲明類 main。我們將在它里面調用處理器來開始對輸
入文件的語法分析。而處理器在每需要輸入文件中的下一個標識符時,它就調用掃
描器對輸入進行詞法分析。類 Main 包含兩項。它首先把變量 do_debug_parse 設
置為 false。然后我們定義了方法 main。我們傳給 main 一個字符串數組,它包
含程序啟動時從命令行上傳來的參數。所以在我們的例子中,字符串數組的第一個
元素是我們啟動程序時輸入的文本文件名。然后這個方法就進入一個 try 段,它
將實際調用處理器。try 段的意思是不管段里試圖做什么,如果有任何錯誤,程序
將退出這個段。在 try 段中的第一行創建一個新的處理器對象。這個處理器對象
創建一個新的詞法器對象。這個新的詞法器對象將使用創建時傳給main的字符串作
為它的輸入。try 段的第二行將啟動處理器。代碼如下所示:
try {
parser p=new parser(new Lexer(new FileReader(argv[0])));
Object result=p.parse().value;
}

在 try 段的后面是 catch 段。catch 段的目的是清除 try 段中的發生的錯誤。
catch 段將捕獲導致我們被踢出try 段的例外,在程序退出之前做必要的清除工作
。在我們的catch 段中沒有做任何事情。在catch 段的后面是方法 finally。這個
方法關閉所有的東西。我們在這個方法里也不做任何事情。catch 段和 finally
方法的代碼如下所示:

catch (Exception e) {
/* do cleanup here -- possibly rethrow e */
} finally {
/* do close out here */
}
}

這樣就結束了方法main和類Main 的內容。現在我們就創建了一個簡單的計算器,
使用 JFlex 作為我們的詞法分析器,CUP 作為我們的語法分析器。

------------------------------------------------------------------------

到 Java 文件的連接 Main.java。它是我們的計算器的主體。它里面有很多的注釋
解釋為什么。它可以被拷貝,并且本文提供的CUP文件和Main文件都可以被拷貝,
這樣你就可以運行示例項目。它們包括如何準備每個文件和運行計算器的指南。做
這些需要Jdk,JFlex,和CUP,它們可以從本文中所列的 Web 站點中免費下載。


更多的有關JFlex的信息請參考JFlex手冊,從本文中所列的 Web 站點免費下載
JFlex時就可以得到。

------------------------------------------------------------------------

編譯計算器程序
為了為運行計算器準備文件,你首先需要在詞法說明文件 lcalc.flex 上使用
JFlex。這將產生文件Lexer.java 。下一步是建立 CUP 文件 ycalc.cup。然后你
編譯 JFlex創建的Lexer.java文件。最后編譯 Main.java 文件來結束整個過程。
做上述事情,你要輸入如下的命令行:
>jflex lcalc.flex
>java java_cup.Main < ycalc.cup
>javac Lexer.java
>javac Main.java

然后為了運行計算器,你需要輸入如下命令行。文件 test.txt 是用于計算器的輸
入文件,可以被掃描和處理。

>java Main test.txt

------------------------------------------------------------------------

輸入/輸出的示例
一個示例輸入文件可以是如下所示:
2+4;
5*(6-3)+1;
6/3*5+20;
4*76/31;

如此這般。其對應的輸出應是如下:

2 + 4 = 6
5 * ( 6 - 3 ) + 1 = 16
6 / 3 * 5 + 20 = 30
4 * 76 / 31 = 9

 本文由用戶 openkk 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!