基本概念
flex
和 bison
经常结合使用,分别用于词法分析和语法分析。
-
词法分析器 (
flex
):flex
用于生成词法分析器或者说是扫描器(scanner)。它将输入的文本分解为称为"tokens"的序列。每个 token 都有一个特定的意义,例如一个数字、一个变量名或一个操作符。 -
语法分析器 (
bison
):bison
用于生成语法分析器或称为解析器(parser)。它的任务是根据由flex
产生的 tokens 来确保输入遵循某种给定的语法,并从中生成一个通常的抽象语法树 (AST) 或其他中间表示。
执行流程:
-
输入流: 当有输入流进来时,首先会被传递给词法分析器 (
flex
生成的)。 -
由
flex
生成的词法分析器处理: 词法分析器会从输入流中扫描字符,并组成 tokens,这些 tokens 是预定义的或基于flex
规则来识别的。 -
tokens 传给
bison
生成的解析器: 当词法分析器识别出一个 token,它会将其传递给语法分析器 (bison
生成的)。如果需要,词法分析器可以提供附加的值(例如,如果 token 是一个数字,则它可以提供该数字的实际值)。 -
bison
解析器处理: 解析器根据 tokens 序列来检查其是否遵循定义的语法。如果符合预期的语法,解析器可以执行相应的动作,如生成抽象语法树或执行其他任务。
所以,从调用关系来看,解析器 (bison
生成的) 调用词法分析器 (flex
生成的)。当解析器需要下一个 token 时,它会调用词法分析器来获取。这通常通过在 bison
代码中使用特殊的宏或函数实现,例如 yylex()
,它会被 flex
生成的代码实现。
这种合作方式允许将输入的文本的词法和语法阶段分离开来,使得编译器或解释器的设计和实现更加模块化。
实例
下面提供一个简单的例子,来说明如何在代码中体现 flex
和 bison
的结合使用。
- 词法分析器 (
flex
文件)
lexer.l
:
%{
#include "parser.h"
%}
%%
[0-9]+ {
yylval = atoi(yytext); // yylval 是 Bison 用来传递 token 数据的变量
return T_NUMBER;
}
"+" { return T_PLUS; }
"-" { return T_MINUS; }
"*" { return T_MUL; }
"/" { return T_DIV; }
"(" { return T_LPAREN; }
")" { return T_RPAREN; }
[ \t\n] ; // Ignore whitespaces
. { printf("Unexpected character: %s\n", yytext); exit(1); }
%%
int yywrap() {
return 1;
}
- 语法分析器 (
bison
文件)
parser.y
:
%{
#include <stdio.h>
int yylex();
void yyerror(const char *s);
%}
%token T_NUMBER
%token T_PLUS T_MINUS T_MUL T_DIV
%token T_LPAREN T_RPAREN
%%
expression:
T_NUMBER
| expression T_PLUS expression { $$ = $1 + $3; }
| expression T_MINUS expression { $$ = $1 - $3; }
| expression T_MUL expression { $$ = $1 * $3; }
| expression T_DIV expression { $$ = $1 / $3; }
| T_LPAREN expression T_RPAREN { $$ = $2; }
;
%%
void yyerror(const char *s) {
printf("Error: %s\n", s);
}
int main() {
yyparse();
return 0;
}
- 生成和编译
首先,需要使用 flex
和 bison
生成 C 源代码:
flex -o lexer.c lexer.l
bison -o parser.c parser.y
然后,可以编译生成的代码:
gcc -o calculator lexer.c parser.c -lfl
现在,可以运行 calculator
程序并尝试输入一些算术表达式。
在这个例子中,词法分析器 (lexer.l
) 会识别数字和算术操作符,并将它们作为 tokens 传递给语法分析器 (parser.y
)。语法分析器会根据表达式的结构生成相应的结果。当语法分析器需要一个新的 token 时,它会调用 yylex()
,这个函数是由 flex
生成的,并从输入中提取下一个 token。
-lfl
是一个链接选项,告诉gcc
链接器链接到名为fl
的库,它实际上是libfl.a
。
libfl.a
是flex
提供的库,其中包含了一些flex
生成的扫描器可能需要的函数。一个常见的函数是yywrap()
。虽然我们在词法分析器文件中定义了这个函数,但在某些情况下,如果没有自定义实现,则链接到libfl.a
会提供默认的实现。
所以,当使用flex
生成词法分析器并与其他代码一起编译时,通常需要链接到这个库以确保提供所有必需的功能。
简而言之,-lfl
是为了确保链接到flex
的库,这样生成的词法分析器可以正常运行。
常用符号
-
yylex:
- 描述: 它是由
flex
生成的函数,并在bison
生成的解析器中调用。 - 功能: 读取输入并返回下一个 token。当
yyparse
需要从输入中获取下一个 token 时,它会调用yylex
。 - 返回: 通常返回 token 的整数代码(定义在
bison
文件中)。
- 描述: 它是由
-
yyerror:
- 描述: 当
bison
检测到语法错误时,它会调用这个函数。 - 功能: 报告语法错误。
- 参数: 一个指向错误消息的字符串。
- 实现: 用户必须在其
bison
输入文件中提供yyerror
的定义。
- 描述: 当
-
yyparse:
- 描述: 由
bison
生成的函数。 - 功能: 开始语法解析。它会反复调用
yylex
来获取 tokens,直到解析完成。 - 返回: 当成功完成解析时返回 0,否则返回非零值。
- 描述: 由
-
yywrap:
- 描述: 在
flex
的输入文件中可能需要定义或者需要链接到-lfl
来提供默认实现的函数。 - 功能: 当输入结束时,
yylex
会调用此函数。如果yywrap
返回 1,解析将停止。如果返回 0,flex
假定有更多的输入文件,因此将继续扫描。 - 默认实现: 默认返回 1,表示没有其他的输入文件。
- 描述: 在
-
yylval:
- 描述: 一个全局变量。
- 功能: 用于从
yylex
(词法分析器)传递值到yyparse
(语法分析器)。例如,当词法分析器识别一个整数时,它将其值存储在yylval
中。 - 类型: 可以是一个联合体,允许存储多种类型的值。其定义通常在
bison
文件的%union
部分中找到。
-
yytext:
- 描述: 在
flex
生成的代码中定义的全局字符数组。 - 功能: 当
flex
匹配到一个模式时,它将匹配的字符串存储在yytext
中。例如,当词法分析器识别到一个单词 “hello” 时,yytext
将包含 “hello”。 - 使用: 用户可以在
flex
的动作代码中引用它,例如使用atoi(yytext)
来将匹配的数字字符串转换为整数。
- 描述: 在
当使用 flex
和 bison
创建词法分析器和语法分析器时,会经常与这些符号交互。了解它们如何工作有助于更有效地利用这些工具。
yyparse(重点)
yyparse
是一个由 bison
(或其前身 yacc
)生成的函数。当我们希望开始语法分析过程时,会调用这个函数。这里是关于 yyparse
的详细介绍:
-
功能:
yyparse
的主要功能是解析输入并构建一个与输入相对应的抽象语法树或执行与语法规则相关的其他动作。- 它利用
yylex
获取输入的 tokens。每当yyparse
需要一个新的 token 时,它会调用yylex
。
-
返回值:
- 当成功完成解析时返回 0。
- 如果解析过程中发生错误(例如语法错误),则返回非零值。
-
错误处理:
- 当
bison
检测到语法错误时,它会调用yyerror
函数。可以提供自定义的yyerror
实现来决定如何处理这些错误。例如,可以选择打印错误消息、记录错误或执行其他错误处理操作。 yyparse
还支持错误恢复机制,允许在检测到错误后继续解析。为此,需要在bison
规则中使用特殊的error
符号。
- 当
-
内部工作:
yyparse
使用一个称为 LALR(1) 的解析算法,它是一个自底向上的解析算法。- 它维护一个状态堆栈,用于跟踪解析的进度和预测接下来的输入。
- 对于每一个输入 token,它可能执行一系列的动作:移入(将 token 压入堆栈)、规约(根据语法规则合并堆栈上的 token)或接受(成功完成解析)。
-
交互与其他组件:
yyparse
与由flex
生成的yylex
函数密切协作。如前所述,它通过调用yylex
来请求新的 tokens。- 它还使用
yylval
全局变量来从yylex
接收 token 值。例如,当yylex
返回一个数字 token 时,这个数字的实际值将存储在yylval
中,然后yyparse
可以访问它。
-
使用:
- 通常,主程序(例如
main
函数)将调用yyparse
来开始解析过程。 - 如果程序需要解析多个输入或多次重复解析,可以多次调用
yyparse
。
- 通常,主程序(例如
要有效地使用 yyparse
和与之相关的其他组件,理解其如何工作和如何与其他部分(如 yylex
、yyerror
等)交互是很有帮助的。