概述
flex与bison
flex与bison简介
词法分析与语法分析
flex用作词法分析,而bison用作语法分析。词法分析把输入分解成一个个有意义的词块,称作token
;语法分析则确定这些词块彼此之间如何关联(使用语法树表达)。比如:
A = B + C;
flex将其分解成A
、=
、B
、+
、C
和;
;接着bison将其确定为一个表达式,并对其建模成表达式树,简化如下
+---+
| = |
+---+
/
+---+ +---+
| A | | + |
+---+ +---+
/
+---+ +---+
| B | | C |
+---+ +---+
正则表达式与词法分析
词法分析通常是使用正则表达式在输入中找寻字符的模式(称为pattern
)。flex程序由一组带有指令的正则表达式组成,这些指令确定输入在匹配了正则表达式的模式后执行的动作(action
),通过flex程序对这些指令进行解析后,生成一个词法分析器,它可以读取输入,匹配输入与正则表达式并执行匹配后关联的动作。以Unix下统计字符的wc
程序为例,使用flex模拟一个同样功能的词法分析器。
/*flex-wc.l*/
%option noyywrap
%{
int chars = 0;
int words = 0;
int lines = 0;
%}
%%
[a-zA-z]+ { words++; chars += strlen(yytext); }
n { lines++; chars++; }
. { chars++; }
%%
int main() {
yylex();
printf("%8d %8d %8dn", lines, words, chars);
return 0;
}
flex程序由flex语法与C语法混合组成,包含3个部分:
- 第一部分
%option ...
和%{ ... %}
包含选项和声明,其中声明是纯C语法。这里声明了一些全局变量来统计词、字和行。 - 第二部分
%% ... %%
包含模式和动作。模式(pattern
)和动作(action
)的格式为<regex pattern> { [action] }
,flex要求模式必须出现在首行且位于行首,动作由{}
包裹,可以分为多行。匹配模式后就执行动作,yytext
指示匹配的输入文本。 - 第三部分,即第二个
%%
之后的部分是纯C语法,它被原封不动的拷贝到flex生成的C代码中,用于调用词法分析的入口(yylex()
是其中的一个入口,它用标准输入作为匹配模式的输入)及处理逻辑。
运行过程
flex flex-wc.l
cc lex.yy.c -lfl
./a.out
flex用flex
程序来转换脚本文件生成lex.yy.c
的文件,然后编译并使用-lfl
指向的flex库文件进行链接。如果没有第三部分也是可以的,你可以将生成的文件作为一个没有入口函数的文件使用,入口函数定义在其他的C文件中(如果没有入口,flex将使用一个极小的默认入口函数),然后进行多文件编译。
记号编号与记号值
当flex词法分析器返回时(比如yylex()
),返回值作为一个记号流,它的定义由两部分组成记号编号(token number
)和记号值(token's value
)。在与bison联合使用时,标记编号的0值意味着文件结束;而为了与ASCII码和内定值冲突,其他的标记编号要从258开始定义。
用下面即将介绍的计算器程序片段为例:
%{
enum {
NUMBER = 258,
ADD = 259,
};
int yylval = 0;
%}
%%
"+" { return ADD; }
[0-9]+ { yylval = atoi(yytext); return NUMBER; }
%%
int main()
{
int token;
while((token = yylex()) != 0) {
printf("%d", token);
if (token == NUMBER)
printf(" = %d", yylval);
printf("n");
}
return 0;
}
如果编译后运行并输入18 + 12
,则程序输出
258 = 18
259
258 = 12
可以看出在动作里添加return
语句可以使yylex()
例程返回这个值,之后可以再次调用yylex()
继续从上一次停止的地方接着匹配输入。
文法与语法分析
语法分析的任务就是找出输入记号之间的关系。常见的表示这种关系的结构就是树,比如带有优先级规则的语法树1 * 2 + 3 * 4 + 5
表示为
+---+
| + |
+---+
/
+---+ +---+
| + | | 5 |
+---+ +---+
/
+---+ +---+
| * | | * |
+---+ +---+
/ /
+-+ +-++-+ +-+
|1| |2||3| |4|
+-+ +-++-+ +-+
根据树的结构特性,每个bison分析法则都可以构建一棵子树,然后组合成一棵更大的树。
BFN文法
为了编写一个语法分析器,需要一定的方法来描述语法分析器所使用的把一系列记号转换为语法分析数的规则。最常用的就是上下文无关文法(简称CFG
),书写这种文法的标准格式就是BFN文法
。如果将1 * 2 + 3 * 4 + 5
表达式用文法表示:
<exp> ::= <factor>
| <exp> + <factor>
<factor> ::= <number>
| <factor> * <number>
每一行就是一条规则,用来说明如何组建语法树的分支。::=
记作“变成“、|
记作“或者“。规则带有递归性质,即这个树分支可以由相似的分支组成;规则还有依赖关系,从上图中可以看出<exp>
是依赖<factor>
,从而形成了优先级。
Bison的规则描述语言
现在使用一个计算器程序作为例子,说明语法分析与词法分析怎么联动,并借此说明用在bison中的BFN文法的语法格式。
先定义语法分析
/* simplest version of calculator */
%{
#include <stdio.h>
int yyerror(const char *, ...);
extern int yylex();
extern int yyparse();
%}
/* declare tokens */
%token NUMBER
%token ADD SUB MUL DIV ABS
%token OP CP
%token EOL
%%
calclist: /* nothing */
| calclist exp EOL { printf("= %dn> ", $2); }
| calclist EOL { printf("> "); } /* blank line or a comment */
;
exp: factor
| exp ADD exp { $$ = $1 + $3; }
| exp SUB factor { $$ = $1 - $3; }
| exp ABS factor { $$ = $1 | $3; }
;
factor: term
| factor MUL term { $$ = $1 * $3; }
| factor DIV term { $$ = $1 / $3; }
;
term: NUMBER
| ABS term { $$ = $2 >= 0? $2 : - $2; }
| OP exp CP { $$ = $2; }
;
%%
int main()
{
printf("> ");
yyparse();
return 0;
}
int yyerror(const char *s, ...)
{
int ret;
va_list va;
va_start(va, s);
ret = vfprintf(stderr, s, va);
va_end(va);
return ret;
}
再定义词法分析
/* recognize tokens for the calculator and print them out */
%{
# include "fb1-5.tab.h"
extern int yyerror(const char *, ...);
%}
%%
"+" { return ADD; }
"-" { return SUB; }
"*" { return MUL; }
"/" { return DIV; }
"|" { return ABS; }
"(" { return OP; }
")" { return CP; }
[-+]?[0-9]+ { yylval = atoi(yytext); return NUMBER; }
n { return EOL; }
"//".*
[ t] { /* ignore white space */ }
. { yyerror("Mystery character %cn", *yytext); }
%%
编译与链接
bison -d fb1-5.y
flex fb1-5.l
cc fb1-5.tab.c lex.yy.c -lfl
bison程序也分为三个部分,定义与声明部分、规则部分、C代码部分。
-
定义与声明包含C代码和bison自身的
token
记号(也是flex
的记号),一般记号用大写表示,如果没有定义记号,却在规则中出现,那么会被当做变量处理。前面提到过bison与flex联合使用时,yylex()
返回的记号值从258开始(例子中的EOF就是预定义的token
,表示匹配输入的结束),yyparse()
会自动处理输入的匹配时的文本和记号值。bison在实现上可以用宏加枚举的方式实现,并在生成的头文件中导出; -
规则与BFN文法本身有些区别,它的格式为
<rules> : [others rule [default <action>]] | <rule expression> { <action> } ... ;
。语法的最左边是规则语法起始符号(start symbol
),整个输入必须被它匹配;右边的是对这个规则的说明(规则本身也存在递归性质,整个规则存在依赖关系),首条规则表达式说明可以为空,并与其他的规则表达式通过或的方式组合起来。每条规则表达式可以联动一个动作,如果整个规则通过这条表达式匹配,则执行这个动作;可以使用$
加数字的方式获取匹配时表达式中的语法符号的语义值,而$$
表示这个规则本身的语义值,比如exp SUB factor { $$ = $1 - $3; }
它表示规则的语义值由exp
、SUB
和factor
等语法符号组成的表达式决定,当匹配时$1
、$2
和$3
分别代表了它们的值,对他们进行运算就得到了这条规则的语义值,其结果又参与了其他依赖这条规则结果的运算,比如例子中的calclist exp EOL { printf("= %dn> ", $2); }
。 -
C代码部分即是整个语法分析器的入口。它使用了预定义的函数,这些函数可以和
flex
一起工作。
把语法、词法分析器构建成一个程序时,词法分析器使用语法分析器生成的头文件,它包含记号的定义和yylval
的定义,因为语法分析器会调用词法分析器。
二义性文法
上面的计算器的例子中文法没有写成
exp : exp ADD exp
| exp SUB exp
| exp MUL exp
| exp DIV exp
| ABS exp
| NUMBER
;
是因为文法的递归性存在二义性,并且不能表达优先级。一旦规则存在优先级,文法就应当以多条相互依赖的方式来编写规则。其根本原因是bison创建的语法分析器总是以同一种方式分析输入,而且这些分析器只相同的文法。比如:
exp : exp SUB exp
| exp ADD exp
| factor
;
这种分析加减法的文法存在二义性,它对于1 - 2 + 3
这样的表达式,既可以解析为(1 - 2) + 3
,也可以解析为1 - (2 + 3)
,两种分析方式带来不同的结果。这个时候bison会给出冲突报告,并全程解析输入时都选定其中一种方式。
flex的使用
正则表达式
flex使用了扩展的POSIX正则表达式。元语言使用了标准的ASCII字符,根据上下文一部分表示模式一部分表示自身的含义。下面没有列出的数字、字母、符号等,将匹配自身。
chars | comments | flex support |
---|---|---|
"String" | 匹配引号中扩起来的字符串,即使字符串包含运算符。需要转椅的特殊字符请使用此项。 | 支持 |
Character | 转义字符。当位于字符串中使用的字符类运算符之前时, 字符表明运算符符号代表文字字符,而不是运算符。有效转义序列包括:a 响铃、b 退格、f 换页、n 换行、r 返回、t 制表符、v 纵向制表符、\ 反斜杠。 | 支持 |
Digits | 转义一到三位的8进制或16进制,比如123 或
|