我会把它放在外行的术语里。
如果您以解析树(不是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)自下而上的解析器中那样,而不是扩展而不是扩展(产量被“减少”),则任一侧的递归都不是问题。
::=
从from 更改Expression
为Term
,并且在第一个之后进行了更改||
,那么它将不再是左递归的吗?但是,如果您只在之后执行操作::=
,而不是之后执行操作||
,那么它仍然是左递归的吗?