青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

隨筆-341  評(píng)論-2670  文章-0  trackbacks-0

Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南

陳梓瀚

 

前言

在日常的開(kāi)發(fā)工作中我們總是時(shí)不時(shí)需要寫(xiě)一些語(yǔ)法分析器。語(yǔ)法分析器不一定指的是一門(mén)語(yǔ)言的編譯器前端,也有可能僅僅是一個(gè)自己設(shè)計(jì)格式的配置文件的讀寫(xiě)程序,或者是一門(mén)用來(lái)簡(jiǎn)化我們開(kāi)發(fā)的DSL(領(lǐng)域?qū)S谜Z(yǔ)言)。我們可以選擇使用XML,不過(guò)因?yàn)?/span>XML的噪音實(shí)在是太多,所以自己寫(xiě)語(yǔ)法分析器在有些情況下是必要的,特別是那種經(jīng)常需要修改的文件,使用XML有時(shí)候會(huì)增加我們的負(fù)擔(dān),除非我們專門(mén)為此開(kāi)發(fā)一個(gè)編輯器程序。

 

這篇文章將緊密結(jié)合一個(gè)帶函數(shù)的四則運(yùn)算計(jì)算器的例子(Documentation\Samples\ExpressionCalculator\ExpressionCalculator.sln)來(lái)說(shuō)明如何使用Vczh Library++提供的工具來(lái)大幅度簡(jiǎn)化我們的語(yǔ)法分析器的開(kāi)發(fā),并最終給出一個(gè)可以編譯的例子。雖然這個(gè)例子實(shí)在是老掉牙了,不過(guò)開(kāi)發(fā)一個(gè)四則運(yùn)算計(jì)算器可以覆蓋大部分開(kāi)發(fā)語(yǔ)法分析的過(guò)程中會(huì)遇到的問(wèn)題,所以也不失為一個(gè)好的例子。

 

這個(gè)例子可以在Vczh Library++的代碼里面找到。


制定語(yǔ)法

我們需要對(duì)帶函數(shù)的四則運(yùn)算計(jì)算器下一個(gè)定義,這樣我們才可以有目的地完成這個(gè)任務(wù)。我們對(duì)四則運(yùn)算式子是很熟悉的,一個(gè)四則運(yùn)算式子包含加減乘除、括號(hào)和數(shù)字。我們還可以支持負(fù)號(hào):-a,其實(shí)是(0-a)的簡(jiǎn)寫(xiě)形式。那么什么是支持函數(shù)呢?這里我們只考慮單參數(shù)函數(shù)的情況,譬如說(shuō)三角函數(shù)和對(duì)數(shù)指數(shù)等等。譬如說(shuō)下面的式子就是滿足定義的帶函數(shù)的四則運(yùn)算式子:

 

sin(1+2) + cos(3*-4)

 

Vczh Library++使用語(yǔ)法的角度來(lái)對(duì)待一個(gè)字符串,因此我們可以把上面的定義轉(zhuǎn)換成語(yǔ)法。一個(gè)語(yǔ)法用來(lái)表示字符串的一個(gè)子集。我們可以通過(guò)語(yǔ)法來(lái)表達(dá)什么樣的字符串是滿足規(guī)定的,什么樣的字符串是不滿足規(guī)定的。不過(guò)一個(gè)具有現(xiàn)實(shí)意義的語(yǔ)法總是會(huì)有一些局限性的,譬如說(shuō)你很難用上下文無(wú)關(guān)的文法來(lái)表達(dá)一個(gè)字符串:a…ab…bc…c,其中三種字母的數(shù)量都相等。幸好在絕大多數(shù)情況下我們都不需要去面對(duì)這些高難度的問(wèn)題,因此可以用一些簡(jiǎn)單的規(guī)則來(lái)處理:

 

RULE = EXPRESSION

 

RULE是這個(gè)規(guī)則的名字,而EXPRESSION是這個(gè)規(guī)則的定義。語(yǔ)法可以由一條規(guī)則組成,也可以由很多條規(guī)則組成。當(dāng)所有的規(guī)則都列出來(lái)之后,那么每一個(gè)規(guī)則的名字都是一個(gè)字符串的集合。大部分情況下你需要指定一個(gè)“總?cè)肟?#8221;來(lái)代表整個(gè)語(yǔ)法。

 

舉個(gè)例子,假設(shè)我們判斷一個(gè)字符串是不是無(wú)符號(hào)整數(shù)。一個(gè)無(wú)符號(hào)整數(shù)只能由數(shù)字字符組成。于是我們可以先用一條規(guī)則來(lái)代表“數(shù)字字符”。這里我們可以使用“|”來(lái)代表“或”,那么下面的規(guī)則就表示DIGIT’0’’1’’9’

DIGIT = ‘0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’

那么,無(wú)符號(hào)整數(shù)就是“很多數(shù)字字符”:

INTEGER = DIGIT | INTEGER DIGIT

無(wú)符號(hào)整數(shù)INTEGER要么是一個(gè)數(shù)字字符,要么就是一個(gè)合法的無(wú)符號(hào)整數(shù)后面再加上一個(gè)數(shù)字字符。無(wú)符號(hào)整數(shù)加上一個(gè)數(shù)字字符仍然是一個(gè)無(wú)符號(hào)整數(shù)。

 

現(xiàn)在可以來(lái)檢驗(yàn)一下。譬如說(shuō)“1”是一個(gè)無(wú)符號(hào)整數(shù),那么從INTEGER開(kāi)始,分析“1”所走的路徑就是

INTEGER

= DIGIT                                          (INTEGER = DIGIT)

= ‘1’                                                (DIGIT = ‘1’)

字符串“123”顯然也應(yīng)該是一個(gè)無(wú)符號(hào)整數(shù)。“123”是一些數(shù)字字符組成的,因此走的路徑跟單個(gè)字符稍微有些不同。這里將會(huì)交替使用INTEGER的兩條路徑來(lái)模擬循環(huán):

INTEGER

= INTEGER DIGIT                        (INTEGER = INTEGER DIGIT)

= INTEGER DIGIT DIGIT            (INTEGER = INTEGER DIGIT)

= DIGIT DIGIT DIGIT                  (INTEGER = DIGIT)

= ‘1’ DIGIT DIGIT                        (DIGIT = ‘1’)

= ‘1’ ‘2’ DIGIT                              (DIGIT = ‘2’)

= ‘1’ ‘2’ ‘3’                                    (DIGIT = ‘3’)

在使用INTEGER分析“123”的時(shí)候,我們可以交替使用INTEGER = DIGITINTEGER = INTEGER DIGIT這兩條規(guī)則來(lái)將一個(gè)INTEGER替換成恰好三個(gè)DIGIT,然后再將DIGIT替換成’1’’2’’3’三個(gè)字符,從而確信“123”滿足INTEGER的定義,因此“123”是一個(gè)無(wú)符號(hào)整數(shù)。

 

替換的過(guò)程并不是唯一的,我們完全可以使用另一種順序來(lái)將INTEGER替換成“123”:

INTEGER

= INTEGER DIGIT                        (INTEGER = INTEGER DIGIT)

= INTEGER ‘3’                              (DIGIT = ‘3’)

= INTEGER DIGIT ‘3’                  (INTEGER = INTEGER DIGIT)

= INTEGER ‘2’ ‘3’                        (DIGIT = ‘2’)

= DIGIT ‘2’ ‘3’                              (INTEGER = DIGIT)

= ‘1’ ‘2’ ‘3’                                    (DIGIT = ‘1’)

這正是語(yǔ)法的一個(gè)特點(diǎn):替換順序與結(jié)果無(wú)關(guān)。

 

現(xiàn)在我們將這個(gè)例子再深入一點(diǎn),如何用語(yǔ)法規(guī)則來(lái)描述一個(gè)逗號(hào)分隔的無(wú)符號(hào)整數(shù)列表呢?逗號(hào)分隔的無(wú)符號(hào)整數(shù)列表可以是一個(gè)整數(shù)“123”,也可以使多個(gè)整數(shù)“1,23,456”。這也是重復(fù)的一種,只是跟INTEGER的那種重復(fù)有所區(qū)別——多了一個(gè)逗號(hào)。根據(jù)上面的描述可以知道,逗號(hào)分隔的無(wú)符號(hào)整數(shù)列表有兩種情況,第一種是單獨(dú)的一個(gè)整數(shù),第二種是一個(gè)已經(jīng)完成的列表后面跟著一個(gè)逗號(hào)和一個(gè)整數(shù)。那么事情就變得簡(jiǎn)單了。假設(shè)我們使用LIST來(lái)代表這個(gè)列表,那么根據(jù)上面的描述我們可以用類似的技巧來(lái)描述它:

LIST = INTEGER | LIST ‘,’ INTEGER

LIST來(lái)分析一個(gè)數(shù)字列表的過(guò)程與用INTEGER分析一個(gè)無(wú)符號(hào)整數(shù)是相似的。因?yàn)槠鶈?wèn)題,這里只展示使用LIST處理“1,23,456”的其中一種方法:

LIST

= LIST ‘,’ INTEGER                                                  (LIST = LIST ‘,’ INTEGER)

= LIST ‘,’ INTEGER ‘,’ INTEGER                                     (LIST = LIST ‘,’ INTEGER)

= INTEGER ‘,’ INTEGER ‘,’ INTEGER                            (LIST = INTEGER)

= DIGIT ‘,’ INTEGER ‘,’ INTEGER                         (INTEGER = DIGIT)

= ‘1’ ‘,’ INTEGER ‘,’ INTEGER                               (DIGIT = ‘1’)

= ‘1’ ‘,’ INTEGER DIGIT ‘,’ INTEGER                            (INTEGER = INTEGER DIGIT)

= ‘1’ ‘,’ DIGIT DIGIT ‘,’ INTEGER                         (INTEGER = DIGIT)

= ‘1’ ‘,’ ‘2’ DIGIT ‘,’ INTEGER                               (DIGIT = ‘2’)

= ‘1’ ‘,’ ‘2’ ‘3’ ‘,’ INTEGER                                              (DIGIT = ‘3’)

= ‘1’ ‘,’ ‘2’ ‘3’ ‘,’ INTEGER DIGIT                         (INTEGER = INTEGER DIGIT)

= ‘1’ ‘,’ ‘2’ ‘3’ ‘,’ INTEGER DIGIT DIGIT             (INTEGER = INTEGER DIGIT)

= ‘1’ ‘,’ ‘2’ ‘3’ ‘,’ DIGIT DIGIT DIGIT                            (INTEGER = DIGIT)

= ‘1’ ‘,’ ‘2’ ‘3’ ‘,’ ‘4’ DIGIT DIGIT                         (DIGIT = ‘4’)

= ‘1’ ‘,’ ‘2’ ‘3’ ‘,’ ‘4’ ‘5’ DIGIT                               (DIGIT = ‘5’)

= ‘1’ ‘,’ ‘2’ ‘3’ ‘,’ ‘4’ ‘5’ ‘6’                                              (DIGIT = ‘6’)

 

在開(kāi)發(fā)實(shí)際的語(yǔ)法分析器的時(shí)候,我們總是需要考慮空格的問(wèn)題。人們用空格讓一個(gè)具有嚴(yán)格限制的字符串變得更加易讀,譬如說(shuō)將“1,23,456”變成“1,  23,  456”會(huì)讓密密麻麻的一堆字符變得非常容易看懂。空格也不是亂加的,有些地方可以加空格,有些地方不能加空格。

 

在上面這個(gè)例子里面,如果要支持空格,那么空格除了不能插在INTEGER中間,應(yīng)該可以放在任何的地方。這個(gè)時(shí)候就帶來(lái)麻煩了,帶空格的語(yǔ)法不是太好寫(xiě)。如果我們讓LIST支持空格,那會(huì)把LIST變成下面這個(gè)樣子:

SPACES = <EMPTY> | SPACES ‘ ’

LIST = SPACES INTEGER SPACES | LIST ‘,’ SPACES INTEGER SPACES

這里<EMPTY>代表空字符串,所以SPACES就是沒(méi)有空格、一個(gè)空格或者很多空格了。因此我們必須在LIST里面所有可以加入空格的地方寫(xiě)空格,這會(huì)讓我們的語(yǔ)法膨脹得很厲害。因此我們必須使用一種方法來(lái)讓我們免除空格帶來(lái)的困擾。

詞法分析

引入詞法分析的目的是讓我們的語(yǔ)法更加簡(jiǎn)潔。我們可以將處理空格、注釋和分割字符串的工作與語(yǔ)法分析完全分開(kāi),那么代碼寫(xiě)起來(lái)就會(huì)更加容易,維護(hù)起來(lái)也會(huì)更加簡(jiǎn)單了。我們總是傾向于讓我們的程序越來(lái)越容易理解和維護(hù)。

 

詞法分析的目標(biāo)是將輸入的字符串適當(dāng)分割并拋棄處理掉沒(méi)有用的部分。“適當(dāng)分割”一般來(lái)說(shuō)沒(méi)有一個(gè)明確的規(guī)則,應(yīng)該根據(jù)具體情況而定,越方便越好。在大部分情況下我們僅把輸入的字符串簡(jiǎn)單的劃分為符號(hào)、數(shù)字、操作符、字符串、空格和注釋等等的簡(jiǎn)單部分。這些劃分一般代表“插入空格會(huì)改變意義”。比如說(shuō)“1234”變成“12 34”之后,就從一個(gè)整數(shù)變成兩個(gè)整數(shù)了。字符串的情況有點(diǎn)特別,雖然字符串中間插入一個(gè)空格還是一個(gè)字符串,但是插入空格后的字符串已經(jīng)不是插入空格前的字符串了,因?yàn)閮?nèi)容已經(jīng)發(fā)生了變化。與此同時(shí),在一個(gè)整數(shù)列表里面,往逗號(hào)后面插入一個(gè)空格不會(huì)影響這個(gè)列表所要表達(dá)的意義,因此將字符串轉(zhuǎn)換成“整數(shù)列表”的工作一般劃分在語(yǔ)法分析而不是詞法分析里。

 

處理詞法分析的方法一般是使用正則表達(dá)式。Vczh Library++提供了一個(gè)使用正則表達(dá)式來(lái)開(kāi)發(fā)詞法分析器的類庫(kù)。關(guān)于正則表達(dá)式的語(yǔ)法請(qǐng)參考Documentation\Chinese\Vczh Library++\Regex\Regex.htm#Grammar,關(guān)于這個(gè)詞法分析器類的內(nèi)容請(qǐng)參考Documentation\Chinese\Vczh Library++\Regex\Regex.htm#RegexToken

 

在使用Vczh Library++進(jìn)行詞法分析的開(kāi)發(fā)之前需要掌握正則表達(dá)式的簡(jiǎn)單用法。這里我們假設(shè)讀者對(duì)正則表達(dá)式已經(jīng)入門(mén)了。精通是沒(méi)有必要的,因?yàn)樵~法分析使用到的正則表達(dá)式的內(nèi)容十分簡(jiǎn)單。我們回到之前的“帶函數(shù)的四則運(yùn)算計(jì)算器”。經(jīng)過(guò)簡(jiǎn)單的整理,我們知道一個(gè)帶函數(shù)的四則運(yùn)算計(jì)算器由數(shù)字、函數(shù)名、操作符和符號(hào)組成。

 

加號(hào)與減號(hào)的優(yōu)先級(jí)一樣,對(duì)于語(yǔ)法分析來(lái)說(shuō)他們其實(shí)沒(méi)有區(qū)別。乘號(hào)與除號(hào)也類似。當(dāng)語(yǔ)法分析結(jié)束,語(yǔ)義分析開(kāi)始的時(shí)候,加號(hào)與減號(hào)的區(qū)別才會(huì)出現(xiàn)。因此在詞法分析里面我們可以把他們當(dāng)成同樣的東西來(lái)對(duì)待,因此有:

BLANK = \s+                                 :空格

ADD = \+|-                                    :加減號(hào)

MUL = \*|/                                   :乘除號(hào)

NUMBER = \d+(.\d+)?               :數(shù)字

ID = [a-zA-Z_]\w*                       :函數(shù)名

OPEN = \(                                      :開(kāi)括號(hào)

CLOSE = \)                                     :閉括號(hào)

 

我們把分類后的結(jié)果叫記號(hào)類型。一個(gè)字符串可以被分成很多記號(hào),每一個(gè)記號(hào)屬于一個(gè)記號(hào)類型。如果一個(gè)記號(hào)不屬于任何記號(hào)類型的話(譬如問(wèn)號(hào)“?”),那么遇到了詞法分析的錯(cuò)誤。這個(gè)時(shí)候我們需要報(bào)告錯(cuò)誤了。

 

Vczh Library++有一個(gè)簡(jiǎn)單的方法讓我們是用正則表達(dá)式表達(dá)記號(hào)類型,并使用他們來(lái)構(gòu)造詞法分析器:

 

     List<WString> patterns;

     const int BLANK        = patterns.Add(L"/s+");

     const int ADD           = patterns.Add(L"/+|-");

     const int MUL           = patterns.Add(L"/*|//");

     const int NUMBER        = patterns.Add(L"/d+(./d+)?");

     const int ID            = patterns.Add(L"[a-zA-Z_]/w*");

     const int OPEN         = patterns.Add(L"/(");

     const int CLOSE        = patterns.Add(L"/)");

     RegexLexer lexer(patterns.Wrap());

 

為了方便書(shū)寫(xiě)正則表達(dá)式,Vczh Library++同時(shí)支持兩種轉(zhuǎn)義符:“\”和“/”。因?yàn)?/span>C++使用了“\”作為字符串的轉(zhuǎn)義符,所以在這里我們可以使用“/”,這樣寫(xiě)起來(lái)會(huì)比較清晰。

 

構(gòu)造詞法分析器的方法很簡(jiǎn)單,我們將所有正則表達(dá)式放到一個(gè)字符串列表List<WString>,然后交給詞法分析器RegexLexer,我們就得到了一個(gè)詞法分析器了。在分析字符串的時(shí)候,每一個(gè)記號(hào)的類型其實(shí)就是該記號(hào)的正則表達(dá)式描述在字符串列表中的位置。如果發(fā)生錯(cuò)誤的話,記號(hào)類型會(huì)變成-1。因?yàn)榱斜淼?/span>Add函數(shù)返回添加的元素在列表中的位置,因此就可以使用上面的寫(xiě)法來(lái)簡(jiǎn)單地構(gòu)造一個(gè)詞法分析器了。

 

我們可以用一種簡(jiǎn)單的方法來(lái)使用這個(gè)詞法分析器。RegexLexer輸出的記號(hào)存放在RegexToken類型里面,我們可以使用任何容器來(lái)存放記號(hào),在這里我們?nèi)匀皇褂?/span>RegexToken

 

RegexToken的定義如下:

     class RegexToken

     {

     public:

         int                start;

         int                length;

         int                token;

         const wchar_t*     reading;

         int                lineIndex;

         int                lineStart;

         int                codeIndex;

};

RegexToken記錄了一個(gè)記號(hào)在輸入的字符串中的位置、所在的行和在該行內(nèi)的位置、記號(hào)類型和指向該位置的指針。這些信息可以用來(lái)做很多事情,譬如在產(chǎn)生錯(cuò)誤信息的時(shí)候可以精確指定錯(cuò)誤發(fā)生的位置。

 

在這里我們需要過(guò)濾空格,也就是過(guò)濾掉BLANK記號(hào),因此我們需要寫(xiě)一個(gè)過(guò)濾函數(shù):

bool IsNotBlank(RegexToken token)

{

          return token.token!=0;

}

我們知道BLANK就是0,因此這里直接以0代替。有了這個(gè)函數(shù)之后,我們就可以將輸入切割成記好了:

List<RegexToken> tokens;

CopyFrom(tokens.Wrap(), lexer.Parse(L"(1 + 2) * abs(-3 - 4)")>>Where(IsNotBlank));

執(zhí)行了這段代碼之后,我們就將字符串切割成記號(hào)了。這里只用了15行就完成了詞法分析器的定義并使用詞法分析器來(lái)分析一個(gè)字符串的任務(wù)了。

 

注意:如果將一個(gè)字符指針傳入lexer.Parse的話,在獲得記號(hào)列表之后將這個(gè)字符指針刪除,那么所有記號(hào)中的reading將全部變成野指針。lexer.Parse的參數(shù)是WString類型,所以這個(gè)例子在執(zhí)行之后,臨時(shí)的字符串對(duì)象會(huì)被刪除,因此記號(hào)列表中的所有reading成員將全部變成野指針。因此在實(shí)踐過(guò)程中最好先使用一個(gè)WString變量去保存輸入的字符串,然后將這個(gè)變量傳入lexer.Parse,之后所有reading成員將指向這個(gè)變量?jī)?nèi)部的一個(gè)有效指針。

 

這個(gè)時(shí)候我們就可以使用tokens里面的信息來(lái)做處理了。不過(guò)Vczh Library++還提供了語(yǔ)法分析器的類庫(kù),讓我們可以不用親自遍歷這些記號(hào)。

帶函數(shù)四則運(yùn)算式子的語(yǔ)法

到了這里,我們可以把數(shù)字、函數(shù)名和符號(hào)當(dāng)成已經(jīng)存在的東西來(lái)看待了,而且再也不需要考慮空格的問(wèn)題了。于是我們可以仔細(xì)組織帶函數(shù)四則運(yùn)算式子的語(yǔ)法:

FACTOR = NUMBER

FACTOR = ‘-‘ FACTOR

FACTOR = ‘(‘ EXP ‘)’

FACTOR = ID ‘(‘ EXP ‘)’

TERM = FACTOR | TERM MUL FACTOR

EXP = TERM | EXP ADD TERM

 

語(yǔ)法的設(shè)計(jì)直接反映了我們的思考過(guò)程。這是一個(gè)帶有遞歸的語(yǔ)法。當(dāng)我們考慮下面的式子的時(shí)候

1*(2+2)*3+4*5*sin(6)+7*8*9

我們首先使用加減法將式子分割為三個(gè)部分

1*(2+2)*3  +  4*5*sin(6)  +  7*8*9

然后使用乘除法將式子分割為九個(gè)部分,然后我們發(fā)現(xiàn)(2+2)sin(6)他們是一個(gè)整體。不過(guò)整體仍然是由部分構(gòu)成的,因此內(nèi)部還包含表達(dá)式。所以不難看出,這里的FACTOR代表“整體”,TERM代表乘除法構(gòu)成的“第二層表達(dá)式”,EXP代表加減法構(gòu)成的“第一層表達(dá)式”。這個(gè)語(yǔ)法同時(shí)還代表“先乘除后加減”的計(jì)算原則。

 

但是這里還有一個(gè)問(wèn)題,我們觀察一下EXP = TERM | EXP ADD TERM這條規(guī)則。我們不難發(fā)現(xiàn)他們其實(shí)是獨(dú)立的兩條規(guī)則的組合:

EXP = TERM

EXP = EXP ADD TERM

第二條EXP的規(guī)則仍然從EXP開(kāi)始,這種遞歸稱為左遞歸。左遞歸直接處理起來(lái)比較困難,因?yàn)槟惴治龅?/span>EXP的時(shí)候很容易陷入一個(gè)死循環(huán),因此我們需要拆開(kāi)它們。

 

我們引入擴(kuò)展規(guī)則的機(jī)制來(lái)解決這個(gè)問(wèn)題。如果我們想表達(dá)一個(gè)循環(huán)的話,我們不得不專門(mén)為它建立一條規(guī)則并命名:

LIST = ITEM | LIST ITEM

如果我們可以簡(jiǎn)化成LIST = ITEM+的話,就不需要專門(mén)為它起一個(gè)名字LIST了,而可以直接在各個(gè)地方使用ITEM+。跟正則表達(dá)式一樣,我們使用+*來(lái)代表循環(huán)。因此EXP就可以被改寫(xiě)成EXP = TERM ( ADD TERM)*了。注意(’(‘的區(qū)別,’(‘代表一個(gè)字符,而(跟平常一樣用來(lái)規(guī)定優(yōu)先級(jí),譬如這里代表重復(fù)“*”的范圍。于是我們可以重新組織語(yǔ)法:

FACTOR = NUMBER

FACTOR = ‘-‘ FACTOR

FACTOR = ‘(‘ EXP ‘)’

FACTOR = ID ‘(‘ EXP ‘)’

TERM = FACTOR (MUL FACTOR)*

EXP = TERM (ADD TERM)*

語(yǔ)法類型與C++表達(dá)

Vczh Library++允許我們直接把語(yǔ)法在C++的框架下表達(dá)出來(lái),因此我們不得不對(duì)語(yǔ)法的表達(dá)形式做一點(diǎn)修改使之可以滿足C++的要求。所以這里我們需要做兩件事情,第一件事情是規(guī)則的類型,第二件事情是如何用C++語(yǔ)句來(lái)表達(dá)規(guī)則。

 

規(guī)則的類型含義比較復(fù)雜,一個(gè)規(guī)則的類型不僅取決于它自身,還取決于它的產(chǎn)出。如果我們用語(yǔ)法規(guī)則來(lái)將記號(hào)直接轉(zhuǎn)換成計(jì)算結(jié)果,那么一般來(lái)說(shuō)規(guī)則的類型就是計(jì)算結(jié)果的類型,譬如說(shuō)數(shù)字。如果我們用語(yǔ)法規(guī)則來(lái)講記號(hào)轉(zhuǎn)換成四則運(yùn)算式子的語(yǔ)法樹(shù),那么規(guī)則的類型就是語(yǔ)法樹(shù)節(jié)點(diǎn)的指針。

 

如果我們把規(guī)則看成一個(gè)函數(shù)的話,那應(yīng)該會(huì)更加容易理解。一個(gè)語(yǔ)法規(guī)則將輸入的記號(hào)列表轉(zhuǎn)換成我們需要的結(jié)果,所以規(guī)則的類型至少包含兩個(gè)部分,一個(gè)是輸入記號(hào)的類型,一個(gè)是輸出類型。Vczh Library++專門(mén)為規(guī)則定義了一個(gè)模板類,而且這里FACTORTERMEXP將會(huì)作為C++的變量直接聲明出來(lái)。在這里我們希望語(yǔ)法規(guī)則能直接將輸入轉(zhuǎn)換成計(jì)算結(jié)果,結(jié)果的類型是double,輸入的類型是RegexToken,因此我們可以這么聲明三個(gè)規(guī)則的名字:

Rule<TokenInput<RegexToken>, double> factor, term, exp;

TokenInput是輸入的其中一種表達(dá)形式,它可以將一個(gè)指針和長(zhǎng)度轉(zhuǎn)換成符合Rule輸入的類型。Vczh Library++還同時(shí)提供了StringInput<T>EnumerableInput<T>,但是我們已經(jīng)將記號(hào)保存在List<RegexToken>里面了,因此使用TokenInput<T>是最合適的。

 

StringInput<T>也好,EnumerableInput<T>也好,TokenInput<T>也好,其實(shí)都是一個(gè)迭代器。Vczh Library++的語(yǔ)法分析器類庫(kù)為迭代器規(guī)定了一個(gè)接口,這三種迭代器都是在那個(gè)接口的框架下實(shí)現(xiàn)的。我們可以簡(jiǎn)單的把一個(gè)把TokenInput<RegexToken>套在List<RegexToken>上:

TokenInput<RegexToken> input(&tokens[0], tokens.Count());

TokenInput在內(nèi)部只保存了一個(gè)指針、長(zhǎng)度和當(dāng)前位置,所以是一個(gè)相當(dāng)輕量級(jí)的類,可以到處復(fù)制并且不會(huì)有多少性能上的損失。不過(guò)TokenInput<T>的生命周期不應(yīng)該比List<T>長(zhǎng),不然TokenInput<T>指向的對(duì)象會(huì)因?yàn)橐呀?jīng)被釋放掉而發(fā)生問(wèn)題。同樣的道理,在TokenInput<T>已經(jīng)被套在List<T>上的時(shí)候,List<T>最好不要被修改。

 

現(xiàn)在輸入的類型已經(jīng)清楚了,可以開(kāi)始研究輸出的類型了。上面的factor的聲明是Rule<TokenInput<RegexToken>, double>,因此factor可以看成是一個(gè)輸入迭代器TokenInput<RegexToken>,修改迭代器位置并輸出double作為結(jié)果的函數(shù)。不過(guò)其實(shí)返回的實(shí)際類型是ParsingResult<double>,因?yàn)橐粋€(gè)規(guī)則在分析一個(gè)迭代器輸入的時(shí)候可能會(huì)產(chǎn)生錯(cuò)誤,這個(gè)時(shí)候不能修改輸入迭代器的位置,而且還要返回錯(cuò)誤的標(biāo)志。因此這里使用ParsingResult<double>,它能告訴你成功還是失敗,而且成功的話會(huì)帶有一個(gè)真正的double類型的返回值,并且修改迭代器的位置,讓它指向跳過(guò)一個(gè)factor后的位置以便繼續(xù)分析。

 

規(guī)則有許多種組合方法。假設(shè)有規(guī)則:

Rule<I, A> a, a2;

Rule<I, B> b;

那么可以組合出以下各種新的規(guī)則:

a+b:類型Rule<I, ParsingPair<A, B>>,代表ab應(yīng)該按順序出現(xiàn)。

*a:類型Rule<I, ParsingList<A>>,代表a應(yīng)該連續(xù)出現(xiàn)0或多次。

+a:類型Rule<I, ParsingList<A>>,代表a應(yīng)該連續(xù)出現(xiàn)1或多次。

a|a2:類型Rule<I, A>,代表要么是a,要么是a2。這里aa2類型應(yīng)該一致。

a>>b:類型Rule<I, B>,代表ab應(yīng)該按順序出現(xiàn),并且拋棄a只保留b的結(jié)果。

a<<b:類型Rule<I, A>,代表ab應(yīng)該按順序出現(xiàn),并且拋棄b只保留a的結(jié)果。

opt(a):類型Rule<I, A>,代表a應(yīng)該出現(xiàn)01次。

 

還有另外兩種組合方法,分別用于轉(zhuǎn)換分析結(jié)果和進(jìn)行錯(cuò)誤恢復(fù)。在這里先介紹轉(zhuǎn)換分析結(jié)果的組合方法。下面舉EXP的例子:

EXP = TERM (ADD TERM)*

寫(xiě)成C++應(yīng)該是:

EXP = TERM + *(tk(ADD) + TERM);

這里ADD的類型是const int,因此我們需要一個(gè)函數(shù)把它轉(zhuǎn)換成一個(gè)規(guī)則。這里使用tk函數(shù)。tk函數(shù)將一個(gè)int轉(zhuǎn)換成Rule<TokenInput<RegexToken>, RegexToken>,用于匹配一個(gè)輸入是ADD類型的記號(hào)。于是我們可以慢慢解開(kāi)這個(gè)規(guī)則的最終類型。這里我們不關(guān)心輸入類型,只關(guān)心輸出類型,因?yàn)樗械囊?guī)則的類型都是Rule<TokenInput<RegexToken>, T>。根據(jù)上文,我們知道TERMEXP的類型一樣,都是返回double

tk(ADD)                                                            T == RegexToken

tk(ADD) + TERM                                            T == ParsingPair<RegexToken, double>

*(tk(ADD) + TERM)                                       T == ParsingList<ParsingPair<RegexToken, double>>

TERM + *(tk(ADD) + TERM)                       T == ParsingPair<double, ParsingList<ParsingPair<RegexToken, double>>>

 

這里問(wèn)題就來(lái)了,EXP的類型跟TERM + *(tk(ADD) + TERM)類型不一樣,那必然需要一個(gè)函數(shù)來(lái)幫我們做轉(zhuǎn)換。假如我們已經(jīng)有了一個(gè)函數(shù):

double Operator(const ParsingPair<double, ParsingList<ParsingPair<RegexToken, double>>>& input)

這個(gè)函數(shù)勇于將輸入的那一大串東西,經(jīng)過(guò)計(jì)算最終轉(zhuǎn)換成一個(gè)double類型的結(jié)果,那么我們就可以使用這個(gè)Operator函數(shù)最終將EXPTERM + *(tk(ADD) + TERM)連起來(lái):

EXP = (TERM + *(tk(ADD) + TERM))[Operator];

 

ParsingPair<double, ParsingList<ParsingPair<RegexToken, double>>>的內(nèi)容實(shí)際上是一個(gè)操作數(shù),加上一個(gè)操作符連著操作數(shù)的列表。于是當(dāng)我們真的需要把它轉(zhuǎn)成一個(gè)double的時(shí)候,就要去遍歷所有“操作符連著操作數(shù)”的列表,最后將計(jì)算結(jié)果全部累加到第一個(gè)操作數(shù)身上。記得我們之前表達(dá)EXP的方法跟現(xiàn)在不一樣嗎?以前是

EXP = TERM | EXP ADD TERM

因?yàn)?/span>Vczh Library++無(wú)法處理左遞歸,才需要我們手動(dòng)拆解成

EXP = TERM (ADD TERM)*

于是為了讓我們處理起來(lái)更簡(jiǎn)單,Vczh Library++提供了一個(gè)lrec函數(shù),讓我們可以享受左遞歸帶來(lái)的方便。

 

lrec把類型ParsingPair<T, ParsingList<U>>通過(guò)一個(gè)函數(shù)T(const T&, const U&)轉(zhuǎn)換成T。這就意味著一個(gè)輸入T U U U … ,加上一個(gè)把一個(gè)TU加起來(lái)變成T的函數(shù),最終把整個(gè)序列處理成T

T U U U

=> T U U

=> T U

=> T

如果把他們套到我們的EXP上面,就可以做下面的計(jì)算

TERM (ADD TERM) (ADD TERM) (ADD TERM)        1+2+3+4

=> TERM (ADD TERM) (ADD TERM)                           3+3+4

=> TERM (ADD TERM)                                                    6+4

=> TERM                                                                            10

這個(gè)轉(zhuǎn)換函數(shù)跟處理EXP ADD TERM是一樣的!

 

因此,只要有了lrec函數(shù),我們可以把

EXP = TERM | (EXP + tk(ADD) + TERM) [F1]

這種Vczh Library++不支持的左遞歸語(yǔ)法表示處理成

EXP = lrec(TERM + *(tk(ADD) + TERM), F2)

其中F1的類型是double (const ParsingPair<ParsingPair<double, RegexToken>, double>&)

F2的類型是double (const double&, const ParsingPair<RegexToken, double>&)

我們不會(huì)因?yàn)樾枰鸾庾筮f歸而帶來(lái)任何不便!

實(shí)現(xiàn)

現(xiàn)在開(kāi)始進(jìn)入激動(dòng)人心的時(shí)刻了,我們可以借助Vczh Library++來(lái)實(shí)現(xiàn)一個(gè)帶函數(shù)四則運(yùn)算式子的計(jì)算器了。現(xiàn)在回顧一下我們的語(yǔ)法:

FACTOR = NUMBER

FACTOR = ‘-‘ FACTOR

FACTOR = ‘(‘ EXP ‘)’

FACTOR = ID ‘(‘ EXP ‘)’

TERM = FACTOR (MUL FACTOR)*

EXP = TERM (ADD TERM)*

把它轉(zhuǎn)換成C++就應(yīng)該是:

Rule<TokenInput<RegexToken>, double> factor, term, exp;

factor   = tk(NUMBER) [Convert]

              | (tk(L"-") >> factor)[Negative]

              | (tk(L"(") >> exp << tk(L")"))

              | (tk(ID) + (tk(L"(") >> exp << tk(L")")))[Call]

              ;

     term      = lrec(factor + *(tk(MUL) + factor), Operator);

exp      = lrec(term + *(tk(ADD) + term), Operator);

 

讓我們來(lái)逐個(gè)閱讀規(guī)則,并分析出結(jié)果轉(zhuǎn)換函數(shù)ConvertNegativeCallOperator的類型。

 

第一個(gè)是tk(NUMBER)[Convert]。這個(gè)規(guī)則將一個(gè)數(shù)字記號(hào)轉(zhuǎn)換為一個(gè)真正的數(shù)字。因?yàn)?/span>tk(NUMBER)的類型是RegexToken,因此Convert的類型是double (const RegexToken&)

 

第二個(gè)是(tk(L”-“) >> factor)[Negative]tk(L”-“)的類型是RegexTokenfactor的類型是double,所以RegexToken>>double其實(shí)就是double。因此Negative的類型是double (const double&)

 

第三個(gè)是tk(L”(“) >> exp << tk(L”)”)。實(shí)際上分析了兩個(gè)括號(hào)和exp之后,括號(hào)被丟掉了,剩下exp的類型是double。因此這一行規(guī)則不需要任何轉(zhuǎn)換函數(shù)。

 

第四個(gè)是(tk(ID) + (tk(L”(“) >> exp << tk(L”)”)))[Call],我們很容易知道Call的類型是double(const ParsingPair<RegexToken, double>&)

 

最后一個(gè)是Operator,這個(gè)之前已經(jīng)討論過(guò)了,類型是double (const double&, const ParsingPair<RegexToken, double>&)

 

知道了這個(gè)之后,我們就可以實(shí)現(xiàn)這些函數(shù)了:

double Convert(const RegexToken& input)

{

          return wtof(WString(input.reading, input.length));

}

 

double Negative(const double& input)

{

          return -input;

}

 

double Operator(const double& left, const ParsingPair<RegexToken, double>& right)

{

          switch(*right.First().reading)

          {

          case L'+':

              return left+right.Second();

          case L'-':

              return left-right.Second();

          case L'*':

              return left*right.Second();

          case L'/':

              return left/right.Second();

          default:

              return 0;

          }

}

 

double Call(const ParsingPair<RegexToken, double>& input)

{

          WString name(input.First().reading, input.First().length);

          double parameter=input.Second();

    

          if(name==L"sin")

          {

              return sin(parameter);

          }

          else if(name==L"cos")

          {

              return cos(parameter);

          }

          else if(name==L"tan")

          {

              return tan(parameter);

          }

          else if(name==L"cot")

          {

              return 1/tan(parameter);

          }

          else if(name==L"sec")

          {

              return 1/cos(parameter);

          }

          else if(name==L"csc")

          {

              return 1/sin(parameter);

          }

          else if(name==L"exp")

          {

              return exp(parameter);

          }

          else if(name==L"ln")

          {

              return log(parameter);

          }

          else if(name==L"abs")

          {

              return abs(parameter);

          }

          else if(name==L"sqrt")

          {

              return sqrt(parameter);

          }

          else if(name==L"sqr")

          {

              return parameter*parameter;

          }

          else

          {

              throw Exception(L"Function "+name+L" not exists.");

          }

}

 

然后我們就可以用這些函數(shù)來(lái)構(gòu)造一個(gè)語(yǔ)法分析器了:

     List<WString> patterns;

     const int BLANK        = patterns.Add(L"/s+");

     const int ADD           = patterns.Add(L"/+|-");

     const int MUL           = patterns.Add(L"/*|//");

     const int NUMBER        = patterns.Add(L"/d+(./d+)?");

     const int ID            = patterns.Add(L"[a-zA-Z_]/w*");

     const int OPEN         = patterns.Add(L"/(");

     const int CLOSE        = patterns.Add(L"/)");

     RegexLexer lexer(patterns.Wrap());

 

     Rule<TokenInput<RegexToken>, double> factor, term, exp;

 

     factor   = tk(NUMBER)[Convert]

              | (tk(L"-") >> factor)[Negative]

              | (tk(L"(") >> exp << tk(L")"))

              | (tk(ID) + (tk(L"(") >> exp << tk(L")")))[Call]

              ;

     term      = lrec(factor + *(tk(MUL) + factor), Operator);

exp      = lrec(term + *(tk(ADD) + term), Operator);

 

WString line=Console::Read();

List<RegexToken> tokens;

CopyFrom(tokens.Wrap(), lexer.Parse(line)>>Where(IsNotBlank));

     TokenInput<RegexToken> input(&tokens[0], tokens.Count());

     double result=exp.ParseFull(input, false);

Console::WriteLine(L"Result is "+ftow(result));

 

是不是很容易寫(xiě)出來(lái)呢?不僅exp可以用來(lái)做分析,其實(shí)任何的Rule<I, T>都有一個(gè)ParseFull函數(shù)用來(lái)分析輸入的記號(hào)列表。

錯(cuò)誤恢復(fù)和定制錯(cuò)誤信息

Vczh Library++對(duì)語(yǔ)法分析提供了強(qiáng)大的錯(cuò)誤處理的支持。我們可以自由定制在語(yǔ)法規(guī)則的任意一點(diǎn)發(fā)生錯(cuò)誤的時(shí)候應(yīng)該采取的處理方法。我們可以

l  記錄一個(gè)錯(cuò)誤并控制錯(cuò)誤信息的文字內(nèi)容

l  決定恢復(fù)或者不恢復(fù)(構(gòu)造一個(gè)假的分析結(jié)果)

l  為了恢復(fù)錯(cuò)誤,決定當(dāng)前的迭代器應(yīng)該跳過(guò)多少個(gè)記號(hào)

 

還是以那個(gè)例子為基礎(chǔ),對(duì)于tk(NUMBER)[Convert],如果我們想在輸入的迭代器所指向的位置不是一個(gè)數(shù)字的時(shí)候,想讓分析立刻失敗(分析器會(huì)自動(dòng)嘗試接下來(lái)的三個(gè)同一等級(jí)的規(guī)則,如果都失敗,那么會(huì)采用這里的分析結(jié)果),那么可以將系統(tǒng)為這個(gè)錯(cuò)誤自動(dòng)生成的錯(cuò)誤信息清除并使用我們自己的信息,然后返回一個(gè)值告訴系統(tǒng)說(shuō)我不僅要自己定制錯(cuò)誤信息,而且還不準(zhǔn)備恢復(fù):

ParsingResult<RegexToken> NeedExpression(TokenInput<RegexToken>& input, Types<TokenInput<RegexToken>>::GlobalInfo& globalInfo)

{

          globalInfo.errors.Clear();

          globalInfo.errors.Add(new CombinatorError<TokenInput<RegexToken>>(L"Here needs an expression.", input));

          return ParsingResult<RegexToken>();

}

于是我們可以在這個(gè)地方使用這個(gè)錯(cuò)誤處理函數(shù):

tk(NUMBER)(NeedExpression)[Convert]

Vczh Library++使用中括號(hào)插入結(jié)果轉(zhuǎn)換函數(shù),用小括號(hào)插入錯(cuò)誤處理函數(shù)。因此我們可以挑選所有需要定制錯(cuò)誤的地方,寫(xiě)出這些函數(shù)然后應(yīng)用在規(guī)則上:

ParsingResult<RegexToken> NeedOpenBrace(TokenInput<RegexToken>& input, Types<TokenInput<RegexToken>>::GlobalInfo& globalInfo)

{

          globalInfo.errors.Clear();

          globalInfo.errors.Add(new CombinatorError<TokenInput<RegexToken>>(L"Here needs a \"(\".", input));

          return ParsingResult<RegexToken>();

}

 

ParsingResult<RegexToken> NeedCloseBrace(TokenInput<RegexToken>& input, Types<TokenInput<RegexToken>>::GlobalInfo& globalInfo)

{

          globalInfo.errors.Clear();

          globalInfo.errors.Add(new CombinatorError<TokenInput<RegexToken>>(L"Here needs an \")\".", input));

          return ParsingResult<RegexToken>();

}

 

ParsingResult<RegexToken> NeedOperator(TokenInput<RegexToken>& input, Types<TokenInput<RegexToken>>::GlobalInfo& globalInfo)

{

          globalInfo.errors.Clear();

          globalInfo.errors.Add(new CombinatorError<TokenInput<RegexToken>>(L"Here needs an operator.", input));

          return ParsingResult<RegexToken>();

}

 

     factor   = tk(NUMBER)(NeedExpression)[Convert]

              | (tk(L"-") >> factor)[Negative]

              | (tk(L"(") >> exp << tk(L")")(NeedCloseBrace))

              | (tk(ID) + (tk(L"(")(NeedOpenBrace) >> exp << tk(L")")(NeedCloseBrace)))[Call]

              ;

     term      = lrec(factor + *(tk(MUL)(NeedOperator) + factor), Operator);

exp      = lrec(term + *(tk(ADD)(NeedOperator) + term), Operator);

 

并不是所有的地方都需要我們親自處理錯(cuò)誤,我們只需要在需要自己定制錯(cuò)誤消息的地方寫(xiě)上錯(cuò)誤處理函數(shù)就好了。我們有一些簡(jiǎn)單的原則來(lái)尋找需要處理錯(cuò)誤的地方。

 

首先,一個(gè)規(guī)則的非第一分支的第一個(gè)記號(hào)不需要處理錯(cuò)誤。這個(gè)很好處理,我們看factor,一共有四個(gè)分支。首先tk(NUMBER)是第一分支的第一個(gè)記號(hào),而tk(L”-“)tk(L”(“)tk(ID)是非第一分支的的第一個(gè)記號(hào)。因?yàn)橹灰谝粋€(gè)分支處理了錯(cuò)誤,那么非第一分支全部在第一個(gè)記號(hào)就失敗的話,那么結(jié)果顯然是采取第一個(gè)分支的錯(cuò)誤結(jié)果。

 

第二,大部分錯(cuò)誤都集中在記號(hào)規(guī)則上。記號(hào)規(guī)則說(shuō)的是tk函數(shù)產(chǎn)生的規(guī)則。因?yàn)榻^大多數(shù)錯(cuò)誤信息都是在描述“這里需要XXX但是卻沒(méi)出現(xiàn)”,因此只需要在第一個(gè)原則所說(shuō)的不需要錯(cuò)誤信息的地方以外的所有記號(hào)規(guī)則出現(xiàn)的地方都寫(xiě)上自己的錯(cuò)誤處理就可以了。

 

第三,因?yàn)榈谝缓偷诙€(gè)原則,因此所有非記號(hào)規(guī)則能產(chǎn)生的所有錯(cuò)誤都被我們定制過(guò)了,因此非記號(hào)規(guī)則不需要任何錯(cuò)誤處理,除非我們想定制能提供更多有用信息的錯(cuò)誤信息,或者執(zhí)行我們自己的錯(cuò)誤恢復(fù)以便盡可能在錯(cuò)誤產(chǎn)生的時(shí)候繼續(xù)分析并產(chǎn)生多條有用的錯(cuò)誤信息

 

因此根據(jù)這三條原則,再加上我們這個(gè)例子只需要第一個(gè)錯(cuò)誤信息,因此選中了那6個(gè)標(biāo)記了紅色的地方進(jìn)行錯(cuò)誤處理并輸出我們自己的錯(cuò)誤信息。

捕捉錯(cuò)誤

最后的問(wèn)題就是如何捕捉錯(cuò)誤了。每一個(gè)Rule<I, T>都提供了一個(gè)Parse函數(shù)和ParseFull函數(shù)。Parse函數(shù)用于在輸入的迭代器中尋找一個(gè)滿足語(yǔ)法要求的最長(zhǎng)前綴或者在遇到錯(cuò)誤的時(shí)候給出有意義的錯(cuò)誤列表。ParseFull則假定迭代器中的完整內(nèi)容滿足語(yǔ)法要求,然后進(jìn)行分析或者在遇到錯(cuò)誤的時(shí)候給出有意義的錯(cuò)誤列表。

 

Vczh Library++內(nèi)部有一套用于將所有用戶自定義的錯(cuò)誤恢復(fù)機(jī)制所產(chǎn)生的錯(cuò)有可恢復(fù)錯(cuò)誤挑選并組合起來(lái)的算法。因此在捕捉到錯(cuò)誤的時(shí)候,第一個(gè)錯(cuò)誤總是處于一個(gè)盡可能元的位置,而且基本上都是有意義的。ParseParseFull函數(shù)都直接返回我們需要的分析結(jié)果,或者在遇到錯(cuò)誤的時(shí)候拋出一個(gè)CombinatorException<I>類型的異常。

 

ParseParseFull的參數(shù)和結(jié)果如下:

     template<typename I, typename O>

     class Rule

     {

         O Parse(const I& input, bool allowError, I* remain=0)const;

         O ParseFull(const I& input, bool allowError)const;

};

input參數(shù)是輸入的迭代器。一般來(lái)說(shuō)輸入的迭代器的當(dāng)前位置是第一個(gè)記號(hào)的位置,當(dāng)然你也可以自己讀了幾個(gè)記號(hào)之后再傳給Parse allowErrortrue的時(shí)候,如果分析出了錯(cuò)誤但是所有錯(cuò)誤都被用戶自定義的錯(cuò)誤恢復(fù)函數(shù)恢復(fù)了,也會(huì)返回分析結(jié)果而不會(huì)拋出異常。allowErrorfalse的時(shí)候,只要有錯(cuò)誤出現(xiàn)就會(huì)拋出異常。remain參數(shù)僅在Parse函數(shù)中有用,在分析結(jié)束之后,如果傳入的指針不是空,那么對(duì)象會(huì)被修改為分析結(jié)束后迭代器的狀態(tài)。

 

如果分析出現(xiàn)錯(cuò)誤并且需要被處理的話,那么ParseParseFUll都會(huì)拋出一個(gè)CombinatorException<I>的異常。CombinatorException<I>的定義如下:

     template<typename I>

     class CombinatorException : public Exception

     {

         const I& GetInput()const;

         const typename Types<I>::GlobalInfo& GetGlobalInfo()const;

};

GetInput返回迭代器的當(dāng)前狀態(tài)。在所有錯(cuò)誤都被恢復(fù)的時(shí)候,迭代器的當(dāng)前狀態(tài)是分析結(jié)束的時(shí)候迭代器的位置。一旦出現(xiàn)了沒(méi)有被恢復(fù)的錯(cuò)誤,那么迭代器的當(dāng)前狀態(tài)是Parse或者ParseFull輸入的迭代器狀態(tài)。GetGlobalInfo返回的對(duì)象有errorListcandidateErrorList兩個(gè)列表,分別是錯(cuò)誤和備選錯(cuò)誤。他們的元素類型都是Ptr<CombinatorError<I>>

 

CombinatorError<I>的定義如下:

     template<typename I>

     class CombinatorError : public Exception

     {

     public:

         typedef typename Types<I>::Input          InputType;

 

         const InputType& GetPosition();

};

Exception的定義如下:

     class Exception : public Object

     {

     public:

         const WString& Message()const;

};

我們可以通過(guò)Message()函數(shù)獲得錯(cuò)誤信息的文字內(nèi)容,然后通過(guò)GetPosition()函數(shù)獲得錯(cuò)誤發(fā)生的時(shí)候迭代器的狀態(tài)。于是我們不僅可以知道出現(xiàn)了多少錯(cuò)誤,還能知道這些錯(cuò)誤時(shí)分別在什么地方出現(xiàn)的。

 

于是讓我們來(lái)看一看帶函數(shù)的四則運(yùn)算計(jì)算器應(yīng)該如何處理用戶輸入的表達(dá)式在分析過(guò)程中產(chǎn)生的錯(cuò)誤:

     Console::Write(L"\r\nexpression>");

     WString line=Console::Read();

     if(line==L"")

     {

         break;

     }

 

     try

     {

         List<RegexToken> tokens;

         CopyFrom(tokens.Wrap(), lexer.Parse(line)>>Where(IsNotBlank));

         for(int i=0;i<tokens.Count();i++)

         {

              if(tokens[i].token==-1)

              {

                   throw Exception(L"Syntax error. Unknown token: \""+WString(tokens[i].reading, tokens[i].length)+L"\".");

              }

         }

         if(tokens.Count()==0)

         {

              throw Exception(L"Syntax error. Expression cannot be empty.");

         }

         try

         {

              TokenInput<RegexToken> input(&tokens[0], tokens.Count());

              double result=exp.ParseFull(input, false);

              Console::WriteLine(L"Result is "+ftow(result));

         }

         catch(const CombinatorException<TokenInput<RegexToken>>& e)

         {

              Ptr<CombinatorError<TokenInput<RegexToken>>> error=e.GetGlobalInfo().errors.Get(0);

              const TokenInput<RegexToken>& position=error->GetPosition();

              if(position.Available())

              {

                   throw Exception(L"Syntax error. "+error->Message()+L" First occurs at \""+WString(position.Current().reading)+L"\".");

              }

              else

              {

                   throw Exception(L"Syntax error. Expression is not complete.");

              }

         }

     }

     catch(const Exception& e)

     {

         Console::SetColor(true, false, false, true);

         Console::WriteLine(e.Message());

         Console::SetColor(true, true, true, false);

}

結(jié)束

使用Vczh Library++開(kāi)發(fā)語(yǔ)法分析器的指南就到此結(jié)束了。如果在閱讀過(guò)程中有什么疑問(wèn)的話可以使用如下方法來(lái)找到我:

電子郵件:vczh@163.com

博客:http://www.shnenglu.com/vczh

Vczh Library++項(xiàng)目主頁(yè):http://vlpp.codeplex.com

 

 

posted on 2010-04-27 20:05 陳梓瀚(vczh) 閱讀(11345) 評(píng)論(12)  編輯 收藏 引用 所屬分類: VL++3.0開(kāi)發(fā)紀(jì)事

評(píng)論:
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-04-27 20:31 | Goteet
reading.... mark  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-04-27 20:38 | radar
頂你,微軟大師兄!  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-04-27 20:48 | 空明流轉(zhuǎn)
頂你個(gè)肺,微軟大師兄~  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-04-28 17:16 | clarketang
呵呵 不錯(cuò) 不知道你這個(gè)跟ANTLR相比 有啥不同?  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-04-28 17:40 | 空明流轉(zhuǎn)
@clarketang
你去用用ANTLR就曉得咧。  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-05-04 03:35 | 2A
過(guò)路者  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-05-04 11:01 | 陳梓瀚(vczh)
@2A
打下來(lái)不給飄過(guò)  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南[未登錄](méi) 2010-05-04 20:40 | jans2002
有機(jī)會(huì)試試!  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2010-06-02 05:26 | johnrambo
向編譯帝致敬  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2012-09-08 21:42 | mail863@163.com
yacc-bison,levmon語(yǔ)法解析工具的關(guān)鍵是堆棧操作和狀態(tài)機(jī),你這個(gè)只是函數(shù)操作都這么復(fù)雜,用于復(fù)雜的語(yǔ)法樹(shù)怎么搞。
舉個(gè)例子就是SQL92語(yǔ)法好解析嗎?  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2012-10-29 17:40 | 融資
不錯(cuò)不錯(cuò),繼續(xù)加油撒  回復(fù)  更多評(píng)論
  
# re: Vczh Library++ 語(yǔ)法分析器開(kāi)發(fā)指南 2012-10-29 17:47 | 剪板機(jī)刀片
路過(guò)者學(xué)習(xí)之  回復(fù)  更多評(píng)論
  
青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            精品动漫一区| 国产无一区二区| 99视频精品全国免费| 亚洲精品国产精品国自产在线| 另类激情亚洲| 99精品视频免费观看| 亚洲色图在线视频| 国产农村妇女精品一区二区| 久久亚洲午夜电影| 牛牛国产精品| 亚洲免费婷婷| 久久久亚洲国产美女国产盗摄| 亚洲三级视频在线观看| 中日韩午夜理伦电影免费| 国产欧美一区二区精品仙草咪| 蜜臀av国产精品久久久久| 欧美日韩国产精品一区二区亚洲 | 久久香蕉国产线看观看av| 亚洲国产一区二区三区在线播 | 亚洲乱码国产乱码精品精天堂 | 久久xxxx| 亚洲视频一区二区| 久久精品国产96久久久香蕉| 亚洲精品欧美日韩| 午夜日韩在线| 中文一区二区在线观看| 久久se精品一区精品二区| 一区二区三区 在线观看视频| 亚洲午夜性刺激影院| 亚洲电影免费观看高清完整版| 99re在线精品| 亚洲国产美女| 欧美有码在线视频| 亚洲午夜一级| 免费视频一区二区三区在线观看| 午夜伦欧美伦电影理论片| 欧美成人嫩草网站| 麻豆精品在线视频| 国产午夜精品久久| 亚洲性视频网址| 一本久道综合久久精品| 久久久精品一品道一区| 午夜在线精品偷拍| 欧美精品九九99久久| 欧美阿v一级看视频| 国产欧美婷婷中文| 亚洲一区免费| 亚洲欧美成人一区二区在线电影| 欧美国产日本| 欧美激情中文不卡| 亚洲高清精品中出| 久久久久久欧美| 久久精品国产视频| 国产热re99久久6国产精品| 亚洲图片欧洲图片av| 亚洲视频免费在线| 欧美视频精品一区| 在线午夜精品| 亚洲永久精品大片| 国产精品久久激情| 亚洲小说区图片区| 久久国产精彩视频| 国产一区二区三区直播精品电影| 亚洲欧美日韩中文视频| 欧美一区二区三区婷婷月色 | 亚洲一区欧美激情| 国产精品成av人在线视午夜片| 一区二区电影免费观看| 亚洲视频网在线直播| 欧美日韩岛国| 亚洲一区精彩视频| 久久久精品一区| 永久域名在线精品| 欧美激情a∨在线视频播放| 亚洲国产一区二区精品专区| 日韩一二在线观看| 欧美性猛交一区二区三区精品| 亚洲一区二区精品| 久久夜色精品国产欧美乱极品| 激情婷婷亚洲| 欧美片网站免费| av成人国产| 久久久久国内| 亚洲精品一区二区三区福利 | 欧美夫妇交换俱乐部在线观看| 亚洲日本在线视频观看| 欧美日韩在线电影| 午夜影院日韩| 亚洲国产乱码最新视频 | 精品91在线| 欧美日韩一区二区精品| 香蕉久久夜色精品| 亚洲东热激情| 性欧美大战久久久久久久久| **性色生活片久久毛片| 欧美日韩免费一区| 久久久久se| 亚洲视频axxx| 欧美激情一区二区三区全黄| 午夜精品福利一区二区蜜股av| 伊甸园精品99久久久久久| 欧美日韩高清不卡| 久久久亚洲国产美女国产盗摄| 宅男66日本亚洲欧美视频| 久久久夜夜夜| 亚洲一区二区成人在线观看| 加勒比av一区二区| 国产精品色午夜在线观看| 欧美99在线视频观看| 西瓜成人精品人成网站| 亚洲美女啪啪| 欧美福利视频网站| 久久精品夜色噜噜亚洲aⅴ| 在线亚洲一区二区| 亚洲国产小视频在线观看| 国产区精品在线观看| 欧美日韩国产欧| 欧美黄在线观看| 久久在线免费| 久久精品亚洲精品国产欧美kt∨| 一区二区高清在线| 亚洲日本一区二区三区| 欧美粗暴jizz性欧美20| 玖玖综合伊人| 久久精品亚洲精品| 欧美一区亚洲| 亚洲男女自偷自拍| 亚洲午夜未删减在线观看| 亚洲免费观看高清完整版在线观看| 伊人成人在线视频| 红桃视频亚洲| 黄色av日韩| 国内成+人亚洲+欧美+综合在线| 国产精品色午夜在线观看| 国产精品av一区二区| 欧美日韩一区二区三区| 欧美日韩国产免费| 欧美日韩亚洲综合| 欧美小视频在线| 国产精品久久久久久久午夜片| 欧美午夜免费影院| 国产精品嫩草影院av蜜臀| 国产精品日韩欧美一区二区三区| 国产精品久久久久aaaa樱花| 国产精品久久久久久影视| 欧美日韩精品在线视频| 欧美日韩精品免费观看视频| 欧美精品一区二区三区一线天视频 | 久久精品一区二区| 麻豆freexxxx性91精品| 欧美1区3d| 亚洲国产婷婷香蕉久久久久久99 | 洋洋av久久久久久久一区| 中日韩男男gay无套| 午夜老司机精品| 欧美在线不卡视频| 在线高清一区| 免费观看久久久4p| 国产欧美日本一区视频| 亚洲精品一二| 在线亚洲自拍| 欧美日韩精品一本二本三本| 亚洲第一精品在线| 国产午夜精品美女毛片视频| 一区二区三区久久精品| 一本色道久久综合狠狠躁篇怎么玩 | 葵司免费一区二区三区四区五区| 久久久久国产精品www| 久久综合久久美利坚合众国| 欧美激情中文字幕乱码免费| 亚洲三级视频在线观看| 亚洲综合国产精品| 老司机午夜精品视频在线观看| 欧美精品播放| 国产一区二区三区在线观看免费 | 久久久久久久久综合| 91久久在线播放| 欧美一级片一区| 欧美日韩一区二区在线观看 | 欧美日韩亚洲视频一区| 国内精品久久久久影院优| 日韩写真视频在线观看| 久久久久女教师免费一区| 亚洲人成网在线播放| 欧美资源在线| 国产精品久久午夜夜伦鲁鲁| 亚洲第一视频网站| 欧美制服丝袜第一页| 亚洲人成在线观看| 久久久久久高潮国产精品视| 欧美婷婷六月丁香综合色| 亚洲欧洲日本在线| 久久久久综合| 亚洲女爱视频在线| 欧美日韩精品免费观看视频完整 | 欧美一二三区精品| 99pao成人国产永久免费视频| 久久久中精品2020中文| 国产欧美日韩在线播放| 亚洲永久免费|