【问题标题】:Why can't C++ be parsed with a LR(1) parser?为什么不能用 LR(1) 解析器解析 C++?
【发布时间】:2010-09-19 14:14:21
【问题描述】:

我正在阅读有关解析器和解析器生成器的信息,并在 wikipedia 的 LR parsing -page 中找到了此语句:

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

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

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

【问题讨论】:

  • 就像:你需要了解递归才能学习递归;-)。
  • “一旦你解析了这个短语,你就会理解解析器。”

标签: c++ parsing grammar formal-languages


【解决方案1】:

LR 解析器在设计上无法处理模棱两可的语法规则。 (早在 1970 年代,当想法被制定出来时,这个理论就变得更容易了)。

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

x * y ;

它有两种不同的解析:

  1. 可以是y的声明,作为x类型的指针
  2. 它可以是 x 和 y 的乘积,会丢弃答案。

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

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

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

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

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

可以作弊,并在 LR 解析器来做这种消歧。 (此代码通常并不简单)。大多数其他解析器类型 有一些方法可以在各个点添加语义检查 在解析中,可用于执行此操作。

如果你作弊足够多,你可以让 LR 解析器为 C 和 C++。海湾合作委员会的家伙做了一段时间,但给了它 准备进行手动编码解析,我想是因为他们想要 更好的错误诊断。

不过,还有另一种方法,既漂亮又干净 并在没有任何符号表的情况下很好地解析 C 和 C++ 黑客:GLR parsers。 这些是完全无上下文的解析器(实际上是无限的 展望)。 GLR 解析器只接受 both 解析, 产生一棵“树”(实际上是一个主要是树状的有向无环图) 表示模棱两可的解析。 解析后传递可以解决歧义。

我们在 C 和 C++ 前端为我们的 DMS 软件再造工具包(截至 2017 年 6 月) 这些处理 MS 和 GNU 方言中的完整 C++17)。 它们已被用于处理数百万行 大型 C 和 C++ 系统,具有完整、精确的解析,生成具有完整源代码详细信息的 AST。 (见the AST for C++'s most vexing parse.

【讨论】:

  • 虽然 'x * y' 的例子很有趣,但在 C 中也会发生同样的情况('y' 可以是 typedef 或变量)。但是C可以被LR(1)解析器解析,那么和C++有什么区别呢?
  • 我的回答者已经观察到 C 有同样的问题,我想你错过了。不,出于同样的原因,它不能被 LR(1) 解析。呃,你是什么意思'y'可以是typedef?也许你的意思是“x”?这不会改变任何事情。
  • Parse 2 在 C++ 中不一定是愚蠢的,因为 * 可能会被覆盖以产生副作用。
  • 我看着 x * y 并轻笑了起来 - 令人惊讶的是,很少有人会想到这样漂亮的小模棱两可。
  • @altie 肯定没有人会重载移位运算符以使其将大多数变量类型写入流,对吧?
【解决方案2】:

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

(逗号分隔的表达式)。

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

【讨论】:

  • 如果能在此页面上对第 147 页进行一些总结,那就太好了。不过我会读那一页。 (+1)
  • 例子是:int(x), y, *const z; //含义:int x;整数y; int *const z; (一系列声明) int(x), y, new int; //含义: (int(x)), (y), (new int)); (逗号分隔的表达式)两个标记序列具有相同的初始子序列但不同的解析树,这取决于最后一个元素。在消除歧义之前可以有任意多个标记。
  • 嗯,在这种情况下,∞ 表示“任意多”,因为前瞻将始终受输入长度的限制。
  • 我对从博士论文中提取的引文感到非常困惑。如果存在歧义,那么根据定义,没有前瞻可能永远“解决”歧义(即决定哪个解析是正确的,因为至少有 2 个解析被语法认为是正确的)。此外,引用提到了 C 的歧义,但解释并未显示歧义,而只是一个非模棱两可的例子,其中解析决定只能在任意长时间的前瞻后做出。
【解决方案3】:

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

为了让“非上下文无关”的 yacc 解析器能够完美解析这个新语法,对 C++ 语法进行的最小修改是什么? (仅使用一个“hack”:类型名/标识符消歧,解析器通知每个 typedef/class/struct 的词法分析器)

我看到了几个:

  1. Type Type; 被禁止。声明为类型名的标识符不能成为非类型名标识符(请注意,struct Type Type 没有歧义,仍然可以使用)。

    names tokens 有 3 种类型:

    • 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 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 指令之外被禁止 这将是向许多 C++ 头文件中存在的愚蠢的sizeof int 歧义迈出的一大步

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

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

【讨论】:

  • C++ 根深蒂固。在实践中没有人会这样做。那些构建前端的人(比如我们)只是硬着头皮做工程以使解析器工作。而且,只要语言中存在模板,您就不会获得纯粹的上下文无关解析器。
【解决方案4】:

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

【讨论】:

  • 处理歧义的解析技术在解析时简单地生成 both AST 变体,并根据类型信息简单地消除不正确的变体。
  • @Ira:是的,这是正确的。这样做的特殊优势是它允许您保持第一阶段解析的分离。虽然它在 GLR 解析器中最为人所知,但我没有特别的理由认为您不能使用“GLL?”来访问 C++?解析器也是如此。
  • “GLL”?好吧,当然,但你必须弄清楚理论并写一篇论文以供其余人使用。更有可能的是,您可以使用自上而下的手动编码解析器,或回溯 LALR() 解析器(但保留“拒绝”)解析,或运行 Earley 解析器。 GLR 的优势在于它是一个非常好的解决方案,有据可查,而且现在已经得到充分证明。 GLL 技术必须具有一些非常显着的优势才能显示 GLR。
  • Rascal 项目(荷兰)声称他们正在构建无扫描仪 GLL 解析器。正在进行中,可能很难找到任何在线信息。 en.wikipedia.org/wiki/RascalMPL
  • @IraBaxter GLL 似乎有新的发展:请参阅这篇 2010 年关于 GLL 的论文 dotat.at/tmp/gll.pdf
【解决方案5】:

我认为您非常接近答案。

LR(1) 表示从左到右的解析只需要一个token 来预读上下文,而LR(∞) 表示无限预读。也就是说,解析器必须知道即将到来的所有内容才能确定它现在的位置。

【讨论】:

  • 我记得在我的编译器类中,对于 n > 0 的 LR(n) 在数学上可简化为 LR(1)。 n = infinity 不是这样吗?
  • 不,n 和无穷大之间的差异是一座无法逾越的山。
  • 答案不是:是的,给定无限的时间? :)
  • 实际上,根据我对 LR(n) -> LR(1) 发生方式的模糊回忆,它涉及创建新的中间状态,因此运行时是一些非'n' 的常数函数。翻译 LR(inf) -> LR(1) 将花费无限时间。
  • “答案不是:是的,给定无限的时间?” - 不:“给定无限的时间”这个短语只是一种无意义的简写方式,表示“在任何有限的时间内都无法完成”。当你看到“无限”时,想:“不是任何有限的”。
【解决方案6】:

C++ 中的“typedef”问题可以使用 LALR(1) 解析器进行解析,该解析器在解析时构建符号表(不是纯 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 的人。)

【讨论】:

  • 这是 GCC 使用的标准 hack 及其前 LR 解析器来处理诸如“x*y;”之类的歧义。唉,解析其他构造仍然需要任意大的前瞻要求,因此 LR(k) 不能成为任何固定 k 的解决方案。 (GCC 切换到递归下降,增加了更多的广告)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-27
  • 2020-03-01
  • 1970-01-01
  • 2020-08-23
  • 2018-03-30
  • 1970-01-01
  • 2011-02-10
相关资源
最近更新 更多