为什么不能用LR(1)解析器解析C ++?
我正在阅读关于解析器和解析器生成器,并在维基百科的LR解析页面中找到了这个语句:
许多编程语言可以使用LR解析器的一些变体来解析。 一个明显的例外是C ++。
为什么这样? C ++的特殊属性是不可能用LR解析器解析的?
使用谷歌,我只发现C可以用LR(1)完美解析,但C ++需要LR(∞)。
Lambda Ultimate上有一个有趣的线索,讨论了C ++的LALR语法。
它包含一个博士论文的链接,其中包括对C ++解析的讨论,其中指出:
“C ++语法不明确,依赖于上下文,可能需要无限的前瞻来解决某些含糊之处”。
它继续给出了一些例子(参见pdf的第147页)。
这个例子是:
int(x), y, *const z;
含义
int x;
int y;
int *const z;
相比于:
int(x), y, new int;
含义
(int(x)), (y), (new int));
(逗号分隔的表达式)。
这两个标记序列具有相同的初始子序列,但具有不同的分析树,这取决于最后一个元素。 在消除歧义之前,可以有任意多个令牌。
按照设计,LR解析器不能处理模糊的语法规则。 (在20世纪70年代当理念被制定出来时,这个理论变得更容易)。
C和C ++都允许以下语句:
x * y ;
它有两个不同的解析:
现在,你可能会认为后者是愚蠢的,应该被忽略。 大多数人会同意你的看法。 然而,有些情况下可能会有副作用(例如,如果乘法过载)。 但那不是重点。 关键是有两种不同的分析,因此根据应该如何分析这个程序,程序可能意味着不同的事情。
编译器必须在适当的情况下接受适当的信息,并且在没有任何其他信息(例如,x的类型的知识)的情况下必须收集这两者以便稍后决定要做什么。 因此,语法必须允许这样做。 这使得语法模糊。
因此纯LR解析不能处理这个问题。 许多其他广泛使用的解析器生成器(如Antlr,JavaCC,YACC或传统Bison,甚至PEG风格的解析器)也不能以“纯粹”的方式使用。
有很多更复杂的情况(解析模板语法需要任意的lookahead,而LALR(k)可以向前看最多k个令牌),但只有一个反例才能击倒纯粹的LR(或其他)解析。
大多数真正的C / C ++解析器通过使用某种类型的确定性分析器来处理这个例子,并带有额外的黑客攻击:它们交织了符号表集合的解析...因此,在遇到“x”时,解析器知道x是否是一种类型或者不是,因此可以在两个潜在的解析之间进行选择。 但是,这样做的解析器不是上下文无关的,LR解析器(纯粹的解析器等)是(最好的)上下文自由的。
人们可以作弊,并在LR解析器中添加每规则缩短时间语义检查来执行此消除歧义。 (这段代码通常并不简单)。 大多数其他解析器类型都有一些方法可以在解析中的各个点添加语义检查,可以用来执行此操作。
如果你足够作弊,你可以让LR解析器适用于C和C ++。 海湾合作委员会的人做了一段时间,但放弃了手动编码解析,我想,因为他们想要更好的错误诊断。
还有另外一种方法,它非常干净,并且可以很好地解析C和C ++,而无需使用任何符号表hackery:GLR解析器。 这些是完整的上下文自由分析器(具有无限的前瞻性)。 GLR解析器只接受两个解析,生成一个“树”(实际上是一个定向的非循环图,主要是树状),它表示模糊的解析。 解析后的传递可以解决歧义。
我们在C和C ++前端为我们的DMS Software Reengineering Tookit使用这项技术(截至2017年6月,这些技术可处理MS和GNU方言中的完整C ++ 17)。 它们已被用于处理数百万行大型C和C ++系统,完整,精确地解析生成AST并提供完整的源代码细节。 (请参阅AST for C ++最令人头痛的解析。)
问题从来没有像这样定义,而应该是有趣的:
什么是对C ++语法的最小修改集合,以便这种新的语法可以通过“非上下文无关”的yacc解析器完美解析? (仅使用'hack':类型名称/标识符消除歧义,解析器通知每个typedef / class / struct的词法分析器)
我看到几个:
Type Type;
是禁止的。 声明为struct Type Type
标识符不能成为非struct Type Type
标识符(请注意, struct Type Type
不是不明确的,并且仍然可以允许)。
有三种类型的names tokens
:
types
:内建类型或由于typedef / class / struct 考虑到模板函数作为不同的标记解决了func<
ambiguity。 如果func
是模板函数名称,那么<
必须是模板参数列表的开始,否则func
是函数指针,而<
是比较运算符。
Type a(2);
是一个对象实例化。 Type a();
和Type a(int)
是函数原型。
int (k);
是完全禁止的,应该写成int k;
typedef int func_type();
和typedef int (func_type)();
被禁止。
一个函数typedef必须是一个函数指针typedef: typedef int (*func_ptr_type)();
模板递归被限制为1024,否则增加的最大值可以作为选项传递给编译器。
int a,b,c[9],*d,(*f)(), (*g)()[9], h(char);
也可能被禁止,取而代之的是int a,b,c[9],*d;
int (*f)();
int (*g)()[9];
int h(char);
每个函数原型或函数指针声明一行。
一个非常喜欢的替代方法是改变糟糕的函数指针语法,
int (MyClass::*MethodPtr)(char*);
被重新合成为:
int (MyClass::*)(char*) MethodPtr;
这与cast操作符(int (MyClass::*)(char*))
typedef int type, *type_ptr;
也可能被禁止:每typedef一行。 因此它会变成
typedef int type;
typedef int *type_ptr;
sizeof int
, sizeof char
, sizeof long long
和co。 可以在每个源文件中声明。 因此,使用int
类型的每个源文件都应该以
#type int : signed_integer(4)
和unsigned_integer(4)
将被禁止在#type
指令之外,这将成为很多C ++头文件中存在的愚蠢的sizeof int
歧义sizeof int
一大步
实施resyntaxed C ++编译器会,如果遇到一个C ++源利用模棱两可的语法,移动source.cpp
过的ambiguous_syntax
文件夹,并会自动创建一个明确的翻译source.cpp
编译它之前。
如果你知道一些,请添加你模糊的C ++语法!
链接地址: http://www.djcxy.com/p/74855.html