乐者为王

Do one thing, and do it well.

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

当我们开发一套语法时,会有很多错误要去修复。在完成语法前,生成的语法分析器不会识别所有有效的句子。在这期间,提供有用信息的错误消息帮助我们追踪到语法问题。一旦我们有了一套正确的语法,然后我们就必须处理用户输入的不合语法的句子,或者甚至由其它程序发生故障生成的不合语法的句子。在这两种情况下,语法分析器对不合语法的输入的响应方式是一个需要重点考虑的问题。

在这里,我们将学习被ANTLR生成的语法分析器使用的自动错误报告和恢复策略。

错误展示

描述ANTLR的错误恢复策略的最好方式是观察由它生成的语法分析器对错误输入的响应。让我们看一个类Java语言的语法,它包含带有字段和方法成员的类定义。该方法有简单的语句和表达式。嵌入动作在语法分析器找到元素时就打印它们。

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
26
27
28
29
grammar Simple;

prog: classDef+ ; // match one or more class definitions

classDef
    : 'class' ID '{' member+ '}' // a class has one or more members
      {System.out.println("class "+$ID.text);}
    ;

member
    : 'int' ID ';'    // field definition
      {System.out.println("var "+$ID.text);}
    | 'int' f=ID '(' ID ')' '{' stat '}'    // method definition
      {System.out.println("method: "+$f.text);}
    ;

stat: expr ';'
      {System.out.println("found expr: "+$stat.text);}
    | ID '=' expr ';'
      {System.out.println("found assign: "+$stat.text);}
    ;

expr: INT
    | ID '(' INT ')'
    ;

INT : [0-9]+ ;
ID  : [a-zA-Z]+ ;
WS  : [ \t\r\n]+ -> skip ;

现在,先让我们使用一些有效的输入运行语法分析器,借以观测正常的输出。

1
2
3
antlr Simple.g
compile *.java
grun Simple prog

输入以下数据:

1
class T { int i; }

你就会看到:

1
2
var i
class T

语法分析器没有显示任何错误,它执行打印语句,报告关于变量i和类定义T的正确识别。

接下来,让我们尝试一个带有方法定义的类,该方法含有一个虚假赋值表达式。

1
grun Simple prog -gui

输入测试数据:

1
2
3
class T {
  int f(x) { a = 3 4 5; }
}

然后你就会看到:

1
2
3
line 2:19 mismatched input '4' expecting ';'
method: f
class T

在4记号处,语法分析器没有找到它期待的“;”,所以它报告一个错误。line 2:19指出有问题的标记是在第2行第19列的字符位置(字符位置从0开始)。因为使用了-gui参数,我们可以看到带有高亮错误节点的语法分析树。

simple-parse-tree

在这里,有两个额外的记号,并且语法分析器给出一个不匹配的通用错误消息。如果只有单个的额外记号,语法分析器可能会智能一点,指出它是一个额外的记号。在接下来的运行测试中,有个额外的“;”在类名和类体之间:

1
grun Simple prog

输入如下:

1
class T ; { int i; }

输出结果:

1
2
3
line 1:8 extraneous input ';' expecting '{'
var i
class T

在“;”处语法分析器报告一个错误,但给出了一个稍微翔实的答案,因为它知道下一个记号就是它实际上在寻找的那个。这个特性被称为单个记号删除(single-token deletion),因为语法分析器可以简单地装作额外的记号不存在并继续执行。

同样的,语法分析器可以在侦测到缺少一个记号时做单个记号插入(single-token insertion)。让我们删掉结束的“}”看看会发生什么。

1
grun Simple prog

然后输入:

1
2
class T {
  int f(x) { a = 3; }

结果是:

1
2
3
4
found assign: a=3;
method: f
line 3:0 missing '}' at '<EOF>'
class T

语法分析器报告它不能找到必须的结束记号“}”。

当语法分析器处于决策点时,会出现另一个常见的语法错误,并且剩余的输入与规则或子规则的任何选项都不一致。例如,如果我们忘记字段声明中的变量名,规则member中的选项都不匹配。语法分析器报告没有可行的选项。

1
grun Simple prog

输入以下代码:

1
class T { int ; }

然后结果是:

1
2
line 1:14 no viable alternative at input 'int;'
class T

在“int”和“;”之间没有空格,因为我们在WS()规则中告诉词法分析器skip()。

如果有词法错误,ANTLR也会放出错误消息,指出哪些字符不能匹配为记号。例如,如果我们提交一个完全未知的字符,我们将得到一个记号识别错误。

1
grun Simple prog

输入:

1
class # { int i; }

输出:

1
2
3
4
line 1:6 token recognition error at: '#'
line 1:8 missing ID at '{'
var i
class <missing ID>

因为没有给出有效的类名,单个记号插入机制召唤了“missing ID”名字,以致类名记号是非空值。如果想控制语法分析器如何召唤记号,可以覆盖DefaultErrorStrategy中的getMissingSymbol()。

Comments