【问题标题】:Make bison start parsing with a rule other than the start rule使 bison 使用启动规则以外的规则开始解析
【发布时间】:2013-09-03 14:34:14
【问题描述】:

目前我正在开发一个源到源编译器,并且我已经编写了一个野牛解析器,可以正确地为输入创建 AST。我现在需要对语法树进行一些转换,因此我需要向树中插入许多节点。

我可以手动创建要添加到语法树的所有结构/联合,但这似乎是非常很多工作。

对我来说创建一个字符串会容易得多,我希望这个字符串可以被我已经拥有的解析器解析。然后解析器应该返回这个字符串的树,我可以将它插入到我的原始语法树中。

不幸的是,字符串不能用我的解析器的开始规则解析,因为它必须由子规则解析(例如,我的解析器解析包含语句的函数列表,字符串是单个语句)。

如何让 bison 解析一个字符串,从不同于起始规则的规则开始?

提前致谢!

【问题讨论】:

    标签: bison


    【解决方案1】:

    有一个简单的 hack,在 bison FAQ. 中有描述。

    基本上,对于您希望能够使用的每个非终端,您创建一个伪令牌,然后创建一个“元启动”非终端来选择您要使用的终端:

    %token START_PROGRAM
    %token START_STATEMENT
    %token START_EXPRESSION
    
    %start meta_start
    
    %%
    
    meta_start: START_PROGRAM program
              | START_STATEMENT statement
              | START_EXPRESSION expression
              ;
    

    (在每个产生式的操作中,您可以将$2 的值保存在调用者可以获取的某个位置。)

    现在您只需要安排您的词法分析器提供正确的开始令牌。您可以通过使用纯解析器和纯词法分析器并通过共享数据结构传递消息来做到这一点。那将是最好的方法,但是出于本答案的目的,我将仅展示如何使用全局变量来执行此操作,因为原理是相同的:

    extern int start_token;
    
    // If all your start non-terminals produce a value of the same type, possibly a union
    // type, you could arrange to return it instead of the error report.
    
    int yyparse(int start) {
      // I left out the code to cause the lexer to scan a given string.
      start_token = start;
      return real_yyparse();
    }
    
    int yylex() {
      int rv = start_token;
      if (start_token)
        start_token = 0;
      else
        rv = real_yylex();
      return rv;
    }
    

    【讨论】:

    • 非常感谢,这似乎是我要走的路。我已经假设解决方案将是这样的,但我找不到任何关于它的东西。再次感谢!
    • 如果您了解状态转换表的结构,您可以简单地确定将哪些状态压入堆栈顶部作为起始状态,例如,找到 DOT 将选择的状态项目 [STARTnonterm DOT nonterm] 如果您要制造此类项目。当然,您不需要这些项目,而且我可以在此处的空白处写下更多内容,但这个想法应该足够清楚。如果您有 GLR 解析器,您只需设置开始转换以包含 所有 个非终结符的开始状态,然后解析器将告诉哪个非终结符匹配(可能不止一个)
    【解决方案2】:

    鉴于您似乎愿意做的事情,您可能会从 this paper 中找到有趣的想法来回收利用。

    它提供了支持来自 C++ 代码的具体语法的方法,例如(这里,从 Bison 解析器本身去糖):

    exp: exp "&" exp
    {
      // Sub−ASTs are used as−is (they are not printed, then parsed,
      // as in the previous example). Spaces are no longer critical.
      $$ = parse(Tweast() <<
                 "if" << $1 << "then" << $3 << "<> 0 else 0");
    };
    

    【讨论】:

    • 这似乎是一个非常有趣的解决方案,但对我来说有点“矫枉过正”(转换 AST 只是我工作的一小部分)。不过,非常感谢您分享这个解决方案!
    【解决方案3】:

    假令牌并不能完全解决问题。当您想要解析某种语言的某些子规则时,您可能还需要重复调​​用该规则(多次调用yyparse)。

    这要求您使用假令牌“启动”解析器,使其在每次调用时返回有趣的规则保持实际非假令牌流的正确状态。另外,您需要一种方法来检测yyparse 调用遇到了EOF。

    此外,理想情况下,您希望能够将调用与 yyparse 和流上的其他操作混合在一起,这意味着可以精确控制 Flex + Yacc 组合执行的前瞻。

    我在 TXR 语言的解析器中解决了所有这些问题。在这种语言中,有一些有趣的子语言:Lisp 语法和正则表达式语法。

    问题是提供一个 Lisp 读取函数,该函数从流中提取单个对象并使流处于合理状态(关于前瞻)。例如,假设流包含以下内容:

     (a b c d) (e f g h)
    

    我们使用到达 Lisp 子语法所需的假令牌来初始化解析器。然后拨打yyparse。当yyparse 完成后,它将消耗所有内容:

     (a b c d) (e f g h)
                ^ stream pointer is here
               ^ the lookahead token is the parenthesis
    

    在此调用之后,如果有人调用函数从流中获取字符,那么不幸的是,他们将得到 e,而不是 ( 括号。

    不管怎样,所以我们调用了yyparse,得到了(a b c d) Lisp 对象,流指针在e,前瞻标记是(

    下次调用yyparse,它会忽略这个前瞻标记,我们会得到一个错误的解析。我们不仅必须使用导致解析器解析 Lisp 表达式的假伪标记来启动解析器,而且还必须让它在 ( 前瞻标记处开始解析。

    这样做的方法是将此令牌插入到启动流中。

    在 TXR 解析器中,我实现了一个令牌流对象,它最多可以接收四个推送令牌。当yylex 被调用时,token 会从这个 pushback 中被拉出来,只有当它为空时才会执行真正的词法分析。

    这在prime_parser函数中使用:

    void prime_parser(parser_t *p, val name, enum prime_parser prim)
    {
      struct yy_token sec_tok = { 0 };
    
      switch (prim) {
      case prime_lisp:
        sec_tok.yy_char = SECRET_ESCAPE_E;
        break;
      case prime_interactive:
        sec_tok.yy_char = SECRET_ESCAPE_I;
        break;
      case prime_regex:
        sec_tok.yy_char = SECRET_ESCAPE_R;
        break;
      }
    
      if (p->recent_tok.yy_char)
        pushback_token(p, &p->recent_tok);
      pushback_token(p, &sec_tok);
      prime_scanner(p->scanner, prim);
      set(mkloc(p->name, p->parser), name);
    }
    

    解析器的recent_tok 成员跟踪最近看到的标记,这使我们能够从最近的解析中访问前瞻标记。

    为了控制 yylex,我在 parser.l 中实现了这个 hack:

    /* Near top of file */
    
    #define YY_DECL \
      static int yylex_impl(YYSTYPE *yylval_param, yyscan_t yyscanner)
    
    /* Later */
    
    int yylex(YYSTYPE *yylval_param, yyscan_t yyscanner)
    {
      struct yyguts_t * yyg = convert(struct yyguts_t *, yyscanner);
      int yy_char;
    
      if (yyextra->tok_idx > 0) {
        struct yy_token *tok = &yyextra->tok_pushback[--yyextra->tok_idx];
        yyextra->recent_tok = *tok;
        *yylval_param = tok->yy_lval;
        return tok->yy_char;
      }
    
      yy_char = yyextra->recent_tok.yy_char = yylex_impl(yylval_param, yyscanner);
      yyextra->recent_tok.yy_lval = *yylval_param;
    
      return yy_char;
    

    如果令牌推回索引不为零,我们弹出罐头令牌并将其返回给 Yacc。否则我们调用yylex_impl,真正的词法分析器。

    请注意,当我们这样做时,我们还会查看词法分析器返回的内容并将其存储在 recent_tok.yy_charrecent_tok.yy_lval 中。

    (如果yy_lval 是堆分配的对象类型怎么办?幸好我们在这个项目中有垃圾收集!)

    在匹配这些子语言的规则中,我必须使用YYACCEPT。请注意byacc_fool 业务:这是让黑客与 Berkeley Yacc 合作所必需的。 (T.E. Dickey 的维护版本,它支持 Bison 可重入解析器方案。)

    spec : clauses_opt              { parser->syntax_tree = $1; }
         | SECRET_ESCAPE_R regexpr  { parser->syntax_tree = $2; end_of_regex(scnr);
         | SECRET_ESCAPE_E n_expr   { parser->syntax_tree = $2; YYACCEPT; }
           byacc_fool               { internal_error("notreached"); }
         | SECRET_ESCAPE_I i_expr   { parser->syntax_tree = $2; YYACCEPT; }
           byacc_fool               { internal_error("notreached"); }
         | SECRET_ESCAPE_E          { if (yychar == YYEOF) {
                                        parser->syntax_tree = nao;
                                        YYACCEPT;
                                      } else {
                                        yybadtok(yychar, nil);
                                        parser->syntax_tree = nil;
                                      } }
         | SECRET_ESCAPE_I          { if (yychar == YYEOF) {
                                        parser->syntax_tree = nao;
                                        YYACCEPT;
                                      } else {
                                        yybadtok(yychar, nil);
                                        parser->syntax_tree = nil;
                                      } }
         | error '\n'               { parser->syntax_tree = nil;
                                      if (parser->errors >= 8)
                                        YYABORT;
                                      yyerrok;
                                      yybadtok(yychar, nil); }
    
         ;
    
        }
    

    为什么是YYACCEPT?我不记得了;好在我们有详细的 ChangeLog 消息:

    * parser.y (spec): Use YYACCEPT in the SECRET_ESCAPE_E clause for
    pulling a single expression out of the token stream. YYACCEPT
    is a trick for not invoking the $accept : spec . $end production
    which is implicitly built into the grammar, and which causes
    a token of lookahead to occur.  This allows us to read a full
    expression without stealing any further token: but only if the
    grammar is structured right.
    

    我认为此评论因遗漏而略有误导。隐含的$end 产生式导致的问题不仅仅是不需要的前瞻:它正在前瞻,因为实际上想要匹配 EOF。我似乎记得YYACCEPT 是一种脱离解析器的方法,这样当下一个标记不是$end 标记时它不会引发语法错误,这是EOF 的内置表示.

    无论如何,Yacc 最终都会向前看;我们不希望它做的是引发语法错误,因为前瞻不是文件结尾,正如规则所期望的那样。当我们有

     (a b c d) (e f g h)
    

    我们有一个匹配表达式的简单语法规则,(e f g h) 看起来像杂散材料,这是语法错误!解析器获得第一个)令牌后,再次调用yylex,得到(,这是语法错误;它希望 yylex 在那时指示 EOF。 YYACCEPT 是解决此问题的方法。我们让 Yacc 调用 yylex 并拉出第二个 (,并记下它,以便我们可以在下一个 yyparse 调用中将其推回;但是我们阻止 Yacc 适应这个令牌。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-09
      • 1970-01-01
      相关资源
      最近更新 更多