轉自:http://coanor.blog.hexun.com/38241166_d.html
1. 簡介
只要你在Unix環境中寫過程序,你必定會邂逅神秘的Lex&YACC,就如GNU/Linux用戶所熟知的Flex&Bison,這里的Flex就是由Vern Paxon實現的一個Lex,Bison則是GNU版本的YACC.在此我們將統一稱呼這些程序為Lex和YACC.新版本的程序是向上兼容的(譯注:即兼容老版本),所以你可以用Flex和Bison來嘗試下我們的實例.
這些程序實用性極廣,但如同你的C編譯器一樣,在其主頁上並沒有描述它們,也沒有關於怎樣使用的信息.當和Lex結合使用時,YACC實在是棒極了,但是Bison的主頁上並沒有描述Bison如何跟Lex結合使用以生成代碼的相應說明.
1.1 本文檔『不是』…
有很多非常棒的書籍描述了Lex & Yacc.如果對這些書籍感興趣,務必加以閱讀並加深理解.相比我們,它們總會提供更多的信息.參見文檔結尾的”進一步閱讀”部分.本文檔的目的在於以步步為營的方式教會你使用Lex&Yacc,使得你可以創建你自己的程序.
本文檔對於Flex和Bison同樣適用,但並不是一個關於它們的手冊.它們都有自己非常好的HOWTO文檔.它們的參考在文檔結尾.
我並不是一個YACC/Lex專家.當我寫這個文檔的時候,實際上我只有2天的使用經驗.所有我想完成的工作就是讓你輕松的度過這兩天.
1.2 下載事項
請注意,所有展示的這些實例,你都可以下載,這些都是機器可讀(沒有加密).更多細節參見主頁.
2.Lex & YACC能為你做什么?
如果使用得當,這些程序(指LEX&YACC)可以讓你輕易的解析復雜的語言,當你需要讀取一個配置文件時,或者你需要編寫一個你自己使用的語言的編譯器時,這對於你來說是莫大的裨益.
本文檔能提供給你一些幫助,你將發現你再也不用手工寫解析器了,Lex & YACC就是為你量身打造的利器.
2.1 每個程序自身是如何工作的?
雖然這些程序協同工作,但是它們各自的目的不同.下一章將對此加以描述.
3. Lex
Lex會生成一個叫做『詞法分析器』的程序.這是一個函數,它帶有一個字符流傳入參數,詞法分析器函數看到一組字符就會去匹配一個關鍵字(key),采取相應措施.一個非常簡單的例子(example1)如下:
%{
#include <stdio.h>
%}
%%
stop printf(“Stop command received\n”);
start printf(“Start command received\n”);
%%
第一部分,位於%{和%}對之間直接包含了輸出程序(stdio.h).我們需要這個程序,因為使用了printf函數,它在stdio.h中定義.
第二部分用’%%’分割開來,所以第二行起始於’stop’,一旦在輸入參數中遇到了’stop’,接下來的那一行(printf()調用)將被執行.
除此之外,還有’start’,其跟stop的行為差不多.
我們再次用’%%’結束代碼段.
為了編譯上面的例子,只需要執行以下命令:
lex example1.l
cc lex.yy –o example –ll
注意:如果你用flex,則就將lex命令用flex代替,還需要將’-ll’選項改成’-lfl’.在RedHat 6.x以及SuSe中需要這樣做.
這樣,Lex將生成’example1’這個文件.運行該文件,它將等待你輸入一些數據.每次你輸入一些不匹配的命令(非’stop’和’start’),它會將你輸入的字符再次輸出.你若輸入’stop’,它將輸出’Stop command received’.
用一個EOF(^D)來結束程序.
也許你想知道,它是怎么運行的,因為我們並沒有定義main()函數.這個函數(指main())已經在lib1(liblex)中定義好了,在此我們選用了編譯選項’-ll’
3.1 匹配中的正則表達式
這個實例(example2)本身並沒什么用處,下一個實例也不會提及正則表達式.但這里它展示了如何在Lex中使用正則表達式,這在后面將非常有用.
%{
#include <stdio.h>
%}
%%
[0123456789]+ printf(“NUMBER\n”);
[a-zA-Z][a-zA-Z0-9]* printf(“word\n”);
該Lex文件描述了兩種token匹配:WORDs和NUMBERs.正則表達式非常恐怖,但是只需要稍花力氣便可以加以理解.我們來解說下NUMBER匹配:
[0123456789]+
這意味着:一個只少有一個數字的序列,這些數字來自0123456789組中.我們也可以將其寫成:
[0-9]+
現在,WORD匹配就有點復雜:
[a-zA-Z][a-zA-Z0-9]*
第一部分僅僅匹配一個’a’到’z’或’A’到’Z’之間的字符,也即一個字母.接着該字母后面需要連上0個或多個字符,這些字符可以是字母,也可以是數字.這里為何用’*’? ’+’表示至少1次的匹配.一個WORD只有一個字符也可以很好的匹配,在第一部分我們已經匹配到了一個字符,所以第二部分可以是0個匹配,所以用’*’.
用這種方式,我們就模仿了很多編程語言中對於一個變量名的要求,即要求變量名『必須』以字母開頭,但是可以在后續字符中用數字.也就是說’temperature1’是一個正確的命名,但是’1temperature’就不是.
像example1一樣編譯example2,並輸入一些文本,如下:
$ ./example2
foo
WORD
bar
WORD
123
NUMBER
bar123
WORD
123bar
NUMBER
WORD
你也許會疑惑,所有的輸出中的空格是從哪來的?理由很簡單:從輸入而來,我們不在空格上匹配任何內容,所以它們又輸出來了.
Flex主頁上有正則表達式的詳細文檔.很多人覺得perl正則表達式主頁的說明非常有用,但是Flex並不實現perl所實現的所有東西.
你只需要確保不寫一些形如’[0-9]*’的空匹配即可,你的詞法分析器(由Flex生成)將不明就里的開始不斷的匹配空字符.
3.2 復雜一點的類C語法示例
假定我們需要解析一個形如下面的文件:
logging{
category lame-servers { null; };
category cname { null; };
};
zone “.” {
type hint;
file “/etc/bind/db.root”;
}
我們在此見到了很多token:
WORD: 如’zone’和’type’
FILENAME:如“/etc/bind/db.root”
QUOTE: 如包含文件名的引號
OBRACE:{
EBRACE: }
SEMICOLON: ;
example3相應的Lex文件如下:
%{
#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 */;
%%
當輸入我們的文件到Lex生成的example3中,我們得到:
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
4. YACC
YACC可以解析輸入流中的標識符(token),這就清楚的描述了YACC和LEX的關系,YACC並不知道『輸入流』為何物,它需要事先就將輸入流預加工成標識符,雖然你可以自己手工寫一個Tokenizer,但我們將這些工作留給LEX來做。
YACC用來為編譯器解析輸入數據,即程序代碼.這些用編程語言寫成的程序代碼一點也不模棱兩可——它們只有一個意思.正因為如此,YACC才不會去對付那些有歧義的語法,並且會抱怨shift/reduce或者reduce/reduce沖突.更多的關於模糊性和YACC『問題』可以在『沖突』一章中找到.
4.1 一個簡單的溫度控制器
假定我們有一個溫度計,我們要用一種簡單的語言來控制它.關於此的一個會話、如下:
heat on
Heater on!
heat off
Header off!
target temperature set!
我們需要識別的標識符為heat, on/off(STATE), target, temperature, NUMBER.
LEX的tokenizer(example4)為:
%{
#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』,第二,我們不再打印輸出了,我們返回標識符的名字.之所這樣做是因為我們將這些返回傳送給了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”);
}
;
第一部分,我們稱之為根(root).它告訴我們有一個『commands』,並且這些『commands』由單個的『command』組成.正如你所見到的那樣,這是一個標准的遞歸結構,因為它又再次包含了『commands』.這意味着該程序可以一個個的遞減一系列的命令.參見『YACC和LEX內部是如何工作的』一章,閱讀更多的遞歸細節.
第二個規則定義了『command』的內容.我們只假定兩種命令.
一個heat_switch由HEAT標識符組成,它后面跟着一個狀態,該狀態在LEX中定義,為『on』或『off』.
target_set稍微有點復雜,它由TARGET標識符、TEMPERATURE以及一個數字組成.
一個完整的YACC文件
前面的那個例子只有YACC文件的語法部分,起始在YACC文件中還有其它內容,那就是我們省略的文件頭部分:
%{
#include <stdio.h>
#include <string.h>
void yywrap()
{
return 1;
}
main()
{
yyparse();
}
%}
%token NUMBER TOKHEAT STATE TOKTARGET TOKTEMPERATURE
函數yyerror()在YACC發現一個錯誤的時候被調用,我們只是簡單的輸出錯誤信息,但其實還可以做一些更漂亮的事情,參見文檔尾的『進階閱讀』部分.
yywrap()函數用於不斷的從一個文件中讀取數據,當遇到EOF時,你可以再輸入一個文件,然后返回0,你也可以使得其返回1,暗示着輸入結束.更多細節,參見『YACC和LEX內部如何工作的?』一章.
接着,這里有一個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()函數,並不需要libl來提供.
注意:如果在編譯過程中報錯說找不到『yylval』,那么在example4.l的#include <y.tab.h>下面加上:
extern YYSTYPE yylval;
具體細節在『YACC和LEX內部是如何工作的?』中解說.
一個簡單的會話:
$ ./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;
temperateure return TOKTEMPERATURE;
\n /* ignore end of line */
[ \t]+ /* ignore whitespace */
%%
如你所見,我們在yytext中用了atoi(),並將結果存儲在yylval中,使得YACC可以『看見』它. 同理,我們再處理STATE匹配,將其與『on』比較,若想等,則將yylval設置為1.
接下來,我們就得考察YACC如何來應對.我們來看看新的temperature target規則設置:
target_set:
TOKTARGET TOKTEMPERATURE NUMBER
{
printf(“\tTemperature set to %d”, $3);
}
;
為得到規則中第三部分的值(NUMBER),我們用『$3』來表示,每次yylex()返回時,yylval的值便依附到了終結符上,其值可以通過『$-常數』來獲取.
為更進一步加深理解,我們來看『heat_switch』規則:
heat_switch:
TOKHEAT STATE
{
if ($2)
printf(“\tHeat turned on\n”);
else
printf(“\tHeat turned off\n”);
}
如果現在運行example5,它將輸出你所輸入的數據.
4.3 解析一個配置文件
讓我們再次回顧先前提到的那個配置文件:
zone “.” {
type hint;
file “/etc/bind/db.root”;
};
之前我們已經將LEX文件寫好了,接下來只需要編寫YACC語法文件,並且對詞法分析器做一些修改,使得其可以返回一些值給YACC.
example6中的詞法分析器如下:
%{
#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;
[ \t]+ /* ignore whitespace */;
\n /* ignore EOL */;
%%
細心的話,你會發現yylval已經改變了!我們不再認為它是一個整數,而是假定為一個char*類型數據.為保持簡單性,我們調用strdup並因此耗費了很多內存.但這並不影響你解析這個文件.
我們需要保存字符串的值,在此我們處理的都是一些命名,文件名以及區域命.在下一章,我們將解說如何對付一些復雜類型的數據.
為通知YACC關於yylval的新類型,我們在YACC的語法文件中添加一行:
#define YYSTYPE char *
語法自身也變得更加復雜.我們將其分割出來,以便慢慢消化:
commands :
| commands command SEMICOLON
;
command :
zone_set
;
zone_set :
ZONETOK quotedname zonecontent
{
printf(“Complete zone for ‘%s’ found\n”, $2);
}
;
這里包含了前面提到的那個『root』遞歸結構. 繼續:
zonecontent : OBRACE zonestatements EBRACE
quotedname : QUOTE FILENAME QUOTE
{
$$ = $2;
}
這里定義了『quotedname』,這里值得注意的是,quotedname的值就是FILENAME的值.
zonestatements : zonestatements zonestatement SEMICOLON
;
zonestatement : statements
| FILETOK quotedname
{
printf(“A zonefile name ‘%s’ was encountered\n”, $2);
}
;
在這里,我們又看到了遞歸結構.
block : OBRACE zonestatements EBRACE SEMICOLON
;
statements : statements statement
;
statement ; WORD | block | quotedname
;
這里則定義了一個塊(block),其中有『statements』定義.
我們執行example6的輸出文件:
$ ./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++解析器.但我們用LEX來生成C++的詞法分析器,YACC並不知道如何直接來處理這些,所以我們不打算這么做.
我認為比較好的做法是,要做一個C++解析器,就需要LEX生成一個C文件,並且讓YACC來生成C++代碼.但當你這么做的時候,在這個過程中你將會遇到問題,因為C++代碼默認情況下並不能找到C的函數,除非你將那些函數定義為extern “C”.
為達此目的,我們在YACC代碼中編寫一個C開頭:
extern “C”
{
int yyparse(void);
int yylex(void);
int yywrap()
{
return 1;
}
}
如果你想聲明並改變yydebug函數,你得這樣做:
extern int yydebug;
main()
{
yydebug = 1;
yyparse();
}
這是因為C++中的一個關於定義的規則,即不允許yydebug的多處定義.
你還可能發現,你需要在你的LEX文件中重復#define YYSTYPE,這是由於C++中嚴格的類型檢查(機制)造成的.
按照如下方式來編譯:
lex bindconfig2.1
yacc –verbose –debug –d bindconfig2.y –o bindconfig2.cc
cc –c lex.yy –o lex.yy.o
c++ lex.yy.o bindconfig2.cc –o bindconfig2
由於-o選項的存在,y.tab.h現在變成bindconfig2.cc.h,記住這點.
總結: 不要自尋煩惱的在C++中編譯你的詞法分析器,讓它呆在C的領地里.在C++中編寫解析器時,(也得)確保向編譯器解釋清楚,即你的C函數都有一個extern “C”聲明.
6.Lex和YACC內部是如何工作的?
在YACC文件中,你定義了你自己的main()函數,它在某個點上調用了yyparse().YACC會創建你的yyparse()函數,並在y.tab.c中結束該函數.
yyparse()函數讀取一個『標識符/值對』(token/value pairs)流,這些流需要事先就提供,這些流可以是你自己手寫的代碼提供的,也可以是LEX生成的.在我們的示例中,我們把這個工作丟給了LEX.
LEX生成的yylex()函數從文件參數FILE *file中讀取字符(文件名為yyin).如果不設置成yyin,則默認為標准輸入,它會輸出到yyout中,如果不加設置,就是stdout.你可以在yywrap()函數中修改位於文件尾的yyin.yywrap()函數.這些修改使得你可以打開另一些文件,並繼續解析.
如果是這種情況,那么就讓yywrap()返回0,如果你想在該文件上結束解析,就讓它返回1.
每次yylex()調用都會返回一個整數值,該值代表了一個標識符類型(token type).它告訴YACC,已經讀取了這種標識符.該標識符可以有一個值,它應該存放在yylval變量中.
yylval的默認類型為int,但是你可以修改其類型,通過在YACC文件中#define YYSTYPE.
詞法分析器需要能夠訪問yylval,為達到此效果,(yylval)必須在詞法分析器(lexer)中被聲明為一個外部變量(extern variable).原來的YACC忽略了這點,並沒有為你干這項工作,所以,你必須添加以下代碼到你的詞法分析器中,就在#include <y.tab.h>下面:
extern YYSTYPE yylval;
當今多數人使用的Bison已經為你把這事自動做好了.
6.1 標識符的值(token values)
在前面我已經說過,yylex()需要返回它遇到了一個什么標識符類型,並將其值存儲在yylval中.當這些標識符在%token命令中定義時,它們就被賦予了一些數字ID,從256開始.
由於這個事實,(我們)可以將所有的ascii字符當作標識符.假定你要寫一個計算器,到現在為止,我們可能已經這樣寫了其詞法分析器:
[0-9]+ yylval = atoi(yytext); return NUMBER;
[ \n]+ /* eat whitespace */
- return MINUS;
\* return MULT;
\+ return PLUS;
…
YACC文件可能是這樣:
exp ; NUMBER
| exp PLUS exp
| exp MINUS exp
| exp MULT exp
…
沒有必要弄這么復雜.用字符作為速記法來作為標識符的數字ID,我們可以這樣來重寫我們的詞法分析器:
[0-9]+ yylval = atoi(yytext); return NUMBER;
[ \n]+ /* eat whitespace */
. return (int) yytext[0];
最后一行匹配任何的單個字符,否則就是不匹配字符.
而YACC的語法文件則是這樣:
exp : NUMBER
| exp ‘+’ exp
| exp ‘-’ exp
| exp ‘*’ exp
這樣更加簡短而直觀,你就不必在文件頭用%token來定義那些ascii字符了.
另一方面,這樣構造還有一些好處,它可以匹配所有丟給它的東西,而避免了將那些不匹配的輸入輸出到標准輸出的默認行為.如果用戶在當前計算器上輸入一個’^’字符,將會導致一個解析錯誤,而不是將其輸出到標准輸出中.
6.2 遞歸:’right is wrong’
遞歸是YACC一個極其重要的特性.沒有遞歸的話,你就確定一個文件是由一系列獨立的命令組成還是由語句組成.由於YACC自身的特性,它只對第一個規則或那個你將其設計為『起始規則』的規則感興趣.起始規則用’%start’符號標記.
YACC中的遞歸以兩種形式出現,左遞歸和右遞歸.左遞歸是你應該經常使用的,它們看起來如下:
Commands : /* empty */
| commands command
這是在說,一個command要么為空,要么它包含了更多的commands,后面再跟一個command.YACC的這種工作方式意味着它現在可以輕易的剔除單個的command群並一步步簡化(處理).
拿上面的例子和下面的右遞歸做比較:
Commands : /* empty */
| command commands
但是這樣做開銷有點大,如果(commands)是%start規則(即起始規則),那么YACC會將所有的commands保存在你的棧數據中(file on the stack),這將耗費大量內存.所以,在解析長的語句時,務必使用左遞歸,例如整個文件.但有時難以避免右遞歸,不過,如果你的語句並不太長,你就沒有必要越軌使用左遞歸.
如果你有一些東西來終結(因此而分割)你的commands,右遞歸就非常適合了,但開銷還是有點大:
Commands : /* empty */
| command SEMICOLON commands
正確的做法是使用左遞歸(這也不是我發明的):
commands : /* empty */
| commands command SEMICOLON
本文檔的早期版本錯誤的使用了右遞歸.Markus Triska友好的提示我們這點(錯誤).
6.3 高級yylval: %union
現在,我們需要定義yylval的類型.但是這並不一直恰如其當.我們可能會多次這樣做,因為需要處理多種數據類型.回到我們假定的那個恆溫器,可能你想選擇控制一個加熱器,例如:
heater mainbuilding
Selected ‘mainbuilding’ heater
Target temperature 23
‘mainbuilding’ heater target temperature now 23
這樣的話,就要求yylval是一個union,它可以存儲字符串,也可以存儲整數,但並不是同時存儲.
回憶之前我們講過,我們提前通知YACC哪種yylval類型會要處理是通過定義YYSTYPE來實現.同樣,我們可以定義YYSTYPE為一個union,YACC中有一種簡便的方法來實現,即%union語句.
在example4基礎上,我編寫example7的YACC語法:
%token TOKHEATER TOKHEAT TOKTARGET TOKTEMERATURE
%union
{
int number;
char *string;
}
%token <number> STATE
%token <number> NUMBER
%token <string> WORD
我們定義了union,它只包含一個整數和一個字符串.接着使用了一個擴展的%token語法,我們向YACC解釋了應該獲取union哪個部分的標識符.
在本例中,我們讓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 TOKHEAT;
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自動選擇了union中的’string’成員.注意這里$2中存儲的一份拷貝,在后面它會告訴用戶發送命令到哪個heater:
target_set
TOKTARGET TOKTEMPERATURE NUMBER
{
printf(“\tHeater ‘%s’ temperature set to %d\n”, heater, $3);
}
;
7.調試
YACC中有許多調試反饋信息.這些調試信息的代價有點高,所以你需要提供一些開關來打開它.
當調試你的語法時,在YACC命令行中添加—debug和—verbose選項,在你的C文件頭中添加以下語句:
int yydebug = 1;
這將生成一個y.output文件,其中說明了所創建的那個狀態機.
當你運行那個生成的二進制文件,它將輸出很多運行時信息.里面包含當前所運行的狀態機以及讀取到的一些標識符.
Peter Jinks寫了一篇關於調試的文章,他在其中講述了一些常見得錯誤以及如何修正這些錯誤.
7.1 狀態機
YACC解析器在內部運行的是一個『狀態機』,該狀態機可以有多種轉台.接着有多個規則來管制狀態間的相互轉化.任何內容都是從『root』規則開始.
在example7的y.output文件中:
state 0
ZPONETOK , 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 satte 3
默認情形下,這個狀態機從『commands』規則開始遞減演化,這也是我們之前的那個遞歸規則,它定義了『commands』並從單個的『command』進行構造,后面跟着一個分號,然后可能再跟着更多的『commands』.
這個狀態機不斷遞減演化,直到它遇到某些它能理解的東西,在本例中,為一個ZONETOK,也即單詞『zone』.然后轉化到狀態1,在此,進一步處理一個zone command:
state 1
zone_set -> ZONETOK .quotedname zonecontent (rule 4)
QUOTE , and go to state 4
Quotedname go to state 5
第一行中有一個『.』,它用來說明我們所處的位置:即剛剛識別到了一個ZONETOK,目前正在尋找一個『quotedname』.顯然,一個『quotedname』會以QUOTE開始,它將我們轉化到狀態4.
為進一步跟蹤,用在『調試』章節中提到的標志來編譯example7.
7.2 沖突:『shift/reduce』以及『reduce/reduce』
一旦YACC警告你出現了沖突,那么你的麻煩來了.要解決這些問題顯得是一種技巧形式,它會教會很多關於你的語言的東西.比你想知道的要多的多的內容.
問題縈繞於如何來翻譯一系列標識符.假定我們定義了一種語言,它需要接受一下兩種命令:
delete heater all
delete heater number1
為達到此目的,我們的語法為:
delete_heaters :
TOKDELETE TOKHEATER mode
{
Deleteheaters($3);
}
mode : WORD
delete_a_heater:
TOKDELETE TOKHEATER WORD
{
delete($3);
}
也許你已經嗅到了麻煩的味道.狀態機從讀取單詞『word』開始,接着它根據下一個標識符覺得轉換到何種狀態.接下來的標識符可以是『mode』,它指定了如何來刪除heater,或者是將要刪除的heater.
然而這里的麻煩是,對於這兩個命令,下一個標識符都將是一個WORD.YACC因此也無法決定下一步該干嘛.這回導致一個『reduce/reduce』警告,而下一個警告就是『delete_a_heater』節點在狀態轉化圖中永遠也不能達到.
這種情況的沖突容易解決(例如,重命名第一個命令為『delete all heater』,或者將『all』作為一個分開的標識符),但有時,(要解決沖突)卻非常困難. 通過『--verbose』參數生成的y.output文件可以提供給你極大的幫助.
8.進一步閱讀
GNU YACC(Bison)有一個非常棒的.info文件,在其中很好的記錄了YACC的語法.其中只提到了一次LEX,然而它還是很棒的.你可以用Emacs中那個非常好的『pinfo』工具閱讀.info文件.同時,在Bison的主頁上可以獲得它:Bison手冊.
FLEX有一個不錯的主頁.如果你粗略了解了FLEX所作所為,那將是非常有益的.FLEX的手冊也可以聯機獲取.
在這些關於YACC和LEX的介紹之后,你可能覺得你想需要更多的信息.下面的書籍我們都還沒有閱讀,但他們聽起來不錯:
Bison—The Yacc-Compitible Parser Generator
——Charles Donnelly && Richard Stallman.
Lex & Yacc
——John R. Levine, Tony Mason ,Doug Brown.
Compilers: Principles, Techniques, and Tools
——By Alfred V.Aho,Ravi
完!
