为啥有些编译器更喜欢手工制作的解析器而不是解析器生成器?
Posted
技术标签:
【中文标题】为啥有些编译器更喜欢手工制作的解析器而不是解析器生成器?【英文标题】:Why some compilers prefer hand-crafted parser over parser generators?为什么有些编译器更喜欢手工制作的解析器而不是解析器生成器? 【发布时间】:2013-03-18 09:10:09 【问题描述】:根据 Vala 文档:“在 0.3.1 之前,Vala 的解析器是经典的 flex 扫描器和 Bison LALR 解析器的组合。但截至 commit eba85a,解析器是手工制作的递归下降解析器。” 我的问题是:为什么?
这个问题可以针对任何不使用解析器生成器的编译器。从解析器生成器到手工解析器的这种转变的利弊是什么?在编译器中使用解析器生成器(Bison、ANTLR)有什么缺点?
作为旁注:我对 Vala 很感兴趣,因为我喜欢让语言具有现代特性和简洁语法但可编译成“本机”和“非托管”高级语言的想法(在 Vala 的情况下为 C) .到目前为止,我只找到了 Vala。我正在考虑通过使 Vala(或类似语言)可编译为 C++(由 Qt 库支持)来获得乐趣。但由于我不想发明全新的语言,我正在考虑采用一些现有的语法。显然,手工制作的解析器没有我可能重用的书面形式语法。欢迎您对这个想法的cmets(整个想法很愚蠢吗?)。
【问题讨论】:
你问过 Jürg Billeter(提交的作者)吗? 嗯,不,我没有。我会尝试联系他。我正在更改我的问题的标题以使其更笼统。 可以使手工制作解析器更快/更节省空间,因为它不必是通用的并且可以使用更具体的技巧。 你可能想看看 Mozilla 的 Rust 项目,它已经以 C++ 为目标。 错误消息和错误恢复在手写解析器中处理得更好。当然有一些很好的方法可以从一些高级声明性语言生成这样的代码,但是这些现代方法对于 yacc 感染的 LR 人员来说是未知的(或者太可怕了)。他们宁愿手动编写递归下降解析器,然后使用基于 PEG 的东西。 【参考方案1】:在我的职业生涯中,我已经编写了六个手工制作的解析器(在大多数情况下是递归下降解析器,也称为自顶向下解析器),并且看到过解析器生成器生成的解析器,我必须承认我对解析器生成器有偏见。
以下是每种方法的一些优缺点。
解析器生成器
优点:
快速获得一个可以工作的解析器(至少在您不知道如何编写代码的情况下)。缺点:
生成的代码难以理解和调试。 难以实施正确的错误处理。生成器将为语法正确的代码创建正确的解析器,但会阻塞不正确的代码,并且在大多数情况下无法提供正确的错误消息。 解析器生成器中的错误可能会停止您的项目。您需要修复其他人代码中的错误(如果有源代码),等待作者修复或解决错误(如果可能的话)。手工递归下降解析器
优点:
生成的代码很容易理解。递归解析器通常具有对应于每种语言结构的一个函数,例如parseWhile 解析“while”语句,parseDeclaration 解析声明等等。理解和调试解析器很容易。 很容易提供有意义的错误消息,从错误中恢复并继续以在特定情况下最有意义的方式进行解析。缺点:
手动编写解析器代码需要一些时间,特别是如果您没有这方面的经验。
解析器可能有点慢。这适用于所有递归解析器,而不仅仅是手写的。具有对应于每种语言构造的一个函数来解析简单的数字文字,解析器可以从例如开始进行十几个或更多嵌套调用。 parseExpression 通过 parseAddition、parseMultiplication 等 parseLiteral。函数调用在像 C 这样的语言中相对便宜,但仍然需要很长时间。
加速递归解析器的一个解决方案是用自下而上的子解析器替换部分递归解析器,这通常更快。这种子解析器的自然候选者是具有几乎统一语法的表达式(即二元和一元表达式),具有几个优先级。表达式的自下而上解析器通常也很容易编写代码,它通常只是一个循环,从词法分析器获取输入标记、一堆值和运算符标记的运算符优先级查找表。
【讨论】:
【参考方案2】:LR(1) 和 LALR(1) 解析器非常非常烦人,原因有两个:
-
解析器生成器不太擅长生成有用的错误消息。
某些类型的歧义,例如 C 风格的 if-else 块,让编写语法变得非常痛苦。
另一方面,LL(1) 语法在这两方面都做得更好。 LL(1) 文法的结构使得它们很容易编码为递归下降解析器,因此处理解析器生成器并不是真正的胜利。
此外,在 Vala 的情况下,解析器和编译器本身以库的形式呈现,因此您可以使用 Vala 编译器库为 Vala 编译器构建自定义后端,并获取所有解析和类型检查等免费。
【讨论】:
+1 我希望错误处理是主要原因。歧义和重复的规则编写有时也可以通过解析器生成器中的扩展来处理(bison 会做一些)【参考方案3】:我知道这不会是确定的,如果你的问题不是特别与 Vala 相关,我不会打扰,但因为它们是......
当时我并没有过多地参与这个项目,所以我对一些细节不是很清楚,但我记得 Vala 切换时的一个重要原因是dogfooding。我不确定这是主要动机,但我确实记得这是一个因素。
可维护性也是一个问题。该补丁用 Vala 中的一个较小的解析器替换了一个用 C/Bison/YACC 编写的较大的解析器(相对而言很少有人对后两者有丰富的经验)(几乎任何对 valac 感兴趣的人都可能知道并且很舒服)。
更好的错误报告也是一个目标,IIRC。
我不知道这是否是一个因素,但手写解析器是递归下降解析器。我知道 ANTLR 会生成这些,ANTLR 是用 Java 编写的,这是一个非常重的依赖项(是的,我知道它不是运行时依赖项,但仍然如此)。
作为旁注:我对 Vala 很感兴趣,因为我喜欢让语言具有现代特性和简洁语法但可编译成“本机”和“非托管”高级语言的想法(在 Vala 的情况下为 C) .到目前为止,我只找到了 Vala。我正在考虑通过使 Vala(或类似语言)可编译为 C++(由 Qt 库支持)来获得乐趣。但由于我不想发明全新的语言,我正在考虑采用一些现有的语法。显然,手工制作的解析器没有我可能重用的书面形式语法。欢迎您提出这个想法的 cmets(整个想法很愚蠢吗?)。
很多 Vala 确实反映了 GObject 做出的决定,在 C++/Qt 中事情可能会或可能不会以相同的方式工作。如果您的目标是用 Qt/C++ 替换 valac 中的 GObject/C,那么您的工作可能比您预期的要多。但是,如果您只是想让 C++ 和 Qt 库可以从 Vala 访问,那当然是可能的。事实上,Luca Bruno 大约一年前就开始研究这个了(参见 wip/cpp 分支)。由于时间不够,而不是技术问题,它已经有一段时间没有看到活动了。
【讨论】:
太棒了!谢谢你的内幕。在决定如何在 Vala 中提供 Qt 库之前,我将了解更多信息。【参考方案4】:根据 Vala 文档:“在 0.3.1 之前,Vala 的解析器是 经典的 flex 扫描器和 Bison LALR 解析器组合。但截至 提交 eba85a,解析器是一个手工制作的递归下降解析器。” 我的问题是:为什么?
我在这里专门询问 Vala,虽然这个问题可能 寻址到任何不使用解析器生成器的编译器。什么 从解析器生成器迁移到 手工解析器?使用解析器生成器有什么缺点 (Bison, ANTLR) 用于编译器?
也许程序员发现了解析器生成器没有发现的一些优化途径,而这些优化途径需要完全不同的解析算法。或者,也许解析器生成器在 C89 中生成代码,而程序员决定为 C99 或 C11 重构会提高易读性。
作为旁注:我对 Vala 感兴趣,特别是因为我喜欢 拥有具有现代特征和简洁语法的语言的想法,但 可编译成“本机”和“非托管”高级语言(C in Vala 的案例)。
简单说明:C 不是原生的。它源于可移植性,因为它旨在抽象出那些在移植时导致程序员如此痛苦的硬件/操作系统特定细节。例如,想想为每个操作系统和/或文件系统使用完全不同的 fopen 的痛苦;我的意思不仅仅是功能不同,而且输入和输出期望也不同,例如。不同的参数,不同的返回值。同样,C11 引入了可移植线程;使用线程的代码将能够使用相同的符合 C11 的代码来针对所有实现线程的操作系统。
到目前为止,我已经找到了 Vala。我想通过制作 Vala 来获得乐趣 (或类似语言)可编译为 C++(由 Qt 库支持)。但由于 我不想发明我正在考虑采用的全新语言 一些现有的语法。显然手工制作的解析器没有 我可以重用的书面形式语法。您对这个想法的看法是 欢迎(整个想法很愚蠢吗?)。
使用手工制作的解析器轻松生成 C++ 代码可能是可行的,所以我不会这么快就放弃这个选项;旧的 flex/bison 解析器生成器可能更有用,也可能不更有用。但是,这不是您唯一的选择。无论如何,我很想深入研究the specification。
【讨论】:
谢谢。 “原生”是指“直接编译为原生代码”,即中间没有字节码,没有 JIT 编译,没有 VM,因此没有 GC。 @vladimir 什么,比如this 或this?通过访问这些链接,您可能获得了深刻的信息:C 也可以被解释。了解 C#(一种典型的 JIT 编译为字节码的语言)也可以编译为机器代码,并且 JIT 是一种也适用于本机机器代码的优化方法,这可能也很有见地>。您可能有兴趣了解 C 是根据称为“抽象机器”的 VM 指定的,并且 C 规范不排除垃圾收集。 @vladimir:javascript 传统上被解释,但现在由 V8 引擎编译为 IA-32、x86-64、ARM 或 MIPS。释放你在“翻译方法”(例如解释、编译)和“编程语言”之间建立的联系。用一种编程语言编写的代码可以编译(翻译)成任何其他图灵完备的编程语言,或解释(直接翻译成行为)。 我必须重新表述关于“本机”和“非托管”的部分。假设一种在运行时很便宜的语言。带有空 GUI 的 C# 或 Java 程序至少需要 40Mb。 GUI 也可能很慢。例如,用于监控高频交易的 Qt 应用程序占用的内存减少了 10 倍,并且能够以比 WPF 快 4-5 倍的速度更新屏幕上的数字。这不是因为我。我擅长 C# 和 WPF(比 Qt 好得多)。所以,我在“本机”和“非托管”下的真正意思是“没有繁重的运行时间”。比如GCC编译的C++。虽然不知何故我不想回到 C++ 语法。 @vladimir C# language specification 的哪一部分声明 GUI 是一项要求? WPF 是 .NET 框架的一部分,该框架也由 CLR 组成。在有效的 C# 实现中只需要 CLR 的几个部分。此外,优化是与规范无关的另一个实现细节。语言没有速度;这是编译器/解释器产生的属性……而产生与 C 程序相同结果的 C# 程序很可能会变成相同的机器代码。【参考方案5】:奇怪的是,这些作者从野牛转到了 RD。大多数人会朝相反的方向走。
我能看到做 Vala 作者所做的唯一真正原因是更好的错误恢复,或者他们的语法可能不是很干净。
我想你会发现大多数新语言都是从手写解析器开始的,因为作者对他们自己的新语言有一种感觉,并准确地弄清楚他们想要做什么。在某些情况下,作者学习如何编写编译器。 C 是一个典型的例子,C++ 也是。在进化的后期,可能会替换生成的解析器。另一方面,现有标准语言的编译器可以通过解析器生成器更快地开发,甚至可能通过现有语法:上市时间是这些项目的关键业务参数。
【讨论】:
GCC 也走了另一条路。他们用手写的递归下降解析器替换了 Bison,用于 3.4 中的 C++,以及 4.1 中的 C 和 Objective C。我不是要说明什么,只是觉得你可能会觉得很有趣。以上是关于为啥有些编译器更喜欢手工制作的解析器而不是解析器生成器?的主要内容,如果未能解决你的问题,请参考以下文章
为啥编译器更喜欢 f(const void*) 而不是 f(const std::string &)?
为啥在终端上写 ppm 时会在终端上弹出 Perl 包管理器而不是 ppm>
与 Promise.all() 中的解析相比,为啥在 while 循环中单独解析 Promise 数组时解析更慢? [复制]