为什么不能用LR(1)解析器解析C ++?


153

我正在阅读有关解析器和解析器生成器的信息,并在Wikipedia的LR解析页面中找到以下语句:

可以使用LR解析器的某些变体来解析许多编程语言。C ++是一个值得注意的例外。

为什么会这样呢?C ++的哪些特殊属性导致无法使用LR解析器进行解析?

使用谷歌,我只发现C可以用LR(1)完美解析,但是C ++需要LR(∞)。


7
就像:您需要了解递归才能学习递归;-)。
Toon Krijthe

5
“一旦您解析了该短语,您就会了解解析器。”
ilya n。

Answers:


92

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));

(以逗号分隔的表达式)。

这两个令牌序列具有相同的初始子序列,但解析树不同,这取决于最后一个元素。在消除歧义之前,可以有任意多个令牌。


29
在此页面上有关于页面147的一些摘要是很酷的。我将阅读该页面。(+1)
令人愉快的

11
示例是:int(x),y​​,* const z; //含义:int x; 诠释 int * const z; (一系列声明)int(x),y​​,新int;//含义:(int(x)),(y),(new int)); (逗号分隔的表达式)两个令牌序列具有相同的初始子序列,但解析树不同,这取决于最后一个元素。在消除歧义之前,可以有任意多个令牌。
Blaisorblade'1

6
好吧,在这种情况下,∞的意思是“任意多个”,因为前瞻总是受到输入长度的限制。
MauganRa 2014年

1
我对博士学位论文的引文感到非常困惑。如果存在歧义,那么按照定义,任何先行预测都不可能“解决”歧义(即,确定哪个解析是正确的,因为语法至少认为2个解析是正确的)。此外,引文提到C的歧义性,但说明并没有显示歧义性,仅是一个不明确的示例,其中解析决定只能在任意长时间的向前看之后才能做出。
dodecaplex

231

通过设计,LR解析器无法处理模棱两可的语法规则。(在1970年代提出想法时,使理论变得更容易)。

C和C ++都允许以下语句:

x * y ;

它具有两个不同的解析:

  1. 它可以是y的声明,作为指向x的指针
  2. 它可以是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。


11
虽然“ x * y”示例很有趣,但在C语言中可能会发生同样的情况(“ y”可以是typedef或变量)。但是C可以通过LR(1)解析器进行解析,所以C ++有什么区别?
Martin Cote

12
我的回答者已经观察到C存在相同的问题,我想您错过了。不,出于相同的原因,它不能被LR(1)解析。,您的意思是“ y”可以是typedef?也许您的意思是“ x”?那没有任何改变。
Ira Baxter,2009年

6
解析2在C ++中不一定是愚蠢的,因为*可能会被覆盖以产生副作用。
Dour High Arch

8
我看着x * y并笑了起来-令人惊讶的是,很少有人想到这样的小模棱两可的东西。
new123456 2011年

51
@altie当然没有人会重载一位移位运算符,以使其将大多数变量类型写入流中,对吗?
Troy Daniels

16

这个问题从来没有像这样定义过,但是应该很有趣:

什么是对C ++语法的最小修改集,以便可以用“无上下文无关”的yacc解析器完美地解析此新语法?(仅使用一个“ hack”:类型名/标识符歧义消除,解析器将每个typedef / class / struct通知词法分析器)

我看到一些:

  1. Type Type;是禁止的。声明为类型名的标识符不能成为非类型名标识符(请注意,struct Type Type它不是模棱两可的,仍然可以允许)。

    共有3种类型names tokens

    • types :内置类型或由于typedef / class / struct
    • 模板功能
    • 标识符:函数/方法和变量/对象

    将模板功能视为不同的标记可解决func<歧义。如果func是模板函数名称,则<必须是模板参数列表的开头,否则func是函数指针,并且<是比较运算符。

  2. Type a(2);是对象实例化。 Type a();并且Type a(int)是功能原型。

  3. int (k); 完全禁止,应写成 int k;

  4. typedef int func_type(); 并且 typedef int (func_type)();被禁止。

    函数typedef必须是函数指针typedef: typedef int (*func_ptr_type)();

  5. 模板递归限制为1024,否则可以将增加的最大值作为选项传递给编译器。

  6. 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*))

  7. typedef int type, *type_ptr; 也可以被禁止:每个typedef一行。因此它将成为

    typedef int type;

    typedef int *type_ptr;

  8. sizeof intsizeof charsizeof long long和合作。可以在每个源文件中声明。因此,每个使用该类型的源文件int都应以

    #type int : signed_integer(4)

    unsigned_integer(4)在该#type 指令之外被禁止,这将是迈向sizeof int这么多C ++头文件中存在的愚蠢歧义的一大步

如果实现了使用重新语法化的C ++的编译器,如果遇到使用歧义语法的C ++源代码,它也会移动source.cpp一个ambiguous_syntax文件夹,并且会source.cpp在编译之前自动创建明确的译文。

如果您知道一些C ++语法,请添加!


3
C ++根深蒂固。实际上,没有人会这样做。那些建立前端的人(像我们一样)只是硬着头皮做工程,以使解析器正常工作。而且,只要语言中存在模板,您就不会获得纯上下文无关的解析器。
Ira Baxter

9

正如您在此处的答案中所看到的那样,由于类型解析阶段(通常是后解析)改变了操作顺序,因此AST的基本形状(C所包含的语法不能由LL或LR解析器确定地解析)。通常期望由第一阶段的解析提供)。


3
处理歧义的解析技术仅会解析它们时生成两个 AST变体,并根据类型信息简单地消除不正确的变体。
Ira Baxter

@Ira:是的,这是正确的。这样做的特别好处是,您可以保持第一阶段解析的分离。尽管它在GLR解析器中最为人所知,但没有特别的理由让我看不到您无法使用“ GLL”打C ++吗?解析器也是如此。
山姆·哈威尔

“ GLL”?好吧,当然,但是您必须去弄清楚理论并写一篇论文供其余使用。更有可能的是,您可以使用自上而下的手动编码解析器,也可以使用回溯的LALR()解析器(但保留“已拒绝”)解析器,或者运行Earley解析器。GLR的优点是它是一个很好的解决方案,已经有充分的文档证明,并且目前已得到充分证明。GLL技术必须具有一些相当显着的优势才能显示GLR。
艾拉·巴克斯特

Rascal项目(荷兰)声称他们正在构建无扫描仪的GLL解析器。正在进行中,可能很难找到任何在线信息。 en.wikipedia.org/wiki/RascalMPL
艾拉·巴克斯特

@IraBaxter GLL似乎有了新进展:请参阅有关GLL dotat.at/tmp/gll.pdf的
Sjoerd

6

我认为您已经很接近答案了。

LR(1)意味着从左到右的解析只需要一个令牌就可以针对上下文进行前瞻,而LR(∞)意味着无限的前瞻。也就是说,解析器必须知道即将发生的一切,才能弄清楚现在的位置。


4
我从编译器类中回忆起,对于n> 0的LR(n)在数学上可简化为LR(1)。对于n =无穷大不是吗?
rmeador

14
不,n与无穷大之间存在着不可逾越的鸿沟。
迅速

4
答案不是:是的,给定无限的时间?:)
史蒂夫·福洛斯

7
实际上,根据我对LR(n)-> LR(1)发生方式的模糊回忆,它涉及创建新的中间状态,因此运行时是'n'的某些非恒定函数。翻译LR(inf)-> LR(1)将花费无限的时间。
亚伦

5
“不是答案:是的,给定无限长的时间?” -否:短语“给予无限长的时间”只是一种无意义的简捷表达方式,“在有限的时间量内无法完成”。当您看到“无限”时,请考虑:“没有任何有限”。
ChrisW

4

可以使用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的人。)


这就是GCC与以前的LR解析器一起使用的标准技巧,用于处理“ x * y”之类的歧义。las,解析其他构造仍然有任意大的超前要求,因此LR(k)不能成为任何固定k的解。(GCC切换到递归血统,带来更多麻烦)。
艾拉·巴克斯特
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.