通过实例深入理解lec和yacc
本框架是一个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代码动作里,对记号属性($, $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 ([-]) /*整数由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[];//由于只匹配一个字符,这时它对应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表示读取全部结束。如果要接着读其它文件,可以这里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:"<<$<<endl; //$1是非终结符tokenlist的属性,由于该终结符是用%type<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,$1相当于$1.m_sId,其值已经在下层产生式中赋值(tokenlist IDENTIFIER)
};
tokenlist://记号流,或者为空,或者由若干数字、标识符、及其它符号组成
{
}
| tokenlist INTEGER
{
cout<<"int: "<<$<<endl;//$2是记号INTEGER的属性,由于该记号是用%token<m_nInt>定义的,即约定对其用YYSTYPE的m_nInt属性,$2会被替换为yylval.m_nInt,已在lex里赋值
}
| tokenlist IDENTIFIER
{
$$+=" " + $;//$$是非终结符tokenlist的属性,由于该终结符是用%type<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,$$相当于$$.m_sId,这里把识别到的标识符串保存在tokenlist属性中,到上层产生式里可以拿出为用
cout<<"id: "<<$<<endl;//$2是记号IDENTIFIER的属性,由于该记号是用%token<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,$2会被替换为yylval.m_sId,已在lex里赋值
}
| tokenlist OPERATOR
{
cout<<"op: "<<$<<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 -;
}
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 ;
}
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
通过实例深入理解lec和yacc的更多相关文章
- servlet的一个web容器中有且只有一个servlet实例或有多个实例的理解1
servlet的一个web容器中有且只有一个servlet实例或有多个实例的理解 (2013-06-19 19:30:40) 转载▼ servlet的非线程安全,action的线程安全 对提交 ...
- 使用LINQ查询数据实例和理解
使用LINQ查询数据实例和理解 var contacts= from customer in db.Customers where customer.Name.StartsWith("A&q ...
- 动态规划(Dynamic Programming)算法与LC实例的理解
动态规划(Dynamic Programming)算法与LC实例的理解 希望通过写下来自己学习历程的方式帮助自己加深对知识的理解,也帮助其他人更好地学习,少走弯路.也欢迎大家来给我的Github的Le ...
- 从Java String实例来理解ANSI、Unicode、BMP、UTF等编码概念
转(http://www.codeceo.com/article/java-string-ansi-unicode-bmp-utf.html#0-tsina-1-10971-397232819ff9a ...
- laravel 服务容器实例——深入理解IoC模式
刚刚接触laravel,对于laravel的服务容器不是很理解.看了<Laravel框架关键技术解析>和网上的一些资料后对于服务容器有了一些自己的理解,在这里分享给大家 1.依赖 IoC模 ...
- 通过实例来理解paxos算法
背景 Paxos算法是莱斯利·兰伯特(Leslie Lamport,就是 LaTeX 中的”La”,此人现在在微软研究院)于1990年提出的一种基于消息传递的一致性算法.由于算法难以理解起初并没有 ...
- java多态的具体表现实例和理解
Java的多态性 面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据. 继承是为了重用父类代码,同时为实现多态性作 ...
- Python类属性与实例属性理解
按理讲,类属性改变,类的实例对象这个属性也应该被改变,但是在python中实际却不是这样 class test(): name = 111 a = test() b = test() a.name = ...
- Cookie实例,理解cookie
一.一句话了解cookie是什么 cookie是服务端发送给客户端的.用来记录一些信息(如用户名),定制主页,聚焦广告的.最终以文件形式存在于客户端电脑磁盘下的小型文档. 二.用实例来认清cookie ...
随机推荐
- [Linked List]Partition List
Total Accepted: 53879 Total Submissions: 190701 Difficulty: Medium Given a linked list and a value x ...
- VC++深入详解读书笔记-第七章对话框
1.在MFC中,所有的控件类都是由CWnd类派生来的,因此,控件实际上也是窗口. 2. 3.对话框的种类 模态对话框 模态对话框是指当其显示时,程序会暂时执行,直到关闭这个模态对话框后,才能继续执行程 ...
- struts2开发经验小结(method="{1}"等)
这里的{1}表示接收前面action里通过通配符传来的值,例如你配置的是<action name="*Crud" class="example.Crud" ...
- 引用JS表单验证大全 以后方便查看用
1:js 字符串长度限制.判断字符长度 .js限制输入.限制不能输入.textarea 长度限制 2.:js判断汉字.判断是否汉字 .只能输入汉字 3:js判断是否输入英文.只能输入英文 4:js只能 ...
- 由PhysicalFileProvider构建的物理文件系统
由PhysicalFileProvider构建的物理文件系统 ASP.NET Core应用中使用得最多的还是具体的物理文件,比如配置文件.View文件以及网页上的静态文件,物理文件系统的抽象通过Phy ...
- SQL Server 对象
第一项:重命名对象 execute sp_rename @objname='Nums',@newname ='Numbers',@objtype ='object'; go 这里要特别小心 @ne ...
- Linux文件系统学习笔记-1
在Linux中, 一切皆文件,不论是目录,设备,套接字等都可以看成文件,而且每一个文件对应一个inode号,这是一一对应的关系. [root@oracle ~]# ls -il 总用量 2624 ...
- KeybMap 键盘映射工具更新至 V1.5(修订)
KeybMap 更新至 V1.5,主要是增加了对一些多媒体键定义修改功能,也可以将任意一键定义为打开指定的程序. 3月9日略做修订. http://www.mympc.org/down/1/2005- ...
- window.opener方法的使用 js跨域
原文:window.opener方法的使用 js跨域 最近公司网站登陆加入了第三方登陆.可以用QQ直接登陆到我们网站,在login页面A中点QQ登陆时,调用了一个window.open文件打开一个lo ...
- thinkphp这样玩关联查询(实例教会你)
thinkphp实例,内连接实现多表中同时查找,并存在了一个数组中,返回到模板中,模板中volist遍历即可使用多表中的字段 $row=M()->query("select realn ...