我正在阅读解析器和解析器生成器,并在维基百科的LR解析页面中找到了这个语句:
可以使用LR解析器的某些变体来解析许多编程语言。一个值得注意的例外是C ++。
为什么会这样? C ++的哪个特定属性导致无法使用LR解析器进行解析?
使用谷歌,我只发现C可以用LR(1)完美解析,但C ++需要LR(∞)。
答案 0 :(得分:224)
LR解析器无法通过设计处理模糊的语法规则。 (在20世纪70年代制定这些想法时,理论变得更容易了。)
C和C ++都允许以下语句:
x * y ;
它有两种不同的解析:
现在,您可能认为后者是愚蠢的,应该被忽略。 大多数人会同意你的意见;但是,有些情况可能会发生 有副作用(例如,如果乘法过载)。但这不是重点。 关键是是两个不同的解析,因此是一个程序 可能意味着不同的东西取决于应该如何解析。
编译器必须在适当的情况下接受适当的信息,并且在没有任何其他信息(例如,x的类型的知识)的情况下必须收集两者以便稍后决定做什么。因此语法必须允许这样。这使得语法含糊不清。
因此纯LR解析无法处理此问题。许多其他广泛可用的解析器生成器,例如Antlr,JavaCC,YACC或传统的Bison,甚至PEG样式的解析器,也不能以“纯粹”的方式使用。
有许多更复杂的情况(解析模板语法需要任意前瞻,而LALR(k)可以向前看大多数k代币),但只有一个反例才能击落纯 LR(或其他)解析。
大多数真正的C / C ++解析器通过使用一些来处理这个例子 一种具有额外黑客攻击的确定性解析器:它们与符号表交织解析 集合...所以当遇到“x”时, 解析器知道x是否是类型,因此可以 在两个潜在的解析之间做出选择。但解析器 这不是上下文,LR解析器 (纯粹的等等)(最好)没有上下文。
可以作弊,并添加每个规则缩减时间语义检查 到LR解析器做消除歧义。 (这段代码通常不简单)。大多数其他解析器类型 有一些方法可以在各个点添加语义检查 在解析中,可以用来做到这一点。
如果你作弊足够,你可以让LR解析器工作 C和C ++。海湾合作委员会的人做了一段时间,但给了它 手动编码解析,我认为因为他们想要 更好的错误诊断。
然而,还有另一种方法,它既干净又干净 并且在没有任何符号表的情况下解析C和C ++就好了 hackery:GLR parsers。 这些是完全无上下文解析器(实际上是无限的 展望)。 GLR解析器只接受两个解析, 产生一个“树”(实际上是一个主要是树状的有向无环图) 这代表了模棱两可的解析。 解析后的传递可以解决歧义。
我们在C和C ++前端使用这种技术 DMS Software Reengineering Tookit(截至2017年6月 这些处理MS和GNU方言中的完整C ++ 17)。 它们已被用于处理数百万行 大型C和C ++系统,具有完整,精确的解析,生成具有完整源代码细节的AST。 (见the AST for C++'s most vexing parse.)
答案 1 :(得分:90)
Lambda the Ultimate上有一个讨论LALR grammar for C++的有趣话题。
它包含指向PhD thesis的链接,其中包含对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));
(以逗号分隔的表达式)。
两个令牌序列具有相同的初始子序列但具有不同的解析树,这取决于最后一个元素。在消除歧义之前,可以有任意多个令牌。
答案 2 :(得分:13)
问题永远不会像这样定义,而应该是有趣的:
对C ++语法的最小修改是什么,这是必要的,以便这个新语法可以被“非上下文”yacc解析器完美解析? (仅使用一个'hack':typename / identifier disambiguation,解析器通知每个typedef / class / struct的词法分析器)
我看到几个:
Type Type;
被禁止。声明为typename的标识符不能成为非typename标识符(请注意struct Type Type
不明确且仍然允许)。
names tokens
有3种类型:
types
:builtin-type或者因为typedef / class / struct 将模板函数视为不同的标记可以解决func<
歧义问题。如果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;
这与演员运营商(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
歧义的重要一步
如果遇到使用模糊语法的C ++源代码,实现resyntaxed C ++的编译器会将source.cpp
移动到ambiguous_syntax
文件夹,并自动创建一个明确的翻译版source.cpp
编译它。
如果您了解一些,请添加不明确的C ++语法!
答案 3 :(得分:8)
正如您在answer here中看到的那样,C ++包含的语法无法通过LL或LR解析器进行确定性解析,因为类型解析阶段(通常是解析后)更改操作的顺序< / em>,因此AST的基本形状(通常预期由第一阶段解析提供)。
答案 4 :(得分:5)
我认为你非常接近答案。
LR(1)意味着从左到右的解析只需要一个令牌来预测上下文,而LR(∞)意味着无限的前瞻。也就是说,解析器必须知道即将发生的所有事情,以便找出它现在的位置。
答案 5 :(得分:1)
可以使用LALR(1)解析器解析C ++中的“ typedef”问题,该解析器在解析时会构建符号表(不是纯LALR解析器)。用这种方法可能无法解决“模板”问题。这种LALR(1)解析器的优点是语法(如下所示)是LALR(1)语法(没有歧义)。
/* C Typedef Solution. */
/* Terminal Declarations. */
<identifier> => lookup(); /* Symbol table lookup. */
/* Rules. */
Goal -> [Declaration]... <eof> +> goal_
Declaration -> Type... VarList ';' +> decl_
-> typedef Type... TypeVarList ';' +> typedecl_
VarList -> Var /','...
TypeVarList -> TypeVar /','...
Var -> [Ptr]... Identifier
TypeVar -> [Ptr]... TypeIdentifier
Identifier -> <identifier> +> identifier_(1)
TypeIdentifier -> <identifier> =+> typedefidentifier_(1,{typedef})
// The above line will assign {typedef} to the <identifier>,
// because {typedef} is the second argument of the action typeidentifier_().
// This handles the context-sensitive feature of the C++ language.
Ptr -> '*' +> ptr_
Type -> char +> type_(1)
-> int +> type_(1)
-> short +> type_(1)
-> unsigned +> type_(1)
-> {typedef} +> type_(1)
/* End Of Grammar. */
可以正确解析以下输入:
typedef int x;
x * y;
typedef unsigned int uint, *uintptr;
uint a, b, c;
uintptr p, q, r;
LRSTAR parser generator读取上面的语法符号,并生成一个解析器,该解析器处理“ typedef”问题,而在解析树或AST中没有歧义。 (披露:我是创建LRSTAR的人。)