编译原理实验报告
实验二:语义分析 2025 年 4 月 11 日
姓名:俞仲炜 学号:3220104929
功能清单
类型定义相关功能函数
在 type.hpp
中我们定义了三个存储类型信息的类,并在 type.cpp
中实现了检查类型是否相等的方法,以便类型检查。具体而言,我们主要借助了 std::dynamic_pointer_cast
函数对待检测类型进行强制转化为某个类型,若转换成功则说明待检测类型即转换后的类型,否则说明不是。
于是,我们有了统一的容器存储变量的类型信息。
符号表
符号表用于存储变量信息,包括符号的名称、唯一名称以及类型。
在 symbol_table.hpp
中,我们定义了符号类以及符号表类,前者全面地存储各个变量的信息(名字,唯一标识符,作用域,类型),后者则负责管理符号与作用域。对于符号表,我直接设置了 int curr_scope_level
变量用于存储符号表当前所在的作用域深度,全局作用域深度为 0
,每进入一层作用域深度就增加 1
,退出则减少 1
;而符号表的实现风格采用了简化的函数式风格,即设置了一个哈希表的 vector
名为 symtbl_stack
,每进入一层就 push 一个哈希表进去,并通过 curr_scope_level
变量访问当前作用域,而哈希表即符号名向符号类的映射。
在 symbol_table.cpp
中,我们实现了符号表的方法,使得其能够控制作用域、添加和查找符号。值得一提的是查找符号方法,对于我们简化的函数式风格符号表,若需要进入更高层的作用域查找,只需 index
减少 1
即可。
于是,我们有了符号表,能够添加和查找符号、更新作用域,并能够检查变量是否被定义、作用域是否正确、类型是什么,等等。
类型检查
类型检查是本次实验实现的最主要的功能。
在 type_checker.hpp
中,我们将类型检查器定义为了类。
在 type_checker.cpp
中,我们实现了 type_checker
的各种方法。我们在创建 type_checker
时会创建符号表并加入编译器内置函数;我们会通过遍历的方式检查一个未知的结点是什么,这也是类型检查的起点;我们通过递归的方式遍历语法树的每一个结点,并根据语法树结点类型,借助符号表进行相应的类型检查,对错误操作进行报错。
例如,检查赋值语句左值右值类型是否一致,检查 if
和 while
语句条件语句类型,检查函数定义是否正确,检查定义是否重复,检查函数调用是否正确,等等。checker
都是根据 SysY 的规则按部就班地实现,在需要的位置进行规则检查,若出问题则通过 ASSERT
宏进行报错。
特别的,我们实现了对数组初始化的检查,并用临时变量存储了初始化后的数组及其打印函数。数组初始化无法按部就班地翻译规则,因其初始化方式相对较多,需要检查的条件也多。
[!NOTE]
本实验中若数组初始化右值中包含变量,则会打印出错,因为变量对应的值仍无法获取.
下面将数组初始化语句的右值里的一对大括号内部称为“一层”,数组初始化检查的主要操作如下:
- 采用递归,对右值的每一层进行检测:
- 进入每层时先通过当前深度和已填充大小确定该层要填充的子数组,然后按序遍历该层每一个元素:
- 若有更深层则直接进入;
- 若为整型则记录。
- 若当前层填充数量超过子数组大小则报错
- 若当前层未填满子数组,则剩余部分填充
0
- 若总填充数量超过数组总大小则报错
- 若深度超越左值维度则报错
- 返回当前层填充数量,交给外层汇入其当前层填充数量
- 进入每层时先通过当前深度和已填充大小确定该层要填充的子数组,然后按序遍历该层每一个元素:
初始化后的数组元素通过 vector 存储,在上述步骤中识别到整型便获取值并 push 进 vector,在填充子数组的位置 push 对应数量的 0,然后在外部函数结合左值的维度信息便可得到初始化后的数组。
亮点摘选
检查函数定义时需要进入新的作用域检查函数内部,而在内部检查 return
语句时需要获取函数的返回类型,但正常来说内部无法知晓函数名,进而无法从符号表获取函数的返回类型。
由于 SysY 不支持嵌套函数,所以可以通过全局变量存储函数名的方式,但全局变量可维护性差,容易命名冲突(不过也可以设置 namespace),所以尝试寻找另一种方法。
最终选择了简单粗暴的直接给返回类型设置一个 “实践中用户不会使用” 的标识符,将其作为一个符号存储到新作用域下的符号表。
TypePtr TypeChecker::checkFuncDef(AST::FuncDefPtr node) {
// ...
symbol_table.enter_scope();
// ...
symbol_table.add_symbol("func_return_type_1145141919810", return_type);
// ...
symbol_table.exit_scope();
// ...
}
TypePtr TypeChecker::checkReturnStmt(AST::ReturnStmtPtr node) {
// ...
SymbolPtr return_type = symbol_table.find_symbol("func_return_type_1145141919810", false);
// ...
}
由于本次实验测试规模较小,故选取了方便记忆的、有规律的字符串作为返回类型的符号标识名。考虑工程实践,可以使用较长(例如长度大于 64)的随机生成的字符串,再拼接上另一段人工创建的字符串作为标识符。
这种方法理论上有问题,但概率上用户正常使用时并不会出问题。
由于是在创建新符号表时第一个加入,并在本地作用域被搜索,该方法也避免了因在外部设置同名变量导致的返回类型符号无法被创建问题。
该方法未添加任何新的机制/变量,复用现有的符号表机制,不失为一种虽不优雅但简单有效的思路。
AI 使用情况
由于对 C++不熟悉,在实现符号表时,向 AI 询问 C++的什么数据结构能实现我想要的效果:
以及,type_checker.cpp
里大部分的 checker
里面的 ASSERT
宏的使用语句是借助 AI 生成的,例如:
以及,问了很多编译遇到的问题,与实验要实现的功能无直接关联,例如:
以及,void TypeChecker::printArray
函数的代码主体是 AI 生成的,AI 直接生成的代码有错误,故实际的代码有人工修正。该函数负责将数组初始化得到的数组打印到终端上,以便调试。