GCC 和 Clang 解析器真的是手写的吗?

Posted

技术标签:

【中文标题】GCC 和 Clang 解析器真的是手写的吗?【英文标题】:Are GCC and Clang parsers really handwritten? 【发布时间】:2011-09-13 05:29:06 【问题描述】:

似乎 GCC 和 LLVM-Clang 正在使用手写递归下降解析器,并且不是机器生成的、基于 Bison-Flex 的自下而上解析。

这里有人可以确认一下吗? 如果是这样,为什么主流编译器框架都使用手写解析器?

更新:interesting blog on this topic here

【问题讨论】:

几乎所有的主流编译器都在使用手写解析器。这有什么问题? 如果您需要性能,您必须(半)手动完成。 不仅是性能 - 更好的错误消息、恢复能力等 MS VisualStudio 怎么样?虽然不是开源的,但来自 MS 的人能否验证他们也在使用手写递归下降解析器? @GeneBushuyev,来自 GCC wiki:“...虽然 时序显示了 1.5% 的加速,但主要的好处是促进了未来的增强......”这种加速似乎相当边缘...... 【参考方案1】:

有一个民间定理说 C 很难解析,而 C++ 基本上是不可能的。

这不是真的。

事实是 C 和 C++ 很难使用 LALR(1) 解析器进行解析,而无需破解解析机制和纠缠符号表数据。事实上,GCC 曾经使用 YACC 和其他类似的hackery 来解析它们,是的,它很丑陋。 现在 GCC 使用手写解析器,但仍然使用符号表hackery。 Clang 的人从未尝试过使用自动解析器生成器。 AFAIK Clang 解析器一直是手动编码的递归下降。

事实是,C 和 C++ 使用更强大的自动生成解析器(例如 GLR parsers)相对容易解析,而且您不需要任何 hack。 Elsa C++ 解析器就是一个例子。我们的C++ Front End 是另一个(就像我们所有的“编译器”前端一样,GLR 是非常出色的解析技术)。

我们的 C++ 前端不如 GCC 快,而且肯定比 Elsa 慢;我们在仔细调整它上投入的精力很少,因为我们还有其他更紧迫的问题(尽管它已被用于数百万行 C++ 代码)。 Elsa 可能比 GCC 慢,因为它更通用。鉴于当今的处理器速度,这些差异在实践中可能并不重要。

但是今天广泛分布的“真正的编译器”源于 10 或 20 年前或更长时间的编译器。效率低下就更重要了,没有人听说过 GLR 解析器,所以人们做了他们知道该怎么做的事情。 Clang 肯定是较新的,但民间定理在很长一段时间内保持其“说服力”。

您不必再这样做了。您可以非常合理地使用 GLR 和其他此类解析器作为前端,同时提高编译器的可维护性。

正确的是,要获得与您友好的邻居编译器的行为相匹配的语法是很困难的。虽然几乎所有 C++ 编译器都实现了(大部分)原始标准,但它们也往往有很多暗角扩展,例如 MS 编译器中的 DLL 规范等。如果你有一个强大的解析引擎,你可以 花时间尝试使最终的语法与现实相匹配,而不是试图改变语法以匹配解析器生成器的限制。

2012 年 11 月编辑:自编写此答案以来,我们已经改进了 C++ 前端以处理完整的 C++11,包括 ANSI、GNU 和 MS 变体方言。虽然有很多额外的东西,但我们不必更改解析引擎;我们刚刚修改了语法规则。我们确实必须改变语义分析; C++11 在语义上非常复杂,而且这项工作淹没了让解析器运行的努力。

2015 年 2 月编辑:...现在可以处理完整的 C++14。 (参见get human readable AST from c++ code 了解简单代码的 GLR 解析,以及 C++ 臭名昭著的“最令人头疼的解析”)。

2017 年 4 月编辑:现在处理(草稿)C++17。

【讨论】:

PostScript:正如让语法与供应商真正所做的相匹配更难,获得名称和类型解析以匹配不同供应商对 C++11 手册的解释更难,因为唯一的如果你能找到它们,你所拥有的证据就是编译方式略有不同的程序。截至 2013 年 8 月,对于 C++11 来说,我们基本上已经过去了,但我对 C++ 委员会感到有些失望,该委员会似乎一心想要以 C 的形式产生一个更大(并且根据经验,更令人困惑)的标准++1 年。 我真的很想知道:您如何处理 foo * bar; 的歧义? @Martin:我们的解析器以 both 方式解析它,生成一个包含特殊“歧义节点”的树,其子节点是替代解析;孩子们最大限度地分享他们的孩子,所以我们最终得到了一个 DAG 而不是一棵树。 解析完成后,我们在 DAG 上运行一个属性语法评估器 (AGE)(如果你不知道,“walk the tree and do stuff”的花哨名称),它计算所有声明的类型身份标识。 ... ... 模棱两可的孩子不可能都是类型一致的; AGE 在发现一个无法合理输入的模棱两可的孩子时会简单地删除它。剩下的就是那些有型的孩子;因此,我们已经确定了“foobar;”的哪个解析。是正确的。这个技巧适用于我们为 C++11 的真正方言构建的真正语法中发现的各种疯狂的歧义,并且*完全将解析与名称的语义分析分开。这种干净的分离意味着更少的工程工作要做(无需调试)。更多讨论请参见***.com/a/1004737/120163。 @TimCas:实际上,我和你一样反对设计语言语法(和语义)的明显愚蠢,这些语法(和语义)非常复杂,很难做到正确(是的,C++ 语言在这里严重受苦)。我希望语言设计委员会能够设计语法,以便更简单的解析技术能够工作,并明确定义语言语义并使用一些语义分析工具对其进行检查。唉,世界好像不是这样的。所以,我认为,尽管有尴尬,你还是尽你所能地建造你必须建造的东西,然后继续生活。【参考方案2】:

是的:

GCC 曾经使用过一个 yacc (bison) 解析器,但在 3.x 系列的某个时刻它被一个手写的递归下降解析器所取代:有关相关补丁的链接,请参见 http://gcc.gnu.org/wiki/New_C_Parser提交。

Clang 还使用手写的递归下降解析器:请参阅http://clang.llvm.org/features.html 末尾附近的“C、Objective C、C++ 和 Objective C++ 的单一统一解析器”部分。

李>

【讨论】:

这是否意味着 ObjC、C 和 C++ 有 LL(k) 语法? 不:即使是三者中最简单的 C 语言也有模棱两可的语法。例如,foo * bar; 可以解析为乘法表达式(结果未使用),或类型为指向foo 的变量bar 的声明。哪一个是正确的取决于当时footypedef 是否在范围内,这不是可以通过任何数量的前瞻来确定的。但这只是意味着递归下降解析器需要添加一些丑陋的额外机器来处理它。 我可以从经验证据中确认,C++11、C 和 Objective C 具有 GLR 解析器可以处理的上下文无关文法。 关于上下文敏感性,this answer 也没有声称:解析这些语言可能是图灵完备的。【参考方案3】:

Clang 的解析器是一个手写的递归下降解析器,其他几个开源和商业 C 和 C++ 前端也是如此。

Clang 使用递归下降解析器有几个原因:

性能:手写解析器允许我们编写快速解析器,根据需要优化热路径,并且我们始终可以控制该性能。拥有快速解析器后,Clang 可以用于其他通常不使用“真正”解析器的开发工具,例如 IDE 中的语法突出显示和代码完成。 诊断和错误恢复:因为您可以完全控制手写递归下降解析器,所以很容易添加检测常见问题并提供出色诊断和错误恢复的特殊情况(例如,请参阅http://clang.llvm.org/features.html#expressivediags) 使用自动生成的解析器,您受限于生成器的功能。 简单:递归下降解析器易于编写、理解和调试。您无需成为解析专家或学习新工具即可扩展/改进解析器(这对于开源项目尤其重要),但您仍然可以获得出色的结果。

总体而言,对于 C++ 编译器来说,这并不重要:C++ 的解析部分并不重要,但它仍然是更容易的部分之一,因此保持简单是值得的。语义分析——特别是名称查找、初始化、重载解析和模板实例化——比解析复杂几个数量级。如果您想要证明,请查看 Clang 的“Sema”组件(用于语义分析)与“Parse”组件(用于解析)中的代码和提交分布。

【讨论】:

是的,语义分析要难很多。我们有大约 4000 行语法规则构成了我们的 C++11 语法,以及用于上面 Doub 列表的“语义分析”的大约 180,000 行属性语法代码,以及另外 100,000 行支持代码。解析确实不是问题,尽管如果你一开始就走错了,那就很难了。 我不太确定手写解析器是否必然更适合错误报告/恢复。在实践中,与增强由自动解析器生成器生成的解析器相比,人们在此类解析器上投入的精力似乎确实更多。关于这个话题似乎有很好的研究;这篇特别的论文真的引起了我的注意:M.G. Burke,1983,一种实用的 LR 和 LL 句法错误诊断和恢复方法,博士论文,纽约大学计算机科学系,见archive.org/details/practicalmethodf00burk ... 继续这个思路:如果您愿意修改/扩展/自定义您的手工构建的解析器以检查特殊情况以获得更好的诊断,那么您应该愿意在更好地诊断机械生成的解析器。对于可以为手动解析进行编码的任何特殊解析,您也可以对机械解析进行检查(对于 (G)LR 解析器,您几乎可以将其作为对约简的语义检查)。在某种程度上似乎令人不快,一个人只是懒惰,但这并不是对机械生成的解析器恕我直言的控诉。 @IraBaxter 如果您能与我们分享一些关于writing a decent parser by hand in C 的资源,我将非常高兴。 如果你想制作玩具,那是一条不错的路。它适用于真实语言,但解析器生成器确实是处理复杂语法的正确方法;我已经在这个问题的另一个答案中指出了这一点。如果您想编写递归下降解析器,我的另一个 SO 答案会告诉您如何去做。请参阅***.com/a/2336769/120163 Price:您可以处理解析器生成器为您处理的复杂情况。【参考方案4】:

gcc's parser is handwritten.。我怀疑clang也是如此。这可能有几个原因:

性能:您为特定任务手动优化的东西几乎总是比一般解决方案执行得更好。抽象通常会影响性能 时机:至少就 GCC 而言,GCC 早于许多免费的开发人员工具(1987 年问世)。当时没有免费版本的 yacc 等,我想这将是 FSF 人员的优先事项。

这可能不是“不是在这里发明”综合症的情况,而更像是“没有什么专门针对我们需要的东西进行优化,所以我们自己编写了”。

【讨论】:

1987 年没有 yacc 的免费版本?我认为 yacc 在 70 年代首次在 Unix 下交付时就有免费版本。而 IIRC(其他海报似乎相同),GCC 曾经 有一个基于 YACC 的解析器。我听说更改它的理由是为了获得更好的错误报告。 我想补充一点,从手写解析器生成好的错误消息通常更容易。 您在时间上的观点不准确。 GCC 曾经有基于 YACC 的解析器,但后来被手写的递归下降解析器取代。【参考方案5】:

那里有奇怪的答案!

C/C++ 语法不是上下文无关的。由于 Foo * bar,它们是上下文敏感的;模棱两可。我们必须建立一个 typedef 列表来知道 Foo 是否是一个类型。

Ira Baxter:我看不出你的 GLR 有什么意义。为什么要构建包含歧义的解析树。解析意味着解决歧义,构建语法树。您在第二遍中解决了这些歧义,所以这并不难看。对我来说它更丑陋......

Yacc 是一个 LR(1) 解析器生成器(或 LALR(1)),但它可以很容易地修改为上下文敏感。它没有什么丑陋的。 Yacc/Bison 是为了帮助解析 C 语言而创建的,所以它可能不是生成 C 解析器的最丑陋的工具......

在 GCC 3.x 之前,C 解析器由 yacc/bison 生成,并在解析期间构建 typedefs 表。通过“in parse” typedefs 表构建,C 语法变得本地上下文无关,而且“本地 LR(1)”。

现在,在 Gcc 4.x 中,它是一个递归下降解析器。它与 Gcc 3.x 中的解析器完全相同,仍然是 LR(1),并且具有相同的语法规则。不同之处在于 yacc 解析器已被手动重写,shift/reduce 现在隐藏在调用堆栈中,并且没有 gcc 3.x yacc 中的“state454 : if (nextsym == '(') goto state398”解析器,因此更容易修补、处理错误和打印更好的消息,并在解析期间执行一些接下来的编译步骤。代价是 gcc 新手的“易于阅读”代码少得多。

为什么他们从 yacc 切换到递归下降?因为避免 yacc 解析 C++ 是非常必要的,而且因为 GCC 梦想成为多语言编译器,即在它可以编译的不同语言之间共享最大的代码。这就是 C++ 和 C 解析器以相同方式编写的原因。

C++ 比 C 更难解析,因为它不是 C 的“本地”LR(1),甚至不是 LR(k)。 看看func<4 > 2>,它是一个用 4 > 2 实例化的模板函数,即func<4 > 2> 必须读作func<1>。这绝对不是 LR(1)。现在考虑func<4 > 2 > 1 > 3 > 3 > 8 > 9 > 8 > 7 > 8>。这是递归下降可以轻松解决歧义的地方,代价是更多的函数调用(parse_template_parameter 是歧义的解析器函数。如果 parse_template_parameter(17tokens) 失败,请再试一次 parse_template_parameter(15tokens), parse_template_parameter(13tokens) ...直到它起作用)。

我不知道为什么不能添加到 yacc/bison 递归子语法中,也许这将是 gcc/GNU 解析器开发的下一步?

【讨论】:

“对我来说,它更丑”。我可以告诉你的是,使用 GLR 和延迟歧义解决方案的生产质量解析器工程对于一个非常小的团队来说是实用的。我见过的所有其他解决方案都涉及多年在公共场合咬牙切齿地讨论使其与 LR、递归下降(如你所说)一起工作所需的后空翻和技巧。您可以假设许多其他很酷的新解析技术,但据我所知,目前这只是咬牙切齿。想法很便宜;执行是昂贵的。 @IraBaxter:老鼠! citeseerx.ist.psu.edu/viewdoc/… @Fizz:关于解析 Fortress 的有趣论文,这是一种复杂的科学编程语言。他们说了几件值得注意的事情:a)经典解析器生成器(LL(k),LALR(1))无法处理复杂的语法,b)他们尝试了 GLR,在规模上遇到了问题,但开发人员缺乏经验,所以他们没有完成 [这不是 GLR 的错] 并且 c) 他们使用了回溯(事务性)Packrat 解析器,并在其中付出了很多努力,包括产生更好的错误消息的工作。关于他们解析“|x||x←mySet,3|x”的示例,我相信 GLR 会做得很好而且不需要空格。 func<4 > 2> 不是func<1>。该代码无法编译。第一个>关闭模板。【参考方案6】:

似乎 GCC 和 LLVM-Clang 使用的是手写递归下降解析器,而不是机器生成的、基于 Bison-Flex 的自下而上解析。

尤其是 Bison,我认为如果不对某些内容进行模棱两可的解析并稍后再做第二遍,我就无法处理语法。

我知道 Haskell 的 Happy 允许使用单子(即依赖于状态的)解析器来解决 C 语法的特定问题,但我知道没有允许用户提供的状态单子的 C 解析器生成器。

理论上,错误恢复是支持手写解析器的一个重点,但我对 GCC/Clang 的经验是错误消息不是特别好。

至于性能 - 有些说法似乎没有根据。使用解析器生成器生成大型状态机应该会产生 O(n) 的结果,我怀疑解析是许多工具的瓶颈。

【讨论】:

这个问题已经有了一个非常高质量的答案,你要补充什么?

以上是关于GCC 和 Clang 解析器真的是手写的吗?的主要内容,如果未能解决你的问题,请参考以下文章

解析 Facebook - clang:错误:链接器命令失败,退出代码 1(使用 -v 查看调用)

Atiit 如何手写词法解析器

面试题|手写JSON解析器

编译器:gcc, clang, llvm

如何制作干净的clang前端?

Clang 访问修饰符顺序和 decltype