我正在阅读"编译器构造,原则和实践" Kenneth Louden的书,试图理解Yacc中的错误恢复。
作者使用以下语法给出了一个例子:
%{
#include <stdio.h>
#include <ctype.h>
int yylex();
int yyerror();
%}
%%
command : exp { printf("%d\n", $1); }
; /* allows printing of the result */
exp : exp '+' term { $$ = $1 + $3; }
| exp '-' term { $$ = $1 - $3; }
| term { $$ = $1; }
;
term : term '*' factor { $$ = $1 * $3; }
| factor { $$ = $1; }
;
factor : NUMBER { $$ = $1; }
| '(' exp ')' { $$ = $2; }
;
%%
int main() {
return yyparse();
}
int yylex() {
int c;
/* eliminate blanks*/
while((c = getchar()) == ' ');
if (isdigit(c)) {
ungetc(c, stdin);
scanf("%d\n", &yylval);
return (NUMBER);
}
/* makes the parse stop */
if (c == '\n') return 0;
return (c);
}
int yyerror(char * s) {
fprintf(stderr, "%s\n", s);
return 0;
} /* allows for printing of an error message */
产生以下状态表(稍后称为表5.11)
缩减中的数字对应于以下作品:
(1) command : exp.
(2) exp : exp + term.
(3) exp : exp - term.
(4) exp : term.
(5) term : term * factor.
(6) term : factor.
(7) factor : NUMBER.
(8) factor : ( exp ).
然后Louden博士给出了以下例子:
考虑一下如果将错误产生添加到 yacc定义
factor : NUMBER {$$ = $1;} | '(' exp ')' {$$=$2;} | error {$$ = 0;} ;
考虑前一个示例中的第一个错误输入 2 ++ 3 (我们继续使用表5.11,尽管额外的错误产生导致表格略有不同。)与解析器之前一样 达到以下目的:
parsing stack input $0 exp 2 + 7 +3$
现在
factor
的错误生成将提供错误 状态7中的合法前瞻和错误将立即转移 进入堆栈并减少到factor
,导致值为0 回。现在解析器已达到以下几点:parsing stack input $0 exp 2 + 7 factor 4 +3$
这是正常情况,解析器将继续执行 通常到最后。效果是将输入解释为 2 + 0 + 3 - 两个 + 符号之间的0是有的,因为这是插入错误伪语的位置,以及错误的操作 生产,错误被视为等同于有价值的因素 0
我的问题很简单:
他是如何通过查看语法知道的,为了从这个特定错误(2 ++ 3)中恢复,他需要在factor
制作中添加错误伪语音?有一个简单的方法吗?或者唯一的方法是使用状态表计算出多个示例,并确认在此给定状态下会发生此特定错误,因此如果我向某个特定生产添加错误伪语音错误将被修复。
感谢任何帮助。
答案 0 :(得分:2)
在这个简单的语法中,您只有很少的错误生成选项,所有这些选项都允许解析继续。
在这种情况下,选择派生树底部的那个是有道理的,但这不是通用的启发式算法。将错误产生放在派生树的顶部更常用,它们可用于重新同步解析。例如,假设我们修改了语法以允许多个表达式,每个表达式都在其自己的行上:(这需要修改yylex
,以便在看到\n
时不会伪造EOF :
program: %empty
| program '\n'
| program exp '\n' { printf("%g\n", $1); }
现在,如果我们想忽略错误并继续解析,我们可以添加重新同步错误产生:
| program error '\n'
上面的'\ n'终端将导致跳过令牌,直到可以移动换行符以减少错误产生,以便解析可以继续下一行。
但并非所有语言都很容易重新同步。类似C语言的语句不一定由;
终止,并且如果错误是例如缺少}
,则如上所述重新同步的天真尝试将导致一定程度的混淆。但是,它将允许解析以某种方式继续,这可能就足够了。
根据我的经验,正确制作错误产品通常需要大量的反复试验;它更像是一门艺术,而不是一门科学。尝试大量错误输入并分析错误恢复将有所帮助。
错误产生的关键是从错误中恢复。产生良好的错误消息是一个无关但同样具有挑战性的问题。到解析器尝试错误恢复时,错误消息已发送到yyerror
。 (当然,该函数可以忽略错误消息并将其留给错误生成以打印错误,但没有明显的理由这样做。)
产生良好错误消息的一种可能策略是在解析器堆栈和先行令牌上执行某种表查找(或计算)。实际上,这就是bison内置扩展错误处理的功能,并且通常会产生非常合理的结果,因此它是一个很好的起点。已经探索了替代战略。一个很好的参考是克林顿杰弗里2003年的论文Generating LR Syntax Error Messages from Examples;您也可以查看Russ Cox's explanation他如何将这个想法应用于Go编译器。