ANTLR4 语义谓词与错误恢复混淆。为什么?

ANTLR4 semantic predicates mess with error recovery. Why?

我在使用语义谓词时在错误恢复中遇到了一些奇怪的行为。

我需要错误恢复(特别是单个标记插入),因为我要解析的文本有很多 "single missing token" 错误。

我还需要语义谓词,因为对于 ANTLR4: Matching all input alternatives exaclty once(第二种选择)之类的东西。

但两者似乎不能很好地融合(我之前看到过这个并向 SO 寻求帮助:ANTLR4 DefaultErrorStrategy fails to inject missing token;然后我找到了答案;现在我没有)。

让语法(如此简单,它匹配任意数量的"A",以空格分隔,以分号结束):

grammar AAAGrammar;

WS : ' '+ -> channel(HIDDEN);
A : 'A';
SEMICOLON : ';';


aaaaa : 
    a* ';'
  ;

a :
    A
  ;

运行 对以下输入,生成的解析树是:

这正是我所期望的,也是我想要的(正确注入了第二个输入中缺少的分号)。

现在对语法进行简单更改,在 "a" 规则中引入语义谓词(这个无害,但我知道 ANTLR4 不会 - 也不应该 - 评估它),使其:

a :
    {true}? A
  ;

运行 再次使用相同的输入: - "A A A;": (aaaaa (a A) (a A) (a A) ;); - "A A A" : (aaaaa (a A) (a A) (a A))(这个也会在 stderr 上发出警告:第 1:5 行在输入 '' 处没有可行的替代方案)。

所以语义谓词完全搞砸了丢失的令牌注入。

这是预期的吗?

为什么?

是否有任何 ANTLR4 语法技巧可以在不删除 sempred 的情况下恢复错误?

编辑:(回复@CoronA 评论)

这里是生成的解析器之间的 diff -u(没有和有语义谓词):

--- withoutsempred.java 2015-05-04 09:39:22.644069398 -0300
+++ withsempred.java    2015-05-04 09:39:13.400046354 -0300
@@ -56,22 +56,24 @@
    public final AaaaaContext aaaaa() throws RecognitionException {
        AaaaaContext _localctx = new AaaaaContext(_ctx, getState());
        enterRule(_localctx, 0, RULE_aaaaa);
-       int _la;
        try {
+           int _alt;
            enterOuterAlt(_localctx, 1);
            {
            setState(7);
            _errHandler.sync(this);
-           _la = _input.LA(1);
-           while (_la==A) {
-               {
-               {
-               setState(4); a();
-               }
+           _alt = getInterpreter().adaptivePredict(_input,0,_ctx);
+           while ( _alt!=2 && _alt!=-1 ) {
+               if ( _alt==1 ) {
+                   {
+                   {
+                   setState(4); a();
+                   }
+                   } 
                }
                setState(9);
                _errHandler.sync(this);
-               _la = _input.LA(1);
+               _alt = getInterpreter().adaptivePredict(_input,0,_ctx);
            }
            setState(10); match(SEMICOLON);
            }
@@ -101,7 +103,9 @@
        try {
            enterOuterAlt(_localctx, 1);
            {
-           setState(12); match(A);
+           setState(12);
+           if (!( true )) throw new FailedPredicateException(this, " true ");
+           setState(13); match(A);
            }
        }
        catch (RecognitionException re) {
@@ -115,12 +119,25 @@
        return _localctx;
    }

+   public boolean sempred(RuleContext _localctx, int ruleIndex, int predIndex) {
+       switch (ruleIndex) {
+       case 1: return a_sempred((AContext)_localctx, predIndex);
+       }
+       return true;
+   }
+   private boolean a_sempred(AContext _localctx, int predIndex) {
+       switch (predIndex) {
+       case 0: return  true ;
+       }
+       return true;
+   }
+
    public static final String _serializedATN =
-       "\uacf5\uee8c\u4f5d\u8b0d\u4a45\u78bd\u1b2f\u3378\t"+
-       "\t\b\n\f"+
-       "\t\b\b\t"+
-       "\t\n\n\f\t\f\r\r"+
-       "\t";
+       "\uacf5\uee8c\u4f5d\u8b0d\u4a45\u78bd\u1b2f\u3378\t"+
+       "\t\b\n\f"+
+       "\t\b\b\t"+
+       "\t\n\n\f\t\f\r\r"+
+       "\t";
    public static final ATN _ATN =
        ATNSimulator.deserialize(_serializedATN.toCharArray());
    static {

我已经调试了这两个代码。

假设输入"A A A"(没有分号),没有语义谓词的版本是

            while (_la==A) {
                {
                {
                setState(4); a();
                }
                }
                setState(9);
                _errHandler.sync(this);
                _la = _input.LA(1);
            }

此块3次,然后进行

            setState(10); match(SEMICOLON);

match(SEMICOLON) 注入了一个丢失的令牌。

现在请注意,具有语义谓词的版本摆脱了 _la = _input.LA(1)(先行)并切换到具有 _alt = getInterpreter().adaptivePredict(_input,0,_ctx) 的更高级预测。

使用完全相同的输入,带有语义谓词的版本变为:

            _alt = getInterpreter().adaptivePredict(_input,0,_ctx);
            while ( _alt!=2 && _alt!=-1 ) {
                if ( _alt==1 ) {
                    {
                    {
                    setState(4); a();
                    }
                    } 
                }
                setState(9);
                _errHandler.sync(this);
                _alt = getInterpreter().adaptivePredict(_input,0,_ctx);
            }

这个block 3次,但是没有异常离开block。最后一个 _alt = getInterpreter().adaptivePredict(_input,0,_ctx) 抛出 org.antlr.v4.runtime.NoViableAltException,完全跳过 match(SEMICOLON)

了解DefaultErrorStrategy采用一种天真的方法来识别解析异常的规则和来源。

特别是,在错误恢复例程的范围内评估谓词非常困难,因此它没有作为 DefaultErrorStrategy 处理的一部分完成。

考虑你的测试语法的这个变体:

aaaaa   : a* SEMI EOF           ;
a       : ( { true }? B )? A    ;
A   : 'A';
B   : 'B';
SEMI: ';';
WS  : ' '+ -> channel(HIDDEN) ;

在输入 AAA 上,打印的错误信息是

line 1:5 no viable alternative at input '<EOF>'
([] ([4] A) ([4] A) ([4] A))

即使谓词 B 是可选的,也没有简单的方法可以确保谓词不相关。并且没有简单的方法来重新运行谓词以在错误恢复操作的上下文中评估其输出。此处唯一有效的运行时结论是无法将错误识别为仅存在于一个规则(或子规则)中。

当然,您可以扩展 DefaultErrorStrategy 来解决特定于您的语法的问题,或者比默认策略可以处理的问题更复杂的问题。

结合扩展 DefaultErrorStrategy,考虑扩展 RecognitionException 以更好地了解原始异常发生的确切位置和方式 - 请注意方法 getExpectedTokens()。

如您所见,在解析过程中处理所有可能的错误形式可能会变得很复杂。一般而言,解析器内的自动更正适用于错误离散、定义明确且易于识别的情况。否则,将它们视为要在分析阶段纠正的语义错误。