编译技术教程:语义分析
系列导航: 《编译技术教程:前言》 《编译技术教程:词法分析》 《编译技术教程:抽象语法树》 《编译技术教程:语法分析》 《编译技术教程:符号表与类型系统》 《编译技术教程:语义分析》 《编译技术教程:附录 - 项目测试》 在之前的阶段中,我们编写了词法分析和语法分析程序,实现了源代码到抽象语法树的转换,这意味着我们就此得到了结构化的语法信息;另外我们还定义了符号表,从而能够将某一处的语义存储并关联到程序的其他部分;最后我们还定义了中间代码的数据结构,让我们能够方便地构建并输出中间代码。而在这些之后,我们需要一个阶段将这些内容全部连接起来,这就是语义分析阶段。 根据语言的不同,语义分析阶段的目标也可能很不相同。例如 C 的语义分析阶段肯定与 SQL 的语义分析差别巨大。不过总体来说,语义分析阶段的思路还是类似的,那就是遍历语法树、维护符号表并构建中间代码(或者类似的翻译操作)。 一、遍历语法树 树的遍历各位同学当然不陌生。但是怎么样遍历才更好却依旧是一个值得思考的问题。假设我们现在用 Node 类表示树的节点: struct Node { int val; std::vector<Node*> children; } 那么我们很容易想到两种遍历的方式。第一种便是在 Node 中增加一个方法 visit。 struct Node { int val; std::vector<Node> children; void visit() { // do something, like `std::cout << val << std::endl;` for (auto child : children) { child.visit(); } } }; 而第二种方式则是将 Node 作为参数。为了避免使用全局变量,我们需要创建一个新的类 Visitor。 struct Visitor { void visit(Node &node) { // do something, like `std::cout << val << std::endl;` for (auto child : children) { visit(child); } } }; 那么就这两种方法来说,哪种更加适合抽象语法树的遍历呢?答案是第二种使用 Visitor 的方法。因为语义分析的关键是跨节点的语义信息传递。采取第一种方法时,信息被隔离在了每一个节点之中,想要传播只能作为 visit 函数的参数和返回值,沿着节点的遍历顺序进行传递。而对于第二种方法,类中所有方法都共享一个类作用域,这意味着信息不仅能沿着遍历顺序传递,还能跨越语法树的分支传播到需要的地方,而不需要经过层层调用。这种功能十分重要,因为实现上下文关系的关键,符号,就是跨越语法树分支存在的。...