乐者为王

Do one thing, and do it well.

ANTLR 4权威参考读书笔记(33)

ANTLR的语法分析器可以生成错误信息,也可以从大量不同情况的错误中恢复。我们还可以自定义错误信息以及将它们重定向到不同的错误监听器。所有这些功能都被封装在指定ANTLR错误处理策略的对象中。接下来我们就将详细研究该策略,以学习更多关于自定义语法分析器如何响应错误的知识。

更改ANTLR的错误处理策略

默认的错误处理机制工作得很好,但在有些非典型的情况下我们可能要更改它。首先,我们可能由于运行时开销而想要禁用一些内联错误处理。其次,我们可能希望在出现第一个语法错误时就退出语法分析器。例如,当为类似bash这样的shell解析命令行时,没必要试图从错误中恢复。无论如何我们不能冒险执行该命令,所以语法分析器可以在一遇到麻烦时就推出。

要了解错误处理策略,请查看接口ANTLRErrorStrategy及其具体的实现类DefaultErrorStrategy。这个类持有与默认错误处理行为相关联的一切。ANTLR语法分析器指示该对象报告错误并恢复。例如,下面是在每个ANTLR生成的规则函数里面的捕获块:

1
2
_errHandler.reportError(this, re);
_errHandler.recover(this, re);

_errHandler是一个持有DefaultErrorStrategy实例的引用的变量。方法reportError()和recover()表示错误报告和同步和返回功能。reportError()根据抛出的异常类型将错误报告委托给3个方法的其中之一。

回到第一种非典型的情况,让我们来降低语法分析器上的错误处理在运行时的负担。看看下面这段ANTLR为语法Simple中member+子规则生成的代码:

1
2
3
4
5
6
7
8
_errHandler.sync(this);
_la = _input.LA(1);
do {
    setState(22); member();
    setState(26);
    _errHandler.sync(this);
    _la = _input.LA(1);
} while ( _la==6 );

对于可以安全地假设输入语法是正确的应用程序,比如网络协议,我们最好避免检测并从错误中恢复的开销。我们可以通过继承DefaultErrorStrategy并用空方法覆写sync()来做到这点。Java编译器可能会内联并消除_errHandler.sync(this)调用。我们将在下个例子中阐述如何通知语法分析器使用不同的错误策略。

另一种非典型的情况是在出现第一个语法错误时就退出语法分析器。为使它工作,我们必须覆写3个关键的恢复方法,如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.antlr.v4.runtime.*;

public class BailErrorStrategy extends DefaultErrorStrategy {
    /** Instead of recovering from exception e, rethrow it wrapped
     *  in a generic RuntimeException so it is not caught by the
     *  rule function catches. Exception e is the "cause" of the
     *  RuntimeException.
     */
    @Override
    public void recover(Parser recognizer, RecognitionException e) {
        throw new RuntimeException(e);
    }

    /** Make sure we don't attempt to recover inline; if the parser
     *  successfully recovers, it won't throw an exception.
     */
    @Override
    public Token recoverInline(Parser recognizer) throws RecognitionException {
        throw new RuntimeException(new InputMismatchException(recognizer));
    }

    /** Make sure we don't attempt to recover from problems in subrules. */
    @Override
    public void sync(Parser recognizer) { }
}

对于测试平台,我们可以重复使用我们典型的样板代码。除了创建并启动语法分析器,我们需要创建一个新的BailErrorStrategy实例,并告诉语法分析器使用它替换默认的策略。

1
parser.setErrorHandler(new BailErrorStrategy());

当我们处理这个问题的时候,我们也应该在出现第一个词汇错误时退出。要做到这点,我们必须重写Lexer中的recover()方法。

1
2
3
4
5
6
7
public static class BailSimpleLexer extends SimpleLexer {
    public BailSimpleLexer(CharStream input) { super(input); }

    public void recover(LexerNoViableAltException e) {
        throw new RuntimeException(e);    // Bail out
    }
}

让我们先尝试一个词法错误,通过在输入开头插入一个#字符,词法分析器会抛出异常并从控制流中返回到主程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ antlr Simple.g
$ compile Simple TestBail
$ run TestBail
# class T { int i; }
EOF
line 1:1 token recognition error at: '#'
Exception in thread "main"
java.lang.RuntimeException: LexerNoViableAltException('#')
at TestBail$BailSimpleLexer.recover(TestBail.java:9)
at org.antlr.v4.runtime.Lexer.nextToken(Lexer.java:165)
at org.antlr.v4.runtime.BufferedTokenStream.fetch(BufferedT...Stream.java:139)
at org.antlr.v4.runtime.BufferedTokenStream.sync(BufferedT...Stream.java:133)
at org.antlr.v4.runtime.CommonTokenStream.setup(CommonTokenStream.java:129)
at org.antlr.v4.runtime.CommonTokenStream.LT(CommonTokenStream.java:111)
at org.antlr.v4.runtime.Parser.enterRule(Parser.java:424)
at SimpleParser.prog(SimpleParser.java:68)
at TestBail.main(TestBail.java:23)
...

语法分析器也会从第一个语法错误中退出(在这里是缺少类名)。

1
2
3
4
5
6
$ run TestBail
class { }
EOF
Exception in thread "main" java.lang.RuntimeException:
org.antlr.v4.runtime.InputMismatchException
...

我们将通过下面更改语法分析器报告错误的方式来演示ANTLRErrorStrategy接口的灵活性。要改变标准的消息“noviable alternative at input X,”,我们可以覆盖reportNoViableAlternative()并将消息更改为其它不同的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.antlr.v4.runtime.*;

public class MyErrorStrategy extends DefaultErrorStrategy {
    @Override
    public void reportNoViableAlternative(Parser parser, NoViableAltException e)
        throws RecognitionException {
        // ANTLR generates Parser subclasses from grammars and
        // Parser extends Recognizer. Parameter parser is a
        // pointer to the parser that detected the error
        String msg = "can't choose between alternatives";    // nonstandard msg
        parser.notifyErrorListeners(e.getOffendingToken(), msg, e);
    }
}

到这里,我们已经涵盖了ANTLR内所有重要的错误报告和恢复设施。因为ANTLRErrorListener和ANTLRErrorStategy接口,我们在错误消息发生的地方具有很大的灵活性:这些消息是什么,以及语法分析器如何从错误中恢复。

Comments