【问题标题】:specific error recovery in bison/yaccbison/yacc 中的特定错误恢复
【发布时间】:2016-05-26 00:05:21
【问题描述】:

我正在阅读 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 的错误生成将提供 error 是 状态 7 中的合法前瞻和 error 将立即转移 到堆栈上并减少到factor,导致值 0 为 回来。现在解析器已经到了以下点:

parsing stack                 input
$0 exp 2 + 7 factor 4         +3$

这是正常情况,解析器会继续执行 通常到最后。效果是将输入解释为2+0+3 - 两个 + 符号之间的 0 在那里,因为这是插入 error 伪标记的位置,以及错误的操作 生产,error被视为等价于具有价值的因素 0.

我的问题很简单:

通过查看语法,他如何知道为了从这个特定错误 (2++3) 中恢复,他需要将 error 伪标记添加到 factor 产生式?有没有简单的方法来做到这一点?或者唯一的方法是使用状态表计算多个示例,并认识到这个特定的错误将发生在这个给定的状态,因此如果我将 error 伪标记添加到某个特定的生产错误将得到修复。

感谢任何帮助。

【问题讨论】:

    标签: parsing yacc


    【解决方案1】:

    在这种简单的语法中,产生错误的选项很少,所有选项都允许继续解析。

    在这种情况下,选择派生树底部的那个是有意义的,但这不是通用的启发式方法。将错误产生放在派生树的顶部更为有用,它们可用于重新同步解析。例如,假设我们修改了语法以允许多个表达式,每个表达式在自己的行中:(这需要修改 yylex 以便它在看到 \n 时不会伪造 EOF):

    program: %empty
           | program '\n'
           | program exp '\n'  { printf("%g\n", $1); }
    

    现在,如果我们只想忽略错误并继续解析,我们可以添加一个重新同步的错误产生:

           | program error '\n'
    

    上面的 '\n' 终端会导致标记被跳过,直到可以换行以减少错误产生,以便解析可以继续下一行。

    不过,并非所有语言都那么容易重新同步。类 C 语言中的语句不一定由; 终止,如果错误是缺少} 等,那么天真的尝试如上所述重新同步会导致一定程度的混乱。但是,它会允许解析以某种方式继续进行,这可能就足够了。

    根据我的经验,正确制作错误作品通常需要大量的尝试和错误;它更像是一门艺术而不是一门科学。尝试大量错误输入并分析错误恢复会有所帮助。

    产生错误的目的是从错误中恢复。产生好的错误信息是一个不相关但同样具有挑战性的问题。当解析器尝试错误恢复时,错误消息已经发送到yyerror。 (当然,该函数可以忽略错误消息并将其留给错误生成器打印错误,但没有明显的理由这样做。)

    产生良好错误消息的一种可能策略是对解析器堆栈和前瞻标记进行某种表查找(或计算)。实际上,这就是 bison 的内置扩展错误处理所做的,并且通常会产生相当合理的结果,因此这是一个很好的起点。已经探索了替代策略。一个很好的参考是 Clinton Jeffrey 2003 年的论文Generating LR Syntax Error Messages from Examples;您还可以查看Russ Cox's explanation,了解他如何将这个想法应用于 Go 编译器。

    【讨论】:

    • 感谢您的回复。但我仍然不清楚。让我重新表述一下这个问题。假设我有一项任务来解决这个特定问题,即处理 2++3 的输入,或者更一般地说 NUMBER + NUMBER。通过查看需要将error 伪标记插入factor 产生式的语法,我如何知道?
    • @flashburn:对于完全误读您的问题,我深表歉意。我写了一个可能更有帮助的新答案,但广泛的答案是“这是一门艺术,而不是一门科学”。 (包含在答案中,但可能不够强调。)
    猜你喜欢
    • 1970-01-01
    • 2010-11-28
    • 2013-01-19
    • 2011-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多