初识 flex & bison

基本概念

flexbison 经常结合使用,分别用于词法分析和语法分析。

  1. 词法分析器 (flex): flex 用于生成词法分析器或者说是扫描器(scanner)。它将输入的文本分解为称为"tokens"的序列。每个 token 都有一个特定的意义,例如一个数字、一个变量名或一个操作符。

  2. 语法分析器 (bison): bison 用于生成语法分析器或称为解析器(parser)。它的任务是根据由 flex 产生的 tokens 来确保输入遵循某种给定的语法,并从中生成一个通常的抽象语法树 (AST) 或其他中间表示。

执行流程:

  1. 输入流: 当有输入流进来时,首先会被传递给词法分析器 (flex 生成的)。

  2. flex 生成的词法分析器处理: 词法分析器会从输入流中扫描字符,并组成 tokens,这些 tokens 是预定义的或基于 flex 规则来识别的。

  3. tokens 传给 bison 生成的解析器: 当词法分析器识别出一个 token,它会将其传递给语法分析器 (bison 生成的)。如果需要,词法分析器可以提供附加的值(例如,如果 token 是一个数字,则它可以提供该数字的实际值)。

  4. bison 解析器处理: 解析器根据 tokens 序列来检查其是否遵循定义的语法。如果符合预期的语法,解析器可以执行相应的动作,如生成抽象语法树或执行其他任务。

所以,从调用关系来看,解析器 (bison 生成的) 调用词法分析器 (flex 生成的)。当解析器需要下一个 token 时,它会调用词法分析器来获取。这通常通过在 bison 代码中使用特殊的宏或函数实现,例如 yylex(),它会被 flex 生成的代码实现。

这种合作方式允许将输入的文本的词法和语法阶段分离开来,使得编译器或解释器的设计和实现更加模块化。

实例

下面提供一个简单的例子,来说明如何在代码中体现 flexbison 的结合使用。

  1. 词法分析器 (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;
}
  1. 语法分析器 (bison 文件)

parser.y:

%{
    #include 
    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;
}
  1. 生成和编译

首先,您需要使用 flexbison 生成 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.aflex 提供的库,其中包含了一些 flex 生成的扫描器可能需要的函数。一个常见的函数是 yywrap()。虽然我们在词法分析器文件中定义了这个函数,但在某些情况下,如果没有自定义实现,则链接到 libfl.a 会提供默认的实现。
所以,当使用 flex 生成词法分析器并与其他代码一起编译时,通常需要链接到这个库以确保提供所有必需的功能。
简而言之,-lfl 是为了确保链接到 flex 的库,这样生成的词法分析器可以正常运行。

常用符号

  1. yylex:

    • 描述: 它是由 flex 生成的函数,并在 bison 生成的解析器中调用。
    • 功能: 读取输入并返回下一个 token。当 yyparse 需要从输入中获取下一个 token 时,它会调用 yylex
    • 返回: 通常返回 token 的整数代码(定义在 bison 文件中)。
  2. yyerror:

    • 描述: 当 bison 检测到语法错误时,它会调用这个函数。
    • 功能: 报告语法错误。
    • 参数: 一个指向错误消息的字符串。
    • 实现: 用户必须在其 bison 输入文件中提供 yyerror 的定义。
  3. yyparse:

    • 描述: 由 bison 生成的函数。
    • 功能: 开始语法解析。它会反复调用 yylex 来获取 tokens,直到解析完成。
    • 返回: 当成功完成解析时返回 0,否则返回非零值。
  4. yywrap:

    • 描述: 在 flex 的输入文件中可能需要定义或者需要链接到 -lfl 来提供默认实现的函数。
    • 功能: 当输入结束时,yylex 会调用此函数。如果 yywrap 返回 1,解析将停止。如果返回 0,flex 假定有更多的输入文件,因此将继续扫描。
    • 默认实现: 默认返回 1,表示没有其他的输入文件。
  5. yylval:

    • 描述: 一个全局变量。
    • 功能: 用于从 yylex(词法分析器)传递值到 yyparse(语法分析器)。例如,当词法分析器识别一个整数时,它将其值存储在 yylval 中。
    • 类型: 可以是一个联合体,允许存储多种类型的值。其定义通常在 bison 文件的 %union 部分中找到。
  6. yytext:

    • 描述: 在 flex 生成的代码中定义的全局字符数组。
    • 功能: 当 flex 匹配到一个模式时,它将匹配的字符串存储在 yytext 中。例如,当词法分析器识别到一个单词 “hello” 时,yytext 将包含 “hello”。
    • 使用: 用户可以在 flex 的动作代码中引用它,例如使用 atoi(yytext) 来将匹配的数字字符串转换为整数。

当使用 flexbison 创建词法分析器和语法分析器时,会经常与这些符号交互。了解它们如何工作有助于更有效地利用这些工具。

yyparse(重点)

yyparse 是一个由 bison(或其前身 yacc)生成的函数。当我们希望开始语法分析过程时,会调用这个函数。这里是关于 yyparse 的详细介绍:

  1. 功能:

    • yyparse 的主要功能是解析输入并构建一个与输入相对应的抽象语法树或执行与语法规则相关的其他动作。
    • 它利用 yylex 获取输入的 tokens。每当 yyparse 需要一个新的 token 时,它会调用 yylex
  2. 返回值:

    • 当成功完成解析时返回 0。
    • 如果解析过程中发生错误(例如语法错误),则返回非零值。
  3. 错误处理:

    • bison 检测到语法错误时,它会调用 yyerror 函数。可以提供自定义的 yyerror 实现来决定如何处理这些错误。例如,可以选择打印错误消息、记录错误或执行其他错误处理操作。
    • yyparse 还支持错误恢复机制,允许在检测到错误后继续解析。为此,需要在 bison 规则中使用特殊的 error 符号。
  4. 内部工作:

    • yyparse 使用一个称为 LALR(1) 的解析算法,它是一个自底向上的解析算法。
    • 它维护一个状态堆栈,用于跟踪解析的进度和预测接下来的输入。
    • 对于每一个输入 token,它可能执行一系列的动作:移入(将 token 压入堆栈)、规约(根据语法规则合并堆栈上的 token)或接受(成功完成解析)。
  5. 交互与其他组件:

    • yyparse 与由 flex 生成的 yylex 函数密切协作。如前所述,它通过调用 yylex 来请求新的 tokens。
    • 它还使用 yylval 全局变量来从 yylex 接收 token 值。例如,当 yylex 返回一个数字 token 时,这个数字的实际值将存储在 yylval 中,然后 yyparse 可以访问它。
  6. 使用:

    • 通常,主程序(例如 main 函数)将调用 yyparse 来开始解析过程。
    • 如果程序需要解析多个输入或多次重复解析,可以多次调用 yyparse

要有效地使用 yyparse 和与之相关的其他组件,理解其如何工作和如何与其他部分(如 yylexyyerror 等)交互是很有帮助的。

你可能感兴趣的:(编译原理,编译原理)