编译技术教程:符号表与类型系统
系列导航: 《编译技术教程:前言》 《编译技术教程:词法分析》 《编译技术教程:抽象语法树》 《编译技术教程:语法分析》 《编译技术教程:符号表与类型系统》 《编译技术教程:语义分析》 《编译技术教程:附录 - 项目测试》 在语法分析部分,我们将单词序列转化为结构化的抽象语法树。然而抽象语法树依旧无法表达完整的程序语义。因为抽象语法树的定义基于上下文无关文法,而无法反映程序执行时的上下文环境。因此为了记录程序中的上下文相关信息,为后续语义分析阶段提供建议,我们需要定义新的数据结构以维护上下文信息。在程序设计语言中,最为关键的上下文信息即符号信息,因此本章节我们着重讨论符号表。 一、符号表 符号表是编译过程中的一个重要结构,主要用于记录各个符号的标识以及相应的信息,例如名称、作用域、类型、大小、维度、存储地址、行数等各项信息。其目的是在编译过程中遇到对应符号时即可快速查询到相关信息。接下来我们也将提供符号表的实现思路以供参考。要注意,符号表的设计与使用将贯穿后续的全部实验流程,请同学们三思而后行! 二、符号表的创建 在不区分作用域的情况下,符号表仅由需记录符号与符号信息的简单映射。但如果语言支持嵌套作用域,那么我们就需要表示各作用域符号表间的嵌套关系。在这种情况下,对于一张符号表来说,通常具有如图的结构:一个指向外层作用域符号表的指针 pre,表主体(符号名与对应信息),若干指向内层作用域符号表的指针 next。此外,在编译的过程中,有一个指向当前作用域符号表的指针 cur。 符号表的生成主要有以下几个操作: 初始时,创建一个全局变量符号表,也是最外层作用域符号表,cur 指向该符号表。 编译时,遇到变量声明语句,解析出需要的信息(一般包括类型、维度、大小等),填入 cur 指向的符号表。 编译时,进入新的作用域(block),生成新的符号表,设置 cur 指向的符号表的 next 指针指向新符号表,新符号表的 pre 设置为 cur,然后将 cur 指向新符号表,后续会在新符号表上填入信息。 编译时,离开作用域(block),通过 cur 指向的符号表的 pre 指针回溯至外层符号表,并对应修改 cur 指针。 不难发现,这样生成的符号表具有树状结构。当然,由于两个无嵌套关系的作用域中的符号并无任何联系,所以我们也可以按栈式结构来组织符号表,即进入新的作用域,就压入新的符号表,离开作用域时,弹出当前符号表。 在 tolangc 中,我们定义了一个栈式符号表。符号表类 SymbolTable 对应某一作用域的符号表。每一个符号表拥有一个 _father 字段,指向更外层作用域的符号表。另有 _symbols 字段存储当前作用域的符号。以符号名称为键,以符号信息(这里为 Symbol 类对象)作为值。 class SymbolTable : public std::enable_shared_from_this<SymbolTable> { // ... private: std::unordered_map<std::string, std::shared_ptr<Symbol>> _symbols; std::shared_ptr<SymbolTable> _father; }; 当进入新的作用域时,我们调用 push_scope 函数,创建新的符号表,并将该符号表的 _father 字段设置为当前符号表。...