一、Bison 的起源与定位
Bison 是一款经典的语法分析器生成工具,它的发展历程与 Unix 系统的演进紧密相连。其前身可以追溯到 20 世纪 70 年代由贝尔实验室开发的 Yacc(Yet Another Compiler-Compiler)。Yacc 作为一款开创性的工具,为编译器的开发提供了极大的便利,让开发者能够通过定义语法规则来自动生成语法分析器。
然而,随着时间的推移,Yacc 的一些局限性逐渐显现,例如缺乏对某些现代语法特性的支持、可移植性不足等。为了弥补这些缺陷,自由软件基金会(FSF)在 Yacc 的基础上开发了 Bison,首个版本于 1985 年发布。Bison 不仅保持了与 Yacc 的兼容性,使得大量基于 Yacc 编写的代码能够平滑迁移,还增加了许多新特性,如更好的错误处理、更丰富的语法规则定义方式等。
Bison 的定位是作为一款通用的语法分析器生成工具,主要用于编译器、解释器、配置文件解析器等需要对输入进行语法分析的程序开发。它接受以巴科斯 - 诺尔范式(BNF)及其扩展形式定义的语法规则作为输入,生成相应的 C、C++ 或其他语言的语法分析器代码。这些生成的分析器能够按照定义的语法规则,对输入的字符流进行解析,识别出合法的语法结构,并执行相应的语义动作。
如今,Bison 已成为众多开发者在开发语法分析相关程序时的重要选择,广泛应用于各种编程语言的编译器实现、文本处理工具、数据格式解析器等领域。
二、Bison 的核心概念
(一)语法分析器
语法分析器是编译器或解释器中的重要组成部分,其主要功能是根据语法规则,对词法分析器生成的记号(token)流进行分析,判断输入是否符合语法规则,并构建相应的语法树(或抽象语法树)。Bison 生成的正是这样的语法分析器,它能够自动处理语法分析过程中的各种情况,如语法正确时的结构构建、语法错误时的提示与恢复等。
(二)上下文无关文法
Bison 基于上下文无关文法(Context-Free Grammar,CFG)进行工作。上下文无关文法由一组终结符、非终结符、开始符号和产生式规则组成。
- 终结符:是语法中不可再分的基本符号,通常对应词法分析器生成的记号,如标识符、常量、运算符等。
- 非终结符:是用于表示语法结构的符号,它们可以通过产生式规则分解为终结符或其他非终结符。
- 开始符号:是文法中最顶层的非终结符,代表整个语法结构的起始点。
- 产生式规则:形式为 A → α,其中 A 是非终结符,α 是由终结符和非终结符组成的符号串,表示 A 可以被替换为 α。
例如,一个简单的算术表达式文法可以定义为:
expr → expr + term
expr → expr - term
expr → term
term → term * factor
term → term / factor
term → factor
factor → ( expr )
factor → number
其中,expr、term、factor 是非终结符,+、-、*、/、(、)、number 是终结符,expr 是开始符号。
(三)LR 分析算法
Bison 生成的语法分析器采用 LR 分析算法(Left-to-Right, Rightmost derivation in reverse)。LR 分析算法是一种高效的自底向上语法分析算法,它通过读取输入记号(从左到右),维护一个状态栈和一个符号栈,根据当前状态和输入记号,查询分析表来决定执行移进(shift)、归约(reduce)、接受(accept)或报错(error)操作。
常见的 LR 分析变体包括 SLR(Simple LR)、LALR(Look-Ahead LR)和 LR (1) 等。Bison 默认使用 LALR (1) 算法,它在 LR (1) 算法的基础上进行了简化,减少了状态数量,同时保持了较好的分析能力,适用于大多数常见的语法结构。
(四)语义动作
在 Bison 中,语义动作是嵌入在产生式规则中的代码片段,用于在语法分析过程中执行特定的操作,如计算表达式的值、构建语法树节点、进行类型检查等。当一个产生式规则被归约时,其对应的语义动作将被执行。
语义动作通常使用 $1、$2、…、$n 来引用产生式规则右部各个符号对应的属性值,使用 $$ 来表示归约后得到的非终结符的属性值。例如,对于产生式 expr → expr + term { $$ = $1 + $3; },当该规则被归约时,将 expr(\(1)和 `term`(\)3)的值相加,结果赋给新的 expr($$)。
三、Bison 的语法规则文件结构
一个完整的 Bison 语法规则文件通常由三个部分组成,各部分之间用 %% 分隔,具体结构如下:
%{
声明部分:包含C/C++的头文件引用、全局变量定义、函数声明等。
%}
文法定义部分:定义终结符和非终结符的类型、优先级和结合性,以及具体的产生式规则和语义动作。
%%
产生式规则部分:详细列出各个产生式规则,每个规则后可以跟随相应的语义动作。
%%
用户自定义函数部分:包含语义动作中用到的辅助函数的实现等。
(一)声明部分(%{... %})
声明部分主要用于包含必要的头文件、定义全局变量和函数原型等,这些内容会被直接复制到生成的语法分析器代码中。例如:
%{
#include <stdio.h>
#include <stdlib.h>
int yylex(void);
void yyerror(const char *s);
%}
其中,yylex 是词法分析器函数,由 Flex 等工具生成,Bison 生成的分析器会调用它来获取下一个记号;yyerror 是错误处理函数,用于在发生语法错误时输出错误信息。
(二)文法定义部分
- 终结符与非终结符定义
Bison 中,通常用 %token 声明终结符,用 %type 声明非终结符的类型。例如:
%token NUMBER PLUS MINUS MULTIPLY DIVIDE LPAREN RPAREN
%type <value> expr term factor
其中,NUMBER、PLUS 等是终结符;<value> 表示非终结符 expr、term、factor 的属性值类型为 value(通常需要在声明部分定义该类型,如 typedef int value;)。
- 优先级和结合性定义
当文法中存在二义性时(即同一个输入序列可以通过不同的归约顺序得到不同的语法树),需要通过定义运算符的优先级和结合性来消除二义性。Bison 中使用 %left(左结合)、%right(右结合)、%nonassoc(不可结合)来定义结合性,优先级由声明的顺序决定,后面声明的优先级更高。例如:
%left PLUS MINUS
%left MULTIPLY DIVIDE
%right UMINUS
上述定义表示 PLUS 和 MINUS 具有相同的优先级,且为左结合;MULTIPLY 和 DIVIDE 优先级高于 PLUS 和 MINUS,也是左结合;UMINUS(一元减)为右结合,优先级最高。
(三)产生式规则部分
产生式规则部分是 Bison 文件的核心,用于定义文法的产生式以及对应的语义动作。其基本格式为:
非终结符:
符号串1 { 语义动作1 }
| 符号串2 { 语义动作2 }
| ...
;
例如,对于算术表达式文法,其产生式规则可以定义为:
expr:
term { $$ = $1; }
| expr PLUS term { $$ = $1 + $3; }
| expr MINUS term { $$ = $1 - $3; }
;
term:
factor { $$ = $1; }
| term MULTIPLY factor { $$ = $1 * $3; }
| term DIVIDE factor { $$ = $1 / $3; }
;
factor:
NUMBER { $$ = $1; }
| LPAREN expr RPAREN { $$ = $2; }
| MINUS factor %prec UMINUS { $$ = -$2; }
;
其中,%prec UMINUS 表示该产生式中的 MINUS 具有 UMINUS 所定义的优先级。
(四)用户自定义函数部分
这部分主要包含语义动作中用到的辅助函数的实现,最常见的是 yyerror 函数的实现。例如:
void yyerror(const char *s) {
fprintf(stderr, "Error: %s\n", s);
}
int main(void) {
yyparse();
return 0;
}
yyparse 是 Bison 生成的语法分析器的主函数,调用它将启动语法分析过程。
四、Bison 与 Flex 的协同使用
在实际的编译器或解析器开发中,Bison 通常与 Flex(一个词法分析器生成工具)协同工作。Flex 负责将输入的字符流转换为一系列记号(终结符),然后 Bison 生成的语法分析器对这些记号进行分析。
(一)Flex 词法分析器的生成
Flex 的输入文件(通常以 .l 为扩展名)定义了词法规则,用于识别不同的终结符。例如,一个简单的算术表达式词法规则文件 calc.l 如下:
%{
#include "calc.tab.h" // 包含Bison生成的头文件,定义了终结符常量
#include <stdlib.h>
%}
%%
[0-9]+ { yylval = atoi(yytext); return NUMBER; }
"+" { return PLUS; }
"-" { return MINUS; }
"*" { return MULTIPLY; }
"/" { return DIVIDE; }
"(" { return LPAREN; }
")" { return RPAREN; }
[ \t\n] { /* 忽略空白字符 */ }
. { yyerror("Unknown character"); }
%%
int yywrap(void) {
return 1;
}
其中,yytext 是 Flex 提供的变量,存储当前匹配的字符串;yylval 是一个全局变量,用于传递记号的属性值(如数字的数值),其类型需要在 Bison 文件中定义(通过 %union 或直接定义)。
运行 flex calc.l 命令,Flex 会生成词法分析器代码 lex.yy.c。
(二)Bison 语法分析器的生成
Bison 的输入文件(通常以 .y 为扩展名)如前面所述的算术表达式文法文件 calc.y。运行 bison -d calc.y 命令,Bison 会生成语法分析器代码 calc.tab.c 和头文件 calc.tab.h(-d 选项用于生成头文件,其中包含了终结符的定义)。
(三)编译与运行
将 Flex 和 Bison 生成的代码以及用户自定义的代码一起编译,即可得到完整的解析器程序。例如:
gcc -o calc calc.tab.c lex.yy.c -lm
其中,-lm 选项用于链接数学库(如果程序中用到了数学函数)。运行生成的 calc 程序,就可以输入算术表达式进行解析和计算了。
五、Bison 的错误处理
Bison 提供了一定的错误处理机制,能够在输入存在语法错误时进行提示,并尽可能地恢复分析过程,继续处理后续的输入。
(一)错误产生式
在 Bison 文法中,可以定义错误产生式来处理特定的语法错误。错误产生式通常包含 error 符号,例如:
expr:
...
| error { yyerror("Invalid expression"); yyerrok; }
;
当语法分析器遇到错误时,会尝试找到包含 error 符号的产生式进行归约,执行相应的语义动作(如输出错误信息)。yyerrok 宏用于重置错误状态,使分析器能够继续处理后续输入。
(二)错误恢复
Bison 的默认错误恢复策略是跳过输入字符,直到找到一个能够恢复分析的同步点(通常是一些高优先级的终结符,如分号、右括号等)。开发者也可以通过自定义错误处理函数和错误产生式,实现更灵活的错误恢复机制。
(三)yyerror 函数
yyerror 函数是 Bison 中用于输出错误信息的函数,必须由用户实现。该函数接受一个字符串参数,即错误信息,通常将其输出到标准错误流。例如:
void yyerror(const char *s) {
fprintf(stderr, "Line %d: %s\n", yylineno, s);
}
其中,yylineno 是 Flex 提供的变量,用于记录当前的行号,方便定位错误位置。
六、Bison 的高级特性
(一)% union 定义属性值类型
当非终结符和终结符的属性值有多种类型时,可以使用 %union 来定义一个联合体类型,包含所有可能的类型。例如:
%union {
int ival;
float fval;
char *sval;
}
%token <ival> INT
%token <fval> FLOAT
%token <sval> STRING
%type <ival> expr_i
%type <fval> expr_f
这样,不同的记号和非终结符可以拥有不同类型的属性值,通过 <ival>、<fval> 等指定。
(二)位置信息跟踪
Bison 可以跟踪每个记号和非终结符在输入中的位置信息(如行号、列号),这对于生成有意义的错误信息非常重要。通过 %locations 声明启用位置跟踪,然后在 Flex 中设置 yylloc 变量(表示当前记号的位置)。例如:
在 Bison 文件中:
%locations
在 Flex 文件中:
%{
#include "calc.tab.h"
#include "calc.tab.h"
#define YY_USER_ACTION yylloc.first_line = yylloc.last_line = yylineno;
%}
这样,Bison 生成的分析器就可以获取每个符号的位置信息,并在错误信息中使用。
(三)GLR 分析器
对于一些复杂的、存在二义性且难以用 LALR (1) 算法处理的文法,Bison 支持生成 GLR(Generalized LR)分析器。GLR 分析器能够同时探索多个可能的分析路径,从而处理更广泛的文法。通过 %glr-parser 声明启用 GLR 模式:
%glr-parser
(四)参数化非终结符
Bison 允许非终结符带有参数,这可以用于传递一些上下文信息,简化语义动作的编写。例如:
%type <ival> expr(int flag)
expr(flag):
term(flag) { $$ = $1; }
| expr(flag) PLUS term(flag) { $$ = $1 + $3; }
;
这里,expr 非终结符带有一个 flag 参数,在产生式中可以使用该参数。
七、Bison 的应用场景
(一)编译器开发
Bison 最主要的应用场景是编译器的开发。无论是开发新的编程语言,还是为现有语言实现编译器前端,Bison 都能帮助开发者快速生成语法分析器,处理语言的语法结构,生成中间代码或抽象语法树,为后续的语义分析、代码优化和代码生成奠定基础。
(二)解释器开发
对于解释型语言,Bison 可以用于生成解释器的语法分析部分,解析输入的源代码,直接执行相应的语义动作,实现对语言的解释执行。
(三)配置文件解析
许多应用程序都需要处理配置文件,配置文件通常有其特定的语法结构。使用 Bison 和 Flex 可以快速开发出配置文件解析器,将配置文件中的信息解析为程序内部的数据结构,方便程序读取和使用配置信息。
(四)数据格式处理
对于一些自定义的数据格式或标准的数据格式(如 JSON、XML 的子集等),Bison 可以用于开发相应的解析器,对数据进行验证和提取,实现数据的导入、导出或转换。
(五)领域特定语言(DSL)开发
在特定的领域中,为了提高开发效率或简化问题描述,常常会设计领域特定语言。Bison 可以用于实现 DSL 的语法分析器,使 DSL