1. 程式人生 > >一個Lex/Yacc完整的示例(可使用C++)

一個Lex/Yacc完整的示例(可使用C++)

作者: 胡彥 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里正則表示式的定義、識別方式。 * 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.lyacc.y共同使用的標頭檔案。
Makefile:		makefile檔案。
lex.yy.c:		用lex編譯lex.l後生成的C檔案。
yacc.tab.c:	用yacc編譯yacc.y後生成的C檔案。
yacc.tab.h:	用yacc編譯yacc.y後生成的C標頭檔案,內含%tokenYYSTYPEyylval
等定義,供lex.yy.cyacc.tab.c使用。 file.txt: 被解析的文字示例。 README.txt: 本說明。


下面列出主要的程式碼檔案:

 main.h: lex.l和yacc.y共同使用的標頭檔案 


  
  1. #ifndef MAIN_HPP
  2. #define MAIN_HPP
  3. #include <iostream>//使用C++庫
  4. #include <string>
  5. #include <stdio.h>//printf和FILE要用的
  6. using namespace std;
  7. /*當lex每識別出一個記號後,是通過變數yylval向yacc傳遞資料的。預設情況下yylval是int型別,也就是隻能傳遞整型資料。
  8. yylval是用YYSTYPE巨集定義的,只要重定義YYSTYPE巨集,就能重新指定yylval的型別(可參見yacc自動生成的標頭檔案yacc.tab.h)。
  9. 在我們的例子裡,當識別出識別符號後要向yacc傳遞這個識別符號串,yylval定義成整型不太方便(要先強制轉換成整型,yacc裡再轉換回char*)。
  10. 這裡把YYSTYPE重定義為struct Type,可存放多種資訊*/
  11. struct Type//通常這裡面每個成員,每次只會使用其中一個,一般是定義成union以節省空間(但這裡用了string等複雜型別造成不可以)
  12. {
  13. string m_sId;
  14. int m_nInt;
  15. char m_cOp;
  16. };
  17. #define YYSTYPE Type//把YYSTYPE(即yylval變數)重定義為struct Type型別,這樣lex就能向yacc返回更多的資料了
  18. #endif


lex.l: lex程式檔案


  
  1. %{
  2. /*本lex的生成檔案是lex.yy.c
  3. lex檔案由3段組成,用2個%%行把這3段隔開。
  4. 第1段是宣告段,包括:
  5. 1-C程式碼部分:include標頭檔案、函式、型別等宣告,這些宣告會原樣拷到生成的.c檔案中。
  6. 2-狀態宣告,如%x COMMENT。
  7. 3-正則式定義,如digit ([0-9])。
  8. 第2段是規則段,是lex檔案的主體,包括每個規則(如identifier)是如何匹配的,以及匹配後要執行的C程式碼動作。
  9. 第3段是C函式定義段,如yywrap()的定義,這些C程式碼會原樣拷到生成的.c檔案中。該段內容可以為空*/
  10. //第1段:宣告段
  11. #include "main.h"//lex和yacc要共用的標頭檔案,裡面包含了一些標頭檔案,重定義了YYSTYPE
  12. #include "yacc.tab.h"//用yacc編譯yacc.y後生成的C標頭檔案,內含%token、YYSTYPE、yylval等定義(都是C巨集),供lex.yy.c和yacc.tab.c使用
  13. extern "C" //為了能夠在C++程式裡面呼叫C函式,必須把每一個需要使用的C函式,其宣告都包括在extern "C"{}塊裡面,這樣C++連結時才能成功連結它們。extern "C"用來在C++環境下設定C連結型別。
  14. { //yacc.y中也有類似的這段extern "C",可以把它們合併成一段,放到共同的標頭檔案main.h中
  15. int yywrap(void);
  16. int yylex(void); //這個是lex生成的詞法分析函式,yacc的yyparse()裡會呼叫它,如果這裡不宣告,生成的yacc.tab.c在編譯時會找不到該函式
  17. }
  18. %}
  19. /*lex的每個正則式前面可以帶有"<狀態>",例如下面的"<COMMENT>\n"。每個狀態要先用%x宣告才能使用。
  20. 當lex開始執行時,預設狀態是INITIAL,以後可在C程式碼裡用"BEGIN 狀態名;"切換到其它狀態(BEGIN是lex/yacc內建的巨集)。
  21. 這時,只有當lex狀態切換到COMMENT後,才會去匹配以<COMMENT>開頭的正則式,而不匹配其它狀態開頭的。
  22. 也就是說,lex當前處在什麼狀態,就考慮以該狀態開頭的正則式,而忽略其它的正則式。
  23. 其應用例如,在一段C程式碼裡,同樣是串"abc",如果它寫在程式碼段裡,會被識別為識別符號,如果寫在註釋裡則就不會。所以對串"abc"的識別結果,應根據不同的狀態加以區分。
  24. 本例子需要忽略掉文字中的行末註釋,行末註釋的定義是:從某個"//"開始,直到行尾的內容都是註釋。其實現方法是:
  25. 1-lex啟動時預設是INITIAL狀態,在這個狀態下,串"abc"會識別為識別符號,串"123"會識別為整數等。
  26. 2-一旦識別到"//",則用BEGIN巨集切換到COMMENT狀態,在該狀態下,abc這樣的串、以及其它字元會被忽略。只有識別到換行符\n時,再用BEGIN巨集切換到初始態,繼續識別其它記號。*/
  27. %x COMMENT
  28. /*非數字由大小寫字母、下劃線組成*/
  29. nondigit ([_A-Za-z])
  30. /*一位數字,可以是0到9*/
  31. digit ([ 0 -9])
  32. /*整數由1至多位數字組成*/
  33. integer ({digit}+)
  34. /*識別符號,以非數字開頭,後跟0至多個數字或非數字*/
  35. identifier ({nondigit}({nondigit}|{digit})*)
  36. /*一個或一段連續的空白符*/
  37. blank_chars ([ \f\r\t\v]+)
  38. /*下面%%後開始第2段:規則段*/
  39. %%
  40. {identifier} { //匹配識別符號串,此時串值由yytext儲存
  41. yylval.m_sId=yytext; //通過yylval向yacc傳遞識別出的記號的值,由於yylval已定義為struct Type,這裡就可以把yytext賦給其m_sId成員,到了yacc裡就可以用$n的方式來引用了
  42. return IDENTIFIER; //向yacc返回: 識別出的記號型別是IDENTIFIER
  43. }
  44. {integer} { //匹配整數串
  45. yylval.m_nInt=atoi(yytext); //把識別出的整數串,轉換為整型值,儲存到yylval的整型成員裡,到了yacc裡用$n方式引用
  46. return INTEGER; //向yacc返回: 識別出的記號型別是INTEGER
  47. }
  48. {blank_chars} { //遇空白符時,什麼也不做,忽略它們
  49. }
  50. \n { //遇換行符時,忽略之
  51. }
  52. "//" { //遇到串"//",表明要開始一段註釋,直到行尾
  53. cout<< "(comment)"<< endl; //提示遇到了註釋
  54. BEGIN COMMENT; //用BEGIN巨集切換到註釋狀態,去過濾這段註釋,下一次lex將只匹配前面帶有<COMMENT>的正則式
  55. }
  56. . { //.表示除\n以外的其它字元,注意這個規則要放在最後,因為一旦匹配了.就不會匹配後面的規則了(以其它狀態<>開頭的規則除外)
  57. yylval.m_cOp=yytext[ 0]; //由於只匹配一個字元,這時它對應yytext[0],把該字元存放到yylval的m_cOp成員裡,到了yacc裡用$n方式引用
  58. return OPERATOR; //向yacc返回: 識別出的記號型別是OPERATOR
  59. }
  60. <COMMENT>\n { //註釋狀態下的規則,只有當前切換到COMMENT狀態才會去匹配
  61. BEGIN INITIAL; //在註釋狀態下,當遇到換行符時,表明註釋結束了,返回初始態
  62. }
  63. <COMMENT>. { //在註釋狀態下,對其它字元都忽略,即:註釋在lex(詞法分析層)就過濾掉了,不返回給yacc了
  64. }
  65. %%
  66. //第3段:C函式定義段
  67. int yywrap(void)
  68. {
  69. puts( "-----the file is end");
  70. return 1; //返回1表示讀取全部結束。如果要接著讀其它檔案,可以這裡fopen該檔案,檔案指標賦給yyin,並返回0
  71. }


yacc.y: yacc程式檔案


  
  1. %{
  2. /*本yacc的生成檔案是yacc.tab.c和yacc.tab.h
  3. yacc檔案由3段組成,用2個%%行把這3段隔開。
  4. 第1段是宣告段,包括:
  5. 1-C程式碼部分:include標頭檔案、函式、型別等宣告,這些宣告會原樣拷到生成的.c檔案中。
  6. 2-記號宣告,如%token
  7. 3-型別宣告,如%type
  8. 第2段是規則段,是yacc檔案的主體,包括每個產生式是如何匹配的,以及匹配後要執行的C程式碼動作。
  9. 第3段是C函式定義段,如yyerror()的定義,這些C程式碼會原樣拷到生成的.c檔案中。該段內容可以為空*/
  10. //第1段:宣告段
  11. #include "main.h"//lex和yacc要共用的標頭檔案,裡面包含了一些標頭檔案,重定義了YYSTYPE
  12. extern "C" //為了能夠在C++程式裡面呼叫C函式,必須把每一個需要使用的C函式,其宣告都包括在extern "C"{}塊裡面,這樣C++連結時才能成功連結它們。extern "C"用來在C++環境下設定C連結型別。
  13. { //lex.l中也有類似的這段extern "C",可以把它們合併成一段,放到共同的標頭檔案main.h中
  14. void yyerror(const char *s);
  15. extern int yylex(void); //該函式是在lex.yy.c裡定義的,yyparse()裡要呼叫該函式,為了能編譯和連結,必須用extern加以宣告
  16. }
  17. %}
  18. /*lex裡要return的記號的宣告
  19. 用token後加一對<member>來定義記號,旨在用於簡化書寫方式。
  20. 假定某個產生式中第1個終結符是記號OPERATOR,則引用OPERATOR屬性的方式:
  21. 1-如果記號OPERATOR是以普通方式定義的,如%token OPERATOR,則在動作中要寫$1.m_cOp,以指明使用YYSTYPE的哪個成員
  22. 2-用%token<m_cOp>OPERATOR方式定義後,只需要寫$1,yacc會自動替換為$1.m_cOp
  23. 另外用<>定義記號後,非終結符如file, tokenlist,必須用%type<member>來定義(否則會報錯),以指明它們的屬性對應YYSTYPE中哪個成員,這時對該非終結符的引用,如$$,會自動替換為$$.member*/
  24. %token<m_nInt>INTEGER
  25. %token<m_sId>IDENTIFIER
  26. %token<m_cOp>OPERATOR
  27. %type<m_sId>file
  28. %type<m_sId>tokenlist
  29. %%
  30. file: //檔案,由記號流組成
  31. tokenlist //這裡僅顯示記號流中的ID
  32. {
  33. cout<< "all id:"<<$ 1<< endl; //$1是非終結符tokenlist的屬性,由於該終結符是用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$1相當於$1.m_sId,其值已經在下層產生式中賦值(tokenlist IDENTIFIER)
  34. };
  35. tokenlist: //記號流,或者為空,或者由若干數字、識別符號、及其它符號組成
  36. {
  37. }
  38. | tokenlist INTEGER
  39. {
  40. cout<< "int: "<<$ 2<< endl; //$2是記號INTEGER的屬性,由於該記號是用%token<m_nInt>定義的,即約定對其用YYSTYPE的m_nInt屬性,$2會被替換為yylval.m_nInt,已在lex裡賦值
  41. }
  42. | tokenlist IDENTIFIER
  43. {
  44. $$+= " " + $ 2; //$$是非終結符tokenlist的屬性,由於該終結符是用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$$相當於$$.m_sId,這裡把識別到的識別符號串儲存在tokenlist屬性中,到上層產生式裡可以拿出為用
  45. cout<< "id: "<<$ 2<< endl; //$2是記號IDENTIFIER的屬性,由於該記號是用%token<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$2會被替換為yylval.m_sId,已在lex裡賦值
  46. }
  47. | tokenlist OPERATOR
  48. {
  49. cout<< "op: "<<$ 2<< endl; //$2是記號OPERATOR的屬性,由於該記號是用%token<m_cOp>定義的,即約定對其用YYSTYPE的m_cOp屬性,$2會被替換為yylval.m_cOp,已在lex裡賦值
  50. };
  51. %%
  52. void yyerror(const char *s) //當yacc遇到語法錯誤時,會回撥yyerror函式,並且把錯誤資訊放在引數s中
  53. {
  54. cerr<<s<< endl; //直接輸出錯誤資訊
  55. }
  56. int main()//程式主函式,這個函式也可以放到其它.c, .cpp檔案裡
  57. {
  58. const char* sFile= "file.txt"; //開啟要讀取的文字檔案
  59. FILE* fp=fopen(sFile, "r");
  60. if(fp== NULL)
  61. {
  62. printf( "cannot open %s\n", sFile);
  63. return -1;
  64. }
  65. extern FILE* yyin; //yyin和yyout都是FILE*型別
  66. yyin=fp; //yacc會從yyin讀取輸入,yyin預設是標準輸入,這裡改為磁碟檔案。yacc預設向yyout輸出,可修改yyout改變輸出目的
  67. printf( "-----begin parsing %s\n", sFile);
  68. yyparse(); //使yacc開始讀取輸入和解析,它會呼叫lex的yylex()讀取記號
  69. puts( "-----end parsing");
  70. fclose(fp);
  71. return 0;
  72. }


Makefile: makefile檔案


  
  1. LEX=flex
  2. YACC=bison
  3. CC=g++
  4. OBJECT=main #生成的目標檔案
  5. $(OBJECT): lex.yy.o yacc.tab.o
  6. $(CC) lex.yy.o yacc.tab.o -o $(OBJECT)
  7. @./$(OBJECT) #編譯後立刻執行
  8. lex.yy.o: lex.yy.c yacc.tab.h main.h
  9. $(CC) -c lex.yy.c
  10. yacc.tab.o: yacc.tab.c main.h