前面的话:在此之前,如果我接到一个解析文本的工作,我会逐行读取并存储我想要的数据再去处理数据。最近,工作中需要去解析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乘法表了~
加油:)