【问题标题】:Bison error recovery for automatic semicolon insertion用于自动分号插入的 Bison 错误恢复
【发布时间】:2018-09-26 07:57:13
【问题描述】:

我正在尝试编写一个 Bison C++ 解析器来解析 JavaScript 文件,但我不知道如何使分号成为可选。

对于 ECMAScript 2018 规范(https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf,第 11.9 章),分号实际上不是可选的,而是在解析过程中自动插入。在规范中指出:

当从左到右解析源文本时,遇到一个标记(称为违规标记),它是 语法的任何产生都不允许,然后在违规之前自动插入分号 如果以下一个或多个条件为真,则令牌:

  • 有问题的令牌与前一个令牌之间至少有一个 LineTerminator[...]

据此,我正在尝试以这种幼稚的方式解决这个问题:

  • 检测错误,使用error 特殊标记;
  • 告诉词法分析器在操作过程中发生了语法错误;如果在当前标记之前遇到换行符,则词法分析器将在下一次 yylex 调用时返回一个新的分号标记;在随后的调用中,它将返回之前发生语法错误时出现问题的令牌。

我的解析器的一个非常简化的结构如下:

program:
   stmt_list END
;

stmt_list:
    %empty
 |  stmt_list stmt
 |  stmt_list error  { /* error detected; tell the lexer about the syntax error */ }
;

stmt:
    value SEMICOLON
|   [other types of statements...]
;

value:
    NUMBER
|   STRING
;

但是这样做,如果文件包含一个没有终止分号但有换行符的有效 JavaScript 语句,当遇到违规标记时,解析器会将语句的其余部分缩减为一个 error 特殊标记。当我告诉词法分析器有关语法错误时,解析器已经将error 标记减少为stmt_list 一个,并且先前的有效指令丢失,使得分号插入无用。

显然我不想让我的解析器丢弃有效语句并转到下一个。

我怎样才能做到这一点?这是正确的方法还是我错过了什么?

【问题讨论】:

    标签: javascript c++ bison error-recovery


    【解决方案1】:

    我不认为这种方法可行。

    请注意,您必须在进行任何减少之前检测到错误。因此,对于语句末尾的分号插入,您需要将错误产生添加到stmt,而不是stmt_list。所以你最终会得到这样的结果:

    stmt_list
         :  %empty
         |  stmt_list stmt
    
    stmt: value ';'   { handle_value_stmt(); }
        | value error { handle_value_stmt(); }
        | [other types of statements...]
    

    不插入分号;它只是假装分号被插入。 (如果无法插入分号,则会触发另一个错误。)

    但是由于不涉及词法分析器,所以不管丢失的分号是否在行尾都会发生,这太热情了。所以理想的解决方案是以某种方式告诉词法分析器生成一个分号标记作为下一个标记。但是在检测到错误的那一刻,词法分析器已经产生了先行标记,并且解析器知道先行标记是什么。它将使用其记录的前瞻令牌继续解析。

    此时还有一个问题是如何与词法分析器进行通信,因为中间规则动作并不能很好地与错误恢复算法配合使用。理论上,您可以使用yyerror 将被调用来报告错误的事实,但这意味着yyerror 需要能够推断这不是一个“真正的”错误,这意味着它必须去戳入yyparse 的胆量。 (我确信这是可能的,但我不知道该怎么做,而且在我看来并不值得推荐。)

    现在,理论上可以告诉解析器丢弃前瞻标记,并告诉词法分析器生成一个分号,然后重复它刚刚发送的标记。因此,如果您足够固执,几乎不可能通过将 hack 叠加到 hack 上来完成这项工作。但是你最终会得到一些非常难以维护、验证和测试的东西。 (确保它适用于所有极端情况也是一个挑战。)

    这还没有考虑其他可以插入分号的情况。

    我的 ASI 方法是通过找出可能的连续标记对来简单地分析语法。 (这很容易做到;您只需要构造 FIRST 和 LAST 集,然后通读查看连续符号的所有产生式。)然后,如果输入由标记 A 后跟一个或多个换行符后跟标记 B 组成,那么它在语法中,A 后面不可能跟 B,那么这是分号插入的候选者。分号插入可能会失败,但这会产生语法错误,因此您不会得到误报。 (您可能必须修复语法错误消息,但此时您至少知道您已插入分号。)

    证明该算法有效更棘手,因为理论上A 可以在某些情况下跟随B 但在当前情况下这是不可能的,而A ; B 是可能的在当前情况下。在这种情况下,您可能会错过可能的分号插入。我没有详细查看最近的 JS 版本,但很久以前,当我写一个 JS 词法分析器时,我成功地证明了不存在这样的情况。


    注意:由于问题是在评论中提出的,我会稍微挥手,虽然我真的不建议采用这种方法。

    如果不深入了解野牛的胆量,就真的不可能“解除”一个令牌,包括error 令牌(或多或少是一个真正的令牌)。当error 标记被移动时,解析实际上已提交到错误生成。所以如果你想取消错误,你必须接受这个事实并解决它。

    error 标记被移动后,解析器将跳过标记,直到遇到可移动标记。因此,如果您设法将自动分号插入到令牌流中,则可以使用该令牌作为保护:

        stmt: value ';'       { handle_value_stmt(); }
            | value error ';' { handle_value_stmt(); }
    

    但是,您可能无法插入自动分号,在这种情况下,您确实需要报告语法错误(并且可能尝试重新同步)。上面的规则只会默默地将标记放到下一个分号,这肯定是错误的。所以第一个近似值是让你的 ASI 插入器总是插入一些东西,这可以用作错误产生中的保护:

        stmt: value ';'       { handle_value_stmt(); }
            | value error ';' { handle_value_stmt(); }
            | value error NO_ASI { handle_real_error(); }
    

    这对于“错误中止”处理来说已经足够了,但是如果你想进行错误恢复,你需要做更多的hackery。

    正如我所说,我真的不建议走这条路。最终结果不会很漂亮,即使它有效(如果您没有考虑到,您仍然可能会发现您认为有效的代码在实际用户输入中失败。)

    【讨论】:

    • 实际上,如果两个标记之间存在换行符,我能够指示词法分析器返回文件以返回前一个标记和一个伪造的分号,但问题在于取消移动 @ 987654335@令牌恢复原始语句。但是,您的方法很有趣。您如何从产品开始生成可能的令牌对?我是解析器世界的新手,所以如果我的一些问题看起来很愚蠢,我很抱歉。
    • @Marco:这不是一个愚蠢的问题,但它有点超出范围。任何解析教科书中都描述了用于计算可空性和 FIRST 集的标准算法,包括在线参考资料(以及一些 SO 问题)。有一个漂亮的 O(n) 算法,但如果你找不到它,最小不动点算法肯定会足够快(这是最坏情况的 O(N²) 并且 N 在这里并不大。)最后一个计算类似;只是朝相反的方向工作。一旦你有了这三样东西,找到可能的配对只是对作品的简单扫描......
    • ...这是Javascript,您需要手动调整。首先,如果 ASI 会导致两个连续的分号,则它是不合法的,因此您必须从列表中删除所有此类对。然后,有诸如换行限制之类的东西,这意味着在行尾的return 之后,ASI总是是可能的(除非下一行的第一个标记是分号);同样,ASI 总是可以在行首的++-- 之前出现。还有一些其他的。
    • 哦,请记住,ASI 并不总是位于行尾。它也出现在} 之前。但是,通过使一些分号可选,在语法中可以很容易地处理这种情况。
    • 您排除不能出现的对。所以你需要找出可能的连续标记对。通过查看每个产生式的右侧并考虑可能的连续符号,您可以找到可能的对。 (如果两个符号真的是连续的,或者它们仅由可以为空的非终结符分隔,则它们可能是连续的。)两个找出对应于符号对的标记对,您需要第一个符号的 LAST 集和 FIRST 集第二个符号。
    猜你喜欢
    • 2013-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多