作者: 胡彦 2013-4-28
代码下载地址:http://pan.baidu.com/share/link?shareid=579088&uk=253544182
本框架是一个lex/yacc完整的示例,包括详细的注释,用于学习lex/yacc程序基本的搭建方法,在linux/cygwin下敲入make就可以编译和执行。大部分框架已经搭好了,你只要稍加扩展就可以成为一个计算器之类的程序,用于《编译原理》的课程设计,或者对照理解其它lex/yacc项目的代码。
本例子虽小却演示了lex/yacc程序最重要和常用的特征:

[plain]  view plain  copy
  1. * lex/yacc程序组成结构、文件格式。  
  2. * 如何在lex/yacc中使用C++和STL库,用extern "C"声明那些lex/yacc生成的、要链接的C函数,如yylex(), yywrap(), yyerror()。  
  3. * 重定义YYSTYPE/yylval为复杂类型。  
  4. * lex里多状态的定义和使用,用BEGIN宏在初始态和其它状态间切换。  
  5. * lex里正则表达式的定义、识别方式。  
  6. * lex里用yylval向yacc返回数据。  
  7. * yacc里用%token<>方式声明yacc记号。  
  8. * yacc里用%type<>方式声明非终结符的类型。  
  9. * 在yacc嵌入的C代码动作里,对记号属性($1, $2等)、和非终结符属性($$)的正确引用方法。  
  10. * 对yyin/yyout重赋值,以改变yacc默认的输入/输出目标。  

本例子功能是,对当前目录下的file.txt文件,解析出其中的标识符、数字、其它符号,显示在屏幕上。linux调试环境是Ubuntu 10.04。

文件列表:

[plain]  view plain  copy
  1. lex.l:      lex程序文件。  
  2. yacc.y:     yacc程序文件。  
  3. main.h:     lex.l和yacc.y共同使用的头文件。  
  4. Makefile:       makefile文件。  
  5. lex.yy.c:       用lex编译lex.l后生成的C文件。  
  6. yacc.tab.c: 用yacc编译yacc.y后生成的C文件。  
  7. yacc.tab.h: 用yacc编译yacc.y后生成的C头文件,内含%token、YYSTYPE、yylval等定义,供lex.yy.c和yacc.tab.c使用。  
  8. file.txt:       被解析的文本示例。  
  9. README.txt: 本说明。  


下面列出主要的代码文件:

 main.h: lex.l和yacc.y共同使用的头文件 

[cpp]  view plain  copy
  1. #ifndef MAIN_HPP  
  2. #define MAIN_HPP  
  3.   
  4. #include <iostream>//使用C++库  
  5. #include <string>  
  6. #include <stdio.h>//printf和FILE要用的  
  7.   
  8. using namespace std;  
  9.   
  10. /*当lex每识别出一个记号后,是通过变量yylval向yacc传递数据的。默认情况下yylval是int类型,也就是只能传递整型数据。 
  11. yylval是用YYSTYPE宏定义的,只要重定义YYSTYPE宏,就能重新指定yylval的类型(可参见yacc自动生成的头文件yacc.tab.h)。 
  12. 在我们的例子里,当识别出标识符后要向yacc传递这个标识符串,yylval定义成整型不太方便(要先强制转换成整型,yacc里再转换回char*)。 
  13. 这里把YYSTYPE重定义为struct Type,可存放多种信息*/  
  14. struct Type//通常这里面每个成员,每次只会使用其中一个,一般是定义成union以节省空间(但这里用了string等复杂类型造成不可以)  
  15. {  
  16.     string m_sId;  
  17.     int m_nInt;  
  18.     char m_cOp;  
  19. };  
  20.   
  21. #define YYSTYPE Type//把YYSTYPE(即yylval变量)重定义为struct Type类型,这样lex就能向yacc返回更多的数据了  
  22.   
  23. #endif  


lex.l: lex程序文件

[cpp]  view plain  copy
  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.   
  11. //第1段:声明段  
  12. #include "main.h"//lex和yacc要共用的头文件,里面包含了一些头文件,重定义了YYSTYPE  
  13. #include "yacc.tab.h"//用yacc编译yacc.y后生成的C头文件,内含%token、YYSTYPE、yylval等定义(都是C宏),供lex.yy.c和yacc.tab.c使用  
  14.   
  15. extern "C"//为了能够在C++程序里面调用C函数,必须把每一个需要使用的C函数,其声明都包括在extern "C"{}块里面,这样C++链接时才能成功链接它们。extern "C"用来在C++环境下设置C链接类型。  
  16. {   //yacc.y中也有类似的这段extern "C",可以把它们合并成一段,放到共同的头文件main.h中  
  17.     int yywrap(void);  
  18.     int yylex(void);//这个是lex生成的词法分析函数,yacc的yyparse()里会调用它,如果这里不声明,生成的yacc.tab.c在编译时会找不到该函数  
  19. }  
  20. %}  
  21.   
  22. /*lex的每个正则式前面可以带有"<状态>",例如下面的"<COMMENT>\n"。每个状态要先用%x声明才能使用。 
  23. 当lex开始运行时,默认状态是INITIAL,以后可在C代码里用"BEGIN 状态名;"切换到其它状态(BEGIN是lex/yacc内置的宏)。 
  24. 这时,只有当lex状态切换到COMMENT后,才会去匹配以<COMMENT>开头的正则式,而不匹配其它状态开头的。 
  25. 也就是说,lex当前处在什么状态,就考虑以该状态开头的正则式,而忽略其它的正则式。 
  26. 其应用例如,在一段C代码里,同样是串"abc",如果它写在代码段里,会被识别为标识符,如果写在注释里则就不会。所以对串"abc"的识别结果,应根据不同的状态加以区分。 
  27. 本例子需要忽略掉文本中的行末注释,行末注释的定义是:从某个"//"开始,直到行尾的内容都是注释。其实现方法是: 
  28. 1-lex启动时默认是INITIAL状态,在这个状态下,串"abc"会识别为标识符,串"123"会识别为整数等。 
  29. 2-一旦识别到"//",则用BEGIN宏切换到COMMENT状态,在该状态下,abc这样的串、以及其它字符会被忽略。只有识别到换行符\n时,再用BEGIN宏切换到初始态,继续识别其它记号。*/  
  30. %x COMMENT  
  31.   
  32. /*非数字由大小写字母、下划线组成*/  
  33. nondigit    ([_A-Za-z])  
  34.   
  35. /*一位数字,可以是0到9*/  
  36. digit       ([0-9])  
  37.   
  38. /*整数由1至多位数字组成*/  
  39. integer     ({digit}+)  
  40.   
  41. /*标识符,以非数字开头,后跟0至多个数字或非数字*/  
  42. identifier  ({nondigit}({nondigit}|{digit})*)  
  43.   
  44. /*一个或一段连续的空白符*/  
  45. blank_chars ([ \f\r\t\v]+)  
  46.   
  47. /*下面%%后开始第2段:规则段*/  
  48. %%  
  49.   
  50. {identifier}    {   //匹配标识符串,此时串值由yytext保存  
  51.             yylval.m_sId=yytext;//通过yylval向yacc传递识别出的记号的值,由于yylval已定义为struct Type,这里就可以把yytext赋给其m_sId成员,到了yacc里就可以用$n的方式来引用了  
  52.             return IDENTIFIER;  //向yacc返回: 识别出的记号类型是IDENTIFIER  
  53.         }  
  54.   
  55. {integer}       {   //匹配整数串  
  56.             yylval.m_nInt=atoi(yytext);//把识别出的整数串,转换为整型值,存储到yylval的整型成员里,到了yacc里用$n方式引用  
  57.             return INTEGER;//向yacc返回: 识别出的记号类型是INTEGER  
  58.         }  
  59.   
  60. {blank_chars}   {   //遇空白符时,什么也不做,忽略它们  
  61.         }  
  62.                   
  63. \n      {   //遇换行符时,忽略之  
  64.         }  
  65. "//"        {   //遇到串"//",表明要开始一段注释,直到行尾  
  66.             cout<<"(comment)"<<endl;//提示遇到了注释  
  67.             BEGIN COMMENT;//用BEGIN宏切换到注释状态,去过滤这段注释,下一次lex将只匹配前面带有<COMMENT>的正则式  
  68.         }  
  69.   
  70. .       {   //.表示除\n以外的其它字符,注意这个规则要放在最后,因为一旦匹配了.就不会匹配后面的规则了(以其它状态<>开头的规则除外)  
  71.             yylval.m_cOp=yytext[0];//由于只匹配一个字符,这时它对应yytext[0],把该字符存放到yylval的m_cOp成员里,到了yacc里用$n方式引用  
  72.             return OPERATOR;//向yacc返回: 识别出的记号类型是OPERATOR  
  73.         }  
  74.   
  75. <COMMENT>\n   {   //注释状态下的规则,只有当前切换到COMMENT状态才会去匹配  
  76.             BEGIN INITIAL;//在注释状态下,当遇到换行符时,表明注释结束了,返回初始态  
  77.         }  
  78.   
  79. <COMMENT>.    {   //在注释状态下,对其它字符都忽略,即:注释在lex(词法分析层)就过滤掉了,不返回给yacc了  
  80.         }  
  81.   
  82. %%  
  83.   
  84. //第3段:C函数定义段  
  85. int yywrap(void)  
  86. {  
  87.     puts("-----the file is end");  
  88.     return 1;//返回1表示读取全部结束。如果要接着读其它文件,可以这里fopen该文件,文件指针赋给yyin,并返回0  
  89. }  


yacc.y: yacc程序文件

[cpp]  view plain  copy
  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.   
  11. //第1段:声明段  
  12. #include "main.h"//lex和yacc要共用的头文件,里面包含了一些头文件,重定义了YYSTYPE  
  13.   
  14. extern "C"//为了能够在C++程序里面调用C函数,必须把每一个需要使用的C函数,其声明都包括在extern "C"{}块里面,这样C++链接时才能成功链接它们。extern "C"用来在C++环境下设置C链接类型。  
  15. {   //lex.l中也有类似的这段extern "C",可以把它们合并成一段,放到共同的头文件main.h中  
  16.     void yyerror(const char *s);  
  17.     extern int yylex(void);//该函数是在lex.yy.c里定义的,yyparse()里要调用该函数,为了能编译和链接,必须用extern加以声明  
  18. }  
  19.   
  20. %}  
  21.   
  22. /*lex里要return的记号的声明 
  23. 用token后加一对<member>来定义记号,旨在用于简化书写方式。 
  24. 假定某个产生式中第1个终结符是记号OPERATOR,则引用OPERATOR属性的方式: 
  25. 1-如果记号OPERATOR是以普通方式定义的,如%token OPERATOR,则在动作中要写$1.m_cOp,以指明使用YYSTYPE的哪个成员 
  26. 2-用%token<m_cOp>OPERATOR方式定义后,只需要写$1,yacc会自动替换为$1.m_cOp 
  27. 另外用<>定义记号后,非终结符如file, tokenlist,必须用%type<member>来定义(否则会报错),以指明它们的属性对应YYSTYPE中哪个成员,这时对该非终结符的引用,如
    .member*/  
  28. %token<m_nInt>INTEGER  
  29. %token<m_sId>IDENTIFIER  
  30. %token<m_cOp>OPERATOR  
  31. %type<m_sId>file  
  32. %type<m_sId>tokenlist  
  33.   
  34. %%  
  35.   
  36. file:   //文件,由记号流组成  
  37.     tokenlist   //这里仅显示记号流中的ID  
  38.     {  
  39.         cout<<"all id:"<<$1<<endl;    //$1是非终结符tokenlist的属性,由于该终结符是用%type<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,$1相当于$1.m_sId,其值已经在下层产生式中赋值(tokenlist IDENTIFIER)  
  40.     };  
  41. tokenlist://记号流,或者为空,或者由若干数字、标识符、及其它符号组成  
  42.     {  
  43.     }  
  44.     | tokenlist INTEGER  
  45.     {  
  46.         cout<<"int: "<<$2<<endl;//$2是记号INTEGER的属性,由于该记号是用%token<m_nInt>定义的,即约定对其用YYSTYPE的m_nInt属性,$2会被替换为yylval.m_nInt,已在lex里赋值  
  47.     }  
  48.     | tokenlist IDENTIFIER  
  49.     {  
  50.         $$+=" " + $2;//
    tokenlist
    相当于$$.m_sId,这里把识别到的标识符串保存在tokenlist属性中,到上层产生式里可以拿出为用  
  51.         cout<<"id: "<<$2<<endl;//$2是记号IDENTIFIER的属性,由于该记号是用%token<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,$2会被替换为yylval.m_sId,已在lex里赋值  
  52.     }  
  53.     | tokenlist OPERATOR  
  54.     {  
  55.         cout<<"op: "<<$2<<endl;//$2是记号OPERATOR的属性,由于该记号是用%token<m_cOp>定义的,即约定对其用YYSTYPE的m_cOp属性,$2会被替换为yylval.m_cOp,已在lex里赋值  
  56.     };  
  57.   
  58. %%  
  59.   
  60. void yyerror(const char *s) //当yacc遇到语法错误时,会回调yyerror函数,并且把错误信息放在参数s中  
  61. {  
  62.     cerr<<s<<endl;//直接输出错误信息  
  63. }  
  64.   
  65. int main()//程序主函数,这个函数也可以放到其它.c, .cpp文件里  
  66. {  
  67.     const char* sFile="file.txt";//打开要读取的文本文件  
  68.     FILE* fp=fopen(sFile, "r");  
  69.     if(fp==NULL)  
  70.     {  
  71.         printf("cannot open %s\n", sFile);  
  72.         return -1;  
  73.     }  
  74.     extern FILE* yyin;  //yyin和yyout都是FILE*类型  
  75.     yyin=fp;//yacc会从yyin读取输入,yyin默认是标准输入,这里改为磁盘文件。yacc默认向yyout输出,可修改yyout改变输出目的  
  76.   
  77.     printf("-----begin parsing %s\n", sFile);  
  78.     yyparse();//使yacc开始读取输入和解析,它会调用lex的yylex()读取记号  
  79.     puts("-----end parsing");  
  80.   
  81.     fclose(fp);  
  82.   
  83.     return 0;  
  84. }  


Makefile: makefile文件

[cpp]  view plain  copy
  1. LEX=flex  
  2. YACC=bison  
  3. CC=g++  
  4. OBJECT=main #生成的目标文件  
  5.   
  6. $(OBJECT): lex.yy.o  yacc.tab.o  
  7.     $(CC) lex.yy.o yacc.tab.o -o $(OBJECT)  
  8.     @./$(OBJECT) #编译后立刻运行  
  9.   
  10. lex.yy.o: lex.yy.c  yacc.tab.h  main.h  
  11.     $(CC) -c lex.yy.c  
  12.   
  13. yacc.tab.o: yacc.tab.c  main.h  
  14.     $(CC) -c yacc.tab.c  
  15.   
  16. yacc.tab.c  yacc.tab.h: yacc.y  
  17. # bison使用-d参数编译.y文件  
  18.     $(YACC) -d yacc.y  
  19.   
  20. lex.yy.c: lex.l  
  21.     $(LEX) lex.l  
  22.   
  23. clean:  
  24.     @rm -f $(OBJECT)  *.o  


 file.txt: 被解析的文本示例

[plain]  view plain  copy
  1. abc defghi  
  2. //this line is comment, abc 123 !@#$  
  3. 123 45678   //comment until line end  
  4. !   @   #   $  


使用方法:
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。
运行结果如下所示:

[plain]  view plain  copy
  1. bison -d yacc.y  
  2. g++ -c lex.yy.c  
  3. g++ -c yacc.tab.c  
  4. g++ lex.yy.o yacc.tab.o -o main           
  5. -----begin parsing file.txt  
  6. id: abc  
  7. id: defghi  
  8. (comment)  
  9. int: 123  
  10. int: 45678  
  11. (comment)  
  12. op: !  
  13. op: @  
  14. op: #  
  15. op: $  
  16. -----the file is end  
  17. all id: abc defghi  
  18. -----end parsing  


参考资料:《Lex和Yacc从入门到精通(6)-解析C-C++包含文件》http://blog.csdn.net/pandaxcl/article/details/1321552

[END]

Logo

更多推荐