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

Why can't C++ be parsed with a LR(1) parser?

本文关键字:C++ 不能 LR 为什么      更新时间:2023-10-16

我读到了关于解析器和解析器生成器的文章,在维基百科的LR解析页面中发现了以下语句:

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

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

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

LR解析器在设计上不能处理模糊的语法规则。(早在20世纪70年代,当这些想法被制定出来时,这一理论就变得更容易了)。

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

x * y ;

它有两种不同的解析:

  1. 它可以是y的声明,作为指向类型x的指针
  2. 它可以是x和y的乘积,去掉答案

现在,你可能会认为后者很愚蠢,应该被忽视。大多数人会同意你的观点;然而,在某些情况下,它可能有副作用(例如,如果乘法运算过载)。但这不是重点。重点是两个不同的解析,因此一个程序可能意味着不同的东西,这取决于应该如何被解析。

编译器必须在适当的情况下接受适当的信息,并且在没有任何其他信息(例如,x类型的知识)的情况下,必须收集这两种信息,以便以后决定该做什么。因此,语法必须允许这样做。这使得语法模糊不清。

因此,纯LR解析无法处理此问题。许多其他广泛可用的解析器生成器,如Antlr、JavaCC、YACC或传统的Bison,甚至PEG风格的解析器,也不能以"纯"的方式使用。

有很多更复杂的情况(解析模板语法需要任意前瞻,而LALR(k)最多可以前瞻k个令牌),但只需要一个反例就可以否决pureLR(或其他)解析。

大多数真实的C/C++解析器通过使用一种具有额外破解的确定性解析器:它们将解析与符号表交织在一起收集从而在遇到"x"时,解析器知道x是否是一个类型,因此可以在两个潜在解析之间进行选择。但是解析器这样做并不是上下文无关的,LR解析器(纯粹的等等)是(充其量)上下文无关的。

可以作弊,并在到LR解析器来消除歧义。(这个代码通常并不简单)。大多数其他解析器类型有一些方法可以在不同的点添加语义检查在解析中,可以使用它来完成此操作。

如果你作弊足够多,你可以让LR解析器为C和C++。海湾合作委员会的人做了一段时间,但付出了我想是因为他们想要更好的错误诊断。

不过,还有另一种方法,那就是干净漂亮并在没有任何符号表的情况下解析C和C++hackery:GLR解析器。这些是完全上下文无关的解析器(实际上具有无限前瞻性)。GLR解析器只接受两个解析器,生成一个"树"(实际上是一个有向无环图,大部分类似树)表示不明确的解析。解析后的通行证可以解决歧义。

我们在C和C++前端使用这种技术DMS软件重组工具包(截至2017年6月它们在MS和GNU方言中处理完整的C++17)。它们已被用于处理数百万条线路大型C和C++系统,通过完整、精确的解析生成具有完整源代码细节的AST。(有关C++最麻烦的解析,请参阅AST。)

在Lambda the 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));

(逗号分隔的表达式)。

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

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

为了让"非上下文无关"的yacc语法分析器能够完美地解析这种新语法,对C++语法的最小修改集是什么?(只使用一个"破解":类型名称/标识符消除歧义,解析器通知lexer每个类型定义/类/结构)

我看到了一些:

  1. Type Type;被禁止。声明为typename的标识符不能成为非typename标识符(请注意,struct Type Type不是不明确的,仍然可以被允许)。

    names tokens有三种类型:

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

    将模板函数视为不同的令牌,解决了func<的模糊性。如果func是模板函数名,则<必须是模板参数列表的开头,否则func是函数指针,<是比较运算符。

  2. Type a(2);是一个对象实例化。CCD_ 11和CCD_。

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

  4. typedef int func_type();和CCD_ 16是禁止的。

    函数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和co。因此,每个使用类型int的源文件都应该以开头

    #type int : signed_integer(4)

    并且CCD_ 34将在该CCD_这将是向这么多C++头中存在的愚蠢的sizeof int模糊性迈出的一大步

如果遇到使用不明确语法的C++源,实现重新同步C++的编译器会将source.cpp移到ambiguous_syntax文件夹中,并在编译之前自动创建一个不明确的翻译source.cpp

如果你知道一些,请添加你不明确的C++语法!

正如您在我的回答中看到的,C++包含的语法无法由LL或LR解析器进行确定性解析,因为类型解析阶段(通常是解析后阶段)改变了操作的顺序,因此也改变了AST的基本形状(通常预期由第一阶段解析提供)。

我想你已经很接近答案了。

LR(1)意味着从左到右的解析只需要一个令牌来展望上下文,而LR(∞)意味着无限展望。也就是说,解析器必须知道发生的一切,才能弄清楚它现在在哪里。

C++中的"typedef"问题可以用LALR(1)解析器进行解析,该解析器在解析时构建符号表(而不是纯LALR解析器)。"模板"问题可能无法用这种方法解决。这种LALR(1)语法分析器的优点是语法(如下所示)是LALR(2)语法(没有歧义)。

/* 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语法分析器生成器读取上面的语法符号,并生成一个语法分析器,该语法分析器处理"typedef"问题,而不会在语法树或AST中出现歧义。(披露:我是LRSTAR的创始人。)