本框架是一個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里正則表達式的定義、識別方式。 * 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.c lex文件由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.h yacc文件由3段組成,用2個%%行把這3段隔開。 第1段是聲明段,包括: 1-C代碼部分:include頭文件、函數、類型等聲明,這些聲明會原樣拷到生成的.c文件中。 2-記號聲明,如%token 3-類型聲明,如%type 第2段是規則段,是yacc文件的主體,包括每個產生式是如何匹配的,以及匹配后要執行的C代碼動作。 第3段是C函數定義段,如yyerror()的定義,這些C代碼會原樣拷到生成的.c文件中。該段內容可以為空*/ //第1段:聲明段 #include "main.h"//lex和yacc要共用的頭文件,里面包含了一些頭文件,重定義了YYSTYPE extern "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=flex YACC=bison CC=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.c yacc.tab.o: yacc.tab.c main.h $(CC) -c yacc.tab.c yacc.tab.c yacc.tab.h: yacc.y # bison使用-d參數編譯.y文件 $(YACC) -d yacc.y lex.yy.c: lex.l $(LEX) lex.l clean: @rm -f $(OBJECT) *.o
file.txt: 被解析的文本示例
abc defghi //this line is comment, abc 123 !@#$ 123 45678 //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.y g++ -c lex.yy.c g++ -c yacc.tab.c g++ lex.yy.o yacc.tab.o -o main -----begin parsing file.txt id: abc id: defghi (comment) int: 123 int: 45678 (comment) op: ! op: @ op: # op: $ -----the file is end all id: abc defghi -----end parsing