1,flex简介
flex是词法分析器(不是Adobe Flex)。它可以扫描一段文字,按照自定义的模式进行匹配,并执行相应的动作。说来抽象,下面准备用简单的例子来直接领略一下flex的风采。本文部分参考http://flex.sourceforge.net/manual/(flex词法分析手册),如果想全面了解flex,需要在本文后,详细阅读flex参考文档。
2,事例
课程(链接)自带的linux已经安装好了flex,如果你用自己的linux版本,那么需要yum或apt-get来安装。
2.1 最简单
一个最简单的可单独运行的程序,读入text文件,输出文件内容。
%{
%}
%option noyywrap
%%
%%
int main()
{
yyin = fopen("text", "r");
yylex();
}
这是flex文件的最基本的结构。
"%{"和"%}"之间是c头文件、各种声明、全局变量的地方。
"%}"和"%%"之间,是定义段(definitions)。包含正则表达式、开始条件(本文后面会涉及)、各种选项。此例子的"%option noyywrap"就是一个选项,表示不需要自定义的"yywrap()”函数。这个函数默认返回1,代表只进行一次文件扫描。为了简化并能继续后序的例子,这里先不做过多解释,就当这个选项必须有即可。
"%%"和"%%"之间是规则段(rules)。在后面的例子中详细介绍。
第二个"%%"之后是c的函数定义,如果有主函数,那么就可以单独运行,也可以被其它程序调用。本篇只涉及flex,所以所有事例代码都包含main函数。yyin是flex内置全局变量。yyin默认是标准输入stdin,你可以指定读入某个文件。yylex()是扫描函数,根据yyin指定的输入流,不断的扫描tokens直到文件结束。
文件内容很简单:"hello flex!"。
运行。把very_easy.flex和text文件放到一个层级,进入terminal(类似windows控制台)输入命令:
flex very_easy.flex
如果正确的话,默认会生成一个lex.yy.c的c文件。通过-o 名字的编译参数,可以生成指定名字的c文件。输入命令:
gcc lex.yy.c
如果语法正确,生成的可执行文件名默认为a.out。你可以增加编译参数来修改名字(例如:gcc lex.yy.c -o very_easy 这样就生成一个叫very_easy的可执行文件)。运行文件:
./a.out
结果如下:"hello flex!”
因为我们什么也没有做,所以flex默认会完整的输出text文件的内容。
2.2 统计行数
统计一个文件里有多少行。
count_lines.flex代码:https://github.com/YellowWang/flex/blob/master/count_lines/count_lines.flex
%{
int num_lines = 0;
%}
%option noyywrap
%%
\n ++num_lines;
.
%%
int main()
{
yyin = fopen("text", "r");
yylex();
printf( "lines = %d\n", num_lines);
}
代码先定义一个全局变量num_lines,用来统计行数,默认为0。
规则段包含一系列的规则,每一个规则形式如下:
pattern action
pattern必须是一行的开头开始,不能有缩进。action跟在pattern后面,必须在一行内。如果action代码比较多,可以将所有代码被"{""}"所包围。action里是c代码。在此例中,\n就是pattern,++num_lines;就是这个pattern的action。\n代表换行符,也就是说当flex扫描文件,每当遇到\n时,就进行后面的action。这样就可以计算文件的行数。action可以不需要,比如例子中的". "。"."后面并没有action,也就代表什么事情也不做。"."的意思是匹配所有字符,那么text里的内容就不会默认输出(因为匹配后没有任何作为)。
注意:pattern的语法是基于正则表达式的标准语法之上,所以需要预先了解一下正则,才能继续往下走。
在main里最后一行,输出文件的行数。
输出结果为:"lines = 9"。
2.3 统计数字个数,并输出最大值
count_digits.flex代码:https://github.com/YellowWang/flex/blob/master/count_digits/count_digits.flex
%{
int num_digits=0, max_digit=0;
%}
%option noyywrap
DIGIT [0-9]+
%%
{DIGIT} {
int n = atoi(yytext);
if (max_digit < n)
max_digit = n;
num_digits++;
}
.
%%
int main()
{
yyin = fopen("text", "r");
yylex();
printf( "num_digits = %d, max_digit = %d\n", num_digits, max_digit);
}
代码定义了2个全局变量。num_digits用来统计总有多少个数字(空白分开连续0到9为一个数字)。max_digit为所有数字里面数值最大的那个。
在定义段( "%}"和"%%"之间,definitions),一个完整定义分为两部分:name definition。name是由"_"或一个字母开头,后面可以是1个或1个以上的字母、数字、"-"、"_"。definition是跟在name后面,需要一个空白符隔开,直到此行行尾。definition可以当作{name}。如此代码中"DIGIT [0-9]+",definition为[0-9]+,可以表示为{DIGIT}。所以这里的name就是为了简化规则段的表示。不如规则段就得是形如:
[0-9]+ {
...
}
不如 { DIGIT} { ... } 简单好看。[0-9]+的意思是匹配0到9,连续出现1个或1个以上。具体可自行看正则表达式。
在规则段,我们主要看一下action。当匹配到一个数字以后,匹配好的字符串保存在yytext全局变量里。因为是字符串形式,所以先要通过atoi函数,把字符串转换为整型。然后找到最大的数字,并累加数字个数。
最后输出结果:"num_digits = 4, max_digit = 567"
2.4 匹配cool语言简单语法
match_words.flex代码: https://github.com/YellowWang/flex/blob/master/match_words/match_words.flex
%{
%}
%option noyywrap
DIGIT [0-9]+
CLASS (?i:class)
INHERITS (?i:inherits)
LET (?i:let)
IN (?i:in)
ASSIGN <-
NOTATION \:|,|\;|\{|\}|\(|\)
BLANK \f|\r|\ |\t|\v
NEWLINE \n
TYPE_IDENTFIER [A-Z][a-zA-Z0-9_]*
OBJ_IDENTFIER [a-z][a-zA-Z0-9_]*
%%
{DIGIT} printf("%s", yytext);
{CLASS} printf("%s", yytext);
{INHERITS} printf("%s", yytext);
{LET} printf("%s", yytext);
{IN} printf("%s", yytext);
{ASSIGN} printf("%s", yytext);
{NOTATION} printf("%s", yytext);
{BLANK} printf("%s", yytext);
{NEWLINE} printf("%s", yytext);
{TYPE_IDENTFIER} printf("%s", yytext);
{OBJ_IDENTFIER} printf("%s", yytext);
.
%%
int main()
{
yyin = fopen("text", "r");
yylex();
}
先看一下要扫描的cool文件:https://github.com/YellowWang/flex/blob/master/match_words/text class Main inherits IO {
main() : SELF_TYPE {
{
let x1:Int <- 1, y:Int in
{
y <- 2;
};
}
};
};
在flex定义段,我们匹配了数字、符号、空白符、变量、对象名、关键字。符号NOTATION如:"\:"、","、";"、"\{"、"\}"、"\("、"\)"。"\"是转义的意思,让后面紧跟的字符为原本的字符,没有特殊意义。空白符BLANK如:"\f"(换页)、"\r"(回车)、"\ "(空格)、"\t"(水平制表)、"\v"(垂直制表)。变量TYPE_IDENTFIER是以大写字母开头,后面跟0个或0个以上的字母、数字、"_"(下划线)。对象名OBJ_IDENTFIER是以小写字母开头,后面跟0个或0个以上的字母、数字、"_"(下划线)。关键字是cool语言内置单词或符号,代表特殊意思。如事例里的"class"、"inherits"、"let"、"in"、"<-"。因为cool语言的关键字是不分大小写,所以正则表达式提供(?i)来指定不区分大小写。(?i:class)就表示class这5个字符都不区分,像Class、cLAss、claSs都是一样的。
除了".",所有规则段里面的action都是原封不动输出,所以最终结果应该和text文件是一样的。如果不一样,那么就可能漏掉了什么,以此可以检查哪些漏掉了。
2.5 注释
%{
int line_num=0;
%}
%option noyywrap
DIGIT [0-9]+
CLASS (?i:class)
INHERITS (?i:inherits)
LET (?i:let)
IN (?i:in)
ASSIGN <-
NOTATION \:|,|\;|\{|\}|\(|\)
BLANK \f|\r|\ |\t|\v
NEWLINE \n
TYPE_IDENTFIER [A-Z][a-zA-Z0-9_]*
OBJ_IDENTFIER [a-z][a-zA-Z0-9_]*
%x comment_line
%x comment_all
%%
{DIGIT} printf("%s", yytext);
{CLASS} printf("%s", yytext);
{INHERITS} printf("%s", yytext);
{LET} printf("%s", yytext);
{IN} printf("%s", yytext);
{ASSIGN} printf("%s", yytext);
{NOTATION} printf("%s", yytext);
{BLANK} printf("%s", yytext);
{NEWLINE} line_num++;printf("%s", yytext);
{TYPE_IDENTFIER} printf("%s", yytext);
{OBJ_IDENTFIER} printf("%s", yytext);
-- BEGIN(comment_line);
<comment_line>[^\n]*
<comment_line>\n line_num++; BEGIN(INITIAL);
"(*" BEGIN(comment_all);
<comment_all>[^*\n]*
<comment_all>"*"+[^)\n]*
<comment_all>"*"+")" BEGIN(INITIAL);
<comment_all>\n line_num++;
.
%%
int main()
{
yyin = fopen("text", "r");
yylex();
printf("line_num=%d\n", line_num);
}
cool语言的注释有两种:"--"是单行注释,"(*"和"*)"是多行注释。单行注释类似c++语言的"//","--"之后到行末所有都被注释掉。"(*"和"*)"类似c语言的"/*"和"*/",不过还是有点区别。在c里,"/* a /* b*/ c */",a和b会被注释掉,"c */"不会,所以会报错,中间不能嵌套。但在cool语言里,"(* a (* b *) c *)”会被全部注释掉,也就是说是可以嵌套的。
先考虑单行注释。这里引入了开始条件(Start conditions)。在之前事例规则段里,只要满足pattern,就会做后面的action。现在不同了,只有pattern处于当前所在的开始条件,才开始起作用。在pattern前加入<condition>,就表示在这个condition条件下,才会考虑这个pattern。一个pattern可以有多个开始条件,形如:"<con1,con2,con3>pattern action"。如果pattern前没有condition,那么开始条件为默认值INITIAL。在定义段可以自定义自己的开始条件,如本例中的"%x comment_line"。"%x condition_name","%x"后就是定义自己的开始条件,也可以多个一起定义,如:"%x con1 con2 con3"。BEGIN函数为开始某个条件,之前的条件会被覆盖掉。如本例中的"-- BEGIN(comment_line);",当匹配到"--"后,就设置为comment_line这个开始条件。这样flex就只考虑以comment_line为条件的pattern。"<comment_line>[^\n]* ",这行的意思是除了换行,匹配所有字符,没有action也就意味着什么也不处理。"<comment_line>\n line_num++; BEGIN(INITIAL);"匹配换行符,累加行数,再把开始条件还原为INITIAL默认值。这样,单行注释的匹配就此完成。
多行注释。为了简便起见,这里实现一个不能嵌套的版本,嵌套版本留给读者当练习题。大概流程为:
1,匹配"(*",进入多行条件comment_all;
2,匹配所有连续的"*",并且后面不是")"、"\n";
3,匹配"\n",累加行数;
4,匹配所有连续的"*",并且后面是")",结束多行注释,设置开始条件为INITIAL。
我们刚才介绍的"%x"是排他开始条件,还有"%s"是包含开始条件。以及其它典型引用可以参考flex手册start conditions章节(http://dinosaur.compilertools.net/flex/flex_11.html#SEC11)。
程序输出为涵盖注释的文件总行数。
3,compiler词法编程作业
compiler课程第一个大作业,Assignments分类里第一个Programming Assignment 1。就是用flex(如果你用c语言的话)做cool语言的词法分析。作业的要求和如何开始、提交,作业说明里都有,要仔细阅读。
pattern匹配的难点是字符串匹配、可以嵌套的多行注释。
作业还需要找到cool语言的几种词法错误,也是难点之一。
action要按照要求,返回相应的内容,提供给未来的语法分析器(将在下一篇blog介绍)。
如果写作业之中遇到什么问题,或者本文有什么误导的地方,欢迎批评指教。
(全文完)