限于作者能力水平,本文可能存在的谬误,因此而给读者带来的损失,作者不做任何承诺。
. 简单介绍 flex 和 bison 的基础使用方法
. 简要分析 flex, bison 生成代码的工作流程
本文所有分析,基于 Ubuntu 16
系统。
flex
用来生成词法分析器
(lexical analysis
, 或 scanner
),而词法分析器
的作用,简单来讲,就是将输入,按定义的正则表示式
模式,解析分割成一个个记号(token)
。
# 生成词法分析器flex
XXX.l(词法分析器规则定义文件) ======> 词法分析器# 通过词法分析器,将输入数据流,解析成一个个记号(tokens)词法分析器
输入数据流 ===========> 一个个记号(tokens)
(1) 初次使用,先运行如下命令安装 flex
:
sudo apt-get install flex
(2) 编写 flex 程序(用来生成词法分析器的规则文件XXX.l)。
我们先来了解一下 flex 程序的编写规则,flex 程序分为3个部分:
定义部分:
包含选项(option),文字块,开始条件,转换等。
%%
规则部分:
包含正则模式行和模式行匹配时执行的C代码。
以空白行开头、或包含在%{和%}之间的部分,都被认为是C代码,它们会被原封不动的拷贝到yylex()函数中。
%%
用户子程序部分:
这个部分通常包含在模式规则匹配时,执行的C代码调用的函数。
这3个部分,用2个%%
分隔,前2个部分是必须的,但它们的内容可以为空,第3部分和它之前的%%可以省略。
了解 flex 程序的编写规则后,接下来,我们以一个统计字符数、单词数目、行数的 flex 程序为例,来演示一下flex的使用,flex 程序count-words.l
如下:
%{
int chars = 0; /* 字符计数 */
int words = 0; /* 单词计数 */
int lines = 0; /* 行计数 */
%}%%[a-zA-Z]+ { words++; chars += strlen(yytext); }
\n { chars++; lines++; }
. { chars++; }%%int main(int argc, char *argv[])
{yylex();printf("%8d%8d%8d\n", chars, words, lines);return 0;
}
我们编写一个简单的 Makefile 来生成和编译生成的的词法分析器:
count-words: count-words.lflex --noyywrap count-words.l # 生成词法分析器代码 lex.yy.cgcc -o $@ lex.yy.c # 编译词法分析器 lex.yy.c clean:-rm -f lex.yy.c count-words
编译和运行:
make # 编译生成词法分析器程序 count-words
./count-words # 运行词法分析器,按 Ctrl + D 结束数据输入
测试中我们发现,词法分析器从标准输入接收数据,这是默认的行为。如果我们想改变该默认行为,转而将文件作为输入,只需按如下修改 flex 程序count-words.l
就可以达到目标:
%option noyywrap%{
/* 统计单个文件数据 */
int chars = 0;
int words = 0;
int lines = 0;/* 统计所有文件数据 */
int total_chars = 0;
int total_words = 0;
int total_lines = 0;
%}%%[a-zA-Z]+ { words++; chars += strlen(yytext); }
\n { chars++; lines++; }
. { chars++; }%%int main(int argc, char *argv[])
{int i;if (argc < 2) { /* 没有给定文件列表,仍然从标准输入获取数据 */yylex();printf("%8d%8d%8d\n", chars, words, lines);return 0;}/* 遍历所有输入文件,统计每一个文件的数据 */for (i = 1; i < argc; i++) {FILE *fp = fopen(argv[i], "r");if (!fp) {perror(argv[i]);return -1;}/* 复位当前文件的统计数据 */chars = words = lines = 0;yyrestart(fp); /* 调用yyrestart() 接口重置词法分析器的输入流到文件 @argv[i] */yylex(); /* 调用词法分析器进行数据统计 */fclose(fp); /* 关闭当前文件 */printf("%8d%8d%8d %s\n", chars, words, lines, argv[i]);/* 记录当前文件的统计数据 */total_chars += chars;total_words += words;total_lines += lines;}if (argc > 1)printf("%8d%8d%8d total\n", total_chars, total_words, total_lines);return 0;
}
修改后重新编译运行:
make
./count-words a.txt b.txt # a.txt, b.txt 作为输入
如果不指定输入(具体是修改yyin
全局变量),程序默认使用标准输入;如果要修改输入,我们可以通过yyrestart()
修改。
通常,我们有必要简单的分析下flex的生成代码,以帮助我们理解和更好的使用工具。下面简要分析flex生成代码的工作流程:
/* * 在分析具体代码前,我们先聊一下 flex 的 fl 库。* flex工具带有一个微型的 fl 库,它定义了 main(), yywrap() 接口。 其中:* . main() 函数调用 yylex() 做词法分析;* . yywrap() 简单地返回 1。yywrap() 的作用是,在yylex()发现到达输入数据末尾时,调用 yywrap(),看是否还有数据,如果有,yywrap() 应该返回0,否则返回1。** 如果我们的flex程序,不自己实现 main() 和 yywrap(),则在编译时,可以给 gcc 指定 -lfl 选项。另外,可以通过给 flex 传递 --noyywrap 选项,或者在 flex 程序中,指定 %option noyywrap 来告诉flex ,我们不调用 yywrap() ,以此来屏蔽编译链接报错。*/
/* 接下来,进入具体的代码流程分析 */
main()yylex() /* 进入词法分析器入口 *//* yylex() 初次调用的初始化。后续 yylex() 调用会在之前的上下文下继续工作。 */if ( !(yy_init) ){(yy_init) = 1;...if ( ! (yy_start) )(yy_start) = 1; /* first start state */if ( ! yyin ) /* 没有设定输入, 默认将 stdin 作为输入 */yyin = stdin;if ( ! yyout ) /* 没有设定输出, 默认将 stdout 作为输入 */yyout = stdout;/* 输入缓冲初始化 */if ( ! YY_CURRENT_BUFFER ) {yyensure_buffer_stack (); /* 创建 yy_buffer_state 输入缓冲管理对象指针栈 */YY_CURRENT_BUFFER_LVALUE =yy_create_buffer(yyin,YY_BUF_SIZE ); /* 创建栈顶 YY_BUF_SIZE 大小的输入缓冲 */}/** 获取栈顶输入缓冲如下状态:* . yy_n_chars: 读到栈顶输入缓冲空间的字符个数* . yytext, yy_c_buf_p: 栈顶输入缓冲空间当前位置指针(char *)* . yyin: 输入缓冲输入文件* . yy_hold_char: 栈顶输入缓冲空间当前字符*/yy_load_buffer_state( );}/* 扫描循环,直到输入结束 */while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */{/* 正则匹配状态机循环 */yy_current_state = (yy_start);yy_match:do {YY_CHAR yy_c = yy_ec[YY_SC_TO_UI(*yy_cp)] ;...yy_current_state = yy_nxt[yy_base[yy_current_state] + (unsigned int) yy_c];++yy_cp;}while ( yy_base[yy_current_state] != 17 );.../* 一个正则模式匹配完成的后惯例动作:* yytext: 当前正则模式匹配的内容* yyleng: 当前正则模式匹配内容长度* yy_hold_char: 当前正则模式匹配内容的最后一个字符(也即当前字符)* *yy_cp = '\0';* yy_c_buf_p: 下一个待解析字符位置指针**/YY_DO_BEFORE_ACTION;do_action: /* This label is used only to access EOF actions. *//* 状态机正则匹配结束,按匹配的正则做用户定义的动作 */switch ( yy_act ){ /* beginning of action switch */...case 1:YY_RULE_SETUP
#line 18 "calculator.l"{ return ADD; } /* 这则模式匹配时,执行的C代码 */YY_BREAK...default:YY_FATAL_ERROR("fatal flex scanner internal error--no action found" );}}
其实 yylex()
的工作逻辑很简单,可以总结如下:
while (1)从输入读取按正则模式匹配输入如果有匹配的模式执行匹配模式的C代码否则报错
注意到,yylex()
是返回值的,不出错的情形下,它返回匹配模式的 token
,这就是它可以和 bison
生成代码一起协作的基础。
上面我们简单介绍了 flex 的基础用法,但很多时候,这些并不足够。下面列举几个对我们日常很常见也很有用的 flex 用法。
(1) 生成可重入词法分析器。
一方面,生成的词法分析器代码,有很多全局变量;
另一方面,词法分析器入口 yylex(),返回后,下一次调用,会接着使用上一次运行后的上下文继续执行。
上面两点,不能满足要求可重入的调用的上下文,此时,我们可以通过%option reentrant
选项来生成可重入的词法分析器。此时,我们通过如下代码片段构建可重入的词法分析器:
yyscan_t scanner;yylex_init(&scanner) / yylex_init_extra(extra, &scanner); /* 创建词法分析器上下文 */
while (yylex(scanner)) { /* 基于独立的上下文进行词法分析 */...
}
yylex_destroy(scanner); /* 销毁词法分析器上下文 */
(2) 改变生成代码函数名。
我们有时候可能不想使用 yylex() 等其它词法分析器接口名,可以通过%option prefix="XXX"
作为词法分析器接口名前缀。如:%option prefix="parse_events_"
,那生成代码中,yylex()
则变为 parse_events_lex()
,当然,还有更多函数名的变换。
(3) 自动维护行号代码。
我们可以自己在规则中,更新 yylineno
来维护行号。当然,也可以通过 %option yylineno
选项,让 flex 帮我们自动生成行号维护代码。
(4) 与 bison 协同工作选项。
默认生成的代码,yylex()
函数是没有参数的,除了可以通过%option reentrant
选项来增加词法分析器的上下文参数外,我们还可以通过%option bison-bridge
和%option bison-locations
来改变 yylex()
原型:
%option bison-bridge: 为 yylex() 增加参数 YYSTYPE *yylval_param, 用来记录词法分析器解析的 token 的值。
%option bison-locations: 为 yylex() 增加参数 YYLTYPE *yylloc_param, 用来存储行列信息。
如果有以下选项配置:
%option reentrant
%option bison-bridge
%option bison-locations
...
%%
...
%%
...
则生成的 yylex()
函数原型为:
int yylex(YYSTYPE *yylval_param, YYLTYPE * yylloc_param, yyscan_t yyscanner);
类似于C中的宏定义,主要是将正则规则中重复的部分抽离出来,免得重写。我们看一个例子:
group [^,{}/]*[{][^}]*[}][^,{}/]*
%%
{group} {BEGIN(INITIAL);REWIND(0);}
%%
...
上例中,在第一部分定义了 group
,然后再第二部分规则中引用,使用 {}
括起来。
%x IFILE%%^"#"[ \t]*include[ \t]*[\"<] { BEGIN IFILE; }[^ \t\n\">]+ {{int c;while ((c = input()) && c != '\n');}yylineno++;if (!newfile(yytext))yyterminate(); /* no such file */BEGIN INITIAL;}.|\n {fprintf(stderr, "%4d bad include line\n", yylineno);yyterminate();}<> { if (!popfile()) yyterminate(); }^. { fprintf(yyout, "%4d %s", yylineno, yytext); }
^\n { fprintf(yyout, "%4d %s", yylineno++, yytext); }
\n { ECHO; yylineno++; }
. { ECHO; }%%
...
上面我们通过 %x
定义了一个 exclusive 的状态 IFILE
,在该状态下,只有以
才会被执行,它用来解析 #include
预处理符号。
词法分析器的初始状态为INITIAL
(即0),可以通过YY_START
或YYSTATE
获取当前状态,通过BEGIN
来切换当前状态,如上例中的BEGIN IFILE;
来切换词法分析器的状态为IFILE
,在状态IFILE
下,只有以
才会被执行。
另外还可以通过%s
定义可共享状态。假设我们通过%s SS
定义了状态SS
,和%x
定义不同的是,除了将
限制在只能在SS状态下执行
外,而剩余的其它规则,也可以在SS状态
下执行。
通常来讲,%x
是更加有用的,因为它将分析限制于特定的上下文,这可以简化我们词法分析器的设计。
bison基于给定的语法,来生成一个可以识别这个语法中有效语句的语法分析器。我们简单的看一下语法分析器的生成流程:
# 生成语法分析器bison -d
XXX.y ==========> XXX.tab.l, XXX.tab.h# 与词法分析器协作分析语法tokens----------------| || V词法分析器 语法分析器^ | | V输入数据流 合乎语法的语句
(1) 初次使用,先运行如下命令安装 bison
:
sudo apt-get install bison
(2) 编写 bison 程序。
我们先来说一下 bison 程序(XXX.y)的编写规则,bison 程序分为3个部分:
定义部分:
包含选项(option),文字块(%{和%}、%code),声明(%union,%start,%token,%type,%left,%right,%nonassoc等)。
%%
规则部分:
包含语法规则和规则匹配时执行的C代码。
%%
用户子程序部分:
这个部分通常包含语法规则匹配时,执行的C代码调用的函数。
这3个部分,用2个%%
分隔,前2个部分是必须的,但它们的内容可以为空,第3部分和它之前的%%可以省略。
接下来,我们一个简单计算器为例,构建计算器的 flex 和 bison 程序如下。
%{
/** calculator.l*/#include "calculator.tab.h"
%}%%"+" { 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 */ }
. { printf("Mystery character %c\n", *yytext); }%%
%{
/** calculator.y*/#include extern int yylex(void);void yyerror(char *s);
%}/* declare tokens */
%token NUMBER
%token ADD SUB MUL DIV ABS
%token OP CP
%token EOL%%callist: /* nothing */| callist exp EOL { printf(" = %d\n", $2); };exp: factor| exp ADD factor { $$ = $1 + $3; }| exp SUB 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(int argc, char *argv[])
{yyparse();
}void yyerror(char *s)
{fprintf(stderr, "error: %s\n", s);
}
编写用来编译的 Makefile :
calculator: calculator.l calculator.ybison -d calculator.yflex calculator.lgcc -o calculator calculator.tab.c lex.yy.c -lflclean:-rm -f calculator lex.yy.c *.tab.*
编译运行:
make # 编译生成计算器程序 calculator
./calculator # 运行计算器程序,按 Ctrl + D 结束数据输入
main() /* calculator.tab.c: bison -d 生成的代码 */yyparse()...yychar = YYEMPTY; /* Cause a token to be read. */goto yysetstate;...yynewstate: /* 下一状态 *//* In all cases, when you get here, the value and location stackshave just been pushed. So pushing a state here evens the stacks. */yyssp++;yysetstate:...yyn = yypact[yystate];...if (yychar == YYEMPTY){YYDPRINTF ((stderr, "Reading a token: "));yychar = yylex (); /* 调用词法分析器解析一个token *//* * 参看 3.1.3 小节,词法分析器工作流程*/}.../* Discard the shifted token. */yychar = YYEMPTY;yystate = yyn;YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN*++yyvsp = yylval;YY_IGNORE_MAYBE_UNINITIALIZED_ENDgoto yynewstate; /* 进入下一分析状态 */
上面我们简单介绍了 bison 的基础用法,但很多时候,这些并不足够。下面列举几个对我们日常很常见也很有用的 bison 用法。
(1) 要求 bison 版本。
%require "2.4"
%%
...
%%
...
(2) 自定义语法分析器入口 yyparse() 函数原型。
默认生成语法分析器入口 yyparse() 函数是没有参数的,但有时候,我们需要给它传递参数,这时我们可以通过%parse-param
选项来自定义参数列表。如:
%parse-param {void *_parse_state}
%parse-param {void *scanner}
%%
...
%%
...
则 yyparse() 函数的原型定义为:
int yyparse (void *_parse_state, void *scanner);
(3) 生成可重入的语法分析器。
%define api.pure
%%
...
%%
...
还可以使用%pure-parser
代替%define api.pure
。这两个选项通常结合%parse-param
使用,给 yyparse()
传递参数。
(4) 定义词法分析器解析符号的ID。
/* declare tokens */
%token NUMBER
%token ADD SUB MUL DIV ABS
%token OP CP
%token EOL%%
...
%%
...
上面的 bison 程序,会在 *.tab.h
中对应生成如下的 token 定义:
#ifndef YYTOKENTYPE
# define YYTOKENTYPEenum yytokentype{NUMBER = 258,ADD = 259,SUB = 260,MUL = 261,DIV = 262,ABS = 263,OP = 264,CP = 265,EOL = 266};
#endif
%token
用来定义 token 编号,给词法分析器的 token 编号。bison 以 258 为生成符号的起始编号,避免和 ascii 值冲突。另一种 token 编号的使用方法是单引号内含字符的方式
,如 ‘+’ ,则用 + 的 ascii 值作为其 token编号,不必额外定义。
(5) 定义语法分析器规则中符号的数值类型。
在语法分析的规则中,有时候需要通过%union指定符号的数值类型。我们看个简单的例子:
%union {struct ast *a;double d;
}/* declare tokens */
%token NUMBER
%token EOL%type exp factor term%%
...
%%
...
其中,用%union声明被转化为如下C代码段:
/* Value type. */
#if ! defined YYSTYPE && ! defined YYSTYPE_IS_DECLAREDunion YYSTYPE
{
#line 8 "calculator.y" /* yacc.c:1909 */struct ast *a;double d;#line 64 "calculator.tab.h" /* yacc.c:1909 */
};typedef union YYSTYPE YYSTYPE;
# define YYSTYPE_IS_TRIVIAL 1
# define YYSTYPE_IS_DECLARED 1
#endif
也即定义为 YYSTYPE
类型。如果不声明%union
,则 YYSTYPE
定义为 int
。声明%token
表示,语法符号NUMBER
的数据类型为,声明%union中,数据成员d的数据类型double
;声明%type exp factor term
表示,语法符号exp,factor,term
的数据类型为,声明%union中,数据成员a的数据类型struct ast *
。
语法会存在二义性,简单来讲,语法二义性就是一段输入,可匹配到不同的语法规则情形。本文对语法的二义性未做描述。
本文仅对 flex 和 bison 做了简单基础性的描述,更多的细节,以及对齐内部的实现原理等方面未做展开,读者可阅读后面的资料,进行补充。
参考资料:
《flex & bison》, John R. Levine
[flex](https://www.gnu.org/savannah-checkouts/gnu/www/software/flex/flex.html)
[GNU Bison](https://www.gnu.org/software/bison/)
推荐阅读:
《A Retargetable C Compiler_Design and Implementation》
《Advanced Compiler Design and Implementation》
《Building an Optimizing Compiler》
《Compiler Construction Principles And Practice》
《Compiler Design in C》
《Compilers Principles Techniques and Tools》
《Crafting a Compiler》
《Engineering a Compiler》
《Introduction to Compiler Construction》
《Language Implementation Patterns》
《Modern Compiler Implementation in C》
《Modern Compiler Design》
《Programming Language Pragmatics》
《The Implementation of Functional Programming Languages》
《计算机程序的构造和解释》