CUP中文文檔

 

弄過編譯器的都知道,在進行語法解析的時候需要使用自動生成工具,CUP就是這樣一種工具,使用CUP可以生成用java語言編寫的語法解析器。花了很長時間翻譯了CUP的使用手冊,希望對正在使用做編譯器項目的親們有幫助。由於個人能力有限,翻譯的過程中難免出現錯誤,希望發現錯誤的親們及時提醒。 

 

引言:關於CUP0.10

版本0.10與其先前的版本0.9相比,增加了很多新的變化和特性。這些變化使CUP看起來更像它的前輩YACC。總之,版本0.9的解析器文件規範已經不被新版本所兼容,爲了寫出適合新版本的規範文件,您有必要詳細閱讀新用戶手冊的附件C。然而,新版本爲用戶提供了更強大的功能和更多的選項,也使得編寫解析器規範文件更容易。

第一章 簡介和示例

這個手冊爲您描述了Java版有用語法解析器生成器(Constructor of Useful Parser簡稱CUP)的最基本操作及用法。CUP是一個使用簡單規範文件生成LALR語法解析器的系統。它與被廣泛使用的YACC程序扮演了同樣的角色,事實上它實現了YACC大多數的功能。然而,CUP是用Java語言開發的,使用內嵌Java代碼的規範文件,生成Java語言的語法解析器。(一下我們將語法解析器簡稱爲解析器)。

 

儘管這本手冊涵蓋了CUP系統的方方面面,卻相對簡單,然而我們假定您至少懂得一點關於LR語法分析(從左向右掃描的一種語法分析方法)。使用過YACC的經驗將有助於您對CUP規範文件的工作原理的理解,很多關於編譯器構造的教材(比如引用[2,3])都會涵蓋這些內容,大多使用YACC(與CUP非常類似的一個解析器生成器)作爲一個具體的例子進行講解。除此之外Andrew Appel的《現代編譯器Java實現》一書在編譯器構造中使用和描述了CUP系統。

 

使用CUP生成解析器包括:創建一個基於特定語法簡單的規範文件、構造一個能將輸入字符串分解成一系列有意義的符號(例如關鍵字、數字、特殊符號)的詞法掃描器兩個步驟。(一下我們將此法掃描器scanner簡稱爲掃描器)。

 

作爲一個例子,考慮一個簡單的計算整數算數表達式的系統。該系統從標準終端上讀取表達式(每個表達式以分號結束),計算它們值,最後在標準終端上輸出結果。這種系統的一個語法規範可以用下面的代碼表示:

expr_list ::= expr_list expr_part | expr_part

  expr_part ::= expr ';'

 expr       ::= expr '+' expr | expr '-' expr | expr '*' expr 

              | expr '/' expr | expr '%' expr | '(' expr ')'  

                   | '-' expr | number

爲了利用這個規範生成一個解析器,首先指定並命名可能在程序中出現的終結符及非終結符。這個例子的非終結符有:

expr_list, expr_part, expr 

終結符我們可以選擇

SEMI, PLUS, MINUS, TIMES, DIVIDE, MOD, NUMBER, LPAREN, RPAREN 

 

有經驗的用戶可能會注意到上述規範中存在的一個問題,就是它看起來非常含糊。一個含糊的文法,會將一個特定的輸入用兩種不同的方式進行解析,有可能會給出兩種不同的結果。以上述規範爲例,對於這樣一個輸入3+4*6,它會先計算3+4然後再乘於6,或者先計算4*6然後再加上3。老版本的CUP要求用戶寫明確的規範,但是現在這個版本爲用戶提供了一個指令來爲符號指定優先級及結合性。這意味這上述不明確的文法在指定了優先級和結合性後仍然可以使用,下面將給出這方面更多的解釋。基於上面的說明,我們可以構造如下一個簡單的CUP規範文件:

//簡單計算器的CUP規範(沒有執行動作)

import java_cup.runtime.*;

/*初始設置、設定詞法掃描器 */

init with {: scanner.init();              :};

scan with {: return scanner.next_token(); :};

/* 終結符 (詞法掃描器返回的符號). */

terminal           SEMI, PLUS, MINUS, TIMES, DIVIDE, MOD;

terminal           UMINUS, LPAREN, RPAREN;

terminal Integer  NUMBER;

/* 非終結符 */

non terminal           expr_list, expr_part;

non terminal Integer expr, term, factor;

/* 優先級 */

precedence left PLUS,  MINUS;

precedence left TIMES, DIVIDE, MOD;

precedence left UMINUS;

/* 文法 */

expr_list ::= expr_list expr_part 

| expr_part;

expr_part ::= expr SEMI;

expr       ::= expr PLUS expr 

           | expr MINUS expr  

           | expr TIMES expr  

           | expr DIVIDE expr  

           | expr MOD expr 

      | MINUS expr %prec UMINUS

      | LPAREN expr RPAREN

      | NUMBER

  ;

 

接下來我們會詳細講解這個規範文件的每一部分。從上面這個例子可以很容易看出規範文件一般包括四個主要的部分。第一部分包括一些預處理及各種聲明,以指定如何構建編譯器,同時也包含了一些運行時代碼,在這個例子的第一部分,我們指定解析器應導入java_cup.runtime包內的所有類,然後給出了一小段初始化代碼及一些調用掃描器來獲得下一個符號的代碼。第二部分聲明終結符和非終結符,以及每個符號對應的類型,這個例子中,終結符被聲明爲無類型及Integer類型兩種,這個類型指的是終結符或非終結符所代表的值的類型,如果沒有指定類型,它們也就沒有值。第三部分指定終結符的優先級和結合性,這部分最後出現的終結符擁有最高的優先級。第四部分描述系統的語法。

 

使用CUP解析器生成器根據這個規範文件生成一個解析器:如果這個規範保存在一個名爲parser.cup的文件中,我們可以用下面這樣的命令調用CUP:

java java_cup.Main < parser.cup //類Unix系統中

或者

java java_cup.Main parser.cup  //從CUP0.10k版本之後

系統會生成兩個Java源文件sym.java和parser.java保存解析器的兩個部分(這兩個名字可以通過命令行選項更改,下面會提到)。正如你所期望的那樣,這兩個文件分別保存了sym和parser類。sym類包含了一系列的常量聲明,每一個聲明代表一個終結符,它可以被掃描器用來指定掃描到的符號,例如這樣一個代碼“return new Symbol(sym.SEMI);”。parser類則實現瞭解析器本身。

 

上面的規範儘管能生成一個完整的解析器,但是不執行任何語義動作,它僅僅指示一次解析是成功還是失敗。爲了計算和打印每個表達式的結果,我們必須在規範文件中的不同位置嵌入攜帶語義動作的Java代碼。在CUP規範中語義動作被包含在代碼串中,即{:和:}之間的內嵌代碼,例如上面例子中的init with和scan with子句。一般而言,系統會記錄{:和:}中的所有符號,但是不去檢查它們是否爲有效的Java代碼。

 

我們所舉例子的一個更加完整的CUP規範(在不同位置內嵌了代表語義動作的java代碼)如下所示:

//簡單算術表達式計算器的CUP規範(包含語義動作)

import java_cup.runtime.*;

/*初始設置、設定詞法掃描器   */

init with {: scanner.init();              :};

scan with {: return scanner.next_token(); :};

/*終結符 (詞法掃描器返回的符號).  */

terminal           SEMI, PLUS, MINUS, TIMES, DIVIDE, MOD;

terminal           UMINUS, LPAREN, RPAREN;

terminal Integer   NUMBER;

/* Non-terminals */

non terminal            expr_list, expr_part;

non terminal Integer    expr;

/* 非終結符*/

precedence left PLUS, MINUS;

precedence left TIMES, DIVIDE, MOD;

precedence left UMINUS;

/* 文法 */

expr_list   ::= expr_list expr_part | expr_part;

expr_part  ::= expr:e  {: System.out.println("= " + e); :}  SEMI;

expr       ::= expr:e1  PLUS    expr:e2 {: RESULT = new Integer(e1.intValue() + e2.intValue()); :} 

   | expr:e1  MINUS  expr:e2 {: RESULT = new Integer(e1.intValue() - e2.intValue()); :} 

   | expr:e1  TIMES   expr:e2 {: RESULT = new Integer(e1.intValue() * e2.intValue()); :} 

   | expr:e1  DIVIDE  expr:e2 {: RESULT = new Integer(e1.intValue() / e2.intValue()); :} 

   | expr:e1  MOD    expr:e2 {: RESULT = new Integer(e1.intValue() % e2.intValue()); :} 

   | NUMBER:n {: RESULT = n; :} 

   | MINUS expr:e{: RESULT = new Integer(0 - e.intValue()); :} %prec UMINUS

   | LPAREN expr:e RPAREN{: RESULT = e; :} 

   ;

其中我們會看到一些改變,最重要的是在規範的不同位置加入了被{:和:}包圍的用於執行語義動作的內嵌代碼串。除此之外,還在不同規則的右邊加入了一些標籤,例如

expr:e1 PLUS expr:e2{: RESULT = new Integer(e1.intValue() + e2.intValue()); :}

第一個非終結符expr添加了標籤e1,第二個非終結符添加了標籤e2。每條規則的值被隱含標記爲RESULT。

 

出現在每條規則中的符號,在解析堆棧都使用一個Symbol類型的對象來表示,它們的標籤則代表這些對象中的實例變量的值。在表達式expr:e1 PLUS expr:e2中,e1和e2代表了Integer類型的對象,被存儲在解析堆棧中代表這些非終結符的Symbol類型對象中,由於結果非終結符expr被聲明爲Integer類型,因此RESULT也是一種Integer類型的對象,最終被存儲在一個新的Symbol對象中。

 

對於每個標籤,將會有兩個用戶可訪問的變量被聲明,分別稱之爲標籤的左值和右值,它們的值可以傳送給內嵌代碼串,這樣用戶就可以定位每個終結符或非終結符在輸入流中的位置。每個變量的名字是標籤加上left或right,例如對於規則expr:e1 PLUS expr:e2,用戶不僅可以訪問變量e1和e2,也可訪問e1left、e1right、e2left及e2right,而且它們四個都是int型的變量。

 

創建一個可以工作的解析器的最後一個步驟是創建一個詞法掃描器(有時也被稱之爲詞法分析器)。這個程序負責讀取輸入字符串,去除空格和註釋,找出每個詞在語法中代表的終結符,最後向解析器返回代表這些終結符的Symbol對象。通過調用掃描器函數就可以獲得這些終結符,例如解析器調用scanner.next_token()方法,掃描器則返java_cup.runtime.Symbol類型的變量(這種變量與CUP先前版本的java_cup.runtime.symbol類型的變量有很大的不同)。每個Symbol對象攜帶一個Object類型的實例變量,其類型應該在詞法掃描器中被指明,變量則存儲了該對象的值,因此其類型應該與該終結符或非終結符在規範文件中被定義時聲明的類型一致。在上面的例子中,如果詞法分析器要返回一個NUMBER符號的值,應該定義一個Symbol對象包含一個表示Integer類型對象變量。與無值終結符或非終結符對應的Symbol對象中相應的變量會儲存一個空值。

 

規範文件中init with 字句中包含代碼,會在獲取任何符號之前被執行,任何符號都將通過scan with子句中的代碼獲取。除此之外,調用掃描器的具體方式由你自己決定,但是每次調用掃描器獲取符號的函數都應該返回一個java_cup.runtime.Symbol(或其子類)的對象。這些Symbol對象將被標註上解析器信息並存儲在一個棧中,重用這些對象會導致解析器註釋信息混亂。版本CUP0.10j會檢測Symbol對象是否被重用,如果檢測到Symbol對象被重用,解析器將會拋出一個錯誤提醒你改正你的掃描器。

 

在下一節中,將會對CUP規範各部分進行更詳細和正式講述。第3節講述使用CUP系統的各種選項。第4節講述如何定製CUP解析器。第5節講述CUP0.10j中增加的詞法掃描器接口。第6節講述錯誤恢復方面的內容。第7節對本手冊進行總結。

 

第二章 規範語法

 

由於我們已經接觸到了一個簡單的例子,現在就給出CUP規範文件各部分最爲完整的描述。一個規範文件包含四個部分共八項內容(其中大部分是可選項),即:包定義及引用、用戶代碼區、符號列表(終結符和非終結符)、優先級聲明、語法。每一部分必須按上面給出的順序進行書寫(附錄A給出了規範文件一個完整示例)。每一部分的具體細節會在下面幾個小節中描述。

 

包定義和引用規範

規範文件以package和import聲明開頭,這兩個聲明是可選的。這些聲明與標準Java程序中的package和import聲明有相同的語法,並扮演相同的角色。package的聲明形式

package name; 

其中name是一個Java包名標示符,有時候可能會由幾個用“.”分割的單詞組成。一般說來,CUP採用了Java的詞法規範。因此Java的註釋風格在CUP中被支持,標示符也類似於Java中的以字母、$、_開頭後節零個或多個字母、數字、$、_標識符規範。package聲明之後,可以再聲明零個或多個import語句。正如在Java程序中的形式一樣,CUP中的import聲明形式可以是:

import package_name.class_name;

或者

import package_name.*;

包聲明語句表示系統生成的sym和parser文件將會放入哪個包內,import聲明會被原封不動地放在系統生成的parser源文件中,以使被導入包內的各種名字可以在內嵌的語義動作代碼內直接使用。

 

用戶代碼區

package和import聲明之後,是一系列用戶代碼,這些代碼同樣是可選的,它們可以作爲解析器的部分代碼被放在生成的parser文件內(查看第四章獲取詳細的parser使用這些代碼的方式)。作爲parser規範文件的一部分,用戶語義動作代碼被存放在一個獨立的非公有化類內,第一個action code聲明區允許其中的代碼存放在這個類內。文法中的內嵌代碼使用的例程和變量應該被放置在這個區域內(一個典型的例子是符號表操作例程)。聲明的形式是:

action code{:……:};

{:……:}中的代碼串會直接存放action class聲明的類中。

 

action code之後是一個parser code聲明,這部分是可選的。這個聲明內的方法和變量將會被直接存放在解析器類內,儘管這個聲明看起來非常普通,但是在定製解析器的時候將會非常有用。它可以將詞法掃描方法包含在解析器內或者重載默認的錯誤報告例程。這個聲明的形式和action code的聲明形式非常類似,即:

parser code {: ... :};

{: ... :}中的代碼串被直接存放在解析器類中。

 

接下來是init with聲明,同樣是可選的,其形式是:

init with {: ... :};

這個聲明內的代碼會在解析器獲取第一個符號之前被執行。通常,這些代碼用來初始化詞法掃描器,以及執行語義動作時會用到的一些表或其他數據結構。這些代碼在parser類內被構造成一個無返回值的方法。

 

規範文件用戶代碼區的最後一個可選部分表示解析器如何從詞法掃描器內獲取下一個符號,其形式:

scan with{: ... :};

正如init子句一樣,其內的代碼串會在解析器內存放在一個方法內,所不同的是,這個方法會有一個java_cup.runtime.Symbol類型的返回值,所以sacn with內放置的代碼應該返回這種類型的一個值。在第5章中將會講述如果scan with字句被忽略時,系統默認執行的動作。

 

對於CUP0.10j,action code,parser code,init with,scan with可以以任意順序出現,但是他們必須放置在符號列表之前。

 

符號列表

用戶代碼區之後,即是符號列表區,這部分是規範文件必寫內容。這些聲明負責爲在語法中出現的終結符和非終結符命名和指定類型。正如上面提到的一樣,每個終結符和非終結符在運行期由一個Symbol對象表示。以終結符爲例來說,這些對象由詞法掃描器返回並存儲在解析器堆棧中,掃描器應該將終結符的值存入Symbol對象中對應類型的實例變量中。以非終結符爲例來說,一旦有些右值規則被解析到,它會替換掉解析堆棧中的一系列Symbol對象。爲了告知解析器應該將哪個符號指定爲何種類型的對象,應該在terminal, non-terminal後進行聲明,其聲明形式爲:

    terminal      classname  name1, name2, ...;

    non terminal  classname  name1, name2, ...;

    terminal                name1, name2, ...;

    non terminal            name1, name2, ...;

其中classname可以是由“.”分割的複合名,指定的classname就代表其後的終結符或非終結符的類型。當用標籤訪問符號的值時,用戶應該使用該符號被聲明的類型。classname可以是任何類型,如果終結符和非終結符未指定任何類型,那麼這種類型的符號就不能攜帶值,引用該種類型符號的標籤也被系統設定爲空值。對於CUP版本0.10j,你也可以將非終結符指定爲“nonterminal”(注意其中沒有空格)或者“non terminal”(其中包含空格)。

 

終結符和非終結符的名字不可以使用CUP的保留字。CUP的保留字包括:“code”,“action”,“parser”,“terminal”,“non”,“nonterminal”,“init”,“scan”,“with”,“start”,“precedence”,“left”,“right”,“nonassoc”,“import”,“package”。

 

優先級和結合性

規範文件的第三部分,可選部分,用於指定終結符的優先級和結合性。這對於解析不明確的語法非常有用,正如在上面例子中所看到的那樣。有三種類型的優先級和結合性聲明:

precedence left     terminal[, terminal...];

precedence right    terminal[, terminal...];

precedence nonassoc terminal[, terminal...];

用逗號隔開的列表表示這些終結符應該具有precedence指定的結合性和優先級級別。precedence指定的優先級與其出現的順序相反,越早出現的級別越低,越晚出現的級別越高。因此,下面的聲明表示乘法和除法具有較高的優先級,而加法和減法具有較低的優先級。

precedence left  ADD,  SUBTRACT;

precedence left  TIMES,  DIVIDE;

 

使用優先級聲明,可以解決移進/規約問題。例如,給上面例子一個這樣的輸入:3+4*8,解析器就不知道是應當對3+4進行規約,還是將*移進堆棧。然而,由於*比+的優先級高,*應該被移進堆棧,最終乘法將早於加法被運算。

 

CUP系統根據這樣的聲明,爲每個終結符賦予相應的優先級。沒有聲明優先級的終結符的優先級被認爲是最低的。CUP也對由終結符和非終結符組成的規則賦予一定的優先級,相應規則的優先級被認爲與規則最後出現的終結符的優先級相同,如果某條規則中沒有終結符,那這條規則就被認爲具有最低的優先級。例如,這樣一條規則:expr::=expr TIMES expr,會被認爲與TIMES具有相同的優先級。在解析的過程中,如果出現移進/規約(shift/reduce)衝突,解析器將決定是將一個具有較高的優先級終結符移進堆棧,還是認爲規則具有較高的優先級而將其解析。如果一個終結符具有較高的優先級,它將被移進堆棧,而如果一條規則具有較高的優先級,它就會被規約(即被解析器解析)。如果兩者具有相同的優先級,那麼終結符的結合性將決定解析器的執行動作。

 

每個終結符的結合性聲明同樣是在優先級與結合性部分指定的。有三種類型的結合性,分別是:左結合(left)、右結合(right)、不結合(nonassoc)。結合性同樣用來處理移進/規約(shift/reduce)衝突,但是僅當終結符與規則具有相同的優先級時才發揮作用,在這種情況下,如果一個可以執行移進動作的終結符是左結合的,就會執行規約動作。這意味這,如果輸入的是加法串3+4+5+6+7,以3+4開始,解析器將會從左向右一直執行規約動作。如果終結符的結合性是右結合,那麼解析器會將其移進堆棧中,規約動作將會按照從右向左的順序進行。因此,如果PLUS的結合性被聲明爲右結合,那麼對於上面的加法串,6+7將會是第一個被執行規約動作。如果一個終結符被聲明爲不結合,如果有連續兩個具有相同優先級而且結合性爲不結合的終結符同時出現,將會發生錯誤。這對於比較運算非常有用,例如,輸入串爲6==7==8==9,解析器就會產生一個錯誤。如果“==”被聲明爲不結合,就會有一個錯誤發生。

 

所有未使用優先級與結合型聲明的終結符,被認爲具有最低的優先級。如果發生一個無法解決的移進/規約錯誤,CUP系統會報告一個錯誤。

 

語法

CUP聲明的最後一個部分是語法。這個部分往往由一個聲明開始,這個聲明是可選的,其聲明形式爲:

start with non-terminal;

這代表哪個非終結符是start或者goal非終結符。如果沒有明確聲明這樣一個非終結符,則語法第一條規則的左邊的非終結符將被指定爲開始或目標非終結符。在每次成功解析後,CUP會返回一個java_cup.runtime.Symbol類型的對象,這個對象中的實例變量包含解析到的最終結果。

 

語法往往開始於start聲明之後,每條規則最左邊是一個非終結符,接着是一個“::=”符號,緊跟着是零個或多個動作、終結符、非終結符,再接着是一個可選的語境優先級分配,最後由一個分號結束。

 

每個在右邊的符號,都可以用一個標籤標記,也可以不標記。標籤放在符號的右邊,與符號用一個冒號(:)隔開。標籤名在每條規則中必須是唯一的,定義標籤後,就可以在動作代碼中使用用來表示所代表符號在運行期的值。一旦定義了標籤,就有兩個變量緊接着被定義,分別是標籤名加上left和標籤名加上right,這兩個變量都是int類型的變量,分別代表該符號在輸入文件的行列位置,這兩個值必須由詞法掃描器在掃描的過程中初始化。最後left和right值被賦給該符號所在的規則最終被規約到的非終結符。

 

如果一個非終結符可以由多種規則定義,那麼這些規則應該放在一塊聲明。在這種情況下,有一個終結符開始,接着是一個“::=”符號,緊接着就是多條規則,每條規則由“|”分開,所有的規則結束後,由一個分號終止。

 

語義動作在每條規則的右邊出現,表現爲內嵌java代碼串,放置在{:……:}中間。當代碼串所在的規則被解析到時,代碼串就由parser負責執行。注意,解析器在將要規約一條規則的時候,會再從輸入文件中讀一個符號,因爲解析器爲了更準確的解析規則,需要更多的預讀字符。

 

在被分配優先級的規則右邊的所有符號和動作之後是語境優先級分配。語境優先級分配,允許一條規則不必依賴於規則最後一個終結符的優先級。上面解析器規範樣例就給出了一個很好的例子:

precedence left PLUS, MINUS;

precedence left TIMES, DIVIDE, MOD;

precedence left UMINUS;

expr ::=    MINUS expr:e{: RESULT = new Integer(0 - e.intValue()); :} 

                  %prec UMINUS

這裏,這條規則的優先級被聲明爲與UMINUS的優先級相同,因此解析器可以根據MINUS是一個一元符號或者是真正的減法操作,而給MINUS兩個不同的優先級。

 

第三章使用CUP

正如上面提到的,CUP是由Java開發的。爲了調用CUP系統,你應該使用Java解釋器(命令行中的java指令)調用靜態方法java_cup.Main(),向其傳遞一組包含選項的字符串。假設是在類Unix系統中,最簡單的方法,就是直接用下面這樣一條指令在命令行中調用:

      java java_cup.Main options < inputfile 

一旦開始運行,CUP需要從標準終端獲得一個規範文件,併產生兩個Java源文件作爲輸出。從CUP版本0.10k開始,最終的命令行參數可以是一個文件名,在這種情況下,CUP將會從指定的文件中讀入規範文件,而不是從標準終端上獲得。

 

除了規範文件外,CUP的行爲可以通過設定不同的選項來改變。合法的選項保存在Main.java的文檔中,下面給出這些選項的說明:

-package name 

    指定生成的parser和sym類應該被放置在的包的名字,默認情況下,及不使用這個選項,這些類將被放置在規範文件所在的目錄下。

-parser name

    指定生成的解析器文件的名字。默認情況下,解析器被命名爲parser。

-symbols name

    指定生成的符號表的名字。默認情況下,其名字爲sym。

-interface

    將符號表生成爲接口,而不是默認情況下的類。

-nonterms

    在符號表類內生成代表非終結符的常量,儘管解析器並不需要這些常量,然而在調試一個生成的解析器時,可以訪問到這些常量,將會得到非常有用信息。

-expect number

    在系統根據規範文件創建解析器時,在運行期可能會檢測到一些含糊的規則,被稱之爲衝突(conflict)。一般情況下,解析器不能決定是應該移進(讀入下一個符號)還是應該規約(用一條規則的左邊的符號代替其右邊的定義的規則,即解析一條規則)。這通常被稱之爲移進/規約衝突(shift/reduce conflict)。類似的,解析器面對兩條不同的規則不能決定應該規約哪一條時,就會發生規約/規約衝突(reduce/reduce conflict)。通常情況下,如果發生一個或多個這樣的衝突,系統就會中斷構造解析器。在一個深思熟慮的系統中,在這些衝突的發生時,中斷系統將會是非常有益的。CUP參照了YACC的做法,用移進解決移進/規約衝突,對於規約/規約衝突優先規約“最高優先級”規則(即在規範中最先被定義的規則)。爲了使系統能夠自動的處理這些衝突,應該在-expect選項中給出有多少這樣的衝突是被允許的。已經用優先級和結合性解決的衝突,在生成系統的時候不會報告。

-compact_red

   使用此選項可以實現對錶壓縮優化。特別的,使用這個選項,可以將解析動作表中每行最普通的規約條目作爲該行的默認條目,這樣可以明顯的節約解析表需要的空間,否則解析表會增長很大。這種優化可以將所有的錯誤條目放置到一行中,這一行默認使用一條規約條目,這可能看起來有些奇怪,如果沒有正確的處理,就有可能使生成的解析器無法正常工作。可是,這樣一些改變確實繼承自LALR解析器(與標準LR解析器相比),而且由此生成的解析器仍然無法越過第一條可以被檢測到錯誤的條目。然而,這種解析器在檢測到錯誤之前會做一些額外的錯誤規約,因此可能會減弱解析器錯誤恢復的能力(如果要相信瞭解這種壓縮技術,請參看引用[2]244-247頁,或者引用[3]190-194頁)。

    這個選項通常應用在Java字節碼中的表級壓縮優化。然而CUP0.10h引入了一種字符串編碼方法。

-nowarn

    這個選項,不報告系統產生的所有警告信息(與錯誤信息相對)。

-nosummary

    通常情況下,在解析結束的時候,系統會打印一個總結列表,包括終結符及非終結符的數量,解析狀態等。這個選項,會阻止系統打印上述信息。

-progress

    這個選項,使系統打印一些代表系統進度的簡短信息,系統進度通過解析器生成過程的各個部分獲得。

-dump_grammar

-dump_states

-dump_tables

-dump

    上面這些選項使系統分別顯示人類可讀的語法、解析器構建狀態(在解決解析衝突的時候經常用到)及解析表(很少被用到)。-dump這個選項可以使系統生成上述所有的信息。

-time

    這個選項,將會向系統產生的信息中加入詳細的時間信息,這通常僅僅對系統的維護進程有用。

-debug

    這個選項,會使系統生成其運行時產生的大量的內部調試信息。這些信息通常只對系統的維護進程有用。

-nopositions

    這個選項,會阻止CUP生成代碼將終結符的行(left)和列(right)值傳送給非終結符,以及從非終結符傳送給終結符。如果這樣的行和列值在解析器中不使用,系統就不會生成這些位置信息,從而使系統節約一些運行期的運算量。這個選項使系統不再包含行(left)和列(right)的變量,因此一些引用這些變量的代碼會使系統產生錯誤。

-noscanner 

    CUP0.10j改進了詞法掃描器(scanner)的集成,並推出了一種新的接口,java_cup.runtime.Scanner。默認情況下,生成的解析器需要使用這個接口,這意味着這些解析器將不能使用比0.10j更老的版本的CUP運行時庫(java_cup.runtimes)。如果你的解析器不使用這種新的詞法掃描器(scanner)接口提供的功能,你就應該使用-noscanner選項來阻止解析器對java_cup.runtime.Scanner接口的引用,從而保持與CUP老版本的兼容。對於大多數人來說,是沒有理由這樣做的。

-version

   在調用CUP的時候指定-version選項可以使系統打印當前工作的CUP版本。這使得在引用其他的環境變量(Makefiles)、安裝腳本及其他應用程序需要知道CUP版本的時候可以自動獲取。

第四章 定製解析器

每個生成的解析器,都包含三個類。sym類(可以使用-symbols選項改名)包含了一系列的int型常量,每個代表一個終結符,如果使用了-nonterms選項,也可以將非終結符(non-terminals)包含進來。parser類(可以使用-parser選項重命名)事實上包含兩個類定義,其中parser類爲公共類真正的實現瞭解析器,另一個CUP$action爲非公有類囊括了規範文件中內嵌的所有語義動作代碼以及action code聲明中包含的代碼。除了用戶支持代碼外,這個類包含一個方法,CUP$do_action,包含了一個大的switch語句,用來選擇和執行各種各樣的支離破碎的用戶指定的語義動作代碼。一般情況下,所有的名字都以CUP$爲前綴,以保留作爲CUP生成的內部代碼使用。

 

parser類包含了實際的生成的解析器,該類是java_cup.time.lr_parser的子類,java_cup.time.lr_parser類實現了一個通用的LR解析器的表驅動框架。生成的parser類,爲通用的LR解析器表驅動框架提供了一系列的表。其中的三個表如下:

規則表

爲語法中的每條規則,提供了左值非終結符的數量,以及右側符號的長度。

動作列表

在每個現行符號在遇到一種狀態時的動作,其中包括移進、規約、錯誤。

reduce-goto表

在每次規約之後指示應該移進哪個狀態。(注意:動作列表和reduce-goto表不是採用簡單的數組存儲的,而是使用一種壓縮的鏈表結構,這樣可以在很大程度上節約存儲空間,詳情請參閱運行時系統源代碼。)

 

除了解析表外,生成的(或繼承)的代碼會提供一系列的方法來定製生成的解析器。這些方法中的一些是取自規範文件中的部分代碼,使用這種方法可以直接定製解析器。另一些方法則是由lr_parser基類提供的,這些方法在新版本中(通過parser code聲明)可以被覆蓋以定製解析器系統。能夠被定製的方法如下所示:

Public void user_init():

這個方法被解析器首先調用來從詞法掃描器獲取第一個符號,這個方法的主體包含規範文件中的init with字句。

Public java_cup.runtime.Symbol scan()

這個方法將詞法掃描器囊括在內,每次解析器需要一個新的終結符時就會調用這個方法。這個方法的主體在當前版本包含規範文件中的scan with字句。

public java_cup.runtime.Scanner getScanner()

返回默認的詞法掃描器。參看第五章。

public void setScanner(java_cup.runtime.Scanner s)

設置默認的詞法掃描器。參看第五章。

public void report_error(String message, Object info)

每當要發佈錯誤信息時,都應當調用該方法。改方法的默認實現是,第一個參數提供需要打印到錯誤流上的文本,第二個參數則被忽略。爲了實現一個更復雜的錯誤報告機制,典型的做法是重寫這個方法。

public void report_fatal_error(String message, Object info)

當一個不可恢復的錯誤發生時,應當調用這個方法。它通過調用report_error()方法報告錯誤信息,然後調用done_parsing()方法中斷解析,最後拋出異常。一般而言,當解析過程應當被提前終止的時候都應當調用done_parsing()方法。

public void syntax_error(Symbol cur_token)

一旦一個語法錯誤被檢查到、嘗試錯誤恢復之前,語法解析器將調用這個方法。這個方法的默認實現是,調用report_error("Syntax error", null);方法。

public void unrecovered_syntax_error(Symbol cur_token)

當解析器遇到無法恢復的語法錯誤時,調用這個方法。其默認的實現是:report_fatal_error("Couldn't repair and continue parse", null);.

protected int error_sync_size()

這個方法返回解析器在成功的認定一個錯誤時應該成功的解析多少個符號。默認的實現是返回3。低於2的值不被推薦。詳細情況請參看錯誤恢復一章。

 

解析過程是由public Symbol parse()方法實現的。這個方法首先取得每個解析表的引用,接着初始化一個CUP$action對象(通過調用protected void init_actions()方法);然後調用user_init()方法,接着通過調用scan()方法取得第一個預讀符號;然後開始解析,知道done_parsing()方法被調用(這個方法是被自動調用的,例如,當解析一個規則後)。它會返回一個包含開始規則值的實例變量的符號對象,或者null如果沒有值需要返回。

 

除了普通的解析器之外,系統提供瞭解析器的帶調試功能的版本。它的功能跟普通的解析器沒有差別,只不過會默認通過調用public void debug_message(String mess)方法將調試信息打印到錯誤輸出流(System.err)。

基於以上的例程,可以通過下面的代碼調用一個CUP解析器。

      /* create a parsing object */

      parser parser_obj = new parser();

      /* open input files, etc. here */

      Symbol parse_tree = null;

      try {

          if (do_debug_parse)

             parse_tree = parser_obj.debug_parse();

          else

          parse_tree = parser_obj.parse();

      } catch (Exception e) {

        /* do cleanup here - - possibly rethrow e */

      } finally {

    /* do close out here */

      }

第五章 掃描器藉口

鑑於在調用CUP的時候指定-version選項可以使系統打印當前工作的CUP版本。這使得在引用其他的環境變量(Makefiles)、安裝腳本及其他應用程序需要知道CUP版本的時候可以自動獲取。在調用CUP的時候指定-version選項可以使系統打印當前工作的CUP版本。這使得在引用其他的環境變量(Makefiles)、安裝腳本及其他應用程序需要知道CUP版本的時候可以自動獲取。在調用CUP的時候指定-version選項可以使系統打印當前工作的CUP版本。這使得在引用其他的環境變量(Makefiles)、安裝腳本及其他應用程序需要知道CUP版本的時候可以自動獲取。java_cup.runtime.Scanner接口,其定義如下:

package java_cup.runtime;

public interface Scanner {

    public Symbol next_token() throws java.lang.Exception;

}

除了在第四章中介紹的方法之外,java_cup.runtime.lr_parser還擁有兩個存取方法,setScanner() 和 getScanner()。Scan()方法的默認實現如下:

public Symbol scan() throws java.lang.Exception {

    Symbol sym = getScanner().next_token();

    return (sym!=null) ? sym : new Symbol(EOF_sym());

  }

生成的解析器也擁有一個帶Scanner參數的構造器,然後調用setScanner()設定掃描器。在大多數情況下,init with和scan with會被忽略。你可以簡單的創建一個解析器,使其引用到你所期望的掃描器,代碼如下:

parser parser_obj = new parser(new my_scanner()); 

或者在解析器被創建之後再設定掃描器,代碼如下:

      /* create a parsing object */

      parser parser_obj = new parser();

      /* set the default scanner */

      parser_obj.setScanner(new my_scanner());

 

需要注意的是,解析器使用預讀策略,不建議在解析的過程中重新設定詞法掃描器。如果在沒有事先調用setScanner()設定掃描器,就調用sann()方法,系統將會拋出一個NullPointerException異常。

 

作爲一個詞法掃描器集成的例子,下面給出了使用JLex或者JFlex生成掃描器的時候應該添加的三行代碼:

%implements java_cup.runtime.Scanner

%function next_token

%type java_cup.runtime.Symbol

JLex 1.2.5及更新的版本中的指令%cup是對以上三行代碼的縮減。在解析器中調用JLex掃描器非常的非常簡單,如下所示:

parser parser_obj = new parser( new Yylex( some_InputStream_or_Reader));

 

注意:CUP在沒有問題的情況下會處理JLex/JFLex遇到文件結束符(EOF)返回空值的規範,因此在JLex規範文件中不再需要%eofval指令(這項特性是在CUP0.1k中加入的)。在CUP發行版中的簡單計算器例子中展示瞭如何利用CUP的掃描器集成特性添加一個手工編寫的掃描器。CUP網站中也提供了一個最小的CUP/JLex集成實例以供學習。

第六章 錯誤恢復

使用CUP構建解析器的最後一個重要的方面是支持動態錯誤恢復。CUP使用了跟YACC相同的錯誤恢復機制。特別的,CUP支持一種特殊的錯誤符號,簡單的使用error表示。這個符號扮演了一種特殊的非終結符角色,它能夠匹配錯誤輸入序列,而非一個用規則定義的字符串。

 

這個錯誤符號僅當檢測到一個語法錯誤的時候才發揮作用。一個語法錯誤被檢測到後,解析器使用error來代替輸入符號序列的一部分,然後繼續解析。作爲實例,我們可能這樣定義一條規則:

stmt ::= expr SEMI | while_stmt SEMI | if_stmt SEMI | ... |

        error SEMI

         ;

這條規則表明,如果輸入序列無法匹配stmt正常的規則,這表明產生了一個語法錯誤,錯誤恢復機制在這個時候就應該發揮作用,它應該跳過錯誤符號序列(也就是用error符號來匹配和代替這個錯誤的符號序列),一直到解析器可以繼續解析的位置,這個位置可以是一個分好,或者是附加在一個語句後的合法的上下文環境。一個錯誤能夠被認定爲得到恢復,只有在error符號後面有足夠數量的符號能夠被成功的解析。這個符號的數量有解析器的error_sync_size()方法決定,默認值爲3。

 

具體而言,解析器首先從解析棧頂部查找錯誤發生時與其有關的最近的一個狀態。這有點類似於從表示更具體的規則(例如:一種特定的語句)向更泛化或封閉的規則(例如:所有語句的一般形式,或者是可以代表所有聲明的規則)進行解析,直到遇到一個錯誤恢復規則。解析器一旦被配置成立即錯誤恢復(通過彈棧到第一個上述狀態)的策略,它就會跳過錯誤符號序列去尋找能夠繼續解析的位置。在丟棄了錯誤符號序列的每個符號之後,解析器將試圖向前解析,這個時候不會執行內嵌的語義代碼。如果解析器在跳過足夠數量的符號後能夠成功的解析,輸入序列就會被重新定位到恢復前的位置,解析器將重新開始解析,這個時候解析器要執行所有的內嵌動作。如果解析器仍然不能繼續解析,當前的符號就會被丟棄,解析器將繼續試圖向前解析。如果已經到達了輸入文件的結尾,仍然無法成功的恢復,或者說沒有找到任何與錯誤恢復相符的狀態,錯誤恢復就宣告失敗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章