线程安全/可重入 bison + flex

Thread-safe / reentrant bison + flex

比起任何解释,我真的更喜欢一个工作示例。到目前为止,无论我在 Bison 的文档站点上读到什么,都与 Flex 所说的相矛盾。有人说将 yylex 声明为

int yylex (yyscan_t yyscanner);

另一个人希望它是:

int yylex(YYSTYPE *lvalp, YYLTYPE *llocp);

我真正需要的是位置信息。我还不确定我是否需要 YYSTYPE(我现在没有用到这些信息,但也许将来我会用到)。


与上述内容无关,作为奖励,我很想知道为什么这个基础设施如此糟糕。这似乎是一件很简单的事情,但实际上却很糟糕。它从不使用默认值。即使编写一个最简单的计算器教科书示例也需要很多天来修复配置错误...为什么?

1。示例代码

此答案的第 2 部分提供了一种关于如何将重入配置到 bison 和 flex 中的解释。示例代码的其他注释在第 3 节中。

1.1 eval.l

%option noinput nounput noyywrap 8bit nodefault                                 
%option yylineno
%option reentrant bison-bridge bison-locations                                  

%{
  #include <stdlib.h>                                                           
  #include <string.h>
  #include "eval.tab.h"                                                   
  
  #define YY_USER_ACTION                                             \
    yylloc->first_line = yylloc->last_line;                          \
    yylloc->first_column = yylloc->last_column;                      \
    if (yylloc->last_line == yylineno)                               \
      yylloc->last_column += yyleng;                                 \
    else {                                                           \
      yylloc->last_line = yylineno;                                  \
      yylloc->last_column = yytext + yyleng - strrchr(yytext, '\n'); \
    }
%}                                                                              
%%
[ \t]+            ;                                                  
#.*               ;                                                  

[[:digit:]]+      *yylval = strtol(yytext, NULL, 0); return NUMBER;  

.|\n              return *yytext;                                    

1.2 eval.y

%define api.pure full
%locations
%param { yyscan_t scanner }

%code top {
  #include <stdio.h>
} 
%code requires {
  typedef void* yyscan_t;
}
%code {
  int yylex(YYSTYPE* yylvalp, YYLTYPE* yyllocp, yyscan_t scanner);
  void yyerror(YYLTYPE* yyllocp, yyscan_t unused, const char* msg);
}

%token NUMBER UNOP
%left '+' '-'
%left '*' '/' '%'
%precedence UNOP
%%
input: %empty
     | input expr '\n'      { printf("[%d]: %d\n", @2.first_line, ); }
     | input '\n'
     | input error '\n'     { yyerrok; }
expr : NUMBER
     | '(' expr ')'         { $$ = ; }
     | '-' expr %prec UNOP  { $$ = -; }
     | expr '+' expr        { $$ =  + ; }
     | expr '-' expr        { $$ =  - ; }
     | expr '*' expr        { $$ =  * ; }
     | expr '/' expr        { $$ =  / ; }
     | expr '%' expr        { $$ =  % ; }

%%

void yyerror(YYLTYPE* yyllocp, yyscan_t unused, const char* msg) {
  fprintf(stderr, "[%d:%d]: %s\n",
                  yyllocp->first_line, yyllocp->first_column, msg);
}

1.3eval.h

请参阅 3.1 了解需要此文件的说明。

#include "eval.tab.h"
#include "eval.lex.h"

1.4 main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "eval.h"
#if !YYDEBUG
  static int yydebug;
#endif

int main(int argc, char* argv[]) {
  yyscan_t scanner;          
  yylex_init(&scanner);
  
  do {
    switch (getopt(argc, argv, "sp")) {
      case -1: break;
      case 's': yyset_debug(1, scanner); continue;
      case 'p': yydebug = 1; continue;
      default: exit(1);
    }
    break;
  } while(1);

  yyparse(scanner);          
  yylex_destroy(scanner);    
  return 0;
}

1.5 生成文件

all: eval

eval.lex.c: eval.l
        flex -o $@ --header-file=$(patsubst %.c,%.h,$@) --debug $<

eval.tab.c: eval.y
        bison -o $@ --defines=$(patsubst %.c,%.h,$@) --debug $<

eval: main.c eval.tab.c eval.lex.c eval.h
        $(CC) -o $@ -Wall --std=c11 -ggdb -D_XOPEN_SOURCE=700 $(filter %.c,$^)

clean:
        rm -f eval.tab.c eval.lex.c eval.tab.h eval.lex.h main

2。 Re-entrancy 个问题

最重要的是要记住Bison/Yacc和Flex/Lex是两个独立的代码生成器。虽然它们经常一起使用,但这不是必需的;任何一个都可以单独使用或与其他工具一起使用。

注意:以下讨论仅适用于普通的“拉”解析器。 Bison 可以生成推送解析器(类似于 Lemon)并且允许有用的控制流反转,这实际上简化了下面提到的几个问题。尤其是完全避免了3.1中分析的循环依赖。我通常更喜欢推送解析器,但它们似乎超出了这个特定问题的范围。

2.1 野牛/Yacc re-entrancy

一个 Bison/Yacc 生成的解析器被调用一次来解析整个 body 文本,因此它不需要在调用之间维护可变持久数据 object。它确实依赖于许多指导解析器进程的表,但这些不可变表具有静态生命周期这一事实并不影响 re-entrancy。 (至少对于 Bison,这些表没有外部链接,但当然它们仍然可以通过插入解析器的 user-written 代码看到。)

然后,主要问题是 externally-visible 可变全局变量 yylvalyylloc,用于扩充 parser-lexer 接口。这些全局变量绝对是 Bison/Yacc 的一部分; Flex-generated 代码甚至没有提及它们,所有对它们的使用都是在 Flex 定义文件中的用户操作中明确执行的。要制作一个 bison 解析器 re-entrant,需要修改解析器用来从词法分析器收集每个标记信息的 API,而 Bison 采用的解决方案是提供额外参数的经典方案它们是指向解析器“returned”的数据结构的指针。所以这个 re-entrancy 要求改变了 Bison-generated 解析器调用 yylex 的方式;而不是调用

int yylex(void);

原型变为:

int yylex(YYSTYPE* yylvalp);

int yylex(YYSTYPE* yylvalp, YYLTYPE* yyllocp);

取决于解析器是否需要存储在yylloc中的位置信息。 (Bison 将自动检测位置信息在操作中的使用,但您也可以坚持将位置 object 提供给 yylex。)

这意味着必须修改扫描器才能与 re-entrant bison 解析器正确通信,即使词法分析器本身不是 re-entrant。 (见下文。)

有少量额外的 Bison/Yacc 变量供用户代码使用,如果使用它们可能会强制更改源代码:

  • yynerrs统计遇到的语法错误的数量;对于 re-entrant 解析器,yynerrsyyparse 的局部变量,因此只能在操作中使用。 (在遗留应用程序中,它有时被 yyparse 的调用者引用;需要为 re-entrant 解析器修改此类用途。)

  • yychar是lookahead符号的token类型,有时用于报错。在 re-entrant 解析器中,它是 yyparse 的局部变量,因此如果错误报告函数需要它,则必须显式传递它。

  • yydebug 控制是否生成解析跟踪,如果已启用调试代码。 yydebug 在 re-entrant 解析器中仍然是全局的,因此不可能只为单个解析器实例启用调试跟踪。 (我认为这是一个错误,但它可以被视为一个功能请求。)

    通过定义预处理器宏 YYDEBUG 或使用 -t command-line 标志启用调试代码。这些由 Posix 定义; Flex 还提供了 --debug 命令行标志; %debug 指令和 parse.trace 配置指令(可以在野牛命令行上使用 -Dparse.trace 设置。

2.2 Flex / Lex re-entrancy

yylex 在解析过程中被重复调用;每次调用它时,它 return 都是一个标记。它需要在调用之间维护大量的持久状态,包括它的当前缓冲区和跟踪词法进程的各种指针。

在默认词法分析器中,此信息保存在全局 struct 中,除特定全局变量(现代 Flex 模板中的宏)外,用户代码不打算引用该信息。

在 re-entrant 词法分析器中,Flex 的所有持久信息都被收集到一个不透明的数据结构中,该数据结构由 yyscan_t 类型的变量指向。必须将此变量传递给对 Fl 的每次调用x 函数,而不仅仅是 yylex。 (例如,该列表包括各种缓冲区管理函数。)Flex 约定是持久状态 object 始终是函数的 last 参数。一些已重新定位到此数据结构中的全局变量具有关联的宏,因此可以通过它们的传统名称 Flex 动作来引用它们。在 yylex 之外,所有访问(和修改,在可变变量的情况下)必须使用 Flex manual 中记录的 getter 和 setter 函数完成。显然,getter/setter 函数列表不包括 Bison 变量的访问器,例如 yylval.

所以 yylex 在 re-entrant 扫描器 中有原型

int yylex(yyscan_t state);

2.3解析器和扫描器之间的通信

Flex/lex本身只识别token;由与每个模式关联的用户操作来传达匹配结果。按照惯例,解析器期望 yylex 将 return 表示标记句法类型的小整数或 0 以指示已到达输入末尾。令牌的文本存储在变量(或 yyscan_t 成员)yytext 中(其长度在 yyleng 中)但是由于 yytext 是指向生成的内部缓冲区的指针scanner,该字符串值只能在下次调用 yylex 之前使用。由于 LR 解析器通常在读取多个标记之前不会处理语义信息,因此 yytext 不是传递语义信息的合适机制。

如上所述,non-reentrant Bison/Yacc 生成的解析器假设使用全局 yylval 来传达语义信息,以及 yylloc 全局来传达语义信息源位置信息,如果需要(仅限 Bison)。

但是,如上所述,在 re-entrant 解析器中,这些变量是 yyparse 的局部变量,并且解析器在每次调用时将 指针 传递给变量到词法分析器。这需要更改 yylex 的原型,以及任何使用 yylval and/or yylloc.

的扫描器操作

可重入 bison-generated 解析器期望的原型是:

int yylex(YYSTYPE* yylvalp, YYLTYPE* yyllocp, yyscan_t state);

(如果不使用位置,则删除 yyllocp 参数。)

Flex 的 %bison-bridge 指令(或 %bison-bridge%bison-locations 的组合,如果正在使用位置跟踪)将确保 yylex 原型是正确的。

扫描器操作中对 yylval 的所有引用也需要修改,因为野牛的可重入 API 传递指向语义值和位置 object 的指针。如果语义类型是 union(通常通过在 bison 源中放置 %union 声明来生成),那么您需要将使用 yylval.tag 的扫描器操作更改为 yylval->tag.类似地,如果您使用单一语义类型,无论是默认类型还是(在 bison 源代码中)用 %define api.value.type 声明的类型,那么您需要将 yylval = ... 替换为 *yylval = ...,如在上面的示例代码中。

3。示例代码注释

3.1。循环header依赖

鉴于上述情况,在声明 YYSTYPE 之前不可能声明 yylex()。在声明 yyscan_t 之前也无法声明 yyparse() 。因为 yylexyyscan_t 在 flex-generated header 和 yyparseYYSTYPE 在 bison-generated header , 两个 header 的包含顺序都不起作用。或者,换句话说,存在循环依赖。

因为 yyscan_t 只是 void* 的类型别名(而不是指向不完整类型的指针,这可以说是一种将指针传递给不透明数据结构的更简洁的方法),循环可以通过插入冗余 typedef:

来破坏
typedef void* yyscan_t;
#include "flex.tab.h"
#include "flex.lex.h"

效果很好。下一步似乎是使用 code requires 块将 typedef 和第二个 #include 放入 bison-generated header flex.tab.h 中将 typedef 放在开头附近,然后 code provides 块将 #include 放在结尾附近(或至少在 YYSTYPE 声明之后)。不幸的是,这不起作用,因为 flex.tab.h 包含在 flex-generated 扫描器代码中。这样做的结果是将 flex-generated header 包含到 flex-generated 源代码中,但这是不受支持的。 (虽然 flex-generated header 确实有 header 保护,但生成的源文件不需要 header 文件存在,因此它包含内容的副本而不是#include 语句,副本不包括 header 守卫。)

在示例代码中,我做了次好的事情:我使用 code requires 块将 typedef 插入 bison-generated header,并创建了一个额外的 eval.h header 文件可以被其他翻译单元使用,其中包括正确顺序的 bison- 和 flex-generated headers。

太丑了。已经提出了其他解决方案,但恕我直言,它们都同样丑陋。这正好是我用的那个。

3.2。源位置

yylex 和 yyerror 原型都不同,具体取决于是否r 解析器不需要源位置。由于这些更改会在各种项目文件中产生反响,我认为最明智的做法是强制使用位置信息,即使它(尚未)被解析器使用。有一天您可能想要使用它,并且 运行 维护它的时间开销并不大(尽管它是可以衡量的,因此您可能希望在 resource-constrained 环境中忽略此建议)。

为了简化负载,我在 flex.l 的第 10-17 行包含了一个简单的通用实现,它在 YY_USER_ACTION 上用于在所有 flex 规则操作的开头插入代码。这个 YY_USER_ACTION 宏应该适用于任何不使用 yyless()yymore()input()REJECT 的扫描仪。正确处理这些功能并不太难,但似乎超出了这里的范围。

3.3 Bison错误恢复

示例代码实现了一个简单的line-oriented计算器,可用于交互式评估。 (不包括其他一些对交互式评估有用的功能。交互式计算器可以从 readline() 集成和访问以前计算的值中受益匪浅;变量和命名常量也很方便。)为了使交互式使用合理,我插入一个非常小的错误恢复策略:flex.y 的第 24 行的 error 生产丢弃标记,直到遇到换行符,然后使用 yyerrok 避免丢弃错误消息。

3.4 调试痕迹

Bison 和 Yacc 生成的解析器遵循 Posix 要求,除非预处理器宏 YYDEBUG 已定义且具有 non-zero 值,否则不会编译生成的源代码中的调试代码。如果将调试代码编译成二进制文件,则调试跟踪由全局变量 yydebug 控制。如果 YYDEBUG 为 non-zero,则 yydebug 的默认值为 0,即禁用跟踪。如果 YYDEBUG 为 0,则 yydebug 未由 bison/yacc-generated 代码定义。如果未定义 YYDEBUG,则它将由生成的代码定义,值为 0,除非使用 -t command-line 选项,在这种情况下它将具有默认值 1。

Bison 将 YYDEBUG 宏定义插入到生成的 header 文件中(尽管 Posix 没有义务这样做),所以我在 [=116] 中对其进行了测试=] 并提供 yydebug 变量的替代定义(如果尚未定义)。这允许启用调试跟踪的代码即使无法打开跟踪也可以编译。

Flex-generated 代码通常使用全局变量 yy_flex_debug 来打开和关闭跟踪;与 yacc/bison 不同,如果调试代码被编译到可执行文件中,yy_flex_debug 的默认值为 1。由于可重入扫描器不能使用全局变量,可重入扫描器将调试启用程序放入 yyscan_t object,可以使用 yyset_debugyyget_debug 访问函数访问它,定义是否已编译调试代码。但是,re-entrant 调试标志的默认值为 0,因此如果您创建一个可重入扫描器,即使跟踪已被编译到可执行文件中,您也需要显式启用跟踪。 (这使得可重入扫描器更像一个解析器。)

如果 运行 使用 -s command-line 选项,示例 main 程序将打开扫描器跟踪,并使用 -sp 选项打开解析器跟踪。