• <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>
            隨筆-341  評論-2670  文章-0  trackbacks-0

            如何手寫語法分析器

            陳梓瀚

            華南理工大學軟件05級本科

            vczh@163.com

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

             

            在寫可配置的語法分析器之前,我覺得還是先說說如何手寫語法分析器好。因為對于大部分人來說,開發一個可配置的語法分析器并沒有什么作用,反而針對某種特定的語法開發特定的語法分析器是特別有必要的。典型的有表達式計算器、某種格式化的文件(HTMLXML等)或者是其他的復雜而且符合樹型結構的字符串。根據目前論壇的反應來看,有一些朋友們對如何開發一套自己的腳本引擎比較感興趣。等基礎的文章都寫完以后我會考慮撰寫一個系列的文章介紹如何開發自己的腳本引擎。

             

            這篇文章會附帶一些必要的代碼以便幫助讀者們理解。為了方便,代碼使用DevC++開發。

             

            一、定義語法

             

            在開發語法分析器之前,有必要講一下語法的定義。這篇文章給出的是一個比較機械化的方法,掌握了這個方法之后手寫語法分析器會變成一件沒什么挑戰性但是很麻煩的工作。因為設計起來太簡單,但是代碼很多。有些人為了連麻煩的工作也不要會去開發可配置的語法分析器。不過這里先不管這么多,首先給出一個比較使用的語法。

             

            我們考慮一個經常從書上或者經常見到的例子:LISP語言。這門語言的表達式相當奇怪,操作符基本上當成函數處理,而且強迫用戶給出優先級。因為LISP的操作符是沒有優先級的。譬如(1+2)*(3+4)LISP會被寫成(* (+ 1 2) (+ 3 4) )

             

            讓我們看一下這種語法的結構。括號內可以寫很多個值,第一個被約定為是函數,之后的是參數。參數的個數是不確定的。一個函數調用的結果仍然是值,這就允許表達式進行嵌套。一個復雜一點的例子:2sinxcosxLISP內被寫成(* 2 (sin x) (cos x))。我們注意到最外層的乘法有3個參數,因此代表連乘。其次,(1)1的結果是一樣的。

             

            于是我們如何規定這種表達式的語法呢?我們可以給出若干條。為了方便我們去掉LISP語言允許的curry屬性,也就是說(+ 1 2)等價于( ( (+) 1) 2)

            1、  數字可以為值

            2、  一個值可以構成參數列表,參數列表后緊接著一個值仍然是參數列表

            3、  表達式可以為值,或者是括號內包含操作符或函數名外加可選的參數列表

             

            于是我們可以使用一種形式化的方法來寫出這個表達式。首先我們可以為表達式命名,譬如表達式我們使用expression或者exp等。其次name=rule代表復雜的rule將會使用一個名字name來代替。最后,a b代表a之后緊接著b

             

            這樣的話,我們就可以使用一種比較簡潔的方法來表示上面提到的簡化后的LISP表達式語法:

            Operator=”+”

            Operator=”-“

            Operator=”*”

            Operator=”/”

            Expression=<數字>

            Expression= “(” Operator Expression Expression “)”

            Expression=“(”Expression “)”

             

            這樣寫的話覺得很煩,我們可以追加多兩種定義語法的語法:

            1A | B代表A或者B都可以,并且如果字符串被A匹配成功的話將不會考慮B

            2[ A ]代表A是可以存在或者不存在的,但是盡量使其存在

             

            于是我們可以把上面的語法改寫成如下形式:

            1)       Operator=”+” | “-” | “*” | “/”

            2)       Expression=<數字> | “(“ Expression “)” | “(“ Operator Expression Expression “)”

             

            第一條語法規則說的是Operator,也就是操作符,可以是加號、減號、乘號或者除號。第二條語法規則說的是一條表達式可以只由數字構成、一個加了括號的表達式或者一個加上了括號的操作符和兩個參數。

             

            二、根據語法寫代碼

             

            到了這里,我們可以考慮一下如何通過語法組織我們的代碼了。上面的語法并沒有包含如何去除空格的語法,這個事情語法表達只會徒增煩惱,因此我們自己解決可能會更好一點。在語法分析的時候,我們都是一點一點讀入字符串的,因此我們的函數的的形式大概如下:

            ·讀入字符串,返回結果或者錯誤信息

            ·如果沒有錯誤的話,則將字符指針偏移到尚未讀取的位置

            ·如果有錯誤的話,保持字符指針不變

             

            好了,現在我們來看第一條語法。我們需要一個方法來檢查輸入是否由我們需要的字符串開頭,當然這里仍然需要考慮空格的問題。我們可以寫一個函數,輸入字符指針和一個字符串。這個函數先過濾掉空格然后檢查剩下的地方是不是由指定的字符串開始的。正確的話返回true并將輸入的字符指針往后諾到尚未讀取的地方:

            /*

            檢查Stream的前綴是否Text

                是返回true并將Stream偏移strlen(Text)個字符

                否則返回false

            此函數會過濾Stream開頭的空格

            */

            bool Is(char*& Stream , const char* Text)

            {

                size_t len=strlen(Text);

                /*保存參數*/

                char* Read=Stream;

                /*過濾空格*/

                while(*Read==' ')Read++;

                if(strncmp(Read,Text,len)==0)

                {

                    Stream=Read+len;

                    return true;

                }

                else

                {

                    return false;

                }          

            }

            代碼很短我就不解釋了。當然,有了這個函數之后我們可以很輕松地寫出一個判斷字符串是否由操作符開頭的函數:

            /*

            檢查Stream是否操作符

                是的話返回操作符的字符并將Stream偏移至操作符之后

                否則返回

            */

            char IsOperator(char*& Stream)

            {

                /*A||B操作符的特性是如果A==true則不對B求值

                所以表達式會在一個檢查成功后停下來

                */

                if(Is(Stream,"+") || Is(Stream,"-") || Is(Stream,"*") || Is(Stream,"/"))

                {

                    /*此時操作符已經被越過,所以返回Read[-1]*/

                    return Stream[-1];

                }

                else

                {

                    return 0;

                }       

            }

            第一條語法到了這里就結束了。然后我們考慮第二條語法。這條語法判斷一個字符串是否表達式,首先判斷一個字符串是否數字,失敗的話再檢查是否由括號打頭。因此我們需要一個判斷字符串是否由數字開頭。這里我們先引進一個struct

            /*表達式分析結果*/

            struct Expression

            {

                int Result;     /*表達式結果*/

                char* Error;    /*錯誤信息,沒有錯誤則為*/

                char* Start;    /*錯誤的位置*/

            }; 

            這個Expression結構用于表達字符串的分析結果。Result是表達式的計算結果,Error如果非0則保存了錯誤信息,此時Start保存了錯誤信息在字符串的什么地方被引發。有了這個Expression之后我們就可以寫出如下判斷字符串是否由數字開頭的函數了。為了方便,這個函數只判斷非負整數。

            /*

            檢查Stream是否數字,是的話則將Stream偏移到數字之后

            */

            Expression GetNumber(char*& Stream)

            {

                /*初始化結果*/

                Expression Result;

                Result.Result=0;

                Result.Error=0;

                Result.Start=0;

                bool GotNumber=false;

                /*保存參數*/

                char* Read=Stream;

                /*過濾空格*/

                while(*Read==' ')Read++;

                while(true)

                {

                    /*讀入一個字符并將Read偏移一個字符*/

                    char c=*Read;

                    /*檢查字符是否為數字*/

                    if('0'<=c && c<='9')

                    {

                        /*把結果添加進Result,進行進位*/

                        Result.Result=Result.Result*10+(c-'0');

                        GotNumber=true;

                        Read++;

                    }

                    else

                    {

                        break;

                    }   

                }

                if(GotNumber)

                {

                    Stream=Read;

                }

                else   

                {

                    Result.Error="這里需要數字";

                    Result.Start=Read;

                }   

                return Result;

            }

            這個函數仍然會過濾掉字符串開頭的空格。如果成功的話,也就是Result.Error==0的時候,參數Stream會被偏移到已經分析的數字后面。

             

            讓我們看一看第二條語法接下來的部分:“(“ Expression “)” | “(“ Operator Expression Expression “)”。我們注意到,這兩個部分都是使用括號開始和結束的,因此在寫代碼的時候可以把它們寫在一起,只把中間的部分分開。這種方法在課本中通常被稱為合并前綴。于是我們可以寫一個GetExpression函數。這個函數首先判斷字符串是不是由數字開頭,否則的話看一看是否由括號開頭。如果是括號開頭的話,那么檢查接下來的是Operator還是一個Expression。如果是Expression則到此結束,如果是Operator的話還要再輸入兩個Expression。然后判斷一下是不是由右括號結束字符串:

            /*檢查Stream是否表達式,是的話則將Stream偏移至表達式之后*/

            Expression GetExpression(char*& Stream)

            {

                /*保存參數*/

                char* Read=Stream;

                /*檢查是否數字*/

                Expression Result=GetNumber(Read);

                if(Result.Error)

                {

                    if(Is(Read,"("))

                    {

                        /*不是數字而是左括號,則將ResultError*/

                        Result.Error=0;

                        char Operator=0;

                        /*檢查是否操作符*/

                        if(Operator=IsOperator(Read))

                        {

                            /*獲得左參數。如果參數獲取失敗會直接返回*/

                            Expression Left=GetExpression(Read);

                            if(Left.Error) return Left;

                            /*保存當前的Read變量,以便在右參數出錯的情況下正確指出錯誤的地點*/

                            char* RightRead=Read;

                            /*獲得右參數。如果參數獲取失敗會直接返回*/

                            Expression Right=GetExpression(Read);

                            if(Right.Error) return Right;

                            /*根據操作進行計算*/

                            switch(Operator)

                            {

                                case '+':

                                    Result.Result=Left.Result+Right.Result;

                                    break;

                                case '-':

                                    Result.Result=Left.Result-Right.Result;

                                    break;

                                case '*':

                                    Result.Result=Left.Result*Right.Result;

                                    break;

                                case '/':

                                    if(Right.Result==0)

                                    {

                                        Result.Error="除錯";

                                        Result.Start=RightRead;

                                    }

                                    else

                                    {   

                                        Result.Result=Left.Result/Right.Result;

                                    }   

                                    break;

                                default:

                                    Result.Error="未知操作符";/*不可能發生,執行到這里則證明其他部分有bug*/

                                    Result.Start=Read;

                                    return Result;

                            }   

                        }

                        else

                        {

                            /*不是操作符則嘗試獲得表達式*/

                            Result=GetExpression(Read);

                            /*獲取失敗則直接返回*/

                            if(Result.Error) return Result;

                        }

                        /*檢查是否有配對的右括號*/

                        if(!Is(Read,")"))

                        {

                            Result.Error="此處缺少右括號";

                            Result.Start=Read;

                        }  

                    }   

                }

                /*如果沒有出錯則更新Stream的位置*/

                if(Result.Error==0)

                {

                    Stream=Read;

                }   

                return Result;          

            }

            到了這里表達式的分析就完成了,我們得到了一個工具:GetExpression。我們可以將一個字符串輸入GetExpression,然后看看返回了什么。當然,有可能返回計算結果,也有可能返回錯誤信息以及錯誤位置。為了解釋如何使用GetExpression,我也寫了一個main函數:

            int main(int argc, char *argv[])

            {

                /*聲明一個長度的字符串緩沖區,可能有溢出的危險,此處不考慮*/

                char Buffer[1000];

                cout<<"輸入一個表達式:"<<ends;

                gets(Buffer);

                {

                    char* Stream=Buffer;

                    Expression Result=GetExpression(Stream);

                    if(Result.Error)

                    {

                        cout<<"發生錯誤"<<endl;

                        cout<<"位置:"<<Result.Start<<endl;

                        cout<<"信息:"<<Result.Error<<endl;

                    }

                    else

                    {

                        cout<<"結果:"<<Result.Result<<endl;

                    }       

                }   

                system("PAUSE");  

                return 0;

            }

            這個函數輸入一個字符串,然后計算結果或者輸出錯誤信息。當然,錯誤的檢查時不完全的,因為GetExpression只負責檢查前綴,至于剩下的部分是什么是不管的。因此實際上還要檢查一下剩下的字符是不是全都是空格,不是的話就要自己報錯了。完整的代碼見附帶的文件夾Code_1_LISP

             

            三、處理左遞歸

             

            上面的方法其實還是不完全的。我們有時候會遇到一些自己產生自己的語法。譬如我們在表達一個使用逗號隔開的數字列表的時候,有如下兩種寫法:

            1)  List=<數字> [“,” List]

            2)  List=[List “,”]<數字>

            這兩種寫法所產生的效果是一致的,但是我們如果按照第二種方法直接寫出代碼的話就會陷入無限循環。這種自己導出自己的特征就叫做左遞歸了。像這種情況左遞歸還是能避免的,但并不是所有的最遞歸都能直接避免的。雖然不能避免,但是仍然有一個通用的辦法來解決,只不過或破壞一點點美感。

             

            分析了LISP的表達式之后,我們進入下一個例子:分析四則運算式子。我們的四則運算式子由加減乘除、括號和數字構成。為了方便不考慮正負。使用語法規則是可以表達出操作符的優先級的。下面就讓我們來思考如何構造四則運算式子的語法。

             

            我們將一個表達式定義為Expression。首先,數字可以成為Expression,其次,加了括號的Expression仍然是Expression

            Expression=<數字> | “(“ Expression “)”

            但是這里有一個問題,操作符號的優先級并不能當純通過寫Expression=Expression “+” Expression來完成。因此我們進入進一步的思考。

             

            我們考慮一下乘除先于加減背后的本質是什么。看一下一條比較長的表達式:

            1*2*3+4*5*6+7*8*9

            我們在計算的時候會把他們分成三個部分:1*2*34*5*67*8*9,分別計算出結果,然后相加。如果我們可以把僅僅由乘除組成的表達式的語法寫出來,那么寫出四則運算式子的語法也就有希望了。事實是可以的。于是我們要對之前的結果做一下調整。無論是數字或者是括號包含的表達式都不可能因為在旁邊添加其他操作符而對優先級有所影響,因此我們抽象出一個類型叫Term

            Term=<數字> | “(“ Expr “)”

            然后我們就可以寫一條只用乘除構成的表達式的語法了:

            Factor=Term | Factor “*” Term | Factor “/” Term

            最后,我們可以寫出一條只用加減和Factor構成的表達式的語法:

            Exp=Factor | Exp “+” Factor | Exp “-“ Factor

            到了這里表達式的語法就大功告成了。上面的三條語法中的Exp就是四則運算的語法了。

             

            我們注意到ExpFactor都是左遞歸的。在這里我介紹一種消除左遞歸的方法。我們考察一下語法Factor=Term | Factor “*” Term這一條。為了形象的表達出什么是Factor,我們反過來可以考察一下Factor究竟可以產生出什么樣的東西來。

             

            一個Factor可以產生出一個Term。然后,一個Factor可以變成Factor “*” Term。如果我們把Factor “*” Term中的Factor替換成已知的結果的話,那么我們可以得到一個結論:一個Factor可以產生出Term “*” Term。同理,我們又可以知道一個Factor可以產生出Term “*” Term “*” Term,為Factor可以產生出Term “*” Term。于是我們大概可以猜出解決左遞歸的方法:

             

            假設存在如下表達式:

            A=B1

            A=Bn

            A=A C1

            A=A Cn

            我們可以將這個語法修改為如下形式:

            A’=C1 | C2 | … | Cn [A’]

            A=(B1 | B2 | … | Bn) [A’]

            我們可以看到現在的A沒有發生變化,但是新的語法已經不存在左遞歸了。我們為了簡化表達,可以引進一種新的語法:我們讓X*代表XXX等等只由A組成的字符串或者空字符串,那么上面這個語法就可以被修改成A=(B1 | B2 | … | Bn) (C1 | C2 | … | Cn)*了。

             

            于是,我們重新寫一下四則運算式子的語法:

            1)       Term=<數字> | “(“ Exp “)”

            2)       Factor = Term ( ( “*” | “/” ) Term) *

            3)       Exp = Factor ( ( “+” | “-“ ) Factor) *

             

            我在這里仍然要寫出四則運算分析的代碼。但是這一次我不求值了,這個新的程序將把四則運算式子轉換成等價的LISP表達式然后輸出。

             

            代碼的結構是這樣的。首先,仍然會存在上文中的函數Is。其次,表達式Expression的結構將被我替換成一個遞歸的二叉樹,異常信息使用C++的異常處理機制實現。

             

            在這里貼出GetTermGetFactor的代碼,GetExpGetFactor結構相似。

             

             

            Expression* GetTerm(char*& Stream);

            Expression* GetFactor(char*& Stream);

            Expression* GetExp(char*& Stream);

             

            /*

            檢查Stream是否一個Term

            */

            Expression* GetTerm(char*& Stream)

            {

                try

                {

                    return GetNumber(Stream);

                }   

                catch(Exception& e)

                {

                    char* Read=Stream;

                    /*檢查左括號*/

                    if(Is(Read,"("))

                    {

                        /*檢查表達式*/

                        Expression* Result=GetExp(Read);

                        if(Is(Read,")"))

                        {

                            /*如果使用右括號結束則返回結果*/

                            Stream=Read;

                            return Result;

                        }

                        else

                        {

                            /*否則拋出異常*/

                            delete Result;

                            throw Exception(Stream,"此處需要右括號");

                        }       

                    }   

                    else

                    {

                        throw e;

                    }   

                }   

            }   

             

            /*

            檢查Stream是否一個Factor

            */

            Expression* GetFactor(char*& Stream)

            {

                /*獲得一個Term*/

                char* Read=Stream;

                Expression* Result=GetTerm(Read);

                while(true)

                {

                    /*檢查接下來是否乘除號*/

                    char Operator=0;

                    if(Is(Read,"*"))

                        Operator='*';

                    else if(Is(Read,"/"))

                        Operator='/';

                    else

                        break;

                    if(Operator)

                    {

                        /*如果是乘除號則獲得下一個Term*/

                        try

                        {

                            Result=new Expression(Operator,Result,GetTerm(Read));

                        }

                        catch(Exception& e)

                        {

                            /*發生異常的時候,首先刪除Result,其次轉發異常*/

                            delete Result;

                            throw e;

                        }       

                    }   

                }

                Stream=Read;

                return Result;   

            } 

            完整的代碼見文件夾Code_2_EXP2LISP

             

            這份代碼跟分析LISP表達式代碼不同的是這里展示了給出樹形結構而不僅僅是計算出結果的代碼。這兩種方法的區別僅僅是獲得了數據之后如何處理的問題,但是代表了兩種經常需要處理的任務。

             

            四、尾聲

             

            這篇文章相比起以前的兩篇正則表達式來的確是短了不少。遞歸下降法是一種適合人腦使用而不是電腦使用的方法。這種方法非常好用,所以大部分編譯原理的教科書都會專門使用一個章節來說明遞歸下降的實現、局限性以及遇到的問題的解決方法。這篇文章不是理論文章,所以有一些本文沒闡述到的問題可以通過人的智商來解決。

             

            在語法處理過程中遇到的一個問題是出現異常的時候如何組織錯誤信息。在寫編譯器的時候我們并不能通過異常處理來向外傳播異常信息,因為編譯器需要輸出許多異常。不過大部分分析工作還是僅僅需要第一個異常信息的。

             

            第二個常見的問題是如何在發生異常的時候處理分析結果。在本文的第二個例子里面,在拋出異常之前總是會手動delete掉已經產生的指針。其實這樣做是很容易漏掉一些處理從而造成內存泄漏的,如果讀者使用C++的話,那么我推薦使用STLauto_ptr或者Boostsmart_ptr,或者干脆自己寫吧。樹型結構的文檔通常不會有循環引用的問題,所以在這種情況下無論如何產生文檔或者產生異常,使用auto_ptr或者smart_ptr都是沒有問題的。

             

            第三個問題是寫不出語法。這個問題沒有什么好的辦法,只有通過練習來解決了。或者干脆做一個YACC出來,經過一次非常深入的思考也能獲得很多經驗。就像寫出一手好的正則表達式的人,要么就是練習了很多次,要么就是寫過正則表達式引擎一樣。不過這種方法比較耗時間,不是非常有興趣的讀者們還是不要這么做的好。

             

            最后說明一下,本文使用四則運算式子作為例子僅僅是為了方便。實際上分析四則運算獅子已經有各種各樣的好方法了。但是讀者們將來卻很難遇到分析四則運算的工作,而是分析各種各樣復雜字符串的工作。這個時候遞歸下降法起得作用是在代碼還沒開始寫之前,就已經把思考不慎密導致的bug都消除了大半了。因為設計語法的過程很容易讓人深入的思考問題。遞歸下降法能夠用最快的速度從語法產生出代碼,但是還是要根據實際情況調整細節。

             

            本文作為《構造正則表達式引擎》一文的補充而出現,因為有一些朋友們反映在析正則表達式的結構以及合法性遇到了一些困難。因為正則表達式的語法跟四則運算很像,因此參考一下本文對這些朋友們來說可能會有幫助。

            正文(docx)以及附帶的代碼,點擊這里下載

            posted on 2008-06-15 05:59 陳梓瀚(vczh) 閱讀(40141) 評論(28)  編輯 收藏 引用 所屬分類: 作品

            評論:
            # re: 如何手寫語法分析器 2008-06-15 06:10 | 空明流轉
            很好,很強大。。。  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 06:12 | 陳梓瀚(vczh)
            這么快就被你發現了……  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 06:23 | 夢雪冰怡
            這篇文章不是理論文章,所以有一些本文沒闡述到的問題可以通過人的智商來解決。
            解決不了就意味著。。我智商@#¥%@#%  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 06:24 | 陳梓瀚(vczh)
            你最后不是自己『發現』了這個方法么,沒事……  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 06:27 | passerby
            不錯。
            收了。  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 06:34 | Kaja
            贊一個  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 06:46 | 54sun
            lz很強,我大學時除了玩游戲和混論壇之外什么都沒做,到了研究生才開始學。  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 06:52 | foxtail
            好文 龍頭老大的作用出來了  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 09:37 | Kim4Apple
            太多東西是從《編譯原理》的書中,摘抄下來了。要看的話,還是買本‘龍書’好了,比較有營養價值。  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 09:39 | Kim4Apple
            這些代碼,應該是編譯原理的,課程設計作業吧?  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 09:41 | 陳梓瀚(vczh)
            那些代碼是今晚臨時想出來的。至于“摘抄”的話還是不夠精確的,我的目的是最大程度拋開理論來講這些東西。我以前看的是英文版的《Parsing Techniques》。看了《PT》之后我找了個機會把龍書粗略翻了一遍,發現遠不及《PT》來得好。唯一的不足就是我拿到的《PT》是90年寫的,現在出了第二版才放出第一版的電子版。而且《PT 2.0》在amazon上售價$80,目錄在字符串分析方面比龍書高級了一等……不過《PT》還是專注于字符串分析的。分析完了之后的事情我建議看《高級編譯器設計與實現》,仍然不【首先】推薦龍書。

            說到課程設計,之前學校不知道從哪里弄了一個老師來教我們的編譯原理,結果什么作業都沒有,課程設計也沒有- -b  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 10:00 | 陳梓瀚(vczh)
            寫這些文章的原因是這樣的:

            半年多前我曾經用了一種相對容易理解的方法重新寫了一遍正則表達式到DFA的算法細節,弄了一篇文章出來。雖然有些人說比《編譯原理》容易理解,但是還沒達到目的。上個月續寫了一篇《正則表達式》,把數據結構的細節都給了出來,《編譯原理》中沒提及的如何處理現在正則表達式引擎處理的那些事情以及為什么不能使用DFA的原因都寫出來了。于是今天中午有個把兩篇文章都看了的網友就找我評論了:“我看你第一篇文章寫代碼寫到手軟(意思是留給自己想的東西太多),第二篇講得太詳細以至于都沒什么需要想的了……”所以到目前為止這三篇文章都是給那些覺得《編譯原理》有點吃力的人看的。當然無論龍書還是《PT》都非常好,只不過缺點跟我第一篇文章一樣,只有理論。所以必然不能適應所有龍書或《PT》的讀者們的要求。

            不過再寫下去的話就是很少有流行的書籍提及的事情了,目前打算開個新的系列寫腳本引擎的事情。只不過基礎還是需要普及的,為了看以后的文章要求讀者們都理解《編譯原理》還是不太實際的。  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 19:26 | soulfvi
            中間肯定要展現樹結構,到最后不需要展現,除非要添加其他語法,總之文章寫得很有條理,但不是所有人都感興趣而已--  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-15 22:36 | 陳梓瀚(vczh)
            不存在全人類都感興趣的,并且需要努力才能產生生產力的領域  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-16 01:25 | Lnn
            你好棒!!  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-16 02:47 | missdeer
            “不存在全人類都感興趣的”,所以不用管“為了看以后的文章要求讀者們都理解《編譯原理》還是不太實際的”  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-16 03:09 | ZelluX
            @陳梓瀚(vczh)
            龍書第二版電子版年初就有了
            第二版的龍書蠻贊的
            Datalog, Inter-pocedural Analysis都提到了  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-16 04:03 | 陳梓瀚(vczh)
            我找來看看  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-16 20:03 | 長江三峽
            不錯  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-26 05:59 | Vampire.Kiss
            老胖,/cy
            請你單獨做輔導要多少錢一個月?  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-26 18:55 | 陳梓瀚(vczh)
            nono,我只輔導經常看得到的人,然后讓對方請吃飯,不收¥,滅哈哈……  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-26 22:41 | Texim
            看你前面的回復好像是你應該有龍書第一版 的英文版 ,我在網上找了半天也沒找到,如果方便的話請發給我一份,謝謝!
            happynxy@foxmail.com  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-06-27 20:45 | 陳梓瀚(vczh)
            我那本不是電子書  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-07-02 20:45 | 白開水
            看了小V這么多文章,最不喜歡這篇。

            1. 不該帶進太多的實現細節。只用指出關鍵的數據結構或者接口就可以。
            2. 不中不西的,既無可配置詞法分析文章的理論優美,又無設計正則表達式引擎灌水文的實際設計雄厚功力。
            3. 無法跟自己前兩篇關于詞法分析的文章和好的結合起來,否則在這里不用自己實際分析字符串,只用ReadToken下就可以了。

            優點當然也有,比如那個struct Expression的設計,帶進了錯誤處理就給了我不少啟發。另外實現的細節,也許對某些人確實比較有用吧````。  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-07-03 06:48 | 陳梓瀚(vczh)
            其實我的宗旨是不吸收不需要的知識。所以沒關系啦,對某些人有用那還是要寫的。嘿嘿。  回復  更多評論
              
            # re: 如何手寫語法分析器 2008-10-03 03:06 | htqx
            對于我這種沒什么理論根基的正適合  回復  更多評論
              
            # re: 如何手寫語法分析器 2009-10-22 20:59 | ushn
            《Parsing Techniques》沒讀過,你有電子版嗎?如果方便的話請發給我一份,謝謝! ushn2018@hotmail.com  回復  更多評論
              
            # re: 如何手寫語法分析器 2015-10-12 03:16 | replica watches uk
            學習了   回復  更多評論
              
            久久午夜羞羞影院免费观看| 色8激情欧美成人久久综合电| 久久久国产精品亚洲一区| 亚洲精品无码久久久久| 国产韩国精品一区二区三区久久| 久久综合丁香激情久久| 日本亚洲色大成网站WWW久久 | 香蕉99久久国产综合精品宅男自| 亚洲欧美一级久久精品| 久久久久久人妻无码| 久久se精品一区二区影院| 久久精品中文字幕一区| 99久久久国产精品免费无卡顿| 久久国产精品免费一区| 99久久精品国产一区二区| 亚洲精品高清久久| 亚洲一区精品伊人久久伊人| 99久久人妻无码精品系列蜜桃| 久久嫩草影院免费看夜色| 久久午夜伦鲁片免费无码| 久久九九久精品国产| 久久99亚洲网美利坚合众国| 久久精品国产清自在天天线| 亚洲国产精品无码久久久蜜芽 | 欧洲精品久久久av无码电影| 国产精品九九久久精品女同亚洲欧美日韩综合区 | 久久久久久久亚洲Av无码| 亚洲人成网站999久久久综合 | 伊人色综合九久久天天蜜桃| 国产精品久久国产精品99盘 | 免费精品久久天干天干| 国产成人AV综合久久| 激情伊人五月天久久综合| 久久天天躁夜夜躁狠狠| 久久久久香蕉视频| 久久九九有精品国产23百花影院| 综合人妻久久一区二区精品| 亚洲精品无码久久毛片| 久久国产精品波多野结衣AV| 久久中文娱乐网| 九九99精品久久久久久|