作者: 胡彥 2013-4-28
代碼下載地址:http://pan.baidu.com/share/link?shareid=579088&uk=253544182
本架構是一個lex/yacc完整的樣本,包括詳細的注釋,用於學習lex/yacc程式基本的搭建方法,在linux/cygwin下敲入make就可以編譯和執行。大部分架構已經搭好了,你只要稍加擴充就可以成為一個計算機之類的程式,用於《編譯原理》的課程設計,或者對照理解其它lex/yacc項目的代碼。
本例子雖小卻示範了lex/yacc程式最重要和常用的特徵:
* lex/yacc程式組成結構、檔案格式。* 如何在lex/yacc中使用C++和STL庫,用extern "C"聲明那些lex/yacc產生的、要連結的C函數,如yylex(), yywrap(), yyerror()。* 重定義YYSTYPE/yylval為複雜類型。* lex裡多狀態的定義和使用,用BEGIN宏在初始態和其它狀態間切換。* lex裡Regex的定義、識別方式。* lex裡用yylval向yacc返回資料。* yacc裡用%token<>方式聲明yacc記號。* yacc裡用%type<>方式聲明非終結符的類型。* 在yacc嵌入的C代碼動作裡,對記號屬性($1, $2等)、和非終結符屬性($$)的正確引用方法。* 對yyin/yyout重賦值,以改變yacc預設的輸入/輸出目標。
本例子功能是,對目前的目錄下的file.txt檔案,解析出其中的標識符、數字、其它符號,顯示在螢幕上。linux調試環境是Ubuntu 10.04。
檔案清單:
lex.l:lex程式檔案。yacc.y:yacc程式檔案。main.h:lex.l和yacc.y共同使用的標頭檔。Makefile:makefile檔案。lex.yy.c:用lex編譯lex.l後產生的C檔案。yacc.tab.c:用yacc編譯yacc.y後產生的C檔案。yacc.tab.h:用yacc編譯yacc.y後產生的C標頭檔,內含%token、YYSTYPE、yylval等定義,供lex.yy.c和yacc.tab.c使用。file.txt:被解析的文本樣本。README.txt:本說明。
下面列出主要的代碼檔案:
main.h: lex.l和yacc.y共同使用的標頭檔
#ifndef MAIN_HPP#define MAIN_HPP#include <iostream>//使用C++庫#include <string>#include <stdio.h>//printf和FILE要用的using namespace std;/*當lex每識別出一個記號後,是通過變數yylval向yacc傳遞資料的。預設情況下yylval是int類型,也就是只能傳遞整型資料。yylval是用YYSTYPE宏定義的,只要重定義YYSTYPE宏,就能重新指定yylval的類型(可參見yacc自動產生的標頭檔yacc.tab.h)。在我們的例子裡,當識別出標識符後要向yacc傳遞這個標識符串,yylval定義成整型不太方便(要先強制轉換成整型,yacc裡再轉換回char*)。這裡把YYSTYPE重定義為struct Type,可存放多種資訊*/struct Type//通常這裡面每個成員,每次只會使用其中一個,一般是定義成union以節省空間的(但這裡用了string等複雜類型造成不可以){string m_sId;int m_nInt;char m_cOp;};#define YYSTYPE Type//把YYSTYPE(即yylval變數)重定義為struct Type類型,這樣lex就能向yacc返回更多的資料了#endif
lex.l: lex程式檔案
%{/*本lex的組建檔案是lex.yy.clex檔案由3段組成,用2個%%行把這3段隔開。第1段是聲明段,包括:1-C代碼部分:include標頭檔、函數、類型等聲明,這些聲明會原樣拷到產生的.c檔案中。2-狀態聲明,如%x COMMENT。3-正則式定義,如digit ([0-9])。第2段是規則段,是lex檔案的主體,包括每個規則(如identifier)是如何匹配的,以及匹配後要執行的C代碼動作。第3段是C函數定義段,如yywrap()的定義,這些C代碼會原樣拷到產生的.c檔案中。該段內容可以為空白*///第1段:聲明段#include "main.h"//lex和yacc要共用的標頭檔,裡麵包含了一些標頭檔,重定義了YYSTYPE#include "yacc.tab.h"//用yacc編譯yacc.y後產生的C標頭檔,內含%token、YYSTYPE、yylval等定義(都是C宏),供lex.yy.c和yacc.tab.c使用extern "C"//為了能夠在C++程式裡面調用C函數,必須把每一個需要使用的C函數,其聲明都包括在extern "C"{}塊裡面,這樣C++連結時才能成功連結它們。extern "C"用來在C++環境下設定C連結類型。{//yacc.y中也有類似的這段extern "C",可以把它們合并成一段,放到共同的標頭檔main.h中int yywrap(void);int yylex(void);//這個是lex產生的詞法分析函數,yacc的yyparse()裡會調用它,如果這裡不聲明,產生的yacc.tab.c在編譯時間會找不到該函數}%}/*lex的每個正則式前面可以帶有"<狀態>",例如下面的"<COMMENT>\n"。每個狀態要先用%x聲明才能使用。當lex開始運行時,預設狀態是INITIAL,以後可在C代碼裡用"BEGIN 狀態名;"切換到其它狀態(BEGIN是lex/yacc內建的宏)。這時,只有當lex狀態切換到COMMENT後,才會去匹配以<COMMENT>開頭的正則式,而不匹配其它狀態開頭的。也就是說,lex當前處在什麼狀態,就考慮以該狀態開頭的正則式,而忽略其它的正則式。其應用例如,在一段C代碼裡,同樣是串"abc",如果它寫在程式碼片段裡,會被識別為標識符,如果寫在注釋裡則就不會。所以對串"abc"的識別結果,應根據不同的狀態加以區分。本例子需要忽略掉文本中的行末注釋,行末注釋的定義是:從某個"//"開始,直到行尾的內容都是注釋。其實現方法是:1-lex啟動時預設是INITIAL狀態,在這個狀態下,串"abc"會識別為標識符,串"123"會識別為整數等。2-一旦識別到"//",則用BEGIN宏切換到COMMENT狀態,在該狀態下,abc這樣的串、以及其它字元會被忽略。只有識別到分行符號\n時,再用BEGIN宏切換到初始態,繼續識別其它記號。*/%x COMMENT/*非數字由大小寫字母、底線組成*/nondigit([_A-Za-z])/*一位元字,可以是0到9*/digit([0-9])/*整數由1至多位元字組成*/integer({digit}+)/*標識符,以非數字開頭,後跟0至多個數字或非數字*/identifier({nondigit}({nondigit}|{digit})*)/*一個或一段連續的空白符*/blank_chars([ \f\r\t\v]+)/*下面%%後開始第2段:規則段*/%%{identifier}{//匹配標識符串,此時串值由yytext儲存yylval.m_sId=yytext;//通過yylval向yacc傳遞識別出的記號的值,由於yylval已定義為struct Type,這裡就可以把yytext賦給其m_sId成員,到了yacc裡就可以用$n的方式來引用了return IDENTIFIER;//向yacc返回: 識別出的記號類型是IDENTIFIER}{integer}{//匹配整數串yylval.m_nInt=atoi(yytext);//把識別出的整數串,轉換為整型值,儲存到yylval的整型成員裡,到了yacc裡用$n方式引用return INTEGER;//向yacc返回: 識別出的記號類型是INTEGER}{blank_chars}{//遇空白符時,什麼也不做,忽略它們}\n{//遇分行符號時,忽略之}"//"{//遇到串"//",表明要開始一段注釋,直到行尾cout<<"(comment)"<<endl;//提示遇到了注釋BEGIN COMMENT;//用BEGIN宏切換到注釋狀態,去過濾這段注釋,下一次lex將只匹配前面帶有<COMMENT>的正則式}.{//.表示除\n以外的其它字元,注意這個規則要放在最後,因為一旦匹配了.就不會匹配後面的規則了(以其它狀態<>開頭的規則除外)yylval.m_cOp=yytext[0];//由於只匹配一個字元,這時它對應yytext[0],把該字元存放到yylval的m_cOp成員裡,到了yacc裡用$n方式引用return OPERATOR;//向yacc返回: 識別出的記號類型是OPERATOR}<COMMENT>\n{//注釋狀態下的規則,只有當前切換到COMMENT狀態才會去匹配BEGIN INITIAL;//在注釋狀態下,當遇到分行符號時,表明注釋結束了,返回初始態}<COMMENT>.{//在注釋狀態下,對其它字元都忽略,即:注釋在lex(詞法分析層)就過濾掉了,不返回給yacc了}%%//第3段:C函數定義段int yywrap(void){puts("-----the file is end");return 1;//返回1表示讀取全部結束。如果要接著讀其它檔案,可以這裡fopen該檔案,檔案指標賦給yyin,並返回0}
yacc.y: yacc程式檔案
%{/*本yacc的組建檔案是yacc.tab.c和yacc.tab.hyacc檔案由3段組成,用2個%%行把這3段隔開。第1段是聲明段,包括:1-C代碼部分:include標頭檔、函數、類型等聲明,這些聲明會原樣拷到產生的.c檔案中。2-記號聲明,如%token3-型別宣告,如%type第2段是規則段,是yacc檔案的主體,包括每個產生式是如何匹配的,以及匹配後要執行的C代碼動作。第3段是C函數定義段,如yyerror()的定義,這些C代碼會原樣拷到產生的.c檔案中。該段內容可以為空白*///第1段:聲明段#include "main.h"//lex和yacc要共用的標頭檔,裡麵包含了一些標頭檔,重定義了YYSTYPEextern "C"//為了能夠在C++程式裡面調用C函數,必須把每一個需要使用的C函數,其聲明都包括在extern "C"{}塊裡面,這樣C++連結時才能成功連結它們。extern "C"用來在C++環境下設定C連結類型。{//lex.l中也有類似的這段extern "C",可以把它們合并成一段,放到共同的標頭檔main.h中void yyerror(const char *s);extern int yylex(void);//該函數是在lex.yy.c裡定義的,yyparse()裡要調用該函數,為了能編譯和連結,必須用extern加以聲明}%}/*lex裡要return的記號的聲明用token後加一對<member>來定義記號,旨在用於簡化書寫方式。假定某個產生式中第1個終結符是記號OPERATOR,則引用OPERATOR屬性的方式:1-如果記號OPERATOR是以普通方式定義的,如%token OPERATOR,則在動作中要寫$1.m_cOp,以指明使用YYSTYPE的哪個成員2-用%token<m_cOp>OPERATOR方式定義後,只需要寫$1,yacc會自動替換為$1.m_cOp另外用<>定義記號後,非終結符如file, tokenlist,必須用%type<member>來定義(否則會報錯),以指明它們的屬性對應YYSTYPE中哪個成員,這時對該非終結符的引用,如$$,會自動替換為$$.member*/%token<m_nInt>INTEGER%token<m_sId>IDENTIFIER%token<m_cOp>OPERATOR%type<m_sId>file%type<m_sId>tokenlist%%file://檔案,由記號流組成tokenlist//這裡僅顯示記號流中的ID{cout<<"all id:"<<$1<<endl;//$1是非終結符tokenlist的屬性,由於該終結符是用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$1相當於$1.m_sId,其值已經在下層產生式中賦值(tokenlist IDENTIFIER)};tokenlist://記號流,或者為空白,或者由若干數字、標識符、及其它符號組成{}| tokenlist INTEGER{cout<<"int: "<<$2<<endl;//$2是記號INTEGER的屬性,由於該記號是用%token<m_nInt>定義的,即約定對其用YYSTYPE的m_nInt屬性,$2會被替換為yylval.m_nInt,已在lex裡賦值}| tokenlist IDENTIFIER{$$+=" " + $2;//$$是非終結符tokenlist的屬性,由於該終結符是用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$$相當於$$.m_sId,這裡把識別到的標識符串儲存在tokenlist屬性中,到上層產生式裡可以拿出為用cout<<"id: "<<$2<<endl;//$2是記號IDENTIFIER的屬性,由於該記號是用%token<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$2會被替換為yylval.m_sId,已在lex裡賦值}| tokenlist OPERATOR{cout<<"op: "<<$2<<endl;//$2是記號OPERATOR的屬性,由於該記號是用%token<m_cOp>定義的,即約定對其用YYSTYPE的m_cOp屬性,$2會被替換為yylval.m_cOp,已在lex裡賦值};%%void yyerror(const char *s)//當yacc遇到語法錯誤時,會回調yyerror函數,並且把錯誤資訊放在參數s中{cerr<<s<<endl;//直接輸出錯誤資訊}int main()//程式主函數,這個函數也可以放到其它.c, .cpp檔案裡{const char* sFile="file.txt";//開啟要讀取的文字檔FILE* fp=fopen(sFile, "r");if(fp==NULL){printf("cannot open %s\n", sFile);return -1;}extern FILE* yyin;//yyin和yyout都是FILE*類型yyin=fp;//yacc會從yyin讀取輸入,yyin預設是標準輸入,這裡改為磁碟檔案。yacc預設向yyout輸出,可修改yyout改變輸出目的printf("-----begin parsing %s\n", sFile);yyparse();//使yacc開始讀取輸入和解析,它會調用lex的yylex()讀取記號puts("-----end parsing");fclose(fp);return 0;}
Makefile: makefile檔案
LEX=flexYACC=bisonCC=g++OBJECT=main #產生的目標檔案$(OBJECT): lex.yy.o yacc.tab.o$(CC) lex.yy.o yacc.tab.o -o $(OBJECT)@./$(OBJECT) #編譯後立刻運行lex.yy.o: lex.yy.c yacc.tab.h main.h$(CC) -c lex.yy.cyacc.tab.o: yacc.tab.c main.h$(CC) -c yacc.tab.cyacc.tab.c yacc.tab.h: yacc.y# bison使用-d參數編譯.y檔案$(YACC) -d yacc.ylex.yy.c: lex.l$(LEX) lex.lclean:@rm -f $(OBJECT) *.o
file.txt: 被解析的文本樣本
abc defghi//this line is comment, abc 123 !@#$12345678//comment until line end!@#$
使用方法:
1-把lex_yacc_example.rar解壓到linux/cygwin下。
2-命令列進入lex_yacc_example目錄。
3-敲入make,這時會自動執行以下操作:
(1) 自動調用flex編譯.l檔案,產生lex.yy.c檔案。
(2) 自動調用bison編譯.y檔案,產生yacc.tab.c和yacc.tab.h檔案。
(3) 自動調用g++編譯、連結出可執行檔main。
(4) 自動執行main。
運行結果如下所示:
bison -d yacc.yg++ -c lex.yy.cg++ -c yacc.tab.cg++ lex.yy.o yacc.tab.o -o main-----begin parsing file.txtid: abcid: defghi(comment)int: 123int: 45678(comment)op: !op: @op: #op: $-----the file is endall id: abc defghi-----end parsing
參考資料:《Lex和Yacc從入門到精通(6)-解析C-C++包含檔案》http://blog.csdn.net/pandaxcl/article/details/1321552
其它文章和代碼請留意我的blog: http://blog.csdn.net/huyansoft
[END]