flex 如何一次返回多个终端

cal*_*vin 2 yacc lex compilation bison flex-lexer

为了使我的问题易于理解,我想使用以下示例:

以下代码在fortran语言中称为非块do-loop

DO 20 I=1, N      ! line 1
DO 20 J=1, N      ! line 2
    ! more codes
20  CONTINUE      ! line 4
Run Code Online (Sandbox Code Playgroud)

要注意的是,标签204个装置的末端内做环和外DO循环。

我希望我的 flex 程序正确解析该功能:当 flex 读取 label 时20,它将返回ENDDO终端两次。

首先,因为我也用bison,所以每次bison 打电话yylex()都拿到一个终端。如果我可以yylex()在某些情况下要求 bison 获取终端,在其他情况下从另一个函数获取终端,也许我可以解决这个问题,但是,我当时不知道。

当然有一些解决方法,例如,我可以使用 flex 的启动条件,但我认为这不是一个好的解决方案。所以我问是否有任何方法可以在没有解决方法的情况下解决我的问题?

ric*_*ici 6

修改 (f)lex 生成的词法扫描器来实现令牌队列很容易,但这不一定是最佳解决方案。(有关更好的解决方案,请参见下文。)(另外,对于您的特定问题,我真的不清楚,在词法分析器中制作额外的标记是否真的合适。)

一般的方法是在yylex函数的顶部插入代码,您可以通过将代码立即放置在该%%行之后和第一条规则之前来实现。(代码必须缩进,这样它就不会被解释为规则。)对于不可重入扫描器,这通常涉及使用局部static变量来保存队列。对于一个简单但愚蠢的示例,使用 C API 但使用 C++ 编译以便访问 C++ 标准库:

%%
  /* This code will be executed each time `yylex` is called, before
   * any generated code. It may include declarations, even if compiled
   * with C89.
   */
  static std::deque<int> tokenq;
  if (!tokenq.empty()) {
    int token = tokenq.front();
    tokenq.pop_front();
    return token;
  }
[[:digit:]]+  { /* match a number and return that many HELLO tokens */
                   int n = atoi(yytext);
                   for (int i = 0; i < n; ++i)
                     tokenq.push_back(HELLO);
              }
Run Code Online (Sandbox Code Playgroud)

上面的代码没有尝试为排队的令牌提供语义值;您可以使用类似 astd::queue<std::pair<int, YYSTYPE>>的令牌队列来实现这一点,但YYSTYPE通常 a的事实union会导致一些复杂情况。此外,如果这是使用令牌队列的唯一原因,很明显可以用一个简单的计数器替换它,这样效率会更高。例如,请参阅此答案该答案与您的问题有些相似(并注意该答案的注释 1 中的建议)。

更好的选择:使用推送解析器

尽管令牌队列解决方案有吸引力且简单,但它很少是最好的解决方案。在大多数情况下,如果您请求 bison 生成“推送解析器”,代码将更清晰、更易于编写。使用推送解析器,每次有令牌可用时,词法分析器都会调用解析器。这使得从词法分析器动作返回多个标记变得微不足道;您只需为每个令牌调用解析器。类似地,如果规则不产生任何标记,它只是无法调用解析器。在这个模型中,唯一实际为returns 的词法分析器动作是<<EOF>>规则,并且只有在使用END标记调用解析器以指示解析完成后才会这样做。

不幸的是,推送解析器的接口不仅会改变,正如手动链接所指示的那样;它的记录也很糟糕。所以这里有一个简单但完整的例子,展示了它是如何完成的。

推送解析器将其状态保存在一个yypstate结构中,每次调用时都需要将其传递给解析器。由于词法分析器对于每个输入文件只被调用一次,因此词法分析器拥有该结构是合理的,这可以通过局部静态变量实现[注 1]:解析器状态在yylex被调用时被初始化,并且EOF规则删除解析器状态以回收它正在使用的任何内存。

通常构建可重入推送解析器最方便,这意味着解析器不依赖全局yylval变量[注2]。相反,必须提供指向语义值的指针作为 的附加参数yypush_parse。如果您的解析器没有引用特定标记类型的语义值,您可以为此参数提供 NULL。或者,如下面的代码所示,您可以在词法分析器中使用局部语义值变量。不必每次调用推送解析器都提供相同的指针。总之,对扫描仪定义的更改是最小的:

%%
  /* Initialize a parser state object */
  yypstate* pstate = yypstate_new();
  /* A semantic value which can be sent to the parser on each call */
  YYSTYPE yylval;
  /* Some example scanner actions */
"keyword"    {  /* Simple keyword which just sends a value-less token */
                yypush_parse(pstate, TK_KEYWORD, NULL); /* See Note 3 */
             }
[[:digit:]]+ { /* Token with a semantic value */
               yylval.num = atoi(yytext);
               yypush_parse(pstate, TK_NUMBER, &yylval);
             }
"dice-roll"  { /* sends three random numbers */
               for (int i = 0; i < 2; ++i) {
                 yylval.num = rand() % 6;
                 yypush_parse(pstate, TK_NUMBER, &yylval);
             }
<<EOF>>      { /* Obligatory EOF rule */
               /* Send the parser the end token (0) */
               int status = yypush_parse(pstate, 0, NULL);
               /* Free the pstate */
               yypstate_delete(pstate);
               /* return the parser status; 0 is success */
               return status;
             }
Run Code Online (Sandbox Code Playgroud)

在解析器中,除了添加必要的声明之外,不需要做太多改动:[注4]

%define api.pure full
%define api.push-pull push
Run Code Online (Sandbox Code Playgroud)

笔记

  1. 如果您还构建了一个可重入词法分析器,您将使用词法分析器状态对象的额外数据部分而不是静态变量。

  2. 如果您在解析器中使用位置对象来跟踪源代码位置,这也适用于yylloc.

  3. 示例代码在检测错误方面做得不好,因为它不检查对yypush_parse. 我常用的一种解决方案是宏上的一些变体SEND

    #define SEND(token) do {                              \
      int status = yypush_parse(pstate, token, &yylval);  \
      if (status != YYPUSH_MORE) {                        \
        yypstate_delete(pstate);                          \
        return status;                                    \
      }                                                   \
    } while (0)
    
    Run Code Online (Sandbox Code Playgroud)

    也可以使用 agoto来避免yypstate_deleteand的多个实例return。天啊。

  4. 您可能需要修改yyerror. 如果您使用位置和/或向 push_parser 提供额外参数,则位置对象和/或额外参数也将出现在yyerror调用中。(错误字符串始终是最后一个参数。)无论出于何种原因,解析器状态对象都没有提供给yyerror,这意味着该yyerror函数不再可以访问诸如 之类的变量yych,这些变量现在是yypstate结构的成员而不是全局变量,因此如果您在错误报告中使用这些变量(这不是真正推荐的做法),那么您将不得不寻找替代解决方案。