线程安全/可重入的野牛+弹性

时间:2018-02-18 09:37:19

标签: bison flex-lexer yacc

我真的更喜欢一个有效的例子来解释。无论我到目前为止在Bison的文档网站上所阅读的内容都与Flex所说的相矛盾。有人说要将yylex声明为

int yylex (yyscan_t yyscanner);

另一个人希望它是:

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

我真正需要的是位置信息。我现在还不确定是否需要YYSTYPE(我现在不能使用这些信息,但将来我可能会这样做。)

与上述无关,作为奖励,我很有兴趣知道为什么这个基础设施如此糟糕。这似乎是一件非常直截了当的事情,但它却是另一回事。它从不适用于默认值。即使编写一个最简单的教科书计算器示例,也需要多天修复配置错误......为什么?

1 个答案:

答案 0 :(得分:8)

1。示例代码

本答案的第2部分提供了一种关于如何将可重入配置为野牛和弹性的解释。示例代码的其他注释在第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, $2); }
     | input '\n'
     | input error '\n'     { yyerrok; }
expr : NUMBER
     | '(' expr ')'         { $$ = $2; }
     | '-' expr %prec UNOP  { $$ = -$2; }
     | expr '+' expr        { $$ = $1 + $3; }
     | expr '-' expr        { $$ = $1 - $3; }
     | expr '*' expr        { $$ = $1 * $3; }
     | expr '/' expr        { $$ = $1 / $3; }
     | expr '%' expr        { $$ = $1 % $3; }

%%

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

1.3 eval.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 Makefile

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。重新进入问题

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

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

2.1 Bison / Yacc re-entrancy

一次调用Bison / Yacc生成的解析器来解析整个文本体,因此不需要在调用之间维护可变的持久数据对象。它依赖于许多指导解析器进度的表,但这些不可变表具有静态生命周期的事实不会影响重入。 (至少使用Bison,这些表没有外部链接,但当然,通过插入到解析器中的用户编写的代码,它们仍然可见。)

然后,主要问题是外部可见的可变全局变量yylvalyylloc,用于增强解析器 - 词法分析器接口。这些全局变量绝对是Bison / Yacc的一部分; Flex生成的代码甚至没有提及它们,并且在Flex定义文件中的用户操作中明确执行它们的所有使用。为了使野牛解析器具有可重入性,有必要修改解析器用来从词法分析器收集有关每个令牌的信息的API,而Bison采用的解决方案是提供附加参数的经典解决方案,这些参数是指向数据的指针结构被&#34;返回&#34;到解析器。所以这个重入需求改变了Bison生成的解析器调用yylex的方式;而不是调用

int yylex(void);

原型成为:

int yylex(YYSTYPE* yylvalp);

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

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

这意味着必须修改词法分析器才能与可重入的野牛解析器正确通信,即使词法分析器本身不可重入。

有少量额外的Bison / Yacc变量供用户代码使用:

  • yynerrs计算遇到的语法错误数;使用可重入的解析器,yynerrsyyparse的本地解析器,因此只能在操作中使用。 (在旧版应用程序中,它有时由yyparse的调用者引用;需要针对可重入的解析器修改此类用法。)

  • yychar是前瞻符号的令牌类型,有时用于错误报告。在可重入的解析器中,它是yyparse的本地解析器,因此如果错误报告功能需要它,则必须明确传递。

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

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

2.2 Flex / Lex re-entrancy

在解析过程中重复调用

yylex;每次调用它时,它都会返回一个令牌。它需要在调用之间保持大量的持久状态,包括其当前缓冲区和跟踪词汇进度的各种指针。

在默认词法分析器中,此信息保存在全局struct中,除了特定的全局变量(主要是现代Flex模板中的宏)之外,不会被用户代码引用。

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

因此,重新进入扫描程序中的yylex 具有原型

int yylex(yystate_t state);

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

Flex / lex本身只识别令牌;由每个模式关联的用户操作决定了匹配的结果。通常,解析器期望yylex将返回表示令牌的句法类型的小整数或0以指示已到达输入的结尾。令牌的文本存储在变量(或yyscan_t成员)yytext中(其长度在yyleng),但由于yytext是指向生成的内部缓冲区的指针扫描仪,字符串值只能在下次调用yylex之前使用。由于LR解析器通常不会处理语义信息,直到读取了几个令牌,因此yytext不是传递语义信息的适当机制。如上所述,Bison / Yacc生成的解析器改为使用全局yylval来传递语义信息。如果需要,Bison还提供yylloc全局来传达源位置信息。

正如我们所见,重入词法分析器和重入解析器都需要更改yylex的原型。如果两个组件都是重入的,则需要应用这两个更改,原型变为:

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

(如果未使用位置,则消除yyllocp参数。)

3。关于示例代码的注释

3.1。循环标头依赖

鉴于上述情况,在声明yylex()之前无法声明YYSTYPE。在声明yyparse()之前,也无法声明yyscan_t。由于yylexyyscan_t位于flex生成的标头中,yyparseYYSTYPE位于bison生成的标头中,因此两个标头的包含顺序都不起作用。或者,换句话说,存在循环依赖。

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

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

工作正常。下一步似乎是将typedef和第二个#include放入野牛生成的标头flex.tab.h中,使用code requires块放置{{1接近开头的typedef块将code provides放在最后(或至少在#include声明之后)。不幸的是,这不起作用,因为YYSTYPE包含在flex生成的扫描程序代码中。这将导致将flex生成的头部包含在flex生成的源代码中,并且不支持。 (尽管flex生成的标头确实有一个标头保护,但生成的源文件不需要存在头文件,因此它包含内容的副本而不是flex.tab.h语句,并且副本不包括头球卫士。)

在示例代码中,我做了下一个最好的事情:我使用#include块将code requires插入到bison生成的头文件中,并创建了一个额外的typedef头文件其他翻译单元可以使用它,包括正确顺序的bison和flex生成的标题。

那很难看。已经提出了其他解决方案,但它们都是,恕我直言,同样难看。这恰好是我使用的那个。

3.2。来源位置

yylex和yyerror原型都取决于解析器是否需要源位置。由于这些更改将通过各种项目文件进行回响,我认为最可取的是强制使用位置信息,即使它尚未被解析器使用。有一天你可能想要使用它,并且维护它的运行时开销并不是很大(虽然它是可测量的,所以你可能想在资源受限的环境中忽略这个建议。)

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

3.3 Bison错误恢复

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

3.4调试跟踪

Bison和Yacc生成的解析器遵循Posix的要求,即除非定义了预处理器宏yyerrok并且具有非零值,否则不会编译生成的源中的调试代码。如果将调试代码编译到二进制文件中,则调试跟踪由全局变量YYDEBUG控制。如果yydebug非零,则YYDEBUG的默认值为0,这会禁用跟踪。如果使用yydebug yydebug YYDEBUG is 0, YYDEBUG is not defined by the bison/yacc-generated code. If - t`命令行选项,则在此情况下它将具有默认值1.

Bison将is not defined, then it will be defined by the generated code, with value 0 unless the宏定义插入到生成的头文件中(虽然Posix没有强制要求),所以我在YYDEBUG中测试它并提供{的替代定义{1}}变量(如果尚未定义)。这允许代码使得调试跟踪能够编译,即使它无法打开跟踪。

Flex生成的代码通常使用全局变量main.c来打开和关闭跟踪;与yacc / bison不同,如果将调试代码编译到可执行文件中,则yydebug的默认值为1。由于可重入扫描程序无法使用全局变量,因此可重入扫描程序将调试启用程序放入yy_flex_debug对象中,可以使用yy_flex_debugyyscan_t访问函数访问它,或者没有编译调试代码。但是,可重入调试标志的默认值为0,因此,如果创建可重入扫描程序,则即使已将跟踪编译到可执行文件中,也需要显式启用跟踪。 (这使得重入扫描器更像解析器。)

如果使用yyset_debug命令行选项运行,并且使用yyget_debug选项进行解析器跟踪,示例main程序将启用扫描程序跟踪。