知也无涯

吾生也有涯,而知也无涯,能学一点算一点…

  • 引言

    这是一个系列文章,旨在了解如何使用Flex/Lex和Yacc/Bison进行词法和语法解析。在前面,已经完成了使用Lex/flex做基础的词法解析。接着,开始这个系列的第二部分,使用flex和bison完成一个简单的计算器。

    为了简化实现,将注意力放在简单flex和bison使用上,这里对计算器做了几个简化:

    • 只支持加、减、乘计算,暂时不支持除法,除法可能涉及到浮点类型,故暂时忽略
    • 不考虑整数溢出的问题,这里使用int类型(那么他存储与计算范围是有限的)

    也就是该程序可以计算加法、减法、乘法,以及他们的任意组合,如: 3+4、 3+4*2、 3+4*2-3、 3+4*2-3*2

    后续,还将考虑增加更急复杂的计算器,包括:

    • 实现,带有变量的计算器程序
    • 实现带来循环、带有条件判断的程序

    这里先从简单的开始。

    初次手写一个cal.l和cal.y

    这是在vim中写出的第一遍代码,包含了词法文件cal.l和语法文件cal.y,详细如下。这其中当然是有很多错误的,之所以,依旧写出来,是为了和正确代码对比,以此看看对哪些概念理解有偏差。如果你是找一个正确例子的话,可以跳过这一段。

    %{
    #inlcude <stdlib.h>
    %}
    /* 十进制整数 */
    %token INTEGER
    
    %union { 
        int a;
    }
    
    /* 操作符 + - * / */
    %token OPERATOR
    
    %%
    program:
        program expression \n { printf("%d",$2); }   // 这里就是以回车结尾,也可以考虑使用 = 结尾
        |
    
    expression:
    		  INTEGER
    		| expression '+' expression {$$ = $1 + $2}
    		| expression '-' expression {$$ = $1 - $2}
    		| expression '*' expression {$$ = $1 * $2}
    
    dec_integer: 
    		INTEGER
    			{
    			$$ = $1.a;
    			}

    这里也有几个已知的问题,例如:运算符的优先级没有定义,也就说4+3*2可能会算成14。没错,如果眼尖的话,还发现有一些拼写错误。

    接着是cal.l文件:

    #inlcude <stdlib.h>
    #include "y.tab.h"
    ​
    %}
    [:digit:]+ {
                yylval.a = atoi(yytext)
                RETURN INTEGER;
    }

    当然,这里面有很多的错误。一会儿来看后面正确的内容。

    修改cal.l和cal.y

    • 首先,是去解决已知的问题:运算符优先级如何去解决?关于什么是优先级、什么是结合律这里不做详述,这里有一篇文章讲得比较细致,几幅图也非常直观:Precedence and Associativity of Operators in Python。虽然是不同的语言,但意思是一样的。理解这个点还是比较重要的,例如在关系型数据库中,之前有遇到过这样的案例,可以思考一下如下的表达式 t.col < 2 or t.col < 11 and t.col > 4 是什么意思:
    -- 猜测一下,如下的 SELECT 查询是否能够返回记录:
    
    CREATE TABLE t(col INT); 
    
    INSERT INTO t values (1);
    
    WHERE t.col < 2 or  t.col < 11 and  t.col > 4

    扯远了,再回到cal.y文件,优先级和结合律需要进行如下修改,以定义”*”优先级高于”+”、”-“:

    %left '+' '-'
    %left '*'

    这里的代码先后,就定义了优先级;优先级越高,定义在越在后面;left表示,左结合。

    • 除了优先级,在cal.y语法规则中的定义部分,如果有字符,并没有使用引号。例如上面的cal.y的第17行的\n,是需要加上引号的,即 '\n'

    • 对于cal.y的中定义的语法规则,并没有定义返回值存储在联合体(YYSTYPE,也就是如下这里cal.y定义的唯一的那个联合体)哪个类型中。例如,没有定义”expression”这个语法规则,返回值是使用哪个值,虽然这里的联合体只定义了一个类型(即int a)。具体的,添加了如下代码:
    %token <a> INTEGER
    %type <a> expression

    完成这样的定义后,在lex的文件cal.l中,就可以对yylval进行赋值,如:yylval.a = atoi(yytext); 这时,对于yacc文件中cal.y,如果引用这个TOKEN,就可以使用$1(或者是$2、$3)来引用lex解析后的返回值,如:expression: INTEGER { $$ = $1;}

    • 于是,重新使用了独立的Token重新定义了运算符,并定义了优先级,如下:
    cat cal.y
    ...
    %token O_ADD O_MINUS O_MULTIPLY O_EQ
    
    %left O_ADD O_MINUS
    %left O_MULTIPLY 
    
    %token <a> INTEGER
    %type <a> expression
    ...
    
    cat cal.l
    ...
    "=" { return O_EQ;};
    "+" { return O_ADD;};
    "-" { return O_MINUS;};
    "*" { return O_MULTIPLY;};
    ...
    • 没有定义 yyerror 函数,程序也会编译不过去,会报如下错误:
    cal.tab.c:(.text+0x53b): undefined reference to `yyerror'

    按照文档,可以定义一个最简单的yerror函数(参考:The Error Reporting Function yyerror),如下:

    void
    yyerror (char const *s)
    {
      fprintf (stderr, "something error: %s\n over", s);
    }
    • 删除了无效的dec_integer规则,如果有无效的规则,也会失败
    • 将[:digit:]修改为[0-9]+。至于为什么[:digit:]不生效,后面做了一些搜索。为了避免歧义,需要额外再加一层中括号,写法也就是[[:digit:]]。详细参考:Patterns@Lexical Analysis With Flex

    完整的计算器程序文件cal.l cal.y

    补充一个main入口函数,修改cal.l、cal.y即可。

    修正后的cal.l

    %{
        #include "cal.tab.h"
        #include <stdio.h> 
    %}
    %option noyywrap
    %%
    [0-9]+ {
    			yylval.a = atoi(yytext);
    			return INTEGER;
        	   }
    
    "=" { return O_EQ;};
    "+" { return O_ADD;};
    "-" { return O_MINUS;};
    "*" { return O_MULTIPLY;};
    
    %%

    修正后的cal.y

    %{
    	#include <stdlib.h>
    	#include <stdio.h>
    
    
    int main(){
    	yyparse();
    }
    
    void
    yyerror (char const *s)
    {
      fprintf (stderr, "something error: %s\n over", s);
    }
    
    %}
    
    
    %union { 
        int a;
    }
    
    %token O_ADD O_MINUS O_MULTIPLY O_EQ
    %token <a> INTEGER
    
    %left O_ADD O_MINUS
    %left O_MULTIPLY 
    %type <a> expression
    
    %%
    program:
        |
        program expression O_EQ { printf("result is : %d",$2); }   
    ;
    expression:
    		  INTEGER { $$ = $1;}
    		| expression O_ADD expression {$$ = $1 + $3; }
    		| expression O_MINUS expression {$$ = $1 - $3; }
    		| expression O_MULTIPLY expression {$$ = $1 * $3;}
    ;

    编译与执行

    然后,就可以生成c文件并编译可执行文件了:

    lex cal.l 
    bison -d cal.y 
    gcc cal.tab.c lex.yy.c -o a.out
    ./a.out
    4+3*2=
    
    也可以像这样:
    lex cal.l && bison -d cal.y && gcc cal.tab.c lex.yy.c -o a.out && ./a.out

    附带注释说明的cal.y文件

    %{                                    // %{ ... %}  包含了完整的C语言代码        
    	#include <stdlib.h>           // 这里包含需要的头文件、入口函数main
    	#include <stdio.h>            //    以及一个简答的yyerror函数
    
    
    int main(){
    	yyparse();
    }
    
    void
    yyerror (char const *s)
    {
      fprintf (stderr, "something error: %s\n over", s);
    }
    
    %}
    
    
    %union {                             // 这是一个非常重要的联合体,用于定义
        int a;                           // 各种不同类型的Token所返回的数据
    }                                    // 广泛的被yylex使用,并用于与.yy文件共享数据
                                         // 也就是 YYSTYPE 
    
    %token O_ADD O_MINUS O_MULTIPLY O_EQ
    %token <a> INTEGER                   // 这里表示INTEGER(这是一个被lex识别的token)
                                         // INTEGER(被lex识别的token返回值为YYSTYPE.a
    %left O_ADD O_MINUS                  // 这里定义 + -为左结合律
    %left O_MULTIPLY                     // 这里定义 * 为左结合律,并且优先级高于 + -
    %type <a> expression                 // 这里定义语法规则(grammer rule)expression
                                         // 的返回值为 YYSTYPE.a
    %%
    program:                             // 这是start symbol,所有的program都需要符合该定义
        |
        program expression O_EQ { printf("result is : %d",$2); }   
    ;
    expression:
    		  INTEGER { $$ = $1;}
    		| expression O_ADD expression {$$ = $1 + $3; }
    		| expression O_MINUS expression {$$ = $1 - $3; }
    		| expression O_MULTIPLY expression {$$ = $1 * $3;}
    ;

  • 这是一个系列文章,旨在了解如何使用Flex/Lex和Yacc/Bison进行词法和语法解析。这个系列,分成了几个部分,包括

    • flex的基本用法
    • 使用flex/bison实现一个简单的计算器
    • 实现一个带有条件判断与循环的计算程序

    了解这个系列需要一定的编译原理知识作为背景知识,了解程序如何从字符串先解析成Token,而后使用语法解析器生成解析树,最后执行该解析树。

    概述

    lex/flex可以按照“词法文件”的定义,将文本解析成单个的Token,然后通过执行“词法文件”中定义的Action来完成一些操作,一般,flex的输出会通过函数/变量将结果传递给yacc/bison进行进一步的语法解析。为了简化,本文将仅通过独立的“词法文件”完成一些操作,以了解flex的基础使用。

    这里完成的程序是一个简单的“count”程序,输入是一个文件,程序输出文件中包含的字符数、词语数、以及行数。

    安装flex并编写词法文件

    1. 安装lex: yum/apt-get install flex

    2. 编写如下词法文档:

    %{                                       //
            int characters = 0;              //    %{ ... }% 之间的部分是"Declarations"
            int words = 0;                   //    Declarations 部分声明的变量,是可以在全局使用的
            int lines = 0;                   //    例如,在该示例的main程序中,就通过extern声明的方式
    %}                                       //    使用了这些变量
    %%                                       //
    \n      {                                //    从这里开始是Translations阶段
                    ++lines;                 //    这里定了Token,以及遇到了Token之后
                    ++characters;            //    应该采取/执行什么,例如这里遇到了\n字符
            }                                //    则,将lines/characters变量都加1
    [ \t]+          characters += yyleng;    //
    [^ \t\n]+ {                              //    注释部分的文本需要删除,程序才能正常编译 
                    ++words;                 //    删除注释的vim命令:1,$s/\/\/.*$//g 
                    characters += yyleng;    //
            }                                //
                                             //
    %%

    直接使用如上代码的话,后面就会在gcc编译的时候遇到如下错误:

    $ lex zzx.l
    $ gcc lex.yy.c wc.c -o wc.out
    /tmp/cc1SPYm2.o:In function yylex':
    lex.yy.c:(.text+0x42f):undefined reference toyywrap'
    /tmp/cc1SPYm2.o:In function input':
    lex.yy.c:(.text+0xf73):undefined reference toyywrap'
    collect2: ld returned 1 exit status
    

    如果你也遇到了这个错误,不用担心,你并不孤单,在Stackoverflow上看到解决该失败的的答案一共有150点赞(up),就知道大家都一样了(参考@Stackoverflow)。因为默认的,lex生成的词法解析程序中,在最后是需要调用的yywrap函数的(关于yywrap),如果不打算提供该函数,则可以使用lex选项 %option noyywrap 禁用该调用。那么上面的代码就需要修改为:

    $ cat zzx.l 
    %{
            int characters = 0;
            int words = 0;
            int lines = 0;
    %}
    %option noyywrap
    %%
    \n      {
                    ++lines;
                    ++characters;
            }
    [ \t]+          characters += yyleng;
    [^ \t\n]+ {
                    ++words;
                    characters += yyleng;
            }
    
    %%

    编写入口函数并调用yylex

    词法文件需要使用工具flex将其编译生成一个c语言文件,然后再使用gcc将其编译成一个可执行文件。编译前,我们需要先编写一个简单的main函数. 再编写一个程序的入口函数(main),并调用yylex()就可以了。具体如下:

    $ cat wc.c
    #include <stdio.h>
    
    int yylex(void);
    
    int main(void)
    {
            extern int characters, words, lines;
    
            yylex();
            printf("%d characters, ", characters);
            printf("%d words, ", words);
            printf("%d lines\n", lines);
            return 0;
    }

    这里需要注意:在程序中,我们通过调用yylex()完成了实际的词法解析过程,并获得执行结果。这是一个非常简单的示例,实际过程比这要更加复杂,在词法文件中,每一次rule解析完成后,再起action部分,通常都会有return语句结束本次yylex调用,所以会是一个反复调用yylex的过程。

    编译并执行

    $ lex zzx.l    
    $ gcc lex.yy.c wc.c -o wc.out
    $ chmod +x wc.out
    $ cat s.txt
    this is a input file.
    this is a input file.
    $ ./wc.out < zzx.l
    404 characters, 36 words, 18 lines
    $ ./wc.out < s.txt
    44 characters, 10 words, 2 lines

    好了,至此,我们就完成一个词法解析的任务,因为这个任务不涉及任何语法(yyac)解析,所以比较适合初学者学习词法解析工具lex。

    补充关于Definitions

    为了再略微增强该示例的,这里对上面的示例又做了一个小调整,新增一行“Definitions”,有时候为了增强可读性,会对一些expression定义一个名称,如下,将\n定义为NL:

    %{                                        //
            int characters = 0;               //   %{ ... }% 之间的部分是"Declarations"
            int words = 0;                    //   Declarations 部分声明的变量,是可以在全局使用的
            int lines = 0;                    //   例如,在该示例的main程序中,就通过extern声明的方式
    %}                                        //   使用了这些变量
                                              //
    NL \n                                     //   这里新增了一行,这是一行 Definitions
                                              //   将\n用字母NL定义,所以下面的\n也就可以使用NL
                                              //   试想,如果表达式很复杂用这种方式,可读性会增强很多
    %%                                        //
    NL      {                                 //   从这里开始是Translations阶段
                    ++lines;                  //   这里定了Token,以及遇到了Token之后
                    ++characters;             //   应该采取/执行什么,例如这里遇到了\n字符
            }                                 //   则,将lines/characters变量都加1
    [ \t]+          characters += yyleng;     //
    [^ \t\n]+ {                               //   注释部分的文本需要删除,程序才能正常编译        
                    ++words;                  //   删除注释的vim命令:1,$s/\/\/.*$//g 
                    characters += yyleng;     //
            }                                 //
                                              //
    %%           

    自此,我们就了解一个词法解析文件的几个主要部分:Definitions、Declarations、rule(以及rule对应的Action)。

    参考资料:

    更多说明

    • Flex / Lex程序通常与Yacc/Bison一起使用,flex负责词法解析,bison则负责语法解析
    • flex与bison接口的函数,就是上面的调用 yylex()函数,该函数每次基于某个规则(rule)解析到一个新的Token的时候,则会执行对应的“Action”(也就是每个Token后面的代码)部分的代码。例如,上面的程序中会执行++lines++words代码。
    • 在rule action部分,我们看到使用了一个yyleng的“变量”用于获取当前被解析的原始字符串的长度。类似的“变量”有:yyleng、yytext、yyin等,完整的列表可以参考:Values Available To the User。另外,这些“变量”并不是真的变量,大部分都是一些“宏”,例如,yytext的真实定义可能是这样的:#define yytext (((struct yyguts_t*)yyscanner)->yytext_r)。了解这一点,有利于理解这些”变量”并不能在外部直接引用。
      • yyin yylex函数处理的字符串来源,默认情况是标准输入,在你的程序,例如可以定义为一个打开的文件描述符(在Posix中,一起都是文件)
      • yylength 用于记录,当前读取的Token的长度
      • yytext 用于记录当前读取的文本
    • 一般的,flex的“Action部分”,会包含一个return,例如如果遇到一个整数,可能会看到类似这样的代码:return INTEGER; 这时候,yylex()遇到一个对应的字符就会返回INTEGER
    • 在实践中,则是按照如下方式实现:
      • 在 yacc/bison的语法文件中定义Token,例如整数为 INTEGER,语法为 %token INTEGER
      • 使用yacc/bison命令生成对应的头文件,头文件则会包含关于 INTEGER的预定义:#define INTEGER 257
      • 只需要在flex词法文件中包含该头文件,就可以使用这里的预定义 INTEGER
      • 那么较为完整的代码看起来就是这样
    cat cal.y
    ...
    %token INTEGER
    ...
    cat cal.tab.h //这是bison生成的文件
    ...
    #define INTEGER 257
    ...
    cat cal.l
    ...
    [[:digit:]]+  { return INTEGER; }
    ...
    • 我们在考虑另一个问题:在lex的rule action可以使用本地的“变量”(其实是“宏”),也会通过return语句给yyparse()中调用yylex时,返回当前Token的类型。如果一个Token是一个[:digit:]+的时候,我们除了需要知道这个Token是一个整数之外,至少yyparse()还需要知道这个是一个什么整数,具体数值是多少,当然并不是所有的token都需要,一般identifier都是需要的。而,前面的yytext都是yylex本地的“变量”。这时候,通常会使用yylvalyylval是一个由yacc/bison定义的变量(默认是int类型),用于存储词法解析需要传递给yyparse的数据,在yacc/bison的语句处理的Action阶段,可以使用变量,以获得词法解析阶段的一些值。例如,一个Token是一个整数、字符串(并非keyword)的时候,我们会将对应的值存储在yylval中。所以,yylval通常会被定义为一个联合体(union类型),用于存储不同类型的值。

    关于这几个概念的更详细细致的解释可以参考最前面提到的“IBM的z/OS系统的文档中关于lex和yacc的介绍”(参考:Tutorial on using lex and yacc)。

  • 编写一个shell脚本,做一些事;改进这个脚本,更好做这件事;再改进这个脚本,帮自己做些其他的事情;再改进这个脚本帮助其他人做一些事……

    简单的脚本处理,一般使用变量$0 $1 $2 …就可以依次获得全部参数,还可以通过$#获得这个脚本一共有多少个参数。如果你需要处理的情况(或者分支)更多的时候,这个方法就不凑效了,这时候,就可以考虑使用getopts了(man getopts)。

    (more…)