看来GCC和LLVM-Clang使用的是手写递归下降解析器,而不是机器生成的,基于Bison-Flex的自底向上解析器。
请问有人可以确认是这种情况吗?如果是这样,为什么主流编译器框架使用手写解析器?
更新:有关此主题的有趣博客
看来GCC和LLVM-Clang使用的是手写递归下降解析器,而不是机器生成的,基于Bison-Flex的自底向上解析器。
请问有人可以确认是这种情况吗?如果是这样,为什么主流编译器框架使用手写解析器?
更新:有关此主题的有趣博客
Answers:
是:
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 ++的单个统一解析器”部分。
foo * bar;
可以解析为乘法表达式(未使用结果)或bar
类型为pointer-to-的变量声明foo
。哪一个是正确的,取决于当时是否包含typedef
for foo
,这不是可以通过任何数量的前瞻来确定的。但这仅意味着递归下降解析器需要添加一些难看的额外机器来处理该问题。
有一个民间定理说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。
foo * bar;
歧义?
Clang的解析器是一个手写的递归下降解析器,其他几个开源和商业C和C ++前端也是如此。
Clang使用递归下降解析器有以下几个原因:
总的来说,对于C ++编译器来说,它没什么大不了的:C ++的解析部分并不重要,但它仍然是较容易的部分之一,因此有必要使其保持简单。语义分析(尤其是名称查找,初始化,重载解析和模板实例化)比解析要复杂几个数量级。如果需要证明,请检查代码的分布并提交Clang的“ Sema”组件(用于语义分析)和其“ Parse”组件(用于解析)。
gcc的解析器是手写的。。我怀疑c也一样。这可能是出于以下几个原因:
这可能不是“未在这里发明”综合症的情况,而是更多的是“没有针对我们所需的内容进行优化,因此我们编写了自己的”。
奇怪的答案在那里!
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解析器开发的下一步?
看来GCC和LLVM-Clang使用的是手写递归下降解析器,而不是机器生成的基于Bison-Flex的自底向上解析器。
尤其是野牛,我认为如果不对某些内容进行模棱两可的解析并稍后再进行第二遍,就无法处理语法。
我知道Haskell的Happy允许使用Monadic(即与状态有关)的解析器,可以使用C语法解决特定的问题,但是我不知道没有C解析器生成器允许用户提供状态Monad。
从理论上讲,错误恢复将是支持手写解析器的重点,但是我在GCC / Clang方面的经验是错误消息不是特别好。
至于性能-一些说法似乎没有根据。使用解析器生成器生成大型状态机应该会导致某些问题,O(n)
我怀疑解析是许多工具的瓶颈。