点击打开链接
编译器和汇编器在工作过程中,往往完成如下的任务:
(1) 读取源代码并且获得程序的结构描述;
(2) 分析程序结构,并且生成相应的目标代码。
Flex和Bison就是为可以帮助完成以上任务。Flex将源代码文件分解为各种词汇(token),Bison找到这些词汇的组成方式。下面通过例子讲述它们的使用方法,在Cygwin环境下调试。
Flex是一个生成扫描器(scanner)的工具,生成的扫描器能够识别文本中的词法模式(lexical pattern)。Flex接受文本格式的Flex文件(扩展名可以为.l,.flx、.lex或者.flex)作为输入,生成一个c源文件:lex.yy.c,其中定义了一个函数yylex(),该函数就是扫描器。它根据Flex文件中定义的模式(pattern)对输入的文本串进行分析,然后执行对应的动作(Action),该模式和对应的动作叫做规则。例如,可以定义一个模式识别自定义标识符,并在对应的动作中规定,如果遇到自定义标识符,将该标识符写入某个数组。另外,lex.yy.c可以编译后执行,也可以被其他源文件中的函数调用。
下面看一个简单的例子(example1.lex):
int num_lines = 0, num_chars = 0;
%option noyywrap
%%
\n ++num_lines; ++num_chars;
. ++num_chars;
%%
int main()
{
yylex();
printf( "# of lines = %d, # of chars = %d\n",num_lines, num_chars );
return 0;
}
在cygwin下,键入如下命令:
$flex example1.lex //该命令生成lex.yy.c文件
$gcc -g -Wall -o scan.exe lex.yy.c //该命令生成可执行文件scan.exe
$./scan 回车
键入几行字符,以Ctrl+D结束。扫描程序scan.exe扫描键入的字符串,识别的根据模式执行动作,不识别的直接输出。本例中遇到换行符'\n'变量num_lines和num_chars加1,遇到字符num_chars加1,最后输出结果:
# of lines = 2, # of chars = 15
如果Flex文件中没有定义main函数,gcc编译时,需用参数-lfl链接fl库(gcc -g -Wall -lfl -o scan.exe lex.yy.c),利用其中的main函数。
Flex文件是词法规范定义文件,给出了单词的构成规则,以及在某个规则下应该执行的动作。它由定义段、规则段和用户代码段三个部分构成,中间用%%隔开。
定义段(definitions section)
%%
规则段(rulessection)
%%
用户代码段(user codesection)
pattern action
pattern使用如下所示的一个正则表达式的扩展集表示。
x 符合字符串"x"
. 除了换行以外的任何字符
[xyz] 一个字符类,在这个例子中,输入可以是'x'、'y'、'z'中的一个
[abj-oZ] 一个带范围的字符类,输入可以是'a', 'b', 从'j'到'o'还有'Z'中的字符
[^A-Z] 一个取反的字符类,任何字母除了大写字母。
[^A-Z\n] 任何字符除了大写字母和换行符
r* 零个或更多r,r可以是任意正则表达式
r+ 一个或更多r
r? 零个或最多一个r
r{2,5} 任何2到5个r
r{2,} 2个或更多r
r{4} 正好4个r
{name} 对name的展开
"[xyz]\"foo"
符合正则表达式 [xyz]"foo 的字符串
\X 如果X是一个'a', 'b', 'f', 'n', 'r', 't', 或者'v',同标准C中\x的处理。否则,就是X,用于对某些符号转义
\0 NUL字符,ASCII码值为0
\123 ASCII码为八进制123的char型
\x2a ASCII码为十六进制0x2a的char型
(r) 符合一个r,括号是用来越过优先级的
rs 正则表达式r,紧跟着一个正则表达式s
r|s 要么是正则表达式r,要么是正则表达式s
r/s 匹配模式r,但是要求其后紧跟着模式s。当需要判断本次匹配是否为“最长匹配(longest match)时,模式s匹配的文本也会被包括进来,但完成判断后开始执行对应的动作(action)之前,这些与模式s相配的文本会被返还给输入。所以动作(action)只能看到模式r匹配到的文本。这种模式类型叫做尾部上下文(trailing context)。(有些‘r/s’组合是flex不能识别的;请参看后面deficiencies/bugs一节中的dangerous trailing context的内容。)
^r 一个r,但是必须是在一行的开始
r$ 一个r,但是必须是在行末r 一个r,但是之前字符串必须符合条件s
<*>r 不考虑开始字符串类型,只符合r
<
除了以上正则表达式外,还有一些字符类表达式,它们括在分界符"[:"和":]"之间,使用时必须再用[]括起来。它们是:
[:alnum:] [:alpha:] [:blank:]
[:cntrl:] [:digit:] [:graph:]
[:lower:] [:print:] [:punct:]
[:space:] [:upper:] [:xdigit:]
每一个表达式都指示了一个字符分类,而且其名称与标准C函数isXXXX的名字对应。例如,[:alnum:]就指示了那些经由函数isalnum()检查后返回true的字符,也就是任何的字母或者数字。注意,有些系统上没有给出C函数isblank()的定义,所以flex自己定义了[:blank:]为一个空格或者一个tab。
下面所举的几个例子,都是等价的:
[[:alnum:]] [[:alpha:][:digit:]] [[:alpha:]0-9][a-zA-Z0-9]
如果扫描器是大小写不敏感的(执行flex命令时用了-i选项),则 [:upper:]和[:lower:]同[:alpha:]是等价的。
当生成的扫描器运行时,它分析输入的文本,寻找匹配规则段中定义的pattern。如果有多个pattern匹配,选择匹配最长字符串的pattern;如果有两个或多个相同长度的匹配,选择flex文件中最先列出的pattern。输入文本中能够匹配某个pattern的字符串叫做一个token。一旦匹配确定,全局字符指针yytext会指向这个token,可以通过该指针引用它,这个token的长度也可通过全局整型变量yyleng读取。同时,该pattern对应的action(动作)会执行,然后,剩余的输入文本会继续被分析匹配。如果没有匹配的pattern,会执行缺省的规则:剩余输入文本中的下一个字符被认为已经匹配,然后拷贝到标准输出中。因此,如果没有定义规则,输入文本会原样不动拷贝到标准输出中。
这里提到一个非常重要的全局变量yytext,它可以定义为字符指针或字符数组。默认是指针型的。可以通过某种方式修改为数组型,这里不多说了。
action(动作)跟在pattern的后面,可以是任意的C语言语句。一行中,pattern以第一个非转义的空白符作为结束标识,剩余部分就是action了。action有如下规则:
(1) 如果某个pattern后的action为空,匹配的token直接舍弃。例如,如果只有一个pattern"zap me",而后面action为空,会拷贝出zap me之外的字符到输出文件中。
(2) 如果action中包括"{",则必有一个对应的"}",中间全部都是pattern对应的action。也可用"%{"和"%}"括起action中语句。
(3) 如果一个action中只有一个"|",表示执行的action同下一规则。
(4) action中可包含return语句在内的任意c代码。如果有return语句,会返回一个值给yylex()。每次yylex()被调用,它会继续处理上一次没处理的token,直到文件末尾或遇到return。
(5) action中可以包含以下特殊命令:
该段中的代码都被原样拷贝到lex.yy.c中。可以定义一些辅助函数或代码,供扫描器yylex()调用,或者调用扫描器(一般来说就是main()了)。这一部分是可选的。如果没有的话,Flex文件中第二个%%是可以省略的。
前面提到,flex的输出是lex.yy.c,它包含扫描例程yylex()、一些用来存放匹配的token的table和一些附加的例程和宏。缺省情况下,yylex()声明如下:
int yylex()
{
... various definitions and the actions in here ...
}
如果要修改扫描例程的名字,可利用"YY_DECL"宏。例如,可以用#define YY_DECL float lexscan( a, b ) float a, b;将扫描例程名字定义为lexscan,返回值为float型,并有两个float型参数。
yylex()被调用后,它从输入文件指针yyin(缺省情况下式stdin)扫描tokens,直到遇到EOF(这是return 0)或action执行了一个return语句。如果扫描器遇到EOF,接下来的调用时未定义的,除非yyin指向一个新的输入文件,或者调用yyrestart()。yyrestart()需要一个文件指针作为参数(如果该指针为nil,则必须设置YY_INPUT从一个源而不是yyin扫描),并使yyin指向那个文件。如果扫描器因为在action遇到return结束,可以重新从结束的地方开始扫描。
缺省情况下,为了提高效率,扫描器通过yyin一次读一个数据块而不是用getc()读一个个字符。如何读可以通过YY_INPUT宏控制。YY_INPUT的调用方法是"YY_INPUT(buf,result,max_size)"。该宏是最多将max_size个字符放在字符数据缓冲区,返回一个整型变量,其值要么是读入的字符数,要么是常量YY_NULL(Unix下是0)指示EOF。YY_INPUT缺省下从yyin读入数据。下面是YY_INPUT的一个定义实例(在flex文件的定义段):
%{
#define YY_INPUT(buf,result,max_size) \
{ \
int c = getchar(); \ 定义为每次读入一个字符
result = (c == EOF) ? YY_NULL : (buf[0] = c, 1); \
}
%}
当扫描器从YY_INPUT收到EOF,它会检查yywrap()函数。如果该函数返回false(即0),则假定该函数已经执行并设置了yyin指向另一个输入文件,扫描继续进行;如果该函数返回true(非0),扫描器终止,返回0给它的调用者。无论哪种情况,开始条件保持不变,没有变回INITIAL。
如果没有提供自己的yywrap(),则要么使用%option noyywrap(相当于返回1),要么使用-lfl链接选项获取例程的缺省版本,该版本下总是return 1。
扫描器将ECHO输出写到yyout中(缺省是stdout),可以通过设置yyout为某个文件指针重新定义。
如下例所示,扫描器扫描a.txt文件,并将ECHO输出和不能识别的内容写到b.txt文件中。
int main(){
yyin=fopen("a.txt","r");
yyout = fopen("b.txt", "w");
yylex();
fclose(yyin);
fclose(yyout);
return 0;
}
flex提供了机制有条件地激活规则。如果规则的pattern有前缀"
开始条件在定义段声明。该行必须不能缩进,并使用%s或%x开始,跟着一个名字列表。%s表示inclusive(包含的)开始条件,%x表示exclusive(排它的)开始条件。开始条件使用BEGIN动作激活。直到下一个BEGIN动作执行,给定开始条件的规则才被激活,其他开始条件变得inactive。如果开始条件是inclusive,没有开始条件的规则都是active的;如果是exclusive的,只有符合开始条件的规则才是active的。如果flex文件中定义了一系列的依赖相同exclusive开始条件的规则,相当于描述了一个独立于其他规则的扫描器。下面通过例子说明inclusive和exclusive的区别。
%s example
%%
bar something_else();
相当于
%x example
%%
第二个例子中,如果没有
另外,<*>匹配所有的开始条件。所以,上例还可以写为:
%x example
%%
<*>bar something_else();
BEGIN(0)返回原始状态,这时只有无开始条件的规则是活动的。这个状态也被叫做开始条件“INITIAL”,所以BEGIN(INITIAL)等价于BEGIN(0)。BEGIN动作也可以在规则段的开始被指定为缩进代码。如下例所示,当yylex()被调用,并且enter_special为真,扫描器会进入"SPECIAL"开始条件。
int enter_special; %x SPECIAL %% if ( enter_special ) BEGIN(SPECIAL);为了阐明开始条件的用法,下面给出一个例子。扫描器对像"123.456"这样的字符有两种不同的解释。缺省情况下它被当做3个token,整数123、一个字符"."和一个整数456。但如果它在的行中前面有字符串"expect-floats",它会被识别为单个token,浮点数123.456。blahblahblah ...more rules follow...
%{ #include%} %s expect %% expect-floats BEGIN(expect); [0-9]+"."[0-9]+ { printf( "found a float, = %f\n",atof( yytext ) ); } \n { /* that's the end of the line, so we need another "expect-number" before we'll recognize any more numbers */ BEGIN(INITIAL); } [0-9]+ { printf( "found an integer, = %d\n",atoi( yytext ) ); } "." printf( "found a dot\n" );
下面这个扫描器识别并舍弃C语言的注释,并保留当前的输入行。
%x comment %% int line_num = 1; "/*" BEGIN(comment);开始条件名字是整数值,可以当做整数值存储。因此,以上的例子可以扩展如下:[^*\n]* /* eat anything that's not a '*' */ "*"+[^*/\n]* /* eat up '*'s not followed by '/'s */ \n ++line_num; "*"+"/" BEGIN(INITIAL);
%x comment foo %% int line_num = 1; int comment_caller; "/*" { comment_caller = INITIAL; BEGIN(comment); } ...而且,当前的开始状态可以通过整型值的YY_START宏访问。例如,上述的对comment_caller的赋值可以写为:comment_caller = YY_START。Flex还为YY_START提供了一个别名:YYSTATE。"/*" { comment_caller = foo; BEGIN(comment); } [^*\n]* /* eat anything that's not a '*' */ "*"+[^*/\n]* /* eat up '*'s not followed by '/'s */ \n ++line_num; "*"+"/" BEGIN(comment_caller);
如果多个pattern使用相同的开始条件,可以用如下方式描述:
{ "\\n" return '\n'; "\\r" return '\r'; "\\f" return '\f'; "\\0" return '\0'; }
特殊规则"<
下面给出规则段中用户可用的values。
1. The flex manual page. http://dinosaur.compilertools.net/flex/manpage.html
2. Bison-Flex 笔记. http://www.cppblog.com/woaidongmao/archive/2008/11/23/67635.html
3. 基于MSYS的 Flex & Bison (编译器开发工具)使用教程. https://code.google.com/p/msys-cn/wiki/ChapterFour