flex和bison的简介
1.1 词法分析和语法分析
分析工作可分为两个部分:词法分析(lexical analysis 或 scanning)和语法分析(styntax analysis 或 parsing)。
词法分析把输入分割为一个一个有意义的词块,称为记号(token);语法分析确定这些记号是如何彼此关联的。
1.2 正则表达式和词法分析
词法分析通常做的就是在输入中寻找字符的模式。 一种简洁的模式描述方式就是正则表达式(regular expression)。 Flex 程序主要由一系列带有指令的正则表达式组成,这些指令是正则表达式匹配后要执行的动作(action)。flex 生成的词法分析器可以读取输入,匹配输入,匹配输入和所有正则表达式并执行匹配后的动作。
Flex 会将正则表达式翻译成一种高效的内部格式,这个格式叫做确定性有穷自动机(Deterministic Fininte Automation, DFA )
1.3我们的第一个Flex程序
fb1_1.l 文件如下:
/**例 1-1 字数统计fb1-1.l */
%{
int chars = 0;
int words = 0;
int lines = 0;
%}
%%
[a-zA-Z]+ {words++; chars += strlen(yytext);}
\n {chars++; lines++;}
. {chars++;}
[^ \t\n\r\f\v]
%%
main(int argc, char ** argv)
{
yylex();
printf("%8d%8d%8d\n", lines, words, chars);
}
上面代码被 %% 分为3个部分,第一个部分是声明和选项设置,其中的 %{%} 之间的部分会被原样拷贝到生成的词法分析器; 第二个部分是模式及动作,第三个部分是C代码,会被原样拷贝到生成的词法分析器。
上面程序有3个模式,第一个模式 [a-zA-Z]+ {words++; chars += strlen(yytext);} 。[a-zA-Z]+,模式是用来匹配一连串的字母,或者说一个单词。其中[a-zA-Z] ,匹配任意一个大小写字母,而 + 匹配一个或多个前面的字符类。 变量 yytext 指向本次匹配的输入文本。 {words++; chars += strlen(yytext);} ,动作是单词数量+1, 字符数量加上本次匹配的文本长度。
第二个模式 \n {chars++; lines++;}。 \n, 模式是匹配换行符。 {chars++; lines++;} 动作是字符数量+1, 行数+1.
第三个模式 . {chars++;}。. ,模式是匹配任意一个字符。{chars++;},动作是字符数量+1.
末尾的 main 函数 c 代码是主程序,负责调用flex 提供的词法分析函数 yylex(),并输出相应结果。
Makefile 文件如下:
fb1:
flex fb1_1.l
gcc -o fb1 lex.yy.c -lfl
clean:
rm -rf fb1 lex.yy.c
flex fb1_1.l ,flex 工具使用将 fb1_1.l 生成C程序 lex.yy.c。gcc 编译C程序生成可执行程序 fb1。
执行如下:
./fb1
1234 1234
1 2 10
1.4 一个单词替换的例子
一些应用简单到可以把所有的内容都写在flex 文件里面。例如下面对一些单词进行了替换并输出。可以看到文件中只有flex 文件的第二部分,即模式及动作。
eng1.l 文件如下
/**
例1-2:英式英语 -> 美式英语
*/
%%
"colour" {printf("color");}
"flavour" {printf("flavor");}
"clever" {printf("smart");}
. {printf("%s ", yytext);}
%%
这个程序读取输入,当匹配到 "colour" "flavour" "clever" 这三个单词时,会打印对应的单词。
Makefile 如下
eng:
flex eng1.l
gcc -o eng1 lex.yy.c -lfl
clean:
rm eng1 lex.yy.c
1.5 让flex 和bison 协同工作
先编写一个词法分析器,接着编写一个语法分析器,并把二者结合起来。
这里所说的词法分析器可以认为是 .l 文件, 语法分析器可认为是 .y 文件。
fb1_3.l 如下:
/* 例1-3:一个简单的flex 词法分析器 fb1_3.l
识别出用于计算器的记号并把他们输出
*/
%%
"+" {printf("PLUS\n");}
"-" {printf("MINUS\n");}
"*" {printf("MUL\n");}
"/" {printf("DIVIDE\n");}
"|" {printf("ABS\n");}
[0-9]+ {printf("NUMBER %s\n", yytext);}
\n {printf("NEWLINE\n");}
[ \t] {printf("WHITE SPACE\n");}
. {printf("Mystery character:%s\n", yytext);}
%%
前5 个模式 "+" "-" "*" "/" "|" 就是操作符本身,动作是打印出匹配的内容。引号告诉flex 使用引号内的文本原义,而不是解释为正则表达式。
第6个模式 [0-9]+ 匹配一个整数。其中 [0-9] 匹配任意一个数字, + 匹配一个或多个前面的项。动作是将 yytext 这个当前匹配的字符串打印出来。
第7个模式 \n 匹配一个换行。
第8个模式 [ \t] 用来忽略空白字符。匹配一个 空格 或 tab 。动作是无需任何动作。
第9个模式 . 用来匹配前面所有模式没有匹配的内容。动作是打印一个提示信息。
该程序也只有flex 程序的第二部分,即模式及动作。flex 库文件 -lfl 提供了一个极小的主程序用来调用词法分析器。
编译,Makefile 文件如下:
fb1_3:
flex fb1_3.l
gcc -o $@ lex.yy.c
clean:
rm -rf lex.yy.c fb1_3
执行结果如下:
./fb1_3
1+2
NUMBER 1
PLUS
NUMBER 2
NEWLINE
1.6 作为协同程序的词法分析器
当程序需要一个记号(token)时,调用yylex() 读取一部分输入然后返回相应的记号(token)。当程序需要下一个记号时,yylex() 会再次被调用。词法分析器以协同程序的方式运行。也就是当它返回时,它会记住当前处理的位置,并从这个位置开始下一次的调用。
一个例子,模式,动作:
"+" {printf("PLUS\n");}
[0-9]+ {printf("NUMBER %s\n", yytext);}
\n {}
前两个模式可以返回记号(token);第3个模式不做任何事情不返回记号(token)。如果有动作返回,词法分析器会在下一次yylex() 调用时继续;如果没有动作返回,则词法分析器立即继续进行。
下面修改词法分析器,它返回的记号(token)可以被语法分析器用来实现一个计算器。
1.7 记号编号和记号值
对于每个记号(token) 有两部分组成,记号编号(token number)和记号值(token's value)。bison 创建一个语法分析器时,会自动的从258 起指派每个记号编号。
记号编号区分不同的记号,记号值可以区分一组相似的记号。例如,在我们的词法分析器中,所有数字都属于Number 这个记号,而记号值表明了具体的数字。
例1-4:计算器词法分析器fb1-4.l , 如下:
/* 例1-4:计算器词法分析器fb1-4.l */
/* 识别出用于计算器的记号,并输出记号编号 */
%{
enum yytokentype {
NUMBER = 258,
ADD = 259,
SUB = 260,
MUL = 261,
DIV = 262,
ABS = 263,
EOL = 264
};
int yylval; /*使用yylval用来存储记号值**/
%}
/**
前5个模式就是操作符本身,用引号引起。引号告诉flex使用引号内文本的原义,而不是解释为正则表达式
*/
%%
"+" {return ADD;}
"-" {return SUB;}
"*" {return MUL;}
"/" {return DIV;}
"|" {return ABS; }
[0-9]+ {yylval = atoi(yytext); return NUMBER;}
\n {return EOL;}
[ \t] {}
. {printf("Mystery character:%c\n", *yytext);}
%%
int main(int argc, char **argv)
{
int tok;
while (tok = yylex()) {
printf("%d", tok);
if (tok == NUMBER) {
printf(" = %d\n", yylval);
} else {
printf("\n");
}
}
}
我们在C 语言的enum 中定义记号编号。定义整型 yylval,用来存储记号值。后续的记号值通常被定义为联合类型,以便于不同类型的记号可以拥有不同的记号值。
这里的每个模式,动作是返回适当的记号代码;对于匹配数字的模式,除了返回记号代码,还将数字字符串转为整数存储在yylval 中。匹配到空白字符的模式,不返回,继续分析接下来的输入。
Main 函数中,调用yylex, 打印出记号值,对于数字(Number)记号,还会打印数字值(存储在yylval )。
Makefile 如下所示:
fb1_4:
flex fb1_4.l
gcc -o $@ lex.yy.c
clean:
rm -rf lex.yy.c fb1_4
执行如下所示
./fb1_4
1+2
258 = 1
259
258 = 2
264
词法分析器就写好了,下面写语法分析器。
1.8 文法与语法分析
语法分析器的任务是找到输入记号之间的关系,一种常见的关系表达式就是语法分析树(parse tree)。基于通常的算术规则,算术表达式 12+34+5 有以下语法分析树.
1.9 BNF文法
我们需要一定的方法来描述语法分析器,将记号转化为语法分析树的规则。这个方法一般是上下文无关文法(Context-free Grammar , CFG)。书写上下文无关文法的标准格式是 Backus-Naur From(BNF)。
<exp> ::= <factor>
| <exp> + <factor>
<factor> ::= NUMBRE
| <factor> * NUMBER
上面的BNF 的每一行都是一条规则,用来说明如何创建语法分析树的分支。 ::= 被读作 “是” 或者 “变成”。 | 是“或者”,创建同类分支的另一种方式。规则左边的名称是语法符号。所有的记号都是语法符号,但有些语法符号不是记号。
1.10 Bison 的规则描述语言
Bsion 的规则基本是BNF,但做了一些简化。
例1-5 简单的计算器 fb1-5.y , fb1-5.y 如下所示:
/** 例1-5 简单的计算器 fb1-5.y
计算器的最简版本
*/
%{
#include <stdio.h>
%}
/*declare tokens */
%token NUMBER
%token ADD SUB MUL DIV ABS
%token EOL
%%
/*
calclist: 不进行任何匹配的规则 从输入开头进行匹配
| exp EOL { printf("=%d\n", $1); } EOL 代表一个表达式的结束 即然不进行任何匹配,我可以直接拿掉
;
*/
calclist: /*空规则 */
| calclist exp EOL { printf("%d=%d\n", $1, $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 {$$ = $1 >= 0 ? $2 : -$2;}
;
/* 1+2*3 */
%%
int main(int argc, char ** argv)
{
yyparse();
}
yyerror(char *s)
{
fprintf(stderr, "error:%s\n", s);
}
bison 程序也包含了通过 %% 分割的三部分:声明部分,规则部分,C代码部分。声明部分通过 %{ %} 包含了C代码,会原样拷贝到目标分析程序。随后是 %token 记号声明,告诉bison 在语法分析程序程序中的记号名称,通常是使用大写字母。
第二部分通过BNF 文法定义规则。bison 使用单一冒号而不是 ::= ,同时用分号 ; 标识一个规则的结束。动作是在每条规则的后面以花括号括起 {} 。
Bison 会记住每条规则,自动帮你分析语法,所以动作代码只需要维护每个语法符号的语义值。第一条规则左边的符号是语法起始符号,整个输入必须被他匹配。本例第一条规则冒号左边是 calclist 。
Bison 规则中每个语法符号都有一个语义值,目标符号(冒号左边的语法符号)的值在动作中用 $$ 代替,冒号右边的语法符号的语义值依次是 $1, $2 ... 直到规则结束。词法分析器返回的记号的记号值存储在yylval 中。
头两条规则定义了calclist 语法符号,通过循环来读入换行符结束的表达式,并打印结果。 这里calclist 的定义是一种常见的规则递归定式来实现一个序列或列表: 第一个规则为空,不进行任何匹配; 第二个规则添加一个项目到列表,这里第二个规则是通过 %2 打印exp 的值。
其余规则实现了计算器。在语义值上进行了相应的算数操作。如果一个规则缺少显式的动作,则语法分析器将$1 赋值给 $$。
1.11 联合编译Flex 和Bison 程序
在联合编译之前,需要对1-4 的词法分析器做一些改动。1 是在第一部分包含bison 创建的头文件,该头文件包含了记号编号的定义和yylval 的定义。2 是删除第三部分测试主历程,因为语法分析器会调用词法分析器。
例1-6: 计算器词法分析器fb1-5.l
/* 例1-6: 计算器词法分析器fb1-5.l */
%{
#include "fb_1_5.tab.h"
%}
/**
前5个模式就是操作符本身,用引号引起。引号告诉flex使用引号内文本的原义,而不是解释为正则表达式
与前面的规则相同,也不需要第三部分代码
*/
%%
"+" {return ADD;}
"-" {return SUB;}
"*" {return MUL;}
"/" {return DIV;}
"|" {return ABS; }
[0-9]+ {yylval = atoi(yytext); return NUMBER;}
\n {return EOL;}
[ \t] {}
. {printf("Mystery character:%c\n", *yytext);}
%%
编译脚本,Makefile 如下:
fb_1_5: fb_1_5.l fb_1_5.y
bison -d fb_1_5.y
flex fb_1_5.l
gcc -o $@ fb_1_5.tab.c lex.yy.c -lfl
clean:
rm -rf fb_1_5.tab.c lex.yy.c fb_1_5 fb_1_5.tab.h
首先以 -d 运行bison,会创建fb1-5.tab.c 和 fb1-5.tab.h,接着它运行flex 来创建 lex.yy.c。然后将两者和库文件编译在一起。
执行过程如下:
./fb_1_5
1+2
=3
2*3
=6
1+2*3
=7
23
=23
1.12 二义性文法:并不多见
1-5 的文法,是否必须这么复杂,为什么不是这样呢?
exp: exp ADD exp
| exp SUB exp
| exp MUL exp
| exp DIV exp
| ABS exp
| NUMBER
;
有两个原因:优先级和二义性。分开的Term、factor 和 exp 的语法符号可以让bison 首先处理ABS,接着是MUL 和DIV,然后是ADD 和 SUB。 具体过程没想出来。
1.13 添加更多规则
两个需求,1 是能处理圆括号。2 是能处理注释。
对第1个需求,在语法分析器中定义两个记号 OP 和 CP(分别代表左右括号)。并添加一条规则,是圆括号表达式称为一个term, 如下所示:
%token OP CP
...
term : NUMBER
...
| OP exp CP {$$ = $2;} 新规则
在新规则中,将 $2 (圆括号中表达式的值)赋值给了 $$。
词法分析器中有两条新规则,来是识别两个新的记号, 如下所示:
"(" {RETURN OP;}
")" {RETURN CP;}
第2个需求是忽略注释,只需在词法分析器中新加一条规则, 如下所示:
"//".*
先匹配双斜线,然后 . 匹配任意字符, * 匹配
此文摘自<flex与bison中文版> 第一章