6.8 处理语法错误
最后更新于:2022-04-01 01:10:34
如果你创建的分析器用于产品,处理语法错误是很重要的。一般而言,你不希望分析器在遇到错误的时候就抛出异常并终止,相反,你需要它报告错误,尽可能的恢复并继续分析,一次性的将输入中所有的错误报告给用户。这是一些已知语言编译器的标准行为,例如C,C++,Java。在PLY中,在语法分析过程中出现错误,错误会被立即检测到(分析器不会继续读取源文件中错误点后面的标记)。然而,这时,分析器会进入恢复模式,这个模式能够用来尝试继续向下分析。LR分析器的错误恢复是个理论与技巧兼备的问题,yacc.py提供的错误机制与Unix下的yacc类似,所以你可以从诸如O’Reilly出版的《Lex and yacc》的书中找到更多的细节。
当错误发生时,yacc.py按照如下步骤进行:
1. 第一次错误产生时,用户定义的p_error()方法会被调用,出错的标记会作为参数传入;如果错误是因为到达文件结尾造成的,传入的参数将为None。随后,分析器进入到“错误恢复”模式,该模式下不会在产生`p_error()`调用,直到它成功的移进3个标记,然后回归到正常模式。
2. 如果在p_error()中没有指定恢复动作的话,这个导致错误的标记会被替换成一个特殊的error标记。
3. 如果导致错误的标记已经是error的话,原先的栈顶的标记将被移除。
4. 如果整个分析栈被放弃,分析器会进入重置状态,并从他的初始状态开始分析。
5. 如果此时的语法规则接受error标记,error标记会移进栈。
6. 如果当前栈顶是error标记,之后的标记将被忽略,直到有标记能够导致error的归约。
### 6.8.1 根据error规则恢复和再同步
最佳的处理语法错误的做法是在语法规则中包含error标记。例如,假设你的语言有一个关于print的语句的语法规则:
~~~
def p_statement_print(p):
'statement : PRINT expr SEMI'
...
~~~
为了处理可能的错误表达式,你可以添加一条额外的语法规则:
~~~
def p_statement_print_error(p):
'statement : PRINT error SEMI'
print "Syntax error in print statement. Bad expression"
~~~
这样(expr错误时),error标记会匹配任意多个分号之前的标记(分号是`SEMI`指代的字符)。一旦找到分号,规则将被匹配,这样error标记就被归约了。
这种类型的恢复有时称为”分析器再同步”。error标记扮演了表示所有错误标记的通配符的角色,而紧随其后的标记扮演了同步标记的角色。
重要的一个说明是,通常error不会作为语法规则的最后一个标记,像这样:
~~~
def p_statement_print_error(p):
'statement : PRINT error'
print "Syntax error in print statement. Bad expression"
~~~
这是因为,第一个导致错误的标记会使得该规则立刻归约,进而使得在后面还有错误标记的情况下,恢复变得困难。
### 6.8.2 悲观恢复模式
另一个错误恢复方法是采用“悲观模式”:该模式下,开始放弃剩余的标记,直到能够达到一个合适的恢复机会。
悲观恢复模式都是在p_error()方法中做到的。例如,这个方法在开始丢弃标记后,直到找到闭合的’}’,才重置分析器到初始化状态:
~~~
def p_error(p):
print "Whoa. You are seriously hosed."
# Read ahead looking for a closing '}'
while 1:
tok = yacc.token() # Get the next token
if not tok or tok.type == 'RBRACE': break
yacc.restart()
~~~
下面这个方法简单的抛弃错误的标记,并告知分析器错误被接受了:
~~~
def p_error(p):
print "Syntax error at token", p.type
# Just discard the token and tell the parser it's okay.
yacc.errok()
~~~
在`p_error()`方法中,有三个可用的方法来控制分析器的行为:
* `yacc.errok()` 这个方法将分析器从恢复模式切换回正常模式。这会使得不会产生error标记,并重置内部的error计数器,而且下一个语法错误会再次产生p_error()调用
* `yacc.token()` 这个方法用于得到下一个标记
* `yacc.restart()` 这个方法抛弃当前整个分析栈,并重置分析器为起始状态
注意:这三个方法只能在`p_error()`中使用,不能用在其他任何地方。
p_error()方法也可以返回标记,这样能够控制将哪个标记作为下一个标记返回给分析器。这对于需要同步一些特殊标记的时候有用,比如:
~~~
def p_error(p):
# Read ahead looking for a terminating ";"
while 1:
tok = yacc.token() # Get the next token
if not tok or tok.type == 'SEMI': break
yacc.errok()
# Return SEMI to the parser as the next lookahead token
return tok
~~~
### 6.8.3 从产生式中抛出错误
如果有需要的话,产生式规则可以主动的使分析器进入恢复模式。这是通过抛出`SyntacError`异常做到的:
~~~
def p_production(p):
'production : some production ...'
raise SyntaxError
~~~
raise SyntaxError错误的效果就如同当前的标记是错误标记一样。因此,当你这么做的话,最后一个标记将被弹出栈,当前的下一个标记将是error标记,分析器进入恢复模式,试图归约满足error标记的规则。此后的步骤与检测到语法错误的情况是完全一样的,p_error()也会被调用。
手动设置错误有个重要的方面,就是p_error()方法在这种情况下不会调用。如果你希望记录错误,确保在抛出SyntaxError错误的产生式中实现。
注:这个功能是为了模仿yacc中的`YYERROR`宏的行为
### 6.8.4 错误恢复总结
对于通常的语言,使用error规则和再同步标记可能是最合理的手段。这是因为你可以将语法设计成在一个相对容易恢复和继续分析的点捕获错误。悲观恢复模式只在一些十分特殊的应用中有用,这些应用往往需要丢弃掉大量输入,再寻找合理的同步点。