Answers:
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解析器无法处理模棱两可的语法规则。(在1970年代提出想法时,使理论变得更容易)。
C和C ++都允许以下语句:
x * y ;
它具有两个不同的解析:
现在,您可能认为后者是愚蠢的,应该忽略。大多数人会同意你的观点。但是,在某些情况下它可能会有副作用(例如,如果乘法过载)。但这不是重点。问题的关键是有有两种不同的解析,因此程序可以根据如何表达不同的意思应该已经被解析。
编译器必须在适当的情况下接受适当的一个,并且在没有其他任何信息(例如,x类型的知识)的情况下,必须收集两者,以便以后决定要做什么。因此,语法必须允许这种情况。这使语法变得模棱两可。
因此,纯LR解析无法处理此问题。也不能以“纯”方式使用许多其他广泛可用的解析器生成器,例如Antlr,JavaCC,YACC或传统的Bison,甚至PEG样式的解析器。
有很多更复杂的情况(解析模板语法需要任意先行,而LALR(k)最多可以预见k个令牌),但是仅需一个反例即可进行纯 LR(或其他)解析。
大多数真正的C / C ++解析器通过使用带有确定性的某种确定性解析器来处理此示例:它们将解析与符号表集合交织在一起,以便在遇到“ x”时,解析器知道x是否为类型或不可以,因此可以在两个潜在的分析之间进行选择。但是执行此操作的解析器不是上下文无关的,而LR解析器(纯解析器等)(最多)是上下文无关的。
可以作弊,并在LR解析器中添加按规则减少时间的语义检查,以消除歧义。(此代码通常并不简单)。其他大多数解析器类型都有一些方法可以在解析的各个点添加语义检查,可以用来执行此操作。
并且,如果您作弊得很充分,则可以使LR解析器适用于C和C ++。我认为GCC成员做了一段时间,但放弃了进行手工编码的解析,因为他们想要更好的错误诊断。
但是,还有另一种方法,它很干净,并且可以很好地解析C和C ++,而无需任何符号表黑客:GLR解析器。这些是完全上下文无关的解析器(有效地具有无限的前瞻性)。GLR解析器只接受两个解析,就产生一个表示歧义解析的“树”(实际上是一个有向无环图,主要是树状的)。解析后的过程可以解决歧义。
我们在DMS软件重新设计Tookit的C和C ++前端中使用了此技术(截至2017年6月,它们处理MS和GNU方言中的完整C ++ 17)。它们已用于处理数百万行大型C和C ++系统,具有完整而精确的解析程序,可生成带有源代码完整细节的AST。(有关C ++最令人讨厌的分析,请参阅AST。)
x * y
并笑了起来-令人惊讶的是,很少有人想到这样的小模棱两可的东西。
这个问题从来没有像这样定义过,但是应该很有趣:
什么是对C ++语法的最小修改集,以便可以用“无上下文无关”的yacc解析器完美地解析此新语法?(仅使用一个“ hack”:类型名/标识符歧义消除,解析器将每个typedef / class / struct通知词法分析器)
我看到一些:
Type Type;
是禁止的。声明为类型名的标识符不能成为非类型名标识符(请注意,struct Type Type
它不是模棱两可的,仍然可以允许)。
共有3种类型names tokens
:
types
:内置类型或由于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
和合作。可以在每个源文件中声明。因此,每个使用该类型的源文件int
都应以
#type int : signed_integer(4)
并unsigned_integer(4)
在该#type
指令之外被禁止,这将是迈向sizeof int
这么多C ++头文件中存在的愚蠢歧义的一大步
如果实现了使用重新语法化的C ++的编译器,如果遇到使用歧义语法的C ++源代码,它也会移动source.cpp
一个ambiguous_syntax
文件夹,并且会source.cpp
在编译之前自动创建明确的译文。
如果您知道一些C ++语法,请添加!
正如您在此处的答案中所看到的那样,由于类型解析阶段(通常是后解析)改变了操作顺序,因此AST的基本形状(C所包含的语法不能由LL或LR解析器确定地解析)。通常期望由第一阶段的解析提供)。
我认为您已经很接近答案了。
LR(1)意味着从左到右的解析只需要一个令牌就可以针对上下文进行前瞻,而LR(∞)意味着无限的前瞻。也就是说,解析器必须知道即将发生的一切,才能弄清楚现在的位置。
可以使用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解析器生成器读取上述语法表示法,并产生一个解析器手柄而不在分析树或AST歧义“的typedef”的问题。(披露:我是创建LRSTAR的人。)