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


90

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

请问有人可以确认是这种情况吗?如果是这样,为什么主流编译器框架使用手写解析器?

更新有关此主题的有趣博客


27
几乎所有主流编译器都使用手写解析器。有什么问题吗?
SK-logic

2
如果需要性能,则必须手动执行(半)。
Gene Bushuyev 2011年

15
而且不仅表现-更好的错误信息,恢复的能力,等等
SK-逻辑

MS VisualStudio呢?尽管不是开源的,但是来自MS的人可以验证他们是否也在使用手写递归下降解析器吗?
OrenIshShalom '17

3
@GeneBushuyev,来自GCC Wiki:“ ...尽管计时显示出1.5%的加速,但主要好处是促进了未来的增强……”这种加速似乎相当微不足道
OrenIshShalom

Answers:


78

是:

  • GCC使用YACC(野牛)解析器曾几何时,但它与在3.x系列的一些点手写递归下降解析器代替:看http://gcc.gnu.org/wiki/New_C_Parser为指向相关补丁提交的链接。

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


3
这是否意味着ObjC,C和C ++具有LL(k)语法?
林德曼

47
否:即使C,这三个中最简单的一个,也有一个模棱两可的语法。例如,foo * bar;可以解析为乘法表达式(未使用结果)或bar类型为pointer-to-的变量声明foo。哪一个是正确的,取决于当时是否包含typedeffor foo,这不是可以通过任何数量的前瞻来确定的。但这仅意味着递归下降解析器需要添加一些难看的额外机器来处理该问题。
马修·斯拉特利

9
我可以从经验证据中确认C ++ 11,C和Objective C具有GLR解析器可以处理的上下文无关文法。
Ira Baxter

2
关于上下文敏感度,此答案均未声明:解析这些语言很可能是图灵完成的。
Ioannis Filippidis 2014年

106

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

这不是真的

真实的是,使用CLR和C ++很难使用LALR(1)解析器进行解析,而又不会破坏解析机制并弄乱符号表数据。实际上,GCC曾经使用YACC和其他黑客程序来解析它们,是的,这很丑陋。 现在,GCC使用手写的解析器,但仍然使用符号表hackery。lang族人从来没有尝试过使用自动解析器生成器。AFAIK Clang解析器始终是手工编码的递归下降。

事实是,C和C ++相对容易使用更强大的自动生成的解析器(例如GLR解析器)进行解析,并且您不需要任何技巧。所述埃尔莎C ++解析器是这样的一个例子。我们的C ++前端是另一个(与我们所有的“编译器”前端一样,GLR是非常出色的解析技术)。

我们的C ++前端不如GCC快,并且肯定比Elsa慢。我们投入了很少的精力进行仔细的调优,因为还有其他更紧迫的问题(尽管它已在数百万行C ++代码中使用)。艾尔莎(Elsa)可能比海湾合作委员会(GCC)慢,只是因为它更笼统。考虑到这些天的处理器速度,在实践中这些差异可能并不重要。

但是今天广泛分布的“真正的编译器”起源于10或20年前或更长时间的编译器。效率低下的问题就更加重要了,而且没人听说过GLR解析器,所以人们做了他们知道如何做的事情。叮当肯定是较新的,但后来民间定理在很长一段时间内一直保持其“说服力”。

您不必再那样做了。您可以非常合理地将GLR和其他此类解析器用作前端,从而改善了编译器的可维护性。

什么真实的,是让符合您的睦邻友好编译器的行为语法是很难的。尽管实际上所有C ++编译器都实现了(大多数)原始标准,但它们也往往具有许多暗角扩展,例如MS编译器中的DLL规范等。如果您拥有强大的解析引擎,则可以花一些时间尝试获取最终的语法以匹配实际情况,而不是尝试弯曲语法以匹配解析器生成器的限制。

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

编辑2015年2月:...现在处理完整的C ++ 14。(请参阅从人类可理解的AST中获取有关CLR代码的GLR解析,以及一些简单的代码,以及C ++臭名昭著的“最令人讨厌的解析”)。

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


6
PostScript:就像使语法与供应商的实际工作相匹配一样困难,而使名称和类型解析与其他供应商对C ++ 11手册的解释相匹配就更加困难,因为您拥有的唯一证据就是程序会稍微编译如果可以找到它们的话,就不一样了。截止到2013年8月,对于C ++ 11来说我们已经远远超过了那个标准,但是我对C ++委员会感到有些失望,该委员会似乎不情愿以C的形式产生更大的标准(并且根据经验,更加令人困惑)。 ++ 1岁。
Ira Baxter

5
我真的很想知道:您如何处理这种foo * bar;歧义?
马丁

14
@Martin:我们的解析器以两种方式解析它,生成一棵包含特殊“歧义节点”的树,其子节点是替代解析器;孩子们最大程度地分享了他们的孩子,因此我们最终得到了DAG而不是树。 经过分析完成,我们运行(“行走的树,做的东西”,如果你不知道它奇特的名字)的属性语法评估(AGE)在其DAG计算类型的所有声明的标识符。...
艾拉·巴克斯特

12
...模棱两可的孩子不能同时保持类型一致;AGE在发现无法明智键入的模棱两可的孩子时,只会将其删除。剩下的就是好孩子。因此,我们确定了“ foo bar”的哪个解析是正确的。此技巧适用于我们为C ++ 11的真实方言构建的真实语法中发现的各种疯狂的歧义,并且*将名称的解析与语义分析完全分开。这种干净的分离意味着要进行的工程工作更少(无需纠结)。有关更多讨论,请参见stackoverflow.com/a/1004737/120163
Ira Baxter 2014年

3
@TimCas:实际上,我正与您一起探讨设计语言语法(和语义)的愚蠢行为,这些愚蠢的问题太复杂了,以至于很难正确地做到这一点(是的,C ++语言在这里遭受的痛苦严重)。我希望语言设计委员会能够设计语法,以便使用更简单的解析技术,并明确定义语言语义并使用某些语义分析工具进行检查。las,世界似乎并非如此。因此,我认为,尽管笨拙,您还是要尽可能地构建自己必须构建的东西,并继续生活。
伊拉·巴克斯特

31

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

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

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

总的来说,对于C ++编译器来说,它没什么大不了的:C ++的解析部分并不重要,但它仍然是较容易的部分之一,因此有必要使其保持简单。语义分析(尤其是名称查找,初始化,重载解析和模板实例化)比解析要复杂几个数量级。如果需要证明,请检查代码的分布并提交Clang的“ Sema”组件(用于语义分析)和其“ Parse”组件(用于解析)。


4
是的,语义分析要困难得多。我们有大约4000行语法规则构成了C ++ 11语法,还有大约180,000行属性语法代码用于上面的“语义分析” Doub列表,另外还有100,000行支持代码。解析实际上不是问题,尽管如果您选择错误的脚步已经足够困难了。
Ira Baxter

1
我不确定手写解析器对于错误报告/恢复是否一定更好。实际上,人们投入了更多精力在此类解析器上,而不是在实践中投入了更多精力来增强自动解析器生成器生成的解析器。关于这个话题似乎有很好的研究。这篇特别的论文确实引起了我的注意:MG Burke,1983年,一种实用的LR和LL句法错误诊断和恢复方法,博士学位论文,纽约大学计算机科学系,请参见archive.org/details/practicalmethodf00burk
Ira Baxter

1
...继续这一思路:如果您愿意修改/扩展/自定义手动分析器以检查特殊情况以更好地进行诊断,那么您应该愿意为更好地诊断机械生成的分析器做出同等的投资。对于可以为手动编码进行编码的任何特殊解析,您也可以为机械编码进行编码(对于(G)LR解析器,您几乎可以将其作为对归约的语义检查来完成)。在某种程度上看似令人食欲不振,但这只是懒惰,但这并不是对机械生成的解析器恕我直言的指责。
Ira Baxter

8

gcc的解析器是手写的。。我怀疑c也一样。这可能是出于以下几个原因:

  • 性能:您为特定任务手动优化的几乎总是比常规解决方案更好。抽象通常会影响性能
  • 定时:至少在GCC方面,GCC早于许多免费的开发人员工具(于1987年问世)。当时还没有免费版本的yacc等,我想这在FSF的工作人员中将是优先考虑的事情。

这可能不是“未在这里发明”综合症的情况,而是更多的是“没有针对我们所需的内容进行优化,因此我们编写了自己的”。


15
1987年没有免费版本的yacc?我认为yacc在70年代首次在Unix下交付时就有免费版本。和IIRC(其他张贴者似乎一样),GCC曾经有一个基于YACC的解析器。我听说更改它的借口是获得更好的错误报告。
伊拉·巴克斯特

7
我想补充一点,从手写解析器生成良好的错误消息通常更容易。
Dietrich Epp

1
您对时间的观点不正确。GCC曾经有基于YACC的解析器,但后来被手写的递归下降解析器取代。
汤米·安德森

7

奇怪的答案在那里!

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

艾拉·巴克斯特(Ira Baxter):我不明白您的GLR观点。为什么要构建包含歧义的解析树。解析意味着解决歧义,构建语法树。您可以在第二遍解决这些歧义,因此这并不难看。对我来说,这要丑得多……

Yacc是LR(1)解析器生成器(或LALR(1)),但是可以轻松地对其进行修改以使其与上下文相关。而且里面没有丑陋的东西。Yacc / Bison的创建是为了帮助解析C语言,因此它可能不是生成C解析器的最丑陋的工具。

在GCC 3.x之前,y解析器由yacc / bison生成,并在解析过程中建立了typedefs表。通过“ in parse” typedefs表的构建,C语法变得在本地不受上下文限制,并且还成为“在本地LR(1)”。

现在,在Gcc 4.x中,它是一个递归下降解析器。它与Gcc 3.x中的解析器完全相同,仍然是LR(1),并且具有相同的语法规则。区别在于yacc解析器已被手工重写,移位/减少现在隐藏在调用堆栈中,并且没有gcc 3.x yacc中的“ state454:if(nextsym =='(')goto state398”解析器,因此更容易修补,处理错误和打印更好的消息,以及在解析过程中执行一些后续的编译步骤,但代价是gcc noob的“易读”代码要少得多。

为什么他们从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解析器开发的下一步?


9
“对我来说,这更加丑陋”。我可以告诉您的是,使用GLR和延迟歧义解决方案设计生产质量解析器对于一个非常小的团队来说是切实可行的。我见过的所有其他解决方案都涉及数年之久的公开宣传,以使它与LR,递归血统(您命名)一起使用需要的后空翻和骇客攻击。您可以假设许多其他很酷的新解析技术,但是据我所知,这只是更多的选择。想法很便宜;执行是亲爱的。
艾拉·巴克斯特


@Fizz:有关解析Fortress(一种复杂的科学编程语言)的有趣论文。他们说了几点注意事项:a)经典解析器生成器(LL(k),LALR(1))无法处理严格的语法,b)他们尝试了GLR,在规模方面遇到了麻烦,但是开发人员没有经验,所以他们没有完成[不是GLR的错],并且c)他们使用了回溯(事务性)Packrat解析器,并付出了很多努力,包括产生更好的错误消息的工作。关于他们解析“ {| x || x←mySet,3 | x}”的示例,我相信GLR可以做到这一点,并且不需要空格。
艾拉·巴克斯特

0

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

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

我知道Haskell的Happy允许使用Monadic(即与状态有关)的解析器,可以使用C语法解决特定的问题,但是我不知道没有C解析器生成器允许用户提供状态Monad。

从理论上讲,错误恢复将是支持手写解析器的重点,但是我在GCC / Clang方面的经验是错误消息不是特别好。

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


3
这个问题已经有非常高质量的答案,您要添加什么?
tod
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.