词法分析器总结--flex&bison
转自:项目总结之词法分析器
无论是词法分析,还是语法分析,给我的第一感觉就是逻辑要严谨。由于项目有自己一套完整的语言和语法,设计好其对应的词法分析器和语法分析器显得尤为重要。
我们采用flex进行词法分析。flex是一个用来生成扫描器(scanners)的工具,其中扫描器就是可以识别文本中词法模式的程序。具体流程为:flex读取给定的输入文件,或标准输入(当没有给定文件名时)读取信息来生成一个扫描器。信息以正则表达式和C代码组成,这种形式称为规则(rule)。flex生成C源代码文件lex.yy.c,其中定义了一个函数yylex()。这个文件通过编译,并用-lfl 链接生成可执行文件。当可执行文件被执行时,它分析输入中可能存在的符合正则表达的内容。当找到任何一个与正则表达式相匹配内容时,相应的C 代码将被执行。
flex输入文件由三段组成:定义(definitions),规则(rules),用户代码(user code)
一、定义段(definitions)
定义段包含了简单名称的声明(这些声明可以简化扫描器的说明)和开始条件。在本项目中,定义段中还包含了选项options。现将介绍一些比较常用的options。
flex 提供一个机制用来在扫描器的说明中,而不是在flex 命令中控制选项。在扫描器的说明文件(flex 的输入文件)的第一段中使用%option 指令就可以实现。你可以用单个%option 指令指定多个选项,也可以使用多个%option指令。
%option 7bit,%option 8bit——指示flex生成一个7bit或8bit的扫描器与-7,-8 选项等价。
%option backup——生成一个备份信息到lex.backup,与-b选项等价。
%option caseful,%option case-sensitive——区分大小写,与-i相反。
%option case-insensitive,%option caseless——忽略大小写,与-i选项等价。
%option debug——让生成的扫描器运行在debug模式,与-d选项等价。
%option default,%option nodefault——%default与-s选项相反,后者与其等价。-s选项作用:使不匹配的输入回显到输出设备的rule失去作用。在此种情况下,如果扫描器不能匹配到任何规则rule的输入,它会终止并返回错误。在查找扫描器的规则漏洞时,-s和%option nodefault都非常有用。
%option interactive——指示flex生成一个交互式的扫描器。交互式扫描器就是向前查看下一个匹配的token是什么。结果就是总向前多看了一个字符,即使是在扫描器已经看够了文本已经排除了token 的歧义。但向前查看给了扫描器强大的交互能力。与-I等价。
%option warn——与-w选项相反。%option nowarn与-w选项等价。
%option array——与%array等价。
%option pointer——与%point等价。
以下为%option中定义,但在命令行里没有的特性。
%option always-interactive——指示flex 生成的扫描器总是把它的输入认为是"interactive"。
%option main——指示flex 为扫描器提供一个缺省的main()函数,它只是简单的调用了yylex()。这个选项暗示noyywrap。
%option never-interactive——flex 生成的扫描器从不认为输入是交互的(不会调用isatty())。这和总是interactive 正好相反。
%option yylineno——flex 生成的扫描器用全局变量yylineno 维护着输入文件的当前行编号。option lex-compat隐含有这个选项。
%option yywrap——如果没有设置(就如%option noyywrap),当扫描器遇到end-of-file 时,不会调用yywrap(),但简单的假定没有更多的文件可以扫描(直到用户把yyin 指向新的文件并再一次调用yylex())。
flex 通过扫描rule 中的action 来判断你是否使用了REJECT 或是yymore 属性。你可用%option reject 表示要使用这个特性而用%option noyymore 表示不使用这个特性。
三个选项使用了字符串值,从'='开始:%option outfile="ABC"等同于-oABC ;%option prefix="XYZ" 等同于-PXYZ;最后,%option yyclass="foo" 只有当生成C++扫描器(-+选项)时才有效。
有些选项可以限制一个例程不出现在生成的扫描器中。下面这些例程如果不被设置(如%option nounput)将不会出现在生成的扫描器中。
input unput yy_push_state yy_pop_sate yy_top_state yy_scan_buffer yy_scan_bytes yy_scan_string
可重入c扫描器(Reentrant C Scanners)
flex能够生成一个可重入的扫描器。通过定义%option reentrant(与-R选项等价)来实现可重入。所生成的扫描器在一个或多个控制线程中不仅可移植,而且安全性好。可重入扫描器通常应用于多线程应用程序。任何一个线程都可以在不考虑与其他线程同步的情况下创建并执行一个可重入的flex扫描器。
默认情况下,flex生成一个不可重入的扫描器。本项目为了实现多线程,因而在定义段指定%option reentrant。
性能考虑(performance consideration)
flex的设计目标就是生成一个高性能的扫描器。它已经对处理大量rule 做了优化。除了用-C 选项进行表格压缩之外,还有一些option/action 会影响到扫描器的速度。从最大影响到最弱,有这一些:
REJECT %option yylineno arbitrary trailing context
pattern sets that require backing up %array %option interactive %option always-interactive
'^'beginning-of-line operator yymore()
头三个的开销最大,后两个的开销最小。注意unput()有可能被用例程实现而造成更多操作,而yyless()是一个开销相当低的宏;所以如果只是回放一些你多扫了的文本,可以用yyless()。
本项目中也用到了名字定义和开始条件。其中名字定义包括数字、字符、空白符,多行注释,单行注释,引号间的字符串,整数、浮点数、实数,标示符,变量,日期。
数字—digit [0-9],字符—character [a-zA-Z],空白符—space [ \t\r](在制表符前面留有空格表示空格符)
多行注释(以/#开头,中间可以为任意非#非\n字符,也可以为一串#后面紧跟非/非\n字符,最后结尾为1个或多个#后跟/)
comstart \/\#
comstop \#+\/
cominside ([^#\n]*|#+[^#/\n])
单行注释 line_comment ^#[^\n]*
引号间的字符串(以双引号开头以双引号结尾。内容为非转义字符和双引号,当遇到转义字符时,进行特殊处理;当遇到双引号时,停止匹配)
dquotes \"
stringstart {dquotes}
stringstop {dquotes}
stringinside [^\\\"]+
注意:在多行注释和引号间的字符串的匹配中,采用了排斥条件(开始条件分为排斥和共享条件)
排斥条件的定义为 %xc(针对多行注释)
%xs(针对引号间的字符串)
整数 integer {digit}+
浮点数 decimal (({digit}+\.{digit}*)|({digit}*\.{digit}+))
decimalfail {digit}+\.\.
实数 real ({integer}|{decimal})[eE][+-]?{digit}+
realfail1 ({integer}|decimal)[eE]
realfail2 ({integer}|decimal)[eE][+-]
标示符 identstart [a-zA-Z\200-\377_]
identcont [a-zA-Z\200-\377_0-9\$]
identifier {identstart}{identcont}*
变量($后跟一个或多个字符) variable \${character}+
日期 date {digit}+\-{digit}+(\-{digit}+)?
datefail1 {digit}+\-{digit}+\-
datefail2 {digit}+\-
二、规则段(rules)
规则段包含模式(pattern)和动作(action),其中模式不能有缩进,而且动作必须在同一行上跟在动作后面。
在规则段可以使用开始条件(start conditions)。flex 提供了一种按条件激活规则rule 的机制。所有模式以"<sc>"为前缀的rule 只有在扫描器是在一个名为"sc"的启动条件时才会被激活。使用BEGIN action 可以激活一个开始条件。直到下一个BEGIN action 被执行,在给出开始条件的rule将被激活并且其他给出其他开始条件的rule 并不会被激活。如果使用的是排他的开始条件,那么只有以开始条件修饰的rule 才会被激活。跟在同一个排他开始条件后的rule 说明在扫描器中,这些rule 是独立于flex 输入中的其他rule。
本项目中涉及到排斥条件的有多行注释、引号间的字符串。
其中MOVELOC,SAVETOKEN为定义段中定义的宏
#define MOVELOC {yylloc->first_column = yylloc->last_column;\
yylloc->last_column = yylloc->first_column + yyleng;}
#define RESETLOC {yylloc->first_column = yylloc->last_column = 1;\
yylloc->first_line++;\
yylloc->last_line++;}
#define SAVETOKEN yylval->str = new std::string(yytext, yyleng)
多行注释(语句输出省略)
{comstart} { MOVELOC;
BEGIN(xc);
}
<xc>{cominside} { MOVELOC; }
<xc>\n { RESETLOC; }
<xc>{comstop} { MOVELOC;
BEGIN(INITIAL);
}
<xc><<EOF>> { BEGIN(INITIAL);
std::cerr << "unterminated /# comment" << endl;
yyterminate();
}
引号间的字符串(输出语句省略)
{stringstart} { MOVELOC;
BEGIN(xs);
SAVETOKEN;
}
<xs>{stringstop} { MOVELOC;
BEGIN(INITIAL);
*(yylval->str) += yytext;
return QUOTES_STRING;
}
<xs>\n { RESETLOC;
*(yylval->str) += yytext;
}
<xs>\\n { MOVELOC;
*(yylval->str) += "\n";
}
<xs>\\t { MOVELOC;
*(yylval->str) += "\t";
}
<xs>\\r { MOVLOC;
*(yylval->str) += "\r";
}
<xs>\\b { MOVELOC;
*(yylval->str) += "\b";
}
<xs>\\f { MOVELOC;
*(yylval->str) += "\f";
}
<xs>\\. { MOVELOC;
*(yylval->str) += yytext[1];
}
<xs>\\\n { RESETLOC;
*(yylval->str) += "\n";
}
<xs>{stringinside} { MOVELOC;
*(yylval->str) += yytext;
}
<xs><<EOF>> { BEGIN(INITIAL);
std::cerr << "unterminated \"" << endl;
delete yylval->str;
yyterminate();
}
三、用户代码段
用户代码段只会简单的拷贝到lex.yy.c中。这个和扫描器一起,调用扫描器或者被扫描器调用。如果被省略,则第二个%%可以省略。
使用了%option reentrant后
1所有的函数都会带一个额外的参数yyscanner。
2所有的全局变量都被它们的宏等价替换。
这些变量包括yytext
,yyleng
, yylineno
, yyin
, yyout
,yyextra
, yylval
, and yylloc,你可以在action部分安全地使用这些宏(如同使用普通变量一样),但不能够在外部直接使用。以yytext为例,在一个可重入的扫描器中,yytext以及其他类似变量都不是全局变量,因而不能通过action外部或是其他函数来直接访问yytext,而应该使用yyget_text访问器函数来实现对yytext的访问。
3在使用yylex之前调用yylex_init,在使用之后调用yylex_destroy。
init以及destroy函数
int yylex_init ( yyscan_t * ptr_yy_globals ) ;
int yylex_init_extra ( YY_EXTRA_TYPE user_defined, yyscan_t * ptr_yy_globals ) ;
int yylex ( yyscan_t yyscanner ) ;
int yylex_destroy ( yyscan_t yyscanner ) ;
函数yylex_init必须在调用任意其他函数之前调用,其参数是一个未初始化的指针地址,并由该函数初始化,这样会覆盖以前的内容。ptr_yy_global中存储的值会传递给yylex和yylex_destroy。flex不会保存传递给yylex_init的变量,因而传递一个局部指针的地址值给yylex_init是很安全的,只要其在调用扫描器到调用yylex_destroy期间一直存在就行。
yylex的可重入版本带一个参数,该参数即为yylex_init通过变量返回的值。
yylex_destroy函数用来释放扫描器使用过的资源。当要重复使用时,就不必destroy。
4获取函数(get或set)提供了访问普通flex变量的途径。
5用户自定义数据可以再yyextra中存储。
在一个可重入的扫描器中,使用全局变量让程序的不同部分通信或是保持状态是不明智的。然而,你需要在action中使用额外的数据或是调用额外的函数。同样,你需要传递信息给你的扫描器。在一个不可重入的扫描器中,实现这的唯一方式就是使用全局变量。flex允许你存储任意的、额外的数据到扫描器中。定义如下:
#define YY_EXTRA_TYPE void*
YY_EXTRA_TYPE yyget_extra ( yyscan_t scanner );
void yyset_extra ( YY_EXTRA_TYPE arbitrary_data , yyscan_t scanner);
项目中最后的代码如下,其中scanner_init初始化yylex,yy_scan_buffer函数(作用是建立输入缓存)从yyext->scanbuf指定的开始位置扫描slen+2个字节,最后两个字节必须是YY_END_OF_BUFFER_CHAR。
- yyscan_t
- scanner_init(const char *str, inl_yylex_extra *yyext)
- {
- int slen = strlen(str);
- yyscan_t scanner;
- if(yylex_init(&scanner) != 0)
- {
- std::cerr << "yylex_init() failed" << std::endl;
- exit(1);
- }
- inl_yyset_extra(yyext, scanner);
- yyext->scanbuf = (char *)malloc(slen + 2);
- yyext->scanbuflen = slen;
- memcpy(yyext->scanbuf, str, slen);
- yyext->scanbuf[slen] = yyext->scanbuf[slen + 1] = YY_END_OF_BUFFER_CHAR;
- yy_scan_buffer(yyext->scanbuf, slen + 2, scanner);
- return scanner;
- }
- void scanner_finish(yyscan_t yyscanner)
- {
- free((*((inl_yylex_extra**)(yyscanner)))->scanbuf);
- inl_yylex_destroy(yyscanner);
- }