【问题标题】:Why is bottom-up parsing more common than top-down parsing?为什么自底向上解析比自顶向下解析更常见?
【发布时间】:2011-05-18 00:19:31
【问题描述】:

似乎递归下降解析器不仅解释最简单,而且设计和维护也最简单。它们不仅限于 LALR(1) 语法,而且代码本身可以被普通人理解。相比之下,自下而上的解析器对其能够识别的语法有限制,并且需要通过特殊工具生成(因为驱动它们的表几乎不可能手动生成)。

那么,为什么自下而上(即 shift-reduce)解析比自上而下(即递归下降)解析更常见?

【问题讨论】:

  • 纯递归下降与 LL(1) 大致相同,严格来说不如 LALR(1) 强大。您可以构建递归下降解析器并使用前瞻黑客、上下文检查(如 GCC 的原始 C++ 解析器所做的那样)等来欺骗,以处理更复杂的语言。但是如果你要作弊,你也可以用 LALR 解析器作弊。因此,当允许任意作弊时,很难比较技术。您可以说作弊更容易侵入手动编码的解析器。但是,最好不要作弊,只需使用非常强大的解析引擎即可。看我的回答。

标签: parsing context-free-grammar


【解决方案1】:

如果您选择功能强大的解析器生成器,您可以编写语法而不用担心特殊属性。 (LA)LR 意味着你不必担心左递归,少一个头痛。 GLR 意味着您不必担心局部歧义或前瞻。

自下而上的解析器往往非常高效。所以,一旦你付出了一些复杂的机器的代价,编写语法就会更容易,解析器也会表现得很好。

您应该期望在任何经常出现某种编程结构的地方看到这种选择:如果它更容易指定,并且性能相当好,即使机器很复杂,复杂的机器也会获胜。作为另一个例子,数据库世界已经转向关系工具,尽管您可以自己手动构建索引文件。编写数据模式更容易,更容易指定索引,并且背后有足够复杂的机器(您不必查看齿轮,只需使用它们),它们几乎可以毫不费力地快速运行。同样的原因。

【讨论】:

  • 好吧,你不必担心左递归,你只需要担心右递归。否则 +1 :)
  • 为什么要担心右递归?自下而上的生成器处理得很好。
  • 至少在 lex/yacc 或 flex/bison 领域,右递归规则破坏了用于 shift-reduce 解析的堆栈。当然,它仍然可以识别语法,但性能会很差。 (至少根据this)。也许其他解析器生成器可以更好地处理这个问题,但我没有使用任何其他此类工具。
  • 只有当列表非常长时才会爆栈,这在实践中非常罕见。对于非常长的递归结构,递归下降解析器将遇到完全相同的问题。有人会说这不是解析器的缺陷,而是使用固定大小的执行堆栈的缺陷,正如 Windows 或 Linux 平台上的大多数编程语言所提供的那样。 (我们中的一些人使用具有堆分配激活记录的语言;只有在虚拟内存用完时才会“炸毁堆栈”)。
  • 对不起,我错误地使用了“Blow out the stack”——我的意思是消耗大量的堆栈空间。我相信 Yacc 和 Bison 都使用堆分配的堆栈来进行移位减少解析(尽管我可能是错的)。
【解决方案2】:

它源于几个不同的东西。

BNF(以及语法等理论)来自计算语言学:研究自然语言解析的人们。 BNF 是一种非常有吸引力的语法描述方式,因此很自然地希望使用这些符号来生成解析器。

不幸的是,自上而下的解析技术在应用于此类符号时往往会失败,因为它们无法处理许多常见情况(例如左递归)。这样就剩下 LR 系列了,它性能很好,可以处理语法,而且由于它们是由机器生成的,谁在乎代码是什么样的?

不过,您是对的:自上而下的解析器更“直观”地工作,因此它们更易于调试和维护,并且只要您稍加练习,它们就与工具生成的解析器一样容易编写. (特别是当您进入转移/减少冲突地狱时。)许多答案都谈到解析性能,但实际上自上而下的解析器通常可以优化为与机器生成的解析器一样快。

这就是为什么许多生产编译器使用手写词法分析器和解析器的原因。

【讨论】:

  • 除了左递归还有哪些情况? (我不知道...)
  • 它不会出现在编程语言中,但是像 "P = ('x' P 'x') | 'x'" 这样的产生式对于除了最强大的 CFG 机器之外的所有机器都是有问题的,即使它是完全明确的。
  • 我不知道怎么做。使用递归下降是可以解析的,没有问题。即: P : if (next item == 'x') { if (P()) then { do first branch} else {do second branch} return true;} else { return false;如果允许更多的前瞻标记,那就更简单了。 (公平地说,我认为大多数 LALR(1) 解析器生成器也需要很长时间才能处理这样的产品)。最重要的是,当他们可以写 P: x+ 时,没有人会写出这样的语法。
  • 实际上,对于任何 k,该语法都不在 LR(k) 或 LL(k) 中。问题是你不知道在任何时候执行左还是右,因为你不知道你是否已经到达字符串的中间。 (尝试在一些测试输入上实际运行你的算法以了解问题:“xxxxx”应该解析而“xxxxxx”不应该。)当然,当你想说“没有人会写这样的语法”时“那你是对的;实际上,大多数编程语言都非常适合 LR(k),这就是你问的自底向上解析如此受欢迎的原因。
  • 恐怕我听不懂。 LL(k) 解析器无法识别这一点,但递归下降解析器可以毫无问题。这又让我想到了为什么自下而上的方法更受欢迎的问题。仅仅因为你并不经常需要这种能力并不意味着它没有用。 (此外,递归下降解析器很容易看到这一点,意识到可以计算 xs 并断言 X 的数量是奇数,然后在线性时间内简单地完成整个事情)
【解决方案3】:

递归下降解析器尝试假设输入字符串的一般结构,这意味着在到达字符串末尾之前会发生大量试错。这使得它们的效率低于自下而上的解析器,后者不需要这样的推理引擎。

随着语法复杂性的增加,性能差异会被放大。

【讨论】:

  • +1 -- 从未考虑过递归下降的常数因子可能很差。
【解决方案4】:

要补充其他答案,重要的是要认识到除了效率之外,自下而上的解析器可以接受比递归下降解析器更多的语法。自上而下的解析器 - 无论是否预测 - 只能有 1 个前瞻令牌,如果当前令牌和紧随其后的任何内容可以使用两种不同的规则派生,则失败。当然,您可以实现解析器以具有更多的前瞻(例如 LL(3)),但是在它变得像自下而上的解析器一样复杂之前,您愿意将它推多远?另一方面,自下而上的解析器(特别是 LALR)维护 firstsfollows 的列表,并且可以处理自上而下解析器无法处理的情况。

当然,计算机科学是关于权衡的。如果您的语法足够简单,那么编写自上而下的解析器是有意义的。如果它很复杂(例如大多数编程语言的语法),那么您可能必须使用自下而上的解析器才能成功接受输入。

【讨论】:

  • 递归下降解析器可以有任意前瞻——例如,C++ 需要任意前瞻,并且由 GCC 和 Clang 解析——这两个都是基于递归下降的系统——很好。在一般情况下,与其他解析器技术相比,LALR 的能力差很多
  • 是的,我确实提到过它们可以有任意数量的前瞻,但这只会增加它们的复杂性。 LALR(1) 的能力怎么不如 LL(1)??
  • LL 允许回溯,LALR 不允许。 en.wikipedia.org/wiki/LALR_parser见文末“与其他解析器的关系”
  • 除了有些语法即使在没有回溯的情况下任意向前看也无法匹配。这就是为什么 bison 添加了 GLR 支持,例如。
  • gcc c++ 解析器是递归下降的。我不认为复杂的语言 => 自下而上。
【解决方案5】:

我有两个猜测,但我怀疑其中任何一个都不能完全解释:

  1. 自上而下的解析可能很慢。递归下降解析器可能需要指数时间来完成它们的工作。这将对使用自顶向下解析器的编译器的可扩展性造成严重限制。

  2. 更好的工具。如果您可以用 EBNF 的某些变体来表达该语言,那么您很有可能可以通过 Lex/Yacc 摆脱大量繁琐的代码。似乎没有那么多工具可以帮助自动完成将自上而下的解析器放在一起的任务。让我们面对现实吧,编写解析器代码并不是玩弄语言的乐趣所在。

【讨论】:

  • 好吧,它们不会需要 LALR(1) 解析器可以支持的各种语法的指数时间。当然,它们可能需要更长的理论时间,但这是更强大的结果。 +1 第二点...没想到。
  • 公平点 - 但如果您坚持使用 LALR(1) 语法,为什么不继续走老路并使用 LALR 解析器呢?
  • 关于 #2:有一些解析器生成器输出递归下降解析器(从未使用过,仅参考 wiki) - 例如ANTLR。
  • 通常是因为我不想一直运行解析器生成器。 (而且因为大多数解析器生成器无法构建正确处理 Unicode 的解析器)
【解决方案6】:

我从未见过自顶向下解析器和 shift-reduce 解析器之间的真正比较:

只有 2 个小程序同时运行,一个使用自上而下的方法,一个使用自下而上的方法,每个大约 200 行代码,

能够解析任何类型的自定义二元运算符和数学表达式,两者共享相同的语法声明格式,然后可能添加变量声明和影响来展示如何实现“hacks”(非上下文无关)。

那么,如何诚实地谈论我们从未做过的事情:严格比较这两种方法?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-04
    • 2020-10-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多