【问题标题】:Bison: distinguish variables by type / YYERROR in GLR-parsersBison:在 GLR 解析器中按类型/YYERROR 区分变量
【发布时间】:2018-06-13 09:47:02
【问题描述】:

我正在使用 GNU bison 开发解析器,但我遇到了一个有趣的问题。我的实际问题有点不同,一般来说不太有趣,所以我会有点不同的说法,这样答案通常会更有用。

我需要根据表达式的类型来区分表达式,例如算术表达式和字符串表达式。它们的顶级非终结符有一些共同的祖先,比如

statement
    : TOK_PRINT expression TOK_SEMICOLON {...}
;
expression
    : arithmeticexpression {...}
    | stringexpression {...}
;

现在我需要能够在两种表达式中都有变量

arithmeticexpression
    : TOK_IDENTIFIER {...}
;
stringexpression
    : TOK_IDENTIFIER {...}
;

(在 stringexpression 的情况下只允许使用 string 类型的变量,在算术表达式的情况下只允许使用 int 或 float 类型的变量) 但这显然会导致 R/R 冲突,这是语言所固有的——无法解决,因为语言是模棱两可的。

当然我可以淡化语言,这样只有一般的“表达式”对象在解析堆栈上传递,但是我必须在动作中做很多手动类型检查,我想避免。 此外,在我的实际用例中,通过语法到变量规则的路径是如此不同,以至于我不得不大量降低语言,以至于我会丢失很多语法规则(即丢失很多结构信息)并且需要将解析引擎手写一些动作中。

我读过有关 GLR 解析的文章,听起来它可以解决我的问题。我正在考虑使用此功能:在对应变量类型错误的路径中使用上述语法和YYERROR

arithmeticexpression
    : TOK_IDENTIFIER {
          if(!dynamic_cast<IntVariable*>(
              symbol_table[*$<stringvalue>1]))
              YYERROR;
      }
;
stringexpression
    : TOK_IDENTIFIER {
          if(!dynamic_cast<StringVariable*>(
              symbol_table[*$<stringvalue>1]))
              YYERROR;
      }
;

但是Bison Manual

  1. 在确定性 GLR 操作期间,YYERROR 的效果是相同的 作为它在确定性解析器中的效果。
  2. 延迟操作中的效果是相似的,但精确点 错误未定义;相反,解析器恢复为确定性 操作,选择一个未指定的堆栈继续 语法错误。
  3. 在语义谓词(参见语义谓词)中 非确定性解析,YYERROR 默默地修剪调用的解析 测试。

我不确定我是否理解正确 - 我是这样理解的:

  1. 不适用于此处,因为 GLR 解析不是确定性的
  2. 是我在上面的代码中的方式,但不应该这样做,因为 YYERROR 杀死的路径是完全随机的(如果我错了,请纠正我)
  3. 不会解决我的问题,因为semantic predicates(不是语义动作!)必须在规则的开头(如果我错了,请纠正我),此时yylval无法访问 TOK_IDENTIFIER 令牌(如果我错了,请纠正我),所以我无法查看符号表来查找变量的类型。

有没有人遇到过这种情况?我对手册的理解有误吗?你会如何处理这个问题?对我来说,这个问题似乎很自然,我会假设人们经常遇到它,以至于野牛会有一个内置的解决方案......

【问题讨论】:

    标签: parsing bison glr


    【解决方案1】:

    注意:至少从 bison 3.0.1 和 bison 3.0.5 开始,在处理语义谓词操作时存在一个错误,导致 bison 在行的中间,导致编译失败。 this answer 中描述了一个简单的修复程序,并已提交到 bison 存储库的 maint branch。 (从存储库构建 bison 不适合胆小的人。但如果您不想自己编辑文件,从该存储库下载 data/c.m4 就足够了。)


    让我们从 GLR 解析器中关于 YYERROR 的具体问题开始。

    实际上,您可以在语义谓词中使用YYERROR,以便通过拒绝它所属的产生式来消除解析的歧义。您不能在语义操作中执行此操作,因为在解析器决定唯一解析之前不会执行语义操作,此时不再存在歧义。

    语义动作可以出现在规则中的任何地方,并且在它们被归约时会立即执行,即使归约是模棱两可的。因为它们是乱序执行的,所以它们不能引用任何前面的语义动作产生的值(并且语义谓词本身没有语义值,即使它们占用了一个栈槽)。此外,由于它们作为可能不是最终解析的一部分的产生式的一部分被有效地推测性地执行,因此它们不应改变解析器状态。

    语义谓词确实可以通过变量yychar 访问前瞻标记(如果有的话)。如果yychar既不是YYEMPTY也不是YYEOF,则关联的语义值和位置信息分别在yylvalyylloc中。 (见Lookahead Tokens)。但是,在使用此功能之前必须小心;如果在语法中没有歧义,bison 解析器很可能会执行语义谓词而不读取前瞻标记。

    所以你的理解很接近,但并不完全正确:

    1. 为了提高效率,GLR 解析器会在解析中没有歧义时进行识别,并在这些点使用普通的直接移位归约算法。在大多数语法中,歧义很少见并且可以快速解决,因此这种优化意味着在大多数解析过程中可以避免与维护多个备选方案相关的开销。在这种模式下(当然,如果可能存在歧义,则不适用),YYERROR 会导致标准错误恢复,就像在 shift-reduce 解析器中一样,在减少动作时。

      李>
    2. 由于在解决歧义之前不会运行延迟操作,因此延迟操作中的YYERROR 将有效地执行,就好像它处于确定状态一样,如上一段中所述。即使有自定义合并过程也是如此;如果自定义合并正在进行中,所有参与自定义合并的备选方案将被丢弃。之后,将继续正常的错误恢复算法。我不认为错误恢复点是“随机的”,但是预测它会发生在哪里并不容易。

    3. 如上所述,语义谓词可以出现在规则中的任何位置,并且可以访问前瞻标记(如果有)。 (这里Bison手册有点误导:语义谓词不能包含YYERROR的调用,因为YYERROR是语句,语义谓词块的内容是表达式。如果语义谓词的值为false ,然后解析器执行YYERROR 操作。)


    实际上,这意味着您可以使用语义谓词代替(或以及)动作,但基本上按照您建议的方式:

    arithmeticexpression
        : TOK_IDENTIFIER %?{
              dynamic_cast<IntVariable*>(symbol_table[*$1])
          }
    stringexpression
        : TOK_IDENTIFIER %?{
              dynamic_cast<StringVariable*>(symbol_table[*$1])
          }
    

    注意:我从$&lt;stringvalue&gt;1 中删除了类型声明,因为:

    1. 基本上不可读,至少对我而言,

    2. 更重要的是,与显式转换一样,它不是类型安全的。

    您应该声明令牌的语义类型:

    %type <stringvalue> ID
    

    这将允许野牛进行类型检查。


    为此目的使用语义谓词是否是一个好主意是另一个问题。


    【讨论】:

    • 非常感谢您的出色回答,非常有帮助!我已经测试了您提出的方式并且它失败了,但是出于一个相当愚蠢的原因 - 我猜我的野牛版本(3.0.4)有一个错误 - 它生成这样的代码: if (! (#line 51 "mfcalc. y" /* glr.c:816 */ // 谓词 )) YYERROR;并且 g++ 不喜欢 if( 后面的 #line。当我手动编辑 .cpp 文件并将 #line 放在下一行时,它可以工作。
    • 在读入合并程序后,我认为允许这两种类型也是有意义的,在所有无效情况下将 nullptr 放在堆栈上,然后有一个选择(单个)的合并程序堆栈元素是!= nullptr。我仍然需要淡化它以使用表达式,但是通过合并,我至少可以避免在操作中使用手写解析器...
    • 另一方面,我不确定合并是否会发生得太早,以一种方式解决问题,留下无效的休息。所以我认为这样做,我们现在在这里,可能是最好的......我只将此步骤添加到我的makefile中: sed -i.bak s/" if (! (#line"/" if (! (\n#line"/g /path/to/parser.cpp
    【解决方案2】:

    此处的示例有效,但是当我将其与Bison: GLR-parsing of valid expression fails without error message 的解决方案放在一起时,我遇到了以下问题:

    变量可以由多个标识符确定。您可以有一个数组索引,也可以是不同对象的成员。我通过使用另一个非终端来模拟这种情况

    lvalue
        : TOK_IDENTIFIER
        | lvalue '[' arithmeticexpression ']'
        | lvalue '.' TOK_IDENTIFIER
    

    但是当我现在拥有

    arithmeticexpression : lvalue
    stringexpression : lvalue
    

    并且(尝试)像上面那样从非终端“左值”访问对象,我得到一个分段错误。所以看来这里的方法不适用于更复杂的情况。


    我现在做的是访问语义 ACTION 中的对象,如果变量的类型错误,则将 $$ 设置为 nullptr

    arithmeticexpression
        : lvalue
        {
            $$ = nullptr;
            if(dynamic_cast<IntVariable*>($lvalue->getVariable()))
                $$ = new ArithmeticExpressionIntVariable($lvalue);
        }
    

    然后我不得不像这样传播nullptr(这是相当多的附加代码)

    arithmeticexpression
        ...
        | arithmeticexpression[left] '+' arithmeticexpressionM[right]
        {
            $$ = nullptr;
            if($left && $right)
                $$ = new ArithmeticExpressionPlus($left, $right);
        }
    

    所以现在,在

    expression
        : arithmeticexpression
        | stringexpression
    (note: I have "Expression* expr;" in the "%union{" declaration
    and "%type <expression> expr" in the prologue)
    

    我有一个歧义:相同的输入文本可以用两种不同的方式解析,但其中只有一种的值为!= nullptr。此时,我需要一个自定义合并过程,它基本上只是选择非空值。

    为此,我像这样在我的野牛文件的序言中提前声明了它

    static Expression* exprMerge (yy::semantic_type x0, yy::semantic_type x1);
    

    并在结语中这样定义

    static Expression* exprMerge (YYSTYPE x0, YYSTYPE x1) \
    {   
        /* otherwise we'd have a legitimate ambiguity */
        assert(!x0.expr || !x1.expr);
        return x0.expr ? x0.expr : x1.expr;
    }
    

    最后,我不得不告诉野牛使用这个程序来解决歧义,使用

    expression
        : arithmeticexpression  %merge <exprMerge>
        | stringexpression %merge <exprMerge>
    

    老实说,我认为这是相当多的努力,如果野牛在尝试合并路径而不是排除路径时测试语义谓词(至少是规则后面的那些),这将是不必要的在他们合并之前。

    但至少这是可行的,而且比手动收集令牌并对其进行排序要省力得多,后者本来可以替代。

    【讨论】:

    • 注意:从词法分析器中,我通过堆上的副本传递 TOK_IDENTIFIER。到目前为止,在将其内容复制到 lvalue : TOK_IDENTIFIER 操作中的 LValueIdentifier 对象后,我已将其删除。这导致了另一个分段错误,因为该操作必须在多个堆栈上执行 - 我不得不将字符串留在堆上(产生内存泄漏)
    • 将 nullptr 传播到合并过程似乎是比语义谓词更好的解决方案。它当然不需要太多代码。一点模板元编程就可以捕捉到这种模式。
    • 对于字符串处理,我的 goto 解决方案是在词法分析器的备忘录表中实习字符串,然后只传递指针/引用而不复制。 (当然,这些字符串必须是不可变的。对我来说,这很好。)整个 memo 表需要一直存在,直到解析完成,但它真的不是很大;我从不复制字符串这一事实比偶尔不必要地保留的字符串节省了更多的内存。至少,在我的应用程序中它运行良好。它还使字符串比较和散列更快。
    • 我用预处理器宏做的,但是模板可能是一个更干净的解决方案,是的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多