我正在用C ++编写解释器,到目前为止,我已经让我的词法分析器生成令牌了。问题是我不确定如何生成" walk"解析树。
我正在考虑使用数组数组来制作我的解析树,但我不确定如何以正确的顺序将令牌实际插入到解析树中。
我不确定是自上而下,左右或自下而上,左右。
有人能为我提供一个简单的LALR(1)算法吗?
答案 0 :(得分:11)
我会反对传统智慧,并说你应该使用自然语言特定的数据结构手动构建AST。
通用" AST数据结构"太过宽泛,不能轻松地工作 - 使用AST来做任何有用的代码都会被访问所需数据的变通方法所掩盖。如果你走这条路(或使用一个解析器生成器),你可能最终会建立一个转换层,从通用结构转到一个实际上对你的语言有意义的AST。为什么不直接避开中间人并构建最终的数据结构?
我建议编写第一个语法传递,它会消耗每个可能构造所需的标记(可能根据需要向前扫描一个标记)。这个语法传递将从链接的数据结构实例中构造AST,这些数据结构代表您语言中的每个可能的构造。例如,如果您的程序可以包含一系列语句,其中每个语句可以是函数声明或函数调用,则可以创建以下数据结构:
AST (struct)
-> std::vector<Statement*> statements
StatementKind (enum class)
-> Function
-> Call
Statement (struct)
-> StatementKind kind
Function : Statement (struct)
-> std::string name
-> std::vector<Statement*> body
Call : Statement (struct)
-> std::string name
-> Function* called
然后构建初始AST的语法传递可能如下所示:
auto ast = parseAST();
其中parseAST
重复调用parseStatement
,消耗和/或查看标记以确定该语句是函数定义还是函数调用,然后调用parseFunction
或{{1适当的。这被称为手写递归下降解析器,并且在我的经验中是迄今为止您可以编写的最简单和最好类型的解析器。是的,需要编写样板文件,但它也是非常清晰和灵活的代码。
语法阶段生成语法错误消息,尝试在发现错误时尽可能地恢复(这是分号分隔语言的一个参数 - 编译器和工具通常可以从错误中恢复更多很容易,因为在尝试解析下一个构造之前遇到错误时跳到下一个分号通常就足够了。)
如果在定义函数之前允许调用函数,这也很容易处理 - 只需解析调用部分而不检查在您第一次解析它时是否存在具有该名称的函数,然后再将其关联
通过AST的第二个语义传递将使用任何缺少的交叉引用数据对其进行注释(例如,使用这些函数定义解析函数调用和函数名称,或者如果找不到名称则生成错误)。完成后,您将拥有一个完整的AST,保证代表一个有效的程序(因为您在此过程中执行了所有语义错误检查),并且可以对其应用优化,然后是代码生成阶段(以及沿途的更多优化)。
请注意,我完全忽略了对LL或LR解析器等的任何提及。您的语言的理论符号和分类很重要(例如,因为它可以证明它是非模糊的),但是来自从实现的角度来看,拥有干净,易于理解且最重要的易于修改的代码比遵守特定的解析机制要重要得多。使用手写解析器的其他编译器的示例包括GCC,Clang和Google的V8等。
就效率而言,AST在构建之后通常会迭代几次,并且需要在内存中保留到该过程的后期(可能直到结束,具体取决于您如何执行代码生成),因此最好使用对象池为AST分配记录,而不是在堆上单独动态分配所有内容。最后,代替字符串,通常最好在原始源代码中使用偏移量和长度。
答案 1 :(得分:5)
您可以使用某些parser generator,例如bison或ANTLR。两者都有很好的文档与教程部分。语法规则的操作部分(提供给bison
或antlr
生成用于解析的C ++代码)将构建抽象语法树。
如果您不想了解要解析和解释的正式语言的syntax(以及semantics),我们无法提供更多帮助。
如果您的语言是中缀计算器,那么野牛会有example。
您可能应该考虑使用类层次结构来表示AST的节点。你有一个叶子类(例如数字),一个用于添加节点的类(有两个儿子作为smart pointers到其他节点)等等。
e.g。
class ASTNode {
/// from http://stackoverflow.com/a/28530559/841108
///... add some things here, e.g. a virtual print method
};
class NumberNode : public ASTNode {
long number;
/// etc...
};
class BinaryOpNode : public ASTNode {
std::unique_ptr<ASTNode> left;
std::unique_ptr<ASTNode> right;
/// etc....
};
class AdditionNode : public BinaryOpNode {
/// etc....
};
class CallNode : public ASTNode {
std::shared_ptr<Function> func;
std::vector<std::unique_ptr<ASTNode>> args;
};
对于可变数据的节点(即任意数量的儿子),您需要一个智能指针向量,如上面的args
。
要遍历树,您将进行递归遍历,以便更好地使用智能指针。另请阅读visitor pattern。使用C ++ 11 std::function - s和匿名闭包-i.e lambdas - ,您可以拥有一个visit
虚函数(您可以为每个节点提供一个闭包)。访问文件树的Unix nftw(3)函数可能是鼓舞人心的。
答案 2 :(得分:2)
人们将AST构建为堆分配的树。 (是的,你可以做一个阵列,但它不是很方便)。我建议你阅读野牛文件;它会向你解释如何建造树木,你可以遵循这种风格。
根据您的问题猜测您的经验水平,如果我是您,我会建立一个基于flex / bison的解析器/ AST构建器至少一次以获得良好的体验,然后再回到自己构建一切。
答案 3 :(得分:0)
我使用了一组BNF生成方法来生成特定类型的节点(结构继承)以生成AST。使用std :: move(),您可以转移指针所有权以避免指针悬空。然后,有一个公共的递归访问方法(切换用例),该方法按照特定的遍历模式(后/前顺序)遍历AST,检查AST节点类型并为每次访问执行accept()。接受被绑定到必须由用户实现的调度程序接口(打印树,执行树等)。对于每次访问,都会调用用户端的相应方法。