用外行的话来说,剩下的递归是什么?


12

根据code.google.com上的一页,“左递归”的定义如下:

左递归只是指任何递归非终结符,当它产生包含自身的语句形式时,其自身的新副本出现在生产规则的左侧。

维基百科提供了两种不同的定义:

  1. 就上下文无关文法而言,如果非r产生的任何产品(“替代”)中的最左符号立即(直接/直接向左递归)或通过其他一些非终结符,则该非终结r是左递归定义(间接/隐藏的左递归)再次重写为r。

  2. “如果我们能找到一些非终结符A,那么语法将是左递归的,它最终会得出一个以其自身为左符号的句子形式。”

我只是在这里刚开始进行语言创建,而我在业余时间做。然而,当它归结为选择语言解析器,无论是左递归被该分析器或支持该分析器是一个问题马上来了前沿和中心。查找诸如“句子形式”之类的术语只会引出更多行话,但是“左”递归的区别几乎必须非常简单。请翻译?

Answers:


21

R如果R要首先确定是否匹配,则规则是左递归的R。当R直接或间接出现在某些产品本身的第一个术语中时,就会发生这种情况。

想象一下玩具的数学表达式版本,其中只有加法和乘法才能避免分散注意力:

Expression ::= Multiplication '+' Expression
            || Multiplication

Multiplication ::= Term '*' Term
                 || Term

Term ::= Number | Variable

如所写,这里没有左递归-我们可以将此语法传递给递归下降解析器。

但是,假设您尝试以这种方式编写它:

Expression ::= Expression '*' Expression
            || Expression '+' Expression
            || Term

Term ::= Number | Variable

这是一种语法,某些解析器可以应对,但是递归下降解析器和LL解析器则不能,因为规则是ExpressionExpression自身开始的。很明显,为什么在递归下降解析器中这会导致无限制的递归而不实际消耗任何输入。

规则是直接引用还是间接引用本身都没有关系。如果if A有一个以开头的备选方案B,并且B有一个以开头的备选方案A,则AB都是间接左递归的,并且在递归下降解析器中,它们的匹配功能将导致无限的相互递归。


因此,在第二个示例中,如果将第一个东西::=从from 更改ExpressionTerm,并且在第一个之后进行了更改||,那么它将不再是左递归的吗?但是,如果您只在之后执行操作::=,而不是之后执行操作||,那么它仍然是左递归的吗?
Panzercrisis

听起来您好像在说很多解析器从左到右,停在每个符号上,并在现场递归评估它。在这种情况下,如果在第一个之后和之后都Expression用切换出第一个,一切都会好起来的;因为迟早它会碰到既不是a 也不是a的东西,从而无需进一步执行就能够确定某些东西不是……Term::=||NumberVariableExpression
Panzercrisis

...但是,如果其中任何一个仍以开头Expression,则可能会找到不是的内容Term,并且会一直检查所有内容是否都是Expression一遍又一遍。是这个吗?
Panzercrisis

1
@Panzercrisis或多或少。您确实需要查找LL,LR和递归下降解析器的含义。
hobbs 2014年

这在技术上是准确的,但可能还不够简单(通俗易懂的术语)。我还要补充一点,在实践中,LL解析器通常具有检测递归并避免递归的能力(有可能拒绝在过程中有效的人为字符串),而且实际上大多数编程语言都在其中定义了语法。这样可以避免无限递归。

4

我会把它放在外行的术语里。

如果您以解析树(不是AST,而是解析器对输入的访问和扩展)的角度来考虑,则左递归会导致树向左和向下生长。正确的递归是完全相反的。

例如,编译器中的通用语法是项目列表。让我们获取一个字符串列表(“红色”,“绿色”,“蓝色”)并进行解析。我可以用几种方法来编写语法。以下示例分别是直接左递归或右递归:

arg_list:                           arg_list:
      STRING                              STRING
    | arg_list ',' STRING               | STRING ',' arg_list 

这些解析的树:

         (arg_list)                       (arg_list)
          /      \                         /      \
      (arg_list)  BLUE                  RED     (arg_list)
       /       \                                 /      \
   (arg_list) GREEN                          GREEN    (arg_list)
    /                                                  /
 RED                                                BLUE

注意它如何在递归方向上增长。

这并不是真正的问题,可以编写左递归语法...如果您的解析器工具可以处理它的话。自底向上的解析器可以很好地处理它。更现代的LL解析器也可以。递归语法的问题不是递归,而是递归而无需推进解析器,或者递归而不消耗令牌。如果我们在递归时总是消耗至少1个令牌,那么最终将到达解析的结尾。左递归定义为不消耗就递归,这是一个无限循环。

此限制纯粹是使用朴素的自上而下的LL解析器(递归下降解析器)实现语法的实现细节。如果您要坚持使用左递归语法,则可以通过在递归前重写生产以消耗至少1个令牌的方式来处理它,从而确保了我们永远不会陷入非生产循环中。对于任何左递归的语法规则,我们都可以通过添加一个中间规则来重写它,该中间规则将语法扩展到仅一个超前级别,从而消耗了递归生成之间的标记。(注意:我并不是说这是重写语法的唯一方法或首选方法,只是指出了通用规则。在这个简单的示例中,最好的选择是使用右递归形式)。由于这种方法是通用的,解析器生成器可以实现它而无需程序员(理论上)。实际上,我相信ANTLR 4可以做到这一点。

对于上面的语法,显示左递归的LL实现看起来像这样。解析器将从预测列表开始...

bool match_list()
{
    if(lookahead-predicts-something-besides-comma) {
       match_STRING();
    } else if(lookahead-is-comma) {
       match_list();   // left-recursion, infinite loop/stack overflow
       match(',');
       match_STRING();
    } else {
       throw new ParseException();
    }
}

实际上,我们真正要处理的是“天真实施”。我们最初以给定的句子为基础,然后递归地调用该预测的函数,然后该函数再次天真地调用相同的预测。

自下而上的解析器在两个方向上都没有递归规则的问题,因为它们不重新解析句子的开头,而是通过将句子重新组合在一起来工作。

如果我们从上至下进行生成,则语法中的递归仅是一个问题。我们的解析器通过使用令牌来“扩展”我们的预测来工作。如果像LALR(Yacc / Bison)自下而上的解析器中那样,而不是扩展而不是扩展(产量被“减少”),则任一侧的递归都不是问题。

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.