我正在编写一个手写递归下降解析器作为一项自我练习。我想看看是否有可能采用迭代方法。一般来说,我想知道我应该具有什么样的心态才能将相互依赖的递归函数转换为迭代解决方案。
在我当前的最小示例中,我有一个令牌列表(这只是一个带有lexeme的类型),它们通过递归下降被消耗,以构建由unique_ptr
表示的抽象语法树:
#include <string>
#include <memory>
#include <vector>
enum Type { Num, Add, Mul, LParen, RParen };
struct Token {
Type type;
std::string lexeme;
};
struct Expr {
Token token;
};
using AST = std::unique_ptr<Expr>;
struct Literal : public Expr {
double value;
};
struct Grouping : public Expr {
AST inner;
};
struct Binary : public Expr {
AST lhs, rhs;
};
using CIT = std::vector<Token>::const_iterator;
auto expression(CIT& it, CIT end) -> AST;
auto literal(CIT &it, CIT end) -> AST {
if (it != end and it->type == Type::Num) {
auto value = std::stod(it->lexeme);
auto token = *it++;
return std::make_unique<Literal>(Literal{ token, value });
}
else if (it != end and it->type == Type::LParen) {
auto token = *it++;
auto ast = std::make_unique<Grouping>(Grouping{ token, expression(it, end) });;
if (it != end and it->type == Type::RParen)
return ast;
else
throw "Mismatched parenthesis";
}
throw "Unable to parse literal";
}
auto multiplication(CIT &it, CIT end) -> AST {
auto ast = literal(it, end);
while (it != end and it->type == Type::Mul) {
auto token = *it++;
ast = std::make_unique<Binary>(Binary{ token, std::move(ast), literal(it, end) });
}
return ast;
}
auto addition(CIT &it, CIT end) -> AST {
auto ast = multiplication(it, end);
while (it != end and it->type == Type::Add) {
auto token = *it++;
ast = std::make_unique<Binary>(Binary{ token, std::move(ast), multiplication(it, end) });
}
return ast;
}
auto expression(CIT &it, CIT end) -> AST {
return addition(it, end);
}
int main() {
std::vector<Token> tokens = {
{ Type::Num, "5"},
{ Type::Add, "+"},
{ Type::LParen, "("},
{ Type::Num, "4"},
{ Type::Mul, "*"},
{ Type::Num, "3"},
{ Type::RParen, ")"},
};
auto it = tokens.begin();
auto ast = expression(it, tokens.end());
}
这里递归调用存在循环依赖:addition
依赖于multiplication
,multiplication
依赖于literal
,而literal
依赖于{ {1}}。
我想看看是否有一种方法可以将这些调用简化为单个迭代调用。我最初的想法是遍历令牌,并在运算符优先级之间进行切换。但是,我不确定该去哪里。
未完成尝试:
addition
我觉得我缺少某种堆栈,因为递归解决方案中的许多迭代解决方案似乎都维护着类似手动调用的堆栈。
任何提示将不胜感激。
编辑:尽管我认为我的问题与所链接的问题有所不同,但我已经审查了有关重复项的帖子。我不是在试图将单个递归函数变成一个迭代函数,而是在试图将多个递归函数变成一个迭代函数。我希望这可以解释为什么我问这个问题。
答案 0 :(得分:1)
您肯定需要一个堆栈才能解析嵌套表达式。递归下降解析器使用调用堆栈,这很方便,因为它使您无需实现托管堆栈数据类型。代价是,由于您不管理堆栈,因此需要依靠主机语言运行时来检测堆栈溢出。而您选择的宿主语言(C ++)不能做到这一点。
预测(自上而下)的解析可以通过显式堆栈轻松实现。包含语法符号的堆栈表示未解决的预测。最初,它由开始符号和输入结束标记组成。 (开始符号在顶部。)
解析算法非常简单:
如果堆栈的顶部是终端类型,则下一个令牌必须是该类型的令牌。
如果是,则处理输入令牌并弹出堆栈;如果堆栈现在为空,则解析完成。
如果输入令牌不是预期的类型,则解析失败。
如果堆栈的顶部是一个非终止符号,则根据对下一个输入令牌的观察来选择该符号的适当产生。 (或者令牌,如果您的解析器需要多个前瞻。)
如果有可能的生产,则会弹出堆栈,然后将预测生产的右侧从右向左推到堆栈上(这样,右侧的第一个符号在堆栈顶部)。
如果预测符号的生产不能以当前的前瞻性开始,则解析失败。
如果有空产品,则必须使用稍微复杂一些的预测算法;将根据其FOLLOW集而不是FIRST集来预测空产量。
在实践中,您实际上想对每个已识别的终端进行某些操作。通常,这需要另一个堆栈(一个或多个),并且大致相当于评估正向波兰语(前缀)表达式。