如何使用Lex/YACC

譯自Lex&YACC HOWTOhtml

1. 簡介

若是你有Unix環境的編程經驗,想必你確定遇到過神祕的Lex和YACC工具,在GUN/Linux中,又分別稱做Flex和Bison,其中Flex是由Vern Paxon實現的Lex版本,Bison是GUN版本的YACC.咱們統一稱他們爲Lex和YACC,這些新版本是向上兼容的,所以你能夠在咱們的示例中使用Flex以及Bison.c++

這兩個程序是很是有用的,可是跟C編譯器同樣,它的用戶手冊上即不會解釋C語言,也不會告訴你如何使用C語言。YACC與Lex一塊兒使用時很是有用,然而,Bison用戶手冊並無介紹如何將Lex代碼集成到Bison程序裏。正則表達式

1.1 這篇文章不能作什麼

關於Lex&YACC的巨做有不少。若是須要了解更多,你應該閱讀它們。它們提供的信息比本文多的多。參考文章未尾"Further Reading"章節。本文的目的是經過實例引導你如何使用Lex&YACC。編程

Flex及BISON自帶的文檔很是優秀,但並不是教程。框架

我無心成爲Lex&YACC專家。當我開始寫此文章時,不過接觸它們兩天而已。我所作的只是想讓這兩天對你而言會更輕鬆。編程語言

能力有限,不要指望文章可以恰如其份符合Lex&YACC風格。示例保持的儘可能簡單,可能有更好的方法,你能夠寫在下面的評論裏。函數

1.2 下載示例

請注意,你能夠下載 全部的示例文件。工具

1.3 License

Copyright (c) 2001 by bert hubert. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, vX.Y or later (the latest version is presently available at http://www.opencontent.org/openpub/).學習

2. Lex&YACC能作什麼

使用恰當的話,這兩個程序可以讓你更容易的解析複雜的語言。例如讀取配置文件,或者爲你本身發明的編程語言寫一個編譯器。字體

經過本文,你會發現,有了Lex&YACC這兩個工具,你永遠不須要本身手工寫一個解析程序。

2.1 各司其職

雖然這兩個程序在一塊兒使用時顯得光耀奪目,可是它們的用途是不一樣的。接下來的章節會解釋每一個程序能作什麼。

3. Lex

Lex 程序生成的的文件被稱做分詞器。它是一個函數,輸入爲字符流,只要發現一段字符可以匹配一個關鍵字,就會採起對應的動做。一個很是簡單的示例:

%{
#include <stdio.h>
%}
%%
stop    printf("Stop command received\n");
start   printf("Start command received\n");
%%

位於%{%}之間的第一個段原封不動的導出到輸出程序。由於使用了printf,所以咱們須要stdio.h

段之間被%%分割了開來,第二段的第一行起於stop健值,表示當從輸入流中讀取到stop時就會執行後面的printf("Stop command received\n");
除了stop,咱們還定義了start,做用與stop同樣。

段以%%結束。

爲了編譯Example1,執行

$ lex example1.lt
cc lex.yy.c -o example -ll

請注意:若是你使用Flex,請用Lex替代之,可能你還要將-ll替換成-lfl.至少RetHat 6.x以及SuSE須要。

上面的命令會生成程序example1,若是你運行它,它會等待你的輸入。只要你的輸入內容與定義的鍵值(stopstart)不匹配,就會將它們輸出。若是你輸入stop,它會輸出 Stop command received

以EOF(^D)結束輸入。

也許你想知道程序爲何能運行,由於咱們壓根沒有定義main函數。其實main函數在libl(liblex)中被定義,經過 -ll被引入了進來。

3.1 正則匹配

上面的示例的實用效果不佳,接下來的亦然。不過它會在Lex中引用正則,這點將會在後面的示例中很是有用。

Example 2:

%{
include <stdio.h>
%}
%%
0123456789]+           printf("NUMBER\n");
a−zA−Z][a−zA−Z0−9]*    printf("WORD\n");
%%

上面這個Lex文件描述兩種匹配的符號:WORDsNUMBERs。學習正則表達式可能有一點困難,但只須花點功夫即可輕鬆的理解它們。來看下NUMBER的匹配:

[0123456789]+

意思是:一系列的一個或多個取自於0123456789中的字符。簡便寫法是:

[0-9]+

WORD的匹配:

[a-zA-Z][z-zA-Z0-9]*

第一個部分(第一個方括號內)僅匹配一個介與'a'和'z'之間的字符,或者說,一個字母。這個初始的字母后面須要跟0個多更多的字符,這些字符便可以是字母也能夠是數字。爲什麼此處使用星號呢?

+意思是1個或更多的匹配,可是一個WORD能夠僅由一個字符組成,即已經匹配的第一個部分。所以第二人部分或許是0個匹配,所以用'*'。

這樣咱們就模仿了大部分編程語言中變量必須由一個字母開頭,可是後面能夠有數字。例如,'temperature1'是個合法的名字,可是'1temperature'不是。

嘗試編譯Example2,方法於Example1同樣。輸入一些文字,如下是一些樣例:

$ ./example2
foo
WORD

bar
WORD

123 
NUMBER

bar123 
WORD

123bar
NUMBER
WORD

Flex的用戶手冊上關於正則表述式描述的很詳細。perl用戶手冊(perler)關於正則部分也頗有用,儘管Flex沒有實現perl的所有。

確認你沒有建立形如[0-9]*這樣能夠匹配模式,不然你的lexer會重複的匹配空字段串。

3.2 一個更復雜的類C語法示例

假設下面是一個咱們想解析的文件:

logging {
    category lame−servers { null; };
    category cname { null; };
};

zone "." {
    type hint;
    file "/etc/bind/db.root";
};

這個文件中有如下幾類符號(tokens)

  1. WORDs ,如zonetype

  2. FILENAMEs ,如/etc/bind/db.root

  3. QUOTEs ,如包括文件名的符號

  4. OBRACEs ,左花括號{

  5. EBRACEs ,右花括號}

  6. SEMICOLONs ,;

對應的Lex文件以下(Example 3):

%{
#include <stdio.h>
%}
%%
[a−zA−Z][a−zA−Z0−9]*    printf("WORD ");
[a−zA−Z0−9\/.−]+        printf("FILENAME ");
\"                      printf("QUOTE ");
\{                      printf("OBRACE ");
\}                      printf("EBRACE ");
;                       printf("SEMICOLON ");
\n                      printf("\n");
[ \t]+                  /* ignore whitespace */;
%%

當咱們將文件輸入分詞器時,獲得:

WORD OBRACE
WORD FILENAME OBRACE WORD SEMICOLON EBRACE SEMICOLON
WORD WORD OBRACE WORD SEMICOLON EBRACE SEMICOLON
EBRACE SEMICOLON

WORD QUOTE FILENAME QUOTE OBRACE
WORD WORD SEMICOLON
WORD QUOTE FILENAME QUOTE SEMICOLON
EBRACE SEMICOLON

與以前提到的配置文件相比,很明顯咱們對其進行了符號化。配置文件的每一個部分都被匹配了而且轉化成指定的符號。

這正是咱們要給YACC使用的。

3.3 咱們看到了什麼

咱們已經看到了Lex可以讀取隨機的輸入而且檢測輸入的每部分是什麼。咱們將其稱之爲符號化。

4. YACC

YACC可以將輸入的符號流解析成指定的值。這裏清晰的描述了YACC與Lex以前的關係。YACC沒有輸入流的概念,它僅接受預處理過的符號集。你能夠本身寫符號生成器,不過本文所有將其交給Lex。

關於語法跟語法分析器的一點小注意:當YACC成熟時,它就被用做編譯器的解析文析的工具。計算機語言不容許有二義性。所以,YACC在遇到有歧義時會抱怨移進/歸約或者歸約/歸約衝突。更多關於YACC與歧義的問題參考衝突章節。

4.1 一個簡單的溫度調節控制器

咱們想用一門簡單的語言去控制一個溫度調節器,例如:

heat on
    Heater on!
heat off
    Heater off!
target temperature 22
    New temperature set!

咱們須要辨別的符號有:heat,on/off(STATE),target,temperature,NUMBER。對應的Lex文件以下(Example 4):

%{
#include <stdio.h>
#include "y.tab.h"
%}
%%
[0−9]+                    return NUMBER;
heat                     return TOKHEAT;
on|off                     return STATE;
target                     return TOKTARGET;
temperature                return TOKTEMPERATURE;
\n                         /* ignore end of line */;
[ \t]+                     /* ignore whitespace */;
%%

注意兩個重要的變化。第一,引入了頭文件y.tab.h。第二,咱們再也不使用print函數,而是直接返回符號的名字。這樣作的目的是爲了接下來將它嵌入到YACC中,然後者對打印到屏幕的內容根本不關心。Y.tab.h定義了這些符號。

可是y.tab.h是從哪獲得的呢?它是由YACC從語法文件中生成的。 咱們的語言很是簡單,如下是它的語法:

commands: /* empty */
                | commands command
                ;

       command:
                heat_switch
                |
                target_set
                ;

       heat_switch:
                TOKHEAT STATE
                {
                    printf("\tHeat turned on or off\n");
                }
                ;
                target_set:
                TOKTARGET TOKTEMPERATURE NUMBER
                {
                    printf("\tTemperature set\n");
                } 
                ;

第一個部分我稱之爲,它告訴咱們有命令集(commands),而且這些命令集由一些獨立的命令(command)組成。如你所見,這些規則是遞歸的,由於他自己又包含了commands.這就意味着經過遞歸能夠將這一系列的命令集進行歸約。閱讀Lex和YACC內部原理獲取更多遞歸的詳細內容。

第二個部分規則定義了command具體是什麼。咱們只支持兩種命令:heat_switchtarget_set。這個是|-符號的意思:一個命令(command)包含了heat_switchtarget_set

heat_switch包含了HEAT符號,即一個簡單的單詞heat以及後面跟一個狀態(在Lex中定義的onoff)。

target_set稍微有些複雜,它由TARGET符號(單詞target),TEMPERATURE符號(單詞)以及一個數字組成。

完整的YACC文件

前面一節僅列出了YACC文件的部分,如下是咱們省略的開頭部分:

%{
#include <stdio.h>
#include <string.h>

void yyerror(const char *str){
    fprintf(stderr,"error:%s\n",str);
}

int yywrap(){
    return 1;
}
main()
{
    yyparse();
}

%}

%token NUMBER TOKHEAT STATE TOKTARET TOKTEMPERATURE

函數yyerror在YACC發生錯誤時被調用 ,咱們只是簡單的將傳入的信息打印了出來,實際有比這更巧妙的處理,參閱"深度閱讀"一節。

函數yywrap可以用因而否繼續讀取其它的文件,當遇到EOF時,你能夠打開其它文件並返回0。或者,返回1,意味着真正的結束。欲知更多,請參閱"Lex和YACC內部工做原理"章節。

函數main是程序的起點。

最後一行簡單的定義了哪些符號將會被用到,若是調用YACC時啓用了-d選項,會將這些符號會輸出到y.tab.h文件。

編譯、運行溫度調節控制器

lex example4.l
yacc -d example4.y
cc lex.yy.c y.tab.c -o example4

有一點小變化。如今咱們使用YACC編譯咱們的程序,它生成y.tab.c和y.tab.h文件.而後纔是調用Lex。編譯時,再也不須要-ll,由於程序中咱們定義了本身的main函數。

注意:若是你獲得一個編譯器錯誤:not being able to find 'yylval',將下面的內容加入到文件example4.l中的#include <y.tab.h>下面

extern YYSTYPE yylval;

Lex 和YACC工做內部原理有相關的解釋。

運行示例:

$ ./example4
heat on
        Heat turned on or off
heat off
        Heat turned on or off
target temperature 10
        Temperature set
target humidity 20
       error: parse error

以上並非咱們要完成的真正目標,而是經過此例按部就班,控制學習曲線,使讀者繼續保持興趣。並不是全部酷的特性都能一次被展現。

4.2 拓展溫度調節器使其可處理參數

上面的示例能夠正確的解析溫度調節器的命令,可是它並不知道應該作什麼,它並不能取到你輸入的溫度值。

接下來工做就是向其中加一點功能使之能夠讀取出具體的溫度值。爲此咱們須要學習如何將Lex中的數字(NUMBER)匹配轉化成一個整數,使其能夠在YACC中被讀取。

當Lex匹配到一個目標時,它就會將匹配到的文字放到yytext中。YACC從變量yylval中取值。在下面的Example5中,是一種直接的方法:

%{
#include <stdio.h>
#include "y.tab.h"
%}
%%
[0−9]+            yylval=atoi(yytext); return NUMBER;
heat            return TOKHEAT;
on|off            yylval=!strcmp(yytext,"on"); return STATE;
target            return TOKTARGET;
temperature        return TOKTEMPERATURE;
\n                /* ignore end of line */;
[ \t]+            /* ignore whitespace */;
%%

如你所見,以yytext做爲參數調用atoi函數,並將其返回值賦給yylval變量,這樣YACC就可使用它。咱們對STATE採用相似的處理方式:若是爲on,yylval爲1。
請注意,在Lex中分別對on和offf進行匹配能夠獲得更快的處理代碼,可是我想展現一點更復雜的規則。

接下來咱們學習YACC如何處理這些。Lex中咱們稱爲yylval,在YACC有另一個名字。下面檢查設置溫度目標的規則:

target_set:
        TOKTARGET TOKTEMPERATURE NUMBER
        {
            printf("\tTemperature set to %d\n",$3d);
        }
        ;

爲了取到規則中的第三個部分的值,(例如,NUMBER),咱們須要使用$3,只要yylex返回,yylval的值就會被顯示在終端中,其值經由$取得。

爲了闡述這個特性,讓咱們觀查新的heat_switch規則:

heat_switch:
        TOKHEAT STATE
        {
            if($2)
                printf("\Heat turned on\n");
            else
                printf("\tHeat turned off\n");
        }
        ;

4.3 解析配置文件

讓咱們繼續討論前面提到的配置文件:

zone "." {
        type hint;
        file "/etc/bind/db.root";
}

以前咱們已經爲其寫過一個分詞器。如今須要爲其寫一個YACC語法文件而且修改那個分詞器以適應YACC。

Example 6:

%{
#include <stdio.h>
#include "y.tab.h"    
%}

%%

zone                return ZONETOK;
file                 return FILETOK;
[a-zA-Z][a-zA-Z0-9]    yylval=strdup(yytext);return WORD;
[a-zA-Z0-9\/.-]+    yylval=strdup(yytext);return FILENAME;
\"                    return QUOTE;
\{                    return OBRACE;
\}                    return EBRACE;
;                    return SEMICOLON;
\n                     /* ignore EOL */
[\t]+                /* ignore whitespace */
%%

仔細看你會發現yylval有所不一樣!咱們再也不指望它是一個整數,而是假設它爲一個字符串。爲了使其保持簡單,採用了strdup而且浪費了一些內存。
使用字符串是由於大多數時候咱們處理的是名字:文件名和區域名。稍後咱們會解釋如何多類型數據。

爲了告訴YACC中yylval的類型,將下面的一行添加到YACC語法中:

#define YYSTYPE char *

語法自己也變得更復雜了,爲了使其更容易理解,咱們將其分紅幾個部分來介紹。

commands:
        |
        commands command SEMICOLON
        ;

        command:
                zone_set
                ;
        zone_set:
                ZONETOKE quotedname zonecontent
                {
                    printf("Complete zone for '%s' found \n",$2)
                }
                ;

上面是個引子,包含了前面提到的遞歸,請注意咱們指明瞭命令集以;結束。咱們定義了一個叫zone_set的命令,它包含ZONE符號(單詞zone),後面跟着一個帶引號的名稱和zonecontentzonecontent很簡單:

zonecontent:
        OBRACE zonestatements EBRACE

它以一個OBRACE({)爲開始,而後跟着zonestatements,再跟着一個EBRACE(})。

qutedame:
    QUOTE FILENAME QUOTE
    {
        $$=$2
    }

上面定義了quotedname:一個在引號中間的文件名。而後特別定義:quotedname符號的值是FILENAME,即quotedname的值是其自己文件名,但不包含包裹着它的引號。這就是命令$$=$2的含意。它指:個人值是我自己的第二個部分。當quotedname在其它規則中被引用時,可經過$取其值,實際獲得的值是經由$$=$2指定的。

zonestatements:
        |
        zonestatements zonestatement SEMICOLON
        ;
zonestatement:
        statements
        |
        FILETOK quotedname
        {
            printf("A zonefile name '%s' was encountered\n",$2);
        }
        ;

以上是zone塊裏面全部申明的框架,咱們又一次看到了遞歸。

block:
        OBRACE zonestatements EBRACE SEMICOLON
        ;
statements:
        | statements statement
        ;
statement: WORD | block | quotedname

上面定義了一個塊,裏面包含了申明語句。
執行它,獲得以下結果:

$ ./example6
zone "." {
        type hint;
        file "/etc/bind/db.root";
        type hint;
};
A zonefile name '/etc/bind/db.root' was encountered
Complete zone for '.' found

5. 用c++製做解析器

儘管Lex和YACC比C++要出現的早,但也能夠生成一個c++版的解析器。雖然Flex包含一個能夠生成c++的分詞器的參數 ,但咱們不會使用它,由於YACC不知道如何直接使用它們。

我比較喜歡經過Lex生成一個c語言文件,而後再用YACC生成c++代碼。不過當你使用連接器生成你程序時,可能會遇到一些問題,由於c++代碼默認不能找到C語言中的函數。除非你用extren申明這些函數。爲了這樣作,在YACC中放入以下的C代碼:

extern "C"
{
    int yyparse(void);
    int yylex(void);
    int yywrap()
    {
        return 1;
    }
}

若是你想申明或者改變yydebug,你得這樣作:

extern int yydebug;
main()
{
    yydebug=1;
    yyparse();
}

你也許已經發現須要將YYSTYPE的定義放到Lex文件中,由於C++是嚴格類型的檢查。

用下以方式編譯:

lex bindconfig2.l
yacc −−verbose −−debug −d bindconfig2.y −o bindconfig2.cc
cc −c lex.yy.c −o lex.yy.o
c++ lex.yy.o bindconfig2.cc −o bindconfig2

由於YACC使用了-o選項,y.tab.h如今被稱做bindconfig2.cc.h。
總結:不要將分詞器編譯成c++。用c++生成語法解析器時須要用exetern "C"語句告訴編譯器C中的函數。

6. Lex和YACC內部工做原理

在YACC文件中,main函數調用了yyparse(),此函數由YACC替你生成的,在y.tab.c文件中。

函數yyparseyylex中讀取符號/值組成的流。你能夠本身編碼實現這點,或者讓Lex幫你完成。在咱們的示例中,咱們選擇將此任務交給Lex。

Lex中的yylex函數從一個稱做yyin的文件指針所指的文件中讀取字符。若是你沒有設置yyin,默認是標準輸入(stdin)。輸出爲yyout,默認爲標準輸出(stdout)。

你能夠在yywrap函數中修改yyin,此函數在每個輸入文件被解析完畢時被調用,它容許你打開其它的文件繼續解析,若是是這樣,yywarp的返回值爲0。若是想結束解析文件,返回1。

每次調用yylex函數用一個整數做爲返回值,表示一種符號類型,告訴YACC當前讀取到的符號類型,此符號是否有值是可選的,yylval即存放了其值。

默認yylval的類型是整型(int),可是能夠經過重定義YYSTYPE以對其進行重寫。分詞器須要取得yylval,爲此必須將其定義爲一個外部變量。原始YACC不會幫你作這些,所以你得將下面的內容添加到你的分詞器中,就在#include<y.tab.h>下便可:

extern YYSTYPE yylval;

Bison會自動幫你作這些。

6.1 符號值

前面提到過,函數yylex須要返回它遇到的符號類型,並將其值放到yylval中。這些符號經由命令%token定義,並對其賦值了數字類型的id號,以256開始。

基於此,全部ascii字符均可以做爲一個符號。比方說你要寫一個計算器,到目前爲止,咱們能夠寫一個以下的分詞器:

[0-9]+            yylval=atoi(yytext);return NUMBER;
[ \n]+            /*eat whitespace */;
-                return MINUS;
\*                return MULT
\+                return PLUS;
...

語法能夠是這樣:

exp:    NUMBER
        |
        exp PLUS exp
        |
        exp MINUS exp
        |
        exp MULT exp

其實不必這樣複雜。經過使用ascii字符爲符號的id,分詞器能夠寫成這樣:

[0-9]+            yylval=atoi(yytext);return NUMBER;
[ \n]+            /*eat whitespace */;
.                return (int)yytext[0];
...

.匹配全部匹配的單字符。對應的語法爲:

exp:    NUMBER
        |
        exp '+' exp
        |
        exp '-' exp
        |
        exp '*' exp

這樣看起來更直接也更短了,你不須要在頭部使用%定義那些字符。

這樣作還有一個優勢,即對於全部的輸入,Lex都會匹配,避免了默認不匹配時將其輸出到標準輸出。比方說用戶在計算器中使用^,會產生一個解析錯誤,而非將其輸出到標準輸出。

6.2 遞歸:'右便是錯'

遞歸是YACC必不可少的。沒有它,你就不能指定一個文件包含一系列的獨立命令或語句。根據規定,YACC僅對第一條規則感興趣,或者使用%start符號指定的起始規則。

YACC中的遞歸分爲兩類:左遞歸和右遞歸。大部分時候你應該使用左遞歸,就像這樣:

commands:    /*empty*/
        |
        commands command

它的意思是,一個命令集要麼是空,要麼它包含更多的命令集以及後面跟着一個命令。YACC的工做方式意味着它能夠輕鬆的砍掉單獨的命令塊(從前面)並逐步歸約它們。
與左遞歸相比,右遞歸迷惑了大部分人,以爲看起來更好:

commands:    /*empty*/
        |
        command commands

但這樣代價過高了。若是使用%start規則,須要YACC將全部的命令放在棧上,消耗不少的內存。所以儘量使用左遞歸解析長語句,好比解析整個文件。
有時則無可避免的使用右遞歸,若是你的語句不是太長,你不須要想盡一切方法使用左遞歸。

若是命令有終結符,右遞歸看起來更天然一些,可是仍然代價昂貴:

commands:    /*empty*/
        |
        command SEMICOLON commands

正確的代碼是使用左遞歸(並不是我本身發明的):

commands:    /* empty */
        |
        commands command SEMICOLON\

本文較早的版本使用了右遞歸,Markus Triska 友情斧正。

6.3 高級yylval:%union

如今,咱們須要定義yylval的類型,雖然這並不老是合適的。有時咱們須要處理多類型的數據。回到早前的溫度調節器示例,假設咱們想要可以選擇一個加熱器進行控制,像這樣:

heater mainbuiling
        Selected 'mainbuilding' heater
target temperature 23
        'mainbuilding' heater target temperature now 23

咱們稱這這種yylval是個聯合體,它便可以處理字符串,也能夠是整數,但不是同時處理這兩種。

以前說過,YACC的yylval類型是取決於YYSTYPE,能夠想象,咱們能夠經過定義YYSTYPE爲聯合體。不過YACC有一個更簡單的方法:使用%union語句。

基於例4,如今咱們寫出以下的YACC語法(Example 7),剛開始爲:

%token TOKHEATER TOKHEAT TOKTARGET TOKTEMPERATURE

%union {
    int number;
    char *string;
}

%token <number> STATE
%token <number> NUMBER
%token <string> WORD

定義了咱們的聯合體,它僅包含數字和字體串,而後使用一個擴展的%token語法,告訴YACC應該取聯合體的哪個部分。

這個例子中,咱們定義STATE 爲一個整數,這點跟前面同樣,NUMBER符號用於讀取溫度值。

不過新的WORD被定義爲一個字符串。

分詞器文件也有不少改變:

%{
#include <stdio.h>
#include <string.h>
#include "y.tab.h"
%}
%%
[0−9]+             yylval.number=atoi(yytext); return NUMBER;
heater             return TOKHEATER;
heat             return TOKHEATER;
on|off             yylval.number=!strcmp(yytext,"on"); return STATE;
target             return TOKTARGET;
temperature     return TOKTEMPERATURE;
[a−z0−9]+         yylval.string=strdup(yytext);return WORD;
\n                 /* ignore end of line */;
[ \t]+             /* ignore whitespace */;
%%

如你所見,咱們再也不直接獲取yylval的值,而是添加一個後綴指示想取得哪一個部分的值。不過在YACC語法中,咱們無須這樣作,由於YACC爲咱們作了神奇的這些:

heater_select:
        TOKHEATER WORD
        {
            printf("\tSelected heater '%s'\n",$2);
            heater=$2;
        }
        ;

因爲上面的%token定義,YACC自動從聯合體中挑選string成員。同時也請注意,咱們保存了一份$2的副本,它在後面被用於告訴用戶是哪個加熱器發出的命令:

target_set:
        TOKTARGET TOKTEMPERATURE NUMBER
        {
            printf("\tHeater '%s' temperature set to %d\n",heater,$3);
        }
        ;

更多詳情請參考example7.y。

7. 調試

特別是剛學習時,調度工具很是重要。幸運的是,YACC可以給出許多反饋信息。這些反饋信息須要必定的開銷,你須要一些開關參數來啓用它們。

當你編譯語法文件時,在YACC命令行中增長 --debug--verbose。在語法的C語言的頭部,添加以下:

int yydebug=1;

這樣會生成文件y.output,裏面解釋了咱們建立的狀態機。

當你運行生成的二進制,它會輸出不少信息,包含狀態機目前的狀態,以及哪些符號被讀取了。

Peter Jinks 寫了一篇關於調式方面的文章,包含一些常見的錯誤及其處理方法。

7.1 狀態機

YACC解析器內部運行着一個叫狀態機的東西。這個名字暗示着這個機器有多種狀態。而規則控制着狀態機從一個狀態到另一個狀態的改變。全部的東西起始於以前我提到的的規則。

引用示例7中y.output的輸出內容:

state 0
    ZONETOK     , and go to state 1
    $default    reduce using rule 1 (commands)
    commands    go to state 29
    command     go to state 2
    zone_set     go to state 3

默認狀況下,這個狀態經由commands規則歸約,這是前面提到的由多個單一命令語句創建起來的遞歸規則造成的命令集,後跟一個;,也許還有更多的命令集。

狀態一直遞減,直到遇到它能理解的東西,在這個例子裏,好比一個ZONETOKE,單詞zone。而後它轉向狀態1,它將處理一個zone 命令:

state 1
    zone_set  −>  ZONETOK . quotedname zonecontent   (rule 4)
    QUOTE       , and go to state 4
    quotedname  go to state 5

上面的第一行有一個.在裏面,它指示所處的位置:咱們正好遇到一個ZONETOK,如今尋找quotedname。很明顯,一個quotedname起始於一個QUTOTE,而它將咱們轉向狀態4。

欲進一步瞭解,用調試一節提到的參數編譯Example 7。

7.2 衝突:'移進/歸約','歸約/歸約'

只要YACC發出關於衝突的警告,可能就有麻煩了。解決這些衝突彷佛是門藝術,也許會讓你對那門語言理解的更深入,遠比你想知道的多。

解決問題圍繞着如何解釋一系列的符號。假設咱們定義了一門語言,它須要接收一系列的命令:

delete heater all
delete heater number1

爲此,咱們這樣定義語法:

delete_heaters:
        TOKDELETE TOKHEATER mode
        {
                deleteheaters($3);
        }
mode:    WORD
delete_a_heater:
        TOKDELETE TOKHEATER WORD
        {
                delete($3);
        }

也許你已經感受到了有問題。狀態機開始讀入單詞'delete',而後須要由接下來的符號決定轉向哪。這個接下來的符號便可以是一個mode,指明瞭如何刪除加熱器,或者一個待刪除的加熱器。

但問題出自於這兩個命令的下一個符號是WORD。YACC不知道應該要怎樣作,這致使了一個'歸約/歸約'警告,以及一個更具體的警告:'delete_a_heater'永遠不能被訪問。

這個示例的衝突很容易解決(例如,將第一個命令重命名爲'delete heaters all',或者將'all'單獨定義爲一個符號),可是有時卻很是困難。用--verbose標記生成的y.output文件可以起到很大的幫助。

8. 深度閱讀

GUN YACC (Bison)帶有一個很常不錯的info文件(.info),它是很是好的YACC語法文檔,除了裏面僅提到了一次Lex,其它的都還好。可使用Emacs閱讀info文件,或者很是不錯的工具pinfo。

Flex有一個不錯的用戶手冊,若是你已經理解Flex是作什麼的,它仍是很是有用的。

讀完了這個Lex和YACC介紹,你可能想找到更多的信息。雖然如下的書我一本都沒看過,不過據說不錯:

  1. Bision-The Yacc-Compatible Parser Generator

  2. Lex&Yacc

  3. Compliers: Principles,Techiniques,and Tools

Tohmas Niemann 寫了一篇文檔,討論如何使用Lex和YACC寫一個編譯器和計算器。
usenet新聞組com.compilers也是很是有用的,不過請記住,那些人並不是專門服務支持,在你發貼以前,閱讀他們的感興趣的頁面,特別是FAQ

Lex-A Lexical Analyzer Generator,M.E.Lesk and E.Schmidt,最原始的論文。
Yacc: Yet Another Compiler

9. 感謝

  • Pete Jinks <pjj%cs.man.ac.uk>

  • Chris Lattner <sabre%nondot.org>

  • John W. Millaway <johnmillaway%yahoo.com>

  • Martin Neitzel <neitzel%gaertner.de>

  • Esmond Pitt <esmond.pitt%bigpond.com>

  • Eric S. Raymond

  • Bob Schmertz <schmertz%wam.umd.edu>

  • Adam Sulmicki <adam%cfar.umd.edu>

  • Markus Triska <triska%gmx.at>

  • Erik Verbruggen <erik%road−warrior.cs.kun.nl>

  • Gary V. Vaughan <gary%gnu.org> (read his awesome Autobook) • Ivo van der Wijk ( Amaze Internet)

相關文章
相關標籤/搜索