flex bison 基础概述
创始人
2024-03-22 11:00:34
0

1. 前言

限于作者能力水平,本文可能存在的谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 本文目标

. 简单介绍 flex 和 bison 的基础使用方法
. 简要分析 flex, bison 生成代码的工作流程

3. flex & bison

3.1 背景

本文所有分析,基于 Ubuntu 16 系统。

3.2 flex

3.1.1 flex 简介

flex用来生成词法分析器(lexical analysis, 或 scanner),而词法分析器的作用,简单来讲,就是将输入,按定义的正则表示式模式,解析分割成一个个记号(token)

# 生成词法分析器flex
XXX.l(词法分析器规则定义文件) ======> 词法分析器# 通过词法分析器,将输入数据流,解析成一个个记号(tokens)词法分析器
输入数据流 ===========> 一个个记号(tokens)

3.1.2 flex 使用例子

(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()修改。

3.1.3 flex 生成代码流程简析

通常,我们有必要简单的分析下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 生成代码一起协作的基础。

3.1.4 flex 小结

上面我们简单介绍了 flex 的基础用法,但很多时候,这些并不足够。下面列举几个对我们日常很常见也很有用的 flex 用法。

3.1.4.1 option 选项

(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);

3.1.4.2 定义

类似于C中的宏定义,主要是将正则规则中重复的部分抽离出来,免得重写。我们看一个例子:

group		[^,{}/]*[{][^}]*[}][^,{}/]*
%%
{group}		{BEGIN(INITIAL);REWIND(0);}
%%
...

上例中,在第一部分定义了 group ,然后再第二部分规则中引用,使用 {} 括起来。

3.1.4.3 定义特定状态下才会执行的规则


%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_STARTYYSTATE获取当前状态,通过BEGIN来切换当前状态,如上例中的BEGIN IFILE;来切换词法分析器的状态为IFILE,在状态IFILE下,只有开头的规则才会被执行。
另外还可以通过%s定义可共享状态。假设我们通过%s SS定义了状态SS,和%x定义不同的是,除了将开头的规则限制在只能在SS状态下执行外,而剩余的其它规则,也可以在SS状态下执行。
通常来讲,%x是更加有用的,因为它将分析限制于特定的上下文,这可以简化我们词法分析器的设计。

3.2 bison

3.2.1 bison 简介

bison基于给定的语法,来生成一个可以识别这个语法中有效语句的语法分析器。我们简单的看一下语法分析器的生成流程:

# 生成语法分析器bison -d
XXX.y ==========> XXX.tab.l, XXX.tab.h# 与词法分析器协作分析语法tokens----------------|                ||                V词法分析器        语法分析器^                |   |                V输入数据流      合乎语法的语句

3.2.2 flex + bison 使用例子

(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 结束数据输入

3.2.3 flex 与 bison 协作流程简析

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; /* 进入下一分析状态 */

3.2.4 bison 小结

上面我们简单介绍了 bison 的基础用法,但很多时候,这些并不足够。下面列举几个对我们日常很常见也很有用的 bison 用法。

3.2.4.1 option 选项

(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表示,语法符号NUMBER的数据类型为,声明%union中,数据成员d的数据类型double;声明%type exp factor term表示,语法符号exp,factor,term的数据类型为,声明%union中,数据成员a的数据类型struct ast *

语法会存在二义性,简单来讲,语法二义性就是一段输入,可匹配到不同的语法规则情形。本文对语法的二义性未做描述。

4. 后记

本文仅对 flex 和 bison 做了简单基础性的描述,更多的细节,以及对齐内部的实现原理等方面未做展开,读者可阅读后面的资料,进行补充。

5. 推荐阅读 & 参考资料

参考资料:

《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》
《计算机程序的构造和解释》

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...