Pinkman

导航

从头学习compiler系列4——flex实践

1,flex简介

    flex是词法分析器(不是Adobe Flex)。它可以扫描一段文字,按照自定义的模式进行匹配,并执行相应的动作。说来抽象,下面准备用简单的例子来直接领略一下flex的风采。本文部分参考http://flex.sourceforge.net/manual/(flex词法分析手册),如果想全面了解flex,需要在本文后,详细阅读flex参考文档。
 

2,事例

    课程(链接)自带的linux已经安装好了flex,如果你用自己的linux版本,那么需要yum或apt-get来安装。

    2.1 最简单

        一个最简单的可单独运行的程序,读入text文件,输出文件内容。
        very_easy.flex代码:https://github.com/YellowWang/flex/blob/master/veryeasy/very_easy.flex

        %{
        %}
        %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直到文件结束。
        text文件:https://github.com/YellowWang/flex/blob/master/veryeasy/text
        文件内容很简单:"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 统计数字个数,并输出最大值

        %{
        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语言简单语法

        %{
        %}
        %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介绍)。
    如果写作业之中遇到什么问题,或者本文有什么误导的地方,欢迎批评指教。
(全文完)



posted on 2013-06-03 19:52  Pinkman  阅读(2728)  评论(2编辑  收藏  举报