• 从头学习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文件,输出文件内容。

            %{
            %}
            %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 统计行数

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



  • 相关阅读:
    图解SQL的Join(转)
    MySQ数据表设计
    关于数据库DML、DDL、DCL区别
    SQL多表连接查询
    Xcode报错Expected selector for Objective-C and Expected method body
    Mac上安装使用MYSQL以及Navicat数据库管理和PHP服务器配置
    更换app开发者账号
    Mac 下的 C++ 开发环境
    spring-retry 重试机制
    Ribbon的主要组件与工作流程
  • 原文地址:https://www.cnblogs.com/pinkman/p/3116007.html
Copyright © 2020-2023  润新知