我的好兄弟之Flex&Bison 第二章 让Flex和Bison一起干活!


前面的话:在此之前,如果我接到一个解析文本的工作,我会逐行读取并存储我想要的数据再去处理数据。最近,工作中需要去解析verilog代码,相信verilog有许多人都用过,各关键字有相对应的含义和用法,很明显不能通过上述的方法来做,大概浏览了github,给我这个没有学过编译原理的人指出了一条明路:yacc&lex,或者,flex&bison。

本系列文章:我写这个系列的博客主要是记录收获的知识和踩过的坑,初学者的缘故,并不对其他人具有指导意义。当然,也可能你也和我有相同的问题或者感想,我们可以好好讨论一下。

我的目标:以后如果碰到需要解析的工作,一个下午搞定。

前章:我的好兄弟之Flex&Bison 第一章 实现人生第一个Flex!


 让Flex和Bison一起干活!

使用Flex和Bison携手制作一个桌面计算器。首先,编写一个词法分析器,然后编写一个语法分析器并把两者结合起来。

一、再学一点Flex

第一章只是实现了一个Flex程序,只是皮毛。现在,再学一点皮毛吧~

1、一个简单计算器的词法

 脑海中的计算器大概就是加减乘除吧,先用Flex进行一个简单的定义

%%
"+"      {printf("PLUS\n");}
"-"      {printf("MINUS\n");}
"*"      {printf("TIMES\n");}
"/"      {printf("DIVIDE\n");}
"|"      {printf("ABS\n");}
[0-9]+    {printf("NUMBER %s\n",yytext);}
\n       {printf("NEWLINE\n");}
[ \t]     {}
.        {printf("Mystery character %s\n",yytext);}
%%

经过第一章,这几种模式我就不再赘述了。

然后我们编译一下,或者直接写一个Makefile

一定有这样的疑问:没有第三段的C代码,也可以运行吗?

是的,flex库文件(-lfl)提供了一个极小的主程序来调用词法分析器,对这个小例子来说是足够用的。

2、yylex()

第一章例子中的第三段C代码中出现了yylex(),那么它是用来干什么的呢?另一个问题,为什么上述的例子中只要换行就会打印信息呢?

每当程序需要一个记号时,它就会调用yylex()来读取一小部分输入然后返回相应的记号。当动作代码识别出一个记号时,yylex()就会将这个记号作为返回值。需要下一个记号时,就会再次调用yylex()。yylex()会记住当前处理的位置,并从此处开始下一次调用。

另外,如果一个模式并不能返回记号时,yylex()会继续分析接下来的输入直到有返回记号或者我们让它停止。

举一个很简单的例子:

[0-9]+    {return NUMBER;}
\n       {return EOL;}
[ \t]     {}

如果我们输入中有空格,并不会NUMBER和EOL的记号造成影响。

3、记号编号和记号值

当flex词法分析器返回一个记号流时,每个记号有两个组成部分,记号编号(token number)和记号值(token's value)。记号编码类似枚举,它是一个整数,并没有既定规律,但是零意味着文件结束。

直接上代码

%{
  enum yytokentype
  {
    NUMBER = 258,
    ADD = 259,
    SUB = 260,
    MUL = 261,
    DIV = 262,
    ABS = 263,
    EOL = 264
  };
  int yylval;
int lexerror(char *s);
%}
%%
"+" { return ADD; }
"-" { return SUB; }
"*" { return MUL; }
"/" { return DIV; }
[0-9]+  { yylval = atoi(yytext); return NUMBER; }
\n      { return EOL; }
[ \t]   { /* ignore white space */ }
.   { lexerror(yytext); }
%%
int lexerror(char *s)
{
  fprintf(stderr, "lexical error: %s\n", s);
}
int main(int argc, char** argv){
  int tok;
  
  while(tok = yylex()){
     printf("%d",tok);
     if(tok == NUMBER) printf(" = %d\n",yylval);
     else printf("\n");
  }
}

打印来看看

打印的是记号的编号。

二、精力转向语法分析器

我们已经有了一个词法分析器了,接下来了解一下语法分析器

1、BNF文法

在计算机分析程序里常用的语言就是上下文无关文法(Context-Free Grammar,CFG)。书写上下文无关文法的标准格式就是BackusNaur范式。

简单的来说,BNF文法就是简洁描述编程语言的语言。它的基本结构为:

<exp> ::= <factor>

      | <exp> <factor>

<factor> ::= NUMBER

      | <factor>  NUMBER

::= 意味着左边还有东西没有定义完,还可以由右边的的表达式继续定义。

| 意味着或,就是还可以用右边的表达式继续定义。

举个例子:

在中文语法里,一个句子一般由“主语”、“谓语”和“宾语”组成,主语可以是名词或者代词,谓语一般是动词,宾语可以使形容词,名词或者代词。那么“主语”、“谓语”和“宾语”就是非终止符,因为还可以继续由“名词”、“代词”、“动词”、“形容词”等替代。

例1. <句子> ::= <主语><谓语><宾语>

例2. <主语> ::= <名词>|<代词>

例3. <谓语>::=<动词>

例4. <宾语>::=<形容词>|<名词>|<代词>

例5. <代词>::=<我>

2、初探Bison规则描述语言

bison的规则基本上就是BNF,但是做了一点点简化。直接上例子

%{
#include <stdio.h>
%}

%token NUMBER
%token ADD SUB MUL DIV ABS
%token EOL

%%
calclist :
 | calclist exp EOL { printf("= %d\n",$2); }
 ;

exp : factor { $$ = $1;}
 | exp ADD factor { $$ = $1 + $3; }
 | exp SUB factor { $$ = $1 - $3; }
 ;

factor : term  { $$ = $1; }
 | factor MUL term { $$ = $1 * $3; }
 | factor DIV term { $$ = $1 / $3; }
 ;

term : NUMBER  { $$ = $1; }
 | ABS term { $$ = $2 >= 0? $2 : -$2; }
 ;
%%

main(int argc,char **argv){
   yyparse();
}
yyerror(char *s){
   fprintf(stderr, "error:%s\n", s);
}

可以看到,bison程序包含和flex程序相同的三部分结构。

第一部分除了声明部分还包括%token记号部分,以便告诉bison在语法分析程序中记号的名称。

第二部分包含通过简单的BNF定义的规则。bison使用单一的冒号而不是::=,同时使用分号表示规则的结束。每个bison规则中的语法符号都有一个语义值,目标符号的值(冒号左边)通过$$表示,右边语法符号的语义值一次为$1、$2,直到这条规则结束。当词法分析器返回记号时,记号值总是储存在yylval里,其他语法符号的语义值则在语法分析器的规则里设置。比如本例中factor、term和exp符号的语义值就是它们所描述的表达式的值。

三、一起干活!

1、修改Flex程序

首先先编译一下bison程序,bison和yacc一致,都是.y结尾的文件。

bison -d fb1-5.y

会创建两个文件,分别是fb1-5.tab.c和fb1-5.tab.h。

不考虑.tab.c,看看.tab.h文件。

有没有很像在flex程序的第一部分,记号编号和yylval。

悟了,编译bison会生成一个.tab.h的文件,我们可以将其通过#inclde<fb1-5.tab.h>引入到.l文件的第一部分,修改一下fb1-5.l的第一部分。

%{
#include"fb1-5.tab.h"
int lexerror(char *s);
%}
%%
...
...

2、进行一个括号的加

细心一点就会发现,目前实现的计算器并没有括号,速速添加进去

在flex程序里添加

%{
......
....
%}

%%
"(" { return LP; }
")" { return RP; }
......
....
%%

在bison程序里添加

%token LP RP

%%
term : NUMBER  { $$ = $1; }
 | ABS term { $$ = $2 >= 0? $2 : -$2; }
 | LP exp RP { $$ = $2; }
 ;
%%

3、计算器成功

现在flex和bison对应的fb1-5.l和fb1-5.y都准备就绪了,因为命令变多了,我们写一个Makefile

fb1-5: fb1-5.l fb1-5.y
    bison -d fb1-5.y
    flex fb1-5.l
    cc -o $@ fb1-5.tab.c lex.yy.c -lfl

运行之

好了,我们的计算器制作完成,缺点就是仅针对整数。

4、遇到的问题

(1)bison规则中的default

(2)关于"%"的小细节

不管是flex程序还是bison程序,并不是必须都要包含三部分。

%{
#include"fb1-5.tab.h"
int lexerror(char *s);
%}
%%

如上图所示,仅仅这样也能编译成功。但是最后一个"%%"是必不可少的,否则就会报错。

妥了,有了计算器再也不用每天伏案默写99乘法表了~

加油:)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM