JavaCC學習筆記

鴨脖發表於2012-10-10

JavaCC學習筆記

 

編譯原理和直譯器構造是很重要的課程,它幾乎是計算機行業的一項最為關鍵的技術,所以這學期下決心要把這門課程學好,學得更加熟練一些。大二的時候我們的編譯原理課程是由李莉老師教授的,而在上課期間,我也自己用c++實現了一個編譯器,這個編譯器是基於Kenneth C. Louden編寫的編譯器,我在上面新增了陣列,指標,函式的定義,以及浮點數的使用和宣告語句。並且自主實現了詞法分析,語法分析以及語義分析和程式碼生成。這次需要使用javaCC來實現詞法和語法的分析,由於有大二的基礎,所以在完成作業的時候感覺輕鬆不少,而且由於對語義分析比較感興趣,所以在做語法分析的同時也做了一些語義分析。所以我將我的筆記主要分為五部分,第一部分為JavaCC工具的介紹以及總結,第二部分為CMM語言的詞法語法分析,第三部分為CMM語言的語義分析,第四部分為JJTree的簡要介紹,第五部分是自己的一些感悟。

 

第一部分:什麼是JavaCC?(參考於網站與書籍)

 

JavaCC是一個詞法分析生成器和語法分析生成器。詞法分析和語法分析是處理輸入字元序列的軟體構件,編譯器和直譯器協同詞法分析和語法分析來“解密”程式檔案,不僅如此,詞法分析和語法分析有更廣泛的用途。JavaCC並不是一個詞法分析器或者語法分析器,它只是一個生成器。就是說,它讀取文字後,基於一定的規則產生詞法分析器和語法分析器的Java程式碼。詞法分析和語法分析變的越來越複雜,軟體工程師直接用Java進行詞法分析和語法分析時必須要充分考慮各規則間的相互作用。例如,在對 C語言的詞法分析中,處理整型常量和浮點常量的程式碼是不能分開的,因為浮點數和整數的前面部分是一樣的。使用諸如JavaCC語法分析產生器,對整型常量 和浮點常量是可以區分的,它們的共同點可在程式碼生成過程中提取出來。這種模組性意味著JavaCC檔案比直接的Java程式更容易寫,更容易讀,也更容易 修改。通過JavaCC語法分析生成器的使用,軟體工程師可以節省大量的時間,並且軟體的質量也更高。

    JavaCC具有如下特點:

JavaCC是一個用Java語言寫的一個Java語法分析生成器,它所產生的檔案都是純Java程式碼檔案,JavaCC和它所自動生成的語法分析器可以在多個平臺上執行。

下面是JavaCC的一些具體特點:

1.       TOP-DOWN:JavaCC產生自頂向下的語法分析器,而YACC等工具則產生的是自底向上的語法分析器。採用自頂向下的分析方法允許更通用的語法(但是包含左遞迴的語法除外)。自頂向下的語法分析器還有其他的一些優點,比如:易於除錯,可以分析語法中的任何非終結符,可以在語法分析的過程中在語法分析樹中上下傳值等。

2.       LARGEUSER COMMUNTIY:是一個用JAVA開發的最受歡迎的語法分析生成器。擁有成百上千的下載量和不計其數是使用者。我們的郵件列表(https://JavaCC.dev.java.net/doc/mailinglist.html )和新聞組(comp.compilers.tools.JavaCC)裡的參與者有1000多人。

3.       LEXICALAND GRAMMAR SPECIFICATIONS IN ONE FILE:詞法規範(如正規表示式、字串等)和語法規範(BNF正規化)書寫在同一個檔案裡。這使得語法易讀和易維護。

4.       TREEBUILDING PREPROCESSOR: JavaCC提供的JJTree工具,是一個強有力的語法樹構造的預處理程式。

5.       EXTREMELYCUSTOMIZABLE:JavaCC提供了多種不同的選項供使用者自定義JavaCC的行為和它所產生的語法分析器的行為。

6.       CERTIFIEDTO BE 100% PURE JAVA:JavaCC可以在任何java平臺V1.1以後的版本上執行。它可以不需要特別的移植工作便可在多種機器上執行。是Java語言”Write Once, Run Everywhere”特性的證明。

7.       DOCUMENTGENERATION:JavaCC包括一個叫JJDoc的工具,它可以把文法檔案轉換成文字本件(Html).

8.       MANYMANY EXAMPLES:JavaCC的發行版包括一系列的包括Java和HTML文法的例子。這些例子和相應的文件是學習JavaCC的捷徑。

9.       INTERNATIONALIZED:JavaCC的詞法分析器可以處理全部的Unicode輸入,並且詞法規範何以包括任意的Unicode字元。這使得語言元素的描述,例如Java識別符號變得容易。

10.   SYNTACTIC AND SEMANTICLOOKAHEAD SPECIFICATIONS:預設的,JavaCC產生的是LL(1)的語法分析器,然而有許多語法不是LL(1)的。JavaCC提供了根據語法和語義向前看的能力來解決在一些區域性的移進-歸約的二義性。例如,一個LL(k)的語法分析器只在這些有移進-歸約衝突的地方保持LL(k),而在其他地方為了更好的效率而保持LL(1)。移進-歸約和歸約-歸約衝突不是自頂向下語法分析器的問題。

11.   PERMITS EXTENDED BNFSPECIFICATIONS:JavaCC允許擴充的BNF正規化——例如(A)*,(A)+等。擴充的BNF正規化在某種程度上解決了左遞迴。事實上,擴充的BNF正規化寫成A ::= y(x)* 或 A ::= Ax|y更容易閱讀。

12.   LEXICAL STATES ANDLEXICAL ACTIONS:JavaCC提供了像lex的詞法狀態和詞法動作的能力。

13.   CASE-INSENSITIVELEXICAL ANALYSIS:詞法描述可以在整個詞法描述的全域性域或者獨立的詞法描述中定義大小寫不敏感的Tokens。

14.   EXTENSIVE DEBUGGINGCAPABILITIES:使用選項DEBUG_PARSER, DEBUG_LOOKAHEAD, 和 DEBUG_TOKEN_MANAGER,使用者可以在語法分析和Token處理中使用深層次的分析。

15.   SPECIAL TOKENS:Tokens可以在詞法說明中被定義成特殊的Tokens從而在語法分析的過程中被忽略,但這些Tokens可以通過工具進行處理。

16.   VERY GOOD ERRORREPORTING:JavaCC的錯誤提示在眾多語法分析生成器中是最好的。JavaCC產生的語法分析器可以清楚的指出語法分析的錯誤並提供完整的診斷資訊。

javaCC有三個工具:

javaCC 來處法檔案(jj)生成解析代

jjTree 來處jjt檔案,生成樹節點代jj檔案,然後再通javaCC生成解析代

jjDoc 根據jj檔案生成bnf正規化文html)。工具都十分重要,分別貫穿著編寫時程。

JavaCC在使用的時候會首先建立一個JJ檔案。這個語法檔案以一些JavaCC所提供的Options的引數設定開始。在這個例子中,Options的引數都是它們的預設值。因此,這些引數實際上是不需要的。程式設計師甚至可以完全忽略Options這一部分,或者省略其中的一個或多個Options的引數,詳細的關於Options的引數設定的問題請參考JavaCC的文件。

接下來的是一個處在”PARSER_BEGIN(name)”和”PARSER_END(name)”中間的編譯單元。這個編譯單元可以是任意的複雜。在這個編譯單元中唯一的限制就是它必須定一個一個叫”name”的類——與PARSER_BEGIN和PARSER_END的引數的相同。這個”name”被用作語法分析產生器生成的java檔案的字首。

 

JavaCC的安裝:

很簡單,直接下載外掛,然後直接安裝在eclipse中就可以使用,這裡不再贅述。

參考資料:http://www.cnblogs.com/Gavin_Liu/archive/2009/03/07/1405029.html

百度百科:http://baike.baidu.com/view/1896120.htm

 

第二部分:CMM詞法分析和語法分析

 

首先說一下自己的感受。以前自己寫詞法分析器的原始碼的時候,真的感覺快崩潰了,因為那些有窮自動機畫起來感覺好複雜啊,一不小心就會出錯,而且如果結構設計不好的話,很可能隨時會推倒重來。詞法分析器就是在不斷地識別token,所以JavaCC好就好在這裡了,它給你留出的介面和程式設計模式很符合人的思維,你只需要宣告一下token的型別,然後使用正規表示式來吧詞法分析和語法分析一塊給做了。而且JavaCC的那種特有的程式設計模式我使用起來感覺很開心,敲程式碼寫語義分析的時候,感覺頭也不花了,耳朵也不鳴了,一連寫了一下午加一個晚上,總之一個字:爽。所以我覺得這正是JavaCC的強大之處吧,就像很多現在的產品框架和遊戲引擎,使用起來真的很順手,生產效率也高了很多。

好了廢話不多說,我們來看看我宣告的token的型別。

SKIP :

{

  " "

| "\r"

| "\t"

| "\n"

| < "//"(~["\n","\r"])*("\n"|"\r"|"\r\n") >

| < "/*"(~["*"])*"*"(~["/"](~["*"])*"*")*"/">

}

 

TOKEN : /* OPERATORS */

{

  < PLUS : "+" >

| < MINUS : "-" >

| < MULTIPLY : "*" >

| < DIVIDE : "/" >

| < ASSIGN : "=" >

}

 

TOKEN :/* RELATIONSHIPOPERATORS*/

{

  < EQ : "==">

| < LT : "< ">

| < NEQ : "<>">

}

TOKEN : /*RESERVEDWORDS*/

{

  < IF : "if">

| < ELSE : "else">

| < WHILE : "while">

| < READ : "read">

| < WRITE : "write">

| < INT : "int">

| < REAL : "real">

| < RETURN : "return">

| < CLASS : "class">

}

 

TOKEN : /*SYMBOLS*/

{

  < UNDERLINE : "_">

| < COMMA:",">

| < SEMICOLON:";">

| < COLON:":">

| < LPARENT: "(" >

| < RPARENT: ")" >

| < LBRACE : "{" >

| < RBRACE : "}" >

| < LARRAY : "[" >

| < RARRAY : "]" >

}

TOKEN :

{

  < IDENTIFIER: ["a"-"z","A"-"Z","_"] ( ["a"-"z","A"-"Z","_","0"-"9"] )* >

| < INT_LITERAL:["1"-"9"](<DIGIT>)*>/*integer*/

| <REAL_LITERAL:(<DIGIT>)+/*real*/

 

 | (<DIGIT>)+"."

 

 | (<DIGIT>)+"."(<DIGIT>)+

 

 | "."(<DIGIT>)+>

| < #DIGIT : [ "0"-"9" ] >

}

這些token各個代表了什麼,可以看它們的名字,名如其意。我按照他們的型別分為了很多類,有保留字,有操作符,有符號,還有需要忽略的token。單行註釋和多行註釋對應的那個正規表示式好難寫啊,搜尋了好久才有點頭緒。

還有一點我需要說明的是,我寫的jj檔案並不是以標準的控制檯輸入作為輸入流的,我是通過檔案流來初始化EG1類的。所以我在相應的目錄資料夾下建立了program.cmm檔案,通過從這個檔案中讀取原始碼來進行詞法分析和語法分析。

其實詞法分析和語法分析是一塊做的,在做的過程中我還參考瞭如下的自動機模型:

我在寫相應的節點函式的時候,是按照大二時候寫的C++程式碼中的函式的名稱來寫的,這樣會讓我感到更加的熟悉。我主要增加了以下幾種:

指標:在identifier之後需要一個*的正則。

函式的定義:被dec_stmt所呼叫。

/*函式定義*/

void function_stmt():{}

{

  (

      < LPARENT >

      (

        [((< INT >|< REAL >)(< MULTIPLY >)*< IDENTIFIER >[< ASSIGN > expression()])(< COMMA >((< INT >|< REAL >)(< MULTIPLY >)*< IDENTIFIER > [< ASSIGN > expression()]))*]

      )

      < RPARENT >< LBRACE >

      (

       statement()

      )*

      < RETURN >expression()< SEMICOLON >

      < RBRACE >

  )

}

還有就是類定義

/*類定義*/

void class_stmt():{}

{

  < CLASS >< IDENTIFIER >statement_sequence()

}

 

這就是我所進行的詞法分析和語法分析。具體的程式碼可以參考我的寫的JJ檔案,這裡不再贅述啦。

 

第三部分:CMM的語義分析

 

在做完了詞法和語法分析之後,看到算不出來結果,感覺很不爽,所以我又簡單的做了一下語義分析。剛開始的時候感覺自己能做到底,但是我忽略了一點,那就是我不能通過這個工具來一遍完成cmm的編譯和解釋執行,反正我是想不出來,或許這個工具真的做不到這一點,因為它是通過正規表示式來進行程式碼生成的。我當時做的時候卻是想著在詞法和語法分析的同時來進行語義分析從而直接把結果計算出來。但是事實證明這麼做是行不通的,儘管我構造了符號表,構造了簡單的語法數節點,但是或許還真的必須先構造語法樹,然後再遍歷語法樹解釋執行才可以。

我做了四則運算,還做了輸入輸出,但是當做到if語句和while語句的時候,自己實在不知道該怎麼做了,才意識到上面那個問題。但是已經做了的部分,語義分析都是沒有問題的,都能夠產生有效的輸出。

這是輸入程式:

這是輸出結果:

從上圖可以看出,我還增加了異常處理哦。

剛才說了,我增加了兩個類,一個是SymTab,用來儲存全域性符號表,另一個是TreeNode,用來描述節點資訊。

 

第四部分:JJTree的簡介

 

和YACC等一樣,JavaCC不直接支援分析樹或抽象語法樹(AST)的生成,如果要完成這些功能,使用者需要自己編寫相應的程式碼。幸運的是,JavaCC有一個擴充支援分析樹或抽象語法樹的生成,這就是JJTree。

實際上,JJTree可以看成是JavaCC的預處理程式。它的輸入檔案的字尾是jjt。經它處理之後的jj檔案就包含了生成分析樹的能力。JJTree採用壓棧出棧的方法生成分析樹。當它碰到一個非終結符要展開時,它會做一個標記,然後開始分析展開後的各個非終結符(此時,分析子樹作為節點壓入棧中),之後從棧中彈出合適個數的節點(分析子樹),並以被展開的非終結符生成一個新節點,以這個新節點為根節點,以剛彈出的節點為子節點生成新的分析子樹(分析樹)。

 

因為前面我們已經完成了我們的jj檔案,在jj檔案中已經完成了對於文法CMM的語言描述,所以這裡在寫jjt檔案的時候,基本上可以把前面的程式碼copy過來就可以了。這樣對於每一個函式,都會成為一棵樹的一個結點,並以函式的名字作為結點的名字,如果不想讓某個函式生成結點列印出來,只需要將該函式的名字後面加上#void即可。如果希望生成的結點的名字不是函式的名字,同樣也可以通過#name(寫在函式名之後)來改變。

 

 

第五部分:感悟

 

自己通過同學的指點,再加上一晚上以及一天的練習和琢磨,終於能很熟練的使用這個工具了。總體來說,這個工具真的很好用,簡潔易懂,開發一個分析器只需要十幾分鍾。但是剛開始也是一頭霧水。所以沒什麼是自己學不會的,就像當時自己開發遊戲一樣,剛開始覺得什麼都做不了,最後不是開發出一款不錯的嘛。所以要相信自己,並且堅持下去。這樣你一定就會成功。