解析器错误 - 自动生成错误处理的模式

Parser errors - pattern for generating error handling automatically

是否有任何已知的方法可以为机器生成的解析器实现良好的错误处理? 是否存在针对此类问题的 "pattern" 或已知算法?

对于 "good" 我的意思是类似于手工制作的递归下降解析器和现代编译器可获得的结果: 解析器不会在第一个错误处停止,可以发出 "meaningful" 个错误,而不仅仅是一次 "unrecognized token in line xyz" 个错误。

理想情况下,这种方法也应该是自动化的,而不是 手工制作。

我不是在寻找库,我需要一种方法,它可以在不同的平台上使用,并且理想情况下会尽可能独立于语言。

从第一个错误开始,人们就一直在尝试找出报告和修复语法错误的方法。有很多关于如何做到这一点的技术论文。 在 scholar.google.com 处搜索字符串 "syntax error repair" 会产生 57 次匹配。

确实有几个问题:

1) 如何向 reader 报告有意义的错误。首先,那里 是解析器检测到错误的地方,也是用户实际犯错的地方 错误。例如,C 程序可能在一个奇怪的地方有一个“++”操作符:

void p {
 x = y ++
     z = 0;
<EOF>

大多数解析器在遇到 "z" 时会阻塞,并将其报告为错误位置。然而,如果错误是在本应使用“+”时使用了“++”,则此报告是错误的。不幸的是,要做到这一点,您需要能够读懂程序员的想法。

你也有报告错误上下文的问题。 您是否将错误报告为表达式中的错误[乍一看,似乎如此]?在一份声明中?在一条线上?在函数体内?在函数声明中?可能您想在可以围绕错误点的最窄句法类别中进行报告。 (请注意,您不能将函数体或声明报告为 "surrounding" 错误点,因为它们也不完整!) 如果错误真的是 ++ 后面缺少分号怎么办?那么错误位置并不是真正的 "in the expression"。如果修复需要插入缺失的字符串引号怎么办?宏连续字符?

因此您必须以某种方式确定什么构成了实际错误,这让我们能够修复错误。

2) 错误修复:为了让工具以有意义的方式进行,它必须修复错误。据推测,这意味着修补输入令牌流以生成合法程序(如果源有多个错误,您可能无法执行此操作)。如果有几个可能的补丁怎么办?很明显,最好的错误报告是 "yyyy is wrong, I suspect you should have used xxxx"。应该考虑修复多大的补丁:只是触发错误的令牌,它后面的令牌,它之前的令牌怎么样?

我注意到很难在手写解析器上执行自动的、一般的错误修复建议,因为指导这种修复所需的语法在任何地方都没有明确可用。因此,您会期望自动修复在语法是显式工件的工具上效果最好。

也可能是错误修复要考虑到常见的错误。如果人们倾向于离开';'关闭语句,并插入一个修复文件,这可能是一个很好的修复。如果他们很少这样做,并且有不止一次修复(例如,将“++”替换为“+”),那么替代修复可能是更好的建议。

3) 修复的语义影响。即使您修复了语法错误,修复后的程序也可能无法正常运行。如果您的错误需要插入标识符,应该使用什么标识符?

FWIW,我们的 DMS Software Reengineering Toolkit 完全由语法驱动进行自动修复。它的运行假设错误点处的标记应该被删除,或者其他一些单个标记应该插入到它的左边。这会捕获丢失的“;”和额外的加号;它通常会成功地进行合法修复。通常不是 "right" 那个。至少它让解析器继续处理源代码的其余部分。

我认为寻找好的自动错误修复将持续很长时间。

FWIW,论文 Syntax Error Repair for a Java-based Parser Generator 报告说 Burke 的 Ph.D。论文:

M.G。 Burke, 1983, A practical method for LR and LL syntactic error diagnosis and recovery, PhD the Department of Computer Science, New York University

还不错。特别是,它通过考虑和修改错误的左上下文以及错误范围来修复错误。好像可以get it from ACM

这可能不是您想听到的,但您最好亲自编写解析器和词法分析器。

这不是一项特别艰巨的任务(尤其是与编写语义分析器和代码生成器相比),并且在错误处理方面会产生最佳结果。

但是不要相信我,请相信第一个原生 C++ 编译器的作者和 D 编程语言的发明者 Walter Bright。

他在 Dr.Dobbs here 上有一篇关于此的文章。 (错误恢复在第 2 页)

我对这个问题有不同的看法,即您不应该将语法错误视为内部编译器错误。任何实用的编译器实际上都在实现 三种 种语言:

  1. 语言 L 即指定的目标语言。正确的程序是该语言的成员。
  2. 语言ML加上编译器识别的所有错误组成。 M \ L 的成员收到信息性错误。
  3. 编译器正常终止的语言Z。该集合应该是所有可能输入字符串的集合,但如果编译器在某些输入上崩溃,则不会。 Z \ M 的成员收到有关编译器如何失败的一般消息,通常采用 "parser failed at line x, char y".
  4. 形式

如果您在解析器中指定语言 M 而不是语言 L[=,则可以使用自动解析器生成工具,正如您正在寻找的那样52=]。这种方法的问题是语言设计者总是指定 L 而不是 M。我想不出有任何类似 M.

标准的案例

这不仅仅是抽象的废话。最近对 C++ 的更改很好地说明了这种区别。曾经是

template< class T > class X;
template< class T > class Y;
X<Y<int>> foo; // syntax in M

第三行有一个错误,因为字符“>>”是右移运算符的标记。那一行必须写成

X<Y<int> > foo; // syntax in L

标准已更改为不需要额外的 space。原因是所有主要的编译器都已经编写了代码来识别这种情况,以便生成有意义的错误消息。换句话说,他们发现 M 语言已经到处实现了。委员会确定后,他们将 M 语法转移到 L.

的新版本中

如果设计师在开发 L 语言的同时考虑 M 语言,我们的整体语言设计会更好语。只是为了他们自己的理智,他们会做出一些努力来最小化 M 的规范大小,这对每个人来说都是一件好事。唉,世界还没到

结果是你需要设计自己的语言M。这就是难题。是否使用自动化工具与此无关。它有帮助,但它并没有摆脱最耗时的部分。

使用传统的 YACC/bison 生成器,您可以获得 yyerror/YYERROR 框架,由于 LALR 解析器的无序回溯特性,使用它生成非常有用的错误消息并不容易。 您甚至可以在那里添加错误恢复规则,因为您可能需要它们来抑制失败规则中的错误错误消息,而您只想削减解析规则。

使用基于 PEG 的解析器,您可以使用更好的 ~{} 后缀错误操作块语法。见例如。 peg manual

  rule = e1 e2 e3 ~{ error("e[12] ok; e3 has failed"); }
         | ...

  rule = (e1 e2 e3) ~{ error("one of e[123] has failed"); }
         | ...

您会在错误的实际位置获得出色的错误消息。但是你必须写 PEG 规则,这不是那么容易写,尤其是。处理运算符优先级时。使用 LALR 解析器更容易。

使用更简单的 recursive descent parser 生成器,您可以获得与 PEG 相同的错误报告优势,但解析速度要慢得多。

请参阅 http://lambda-the-ultimate.org/node/4781

中的相同讨论