我一直在编写一些递归上升解析器,而我一直在努力解决的问题之一就是离开递归。对我来说,似乎很明显可以递归表达正确的递归,比如
addExpr
: primaryExpr '+' addExpr
| primaryExpr;
通过
的方式parseAddExpr() {
auto x = parsePrimaryExpr();
if (next_token == '+') {
auto result = make_unique<PlusExpr>();
result->lhs = x;
result->rhs = parseAddExpr();
return std::move(result);
}
return std::move(x);
}
但是对于左递归,我能想到的只是一个while循环。
mulExpr
: mulExpr '*' addExpr
| addExpr;
parseMulExpr() {
auto expr = parseAddExpr();
while(next_token == '*') {
auto mul_expr = make_unique<MulExpr>();
mul_expr->lhs = std::move(expr);
mul_expr->rhs = parseAddExpr();
expr = std::move(mul_expr);
}
return std::move(expr);
}
我的意思是,这个功能很好,但我总觉得它应该有一个递归版本。是否可以以递归方式而不是迭代方式实现左关联运算符?
答案 0 :(得分:8)
您的函数是递归下降,而不是递归 ascent 。递归下降解析器与你遇到的左递归有关的问题是众所周知和研究的。任何涵盖解析的编译器课程或教科书都将讨论问题以及解决问题的方法。
使用迭代是处理它的一种非常正常,有效的方法。例如See these course notes。在这些备注中,规则T -> T '*' S | T '/' S | S
(已添加分部的mulExpr
规则)转换为规则T -> S { ('*' | '/') S }
,其中大括号{...}
表示“零或更多重复” ”
根据你的评论,我认为你对“递归下降”意味着什么以及“递归上升”意味着什么感到困惑。
递归下降的基本思想是为语法中的每个非终结符创建一个函数。对应于某些非终结 A 的函数的作用是在可能的情况下完全解析 A 的一个实例,根据需要调用右侧出现的非终结符的函数 A 在语法中的作品。
因此,例如,你的语法有一个非终结addExpr
这两个作品:
addExpr -> primaryExpr '+' addExpr
addExpr -> primaryExpr
因此,递归下降解析器将具有addExpr
的函数,该函数尝试匹配addExpr
个作品之一的右侧,调用primaryExpr
和{{的函数1}}(本身!)必要时因为那些非终结符出现在addExpr
的作品中。
事实上,这正是您addExpr
函数中的内容。它会查找匹配其中一个parseAddExpr
作品的方法,并根据需要调用addExpr
和parsePrimaryExpr
。
递归上升是一种(非常罕见的)实现LR解析的方法。 LR解析器具有状态表,每个状态有一行,每个终端有一列。在递归上升解析器中,我们不将表表示为数据。相反,我们为每个状态创建一个函数,并且该状态的行在函数中体现为switch语句。
在LR解析器中,不通常是状态与非终结符之间或状态与产生之间的一对一对应关系。通常会有更多的州而不是制作。每个州代表制作中位置的集。
查看帖子中的功能,我没有看到你构建或体现状态表的证据。我看到的是一组函数,它们直接对应于语法的非终结符。这种对应是递归下降的标志。
此外,你在左递归制作中遇到麻烦这一事实是一个死的赠品,你正在使用递归下降。 LR解析器没有左递归和递归下降解析器的问题。