湖南大学编译原理实验四cminus_compiler-2021-fall
Lab4 实验文档
0. 前言
本次实验和 Lab3 一样,需要使用 LightIR
框架自动产生 cminus-f
语言的LLVM IR。
经过 Lab3 的练手,相信大家已经掌握了 LightIR 的使用,并且对于 LLVM IR 也有了一定理解。在本次实验中,我们要使用访问者模式来实现 IR 自动生成。
对于产生的IR, 我们可以调用 clang 生成可执行文件,这样一个初级的 cminus-f 编译器就完成啦!
主要工作
- 阅读cminus-f 的语义规则
成为语言律师,我们将按照语义实现程度进行评分 - 阅读LightIR 核心类介绍
- 阅读实验框架,理解如何使用框架以及注意事项
- 修改
src/cminusfc/cminusf_builder.cpp
来实现自动 IR 产生的算法,使得它能正确编译任何合法的 cminus-f 程序 - 在
report.md
中解释你们的设计,遇到的困难和解决方案
1. 实验框架
本次实验使用了由C++编写的 LightIR 来生成 LLVM IR。为了便于大家进行实验,该框架自动完成了语法树到 C++ 上的抽象语法树的转换。
我们可以使用访问者模式来设计抽象语法树
中的算法。大家可以参考打印抽象语法树的算法,
以及运行 test_ast
来理解访问者模式下算法的执行流程。
在include/cminusf_builder.hpp
中,我还定义了一个用于存储作用域的类Scope
。它的作用是辅助我们在遍历语法树时,管理不同作用域中的变量。它提供了以下接口:
// 进入一个新的作用域
void enter();
// 退出一个作用域
void exit();
// 往当前作用域插入新的名字->值映射
bool push(std::string name, Value *val);
// 根据名字,寻找到值
Value* find(std::string name);
// 判断当前是否在全局作用域内
bool in_global();
你们需要根据语义合理调用enter
与exit
,并且在变量声明和使用时正确调用push
与find
。在类CminusfBuilder
中,有一个Scope
类型的成员变量scope
,它在初始化时已经将input
、output
等函数加入了作用域中。因此,你们在进行名字查找时不需要顾虑是否需要对特殊函数进行特殊操作。
2. 运行与调试
运行 cminusfc
mkdir build && cd build
cmake .. -DLLVM_DIR=/path/to/your/llvm/install/lib/cmake/llvm/
make -j
# 安装它以便于链接 libcminus_io.a
make install
编译后会产生 cminusfc
程序,它能将cminus文件输出为LLVM IR,也可以利用clang将IR编译成二进制。程序逻辑写在src/cminusfc/cminusfc.cpp
中。
当需要对 .cminus
文件测试时,可以这样使用:
# 假设 cminusfc 的路径在你的$PATH中
# 利用构建好的 Module 生成 test.ll
# 注意,如果调用了外部函数,如 input, output 等,则无法使用lli运行
cminusfc test.cminus -emit-llvm
# 假设libcminus_io.a的路径在$LD_LIBRARY_PATH中,clang的路径在$PATH中
# 1. 利用构建好的 Module 生成 test.ll
# 2. 调用 clang 来编译 IR 并链接上静态链接库 libcminus_io.a,生成二进制文件 test
cminusfc test.cminus
自动测试
助教贴心地为大家准备了自动测试脚本,它在 tests/lab4
目录下,使用方法如下:
# 在 tests/lab4 目录下运行:
./lab4_test.py
如果完全正确,它会输出:
===========TEST START===========
Case 01: Success
Case 02: Success
Case 03: Success
Case 04: Success
Case 05: Success
Case 06: Success
Case 07: Success
Case 08: Success
Case 09: Success
Case 10: Success
Case 11: Success
Case 12: Success
============TEST END============
通过修改脚本,还可以方便地添加自定义测试用例
请注意助教提供的测试样例仅涵盖了最基础的测试情况,请自行设计测试样例进行测试。
logging
logging 是帮助大家打印调试信息的工具,如有需求可以阅读文档后进行使用
建议
- 比较你们编写的编译器产生的 IR 和 clang 产生的IR来找出可能的问题或发现新的思路
- 使用 logging 工具来打印调试信息
- 使用 GDB 等软件进行单步调试来检查错误的原因
- 合理分工
3. 提交要求
目录结构
.
├── CMakeLists.txt
├── Documentations
│ ├── ...
│ ├── common
│ | ├── LightIR.md <- LightIR 相关文档
│ | ├── logging.md <- logging 工具相关文档
│ | └── cminusf.md <- cminus-f 的语法和语义文档
│ └── lab4
│ └── README.md <- lab4 实验文档说明(你在这里)
├── include <- 实验所需的头文件
│ ├── ...
│ ├── lightir/*
│ ├── cminusf_builder.hpp
| └── ast.hpp
├── Reports
│ ├── ...
│ └── lab4
│ └── report.md <- lab4 所需提交的实验报告,请详细说明你们的设计(需要上交)
├── src
│ ├── ...
│ └── cminusfc
│ ├── cminusfc.cpp <- cminusfc 的主程序文件
│ └── cminusf_builder.cpp <- lab4 需要修改的文件,你们要在该文件中用访问者模式实现自动 IR 生成的算法(需要上交)
└── tests
├── ...
└── lab4
├── testcases <- 助教提供的测试样例
└── lab4_test.py <- 助教提供的测试脚本
实验报告
实验要求
请按照自己的理解,写明本次实验需要干什么
答 :在理解cminus-f
语法与语义的基础上,我们需要补充完成 cminusf_builder.cpp
中的16个函数,来实现自动 IR 产生的算法,使得它能正确编译任何合法的 cminus-f
程序。在自动产生 IR 的过程中,利用访问者模式自顶向下遍历抽象语法树的每一个结点,调用我们补充完成的函数对每一个抽象语法树的结点进行分析,如果程序是合法的则编译应该显示Success
,否则编译不通过显示Fail
。
例如void CminusfBuilder::visit(ASTExpressionStmt &node)
函数是对表达式的结点进行访问,在cminus-f
语法与语义中,表达式语句由一个可选的表达式(即可以没有表达式)和一个分号组成。其结构体如下
struct ASTAssignExpression: ASTExpression {
virtual void accept(ASTVisitor &) override final;
std::shared_ptr<ASTVar> var;
std::shared_ptr<ASTExpression> expression;
};
我们则只需要判断表达式是否为空,如果存在表达式,则通过node.expression->accept(*this);
递归访问,否则则说明没有表达式。
void CminusfBuilder::visit(ASTExpressionStmt &node) { //表达式
if (node.expression != nullptr) node.expression->accept(*this); //递归访问
}
实验难点
实验中遇到哪些挑战
答:
-
因为
cminusf_builder.cpp
中的16个函数都是void
类型的,没有返回值,一时不知道怎么保存计算的结果,后面思考了一段时间察觉到全局变量好像可以实现,每次全局变量的类型或者值发生变化,就更新全局变量,这样在访问下一个结点时获取全局变量就可以取得最新的值,这样信息可以传入或者传出信息给下层或上层时。 -
在
cminus-f
的语义中,有很多需要注意的小细节,比如五种类型转化,
- 赋值时
- 返回值类型和函数签名中的返回类型不一致时
- 函数调用时实参和函数签名中的形参类型不一致时
- 二元运算的两个参数类型不一致时
- 下标计算时
要区分类型转化时是左边的类型进行转化还是右边的类型进行转化,例如:浮点数和整型一起运算时,谁是整型就需要进行类型提升,转换成浮点数类型。而在函数调用时若实参类型和函数形参不相同时,需要把实参的类型转化为形参的类型。本次实验考虑了很多次类型转化,需要很小心。
- 对于
void CminusfBuilder::visit(ASTVar &node)
函数,虽然语法中说了其var 可以是一个整型变量、浮点变量,或者一个取了下标的数组变量,但我在实现的过程中感觉有些困难。
实验设计
答:
全局变量
- 正如难点一中所提到的在函数没有返回值的时候,我们需要借助全局变量在上层或下层之间传递值,在本次实验中我存临时值的全局变量为
node_val
,当调用void CminusfBuilder::visit(ASTAdditiveExpression &node)
函数进行加减运算时,及时更新全局变量,这样在上层或下层使用时就可以拿到准确的值。实验中要认真严谨思考访问什么结点时会更新这个变量的值,及时更新。 - 除此之外,我还定义了二个全局变量:
cur_fun
指向当前的函数,当访问的结点类型为ASTFunDeclaration
时,可以及时更新当前函数,方便IF语句和While语句创建块,比如auto condBB = BasicBlock::create(module.get(), "", cur_fun);
,以及void CminusfBuilder::visit(ASTReturnStmt &node)
中获取函数返回值类型。require_lvalue
判断是否需要左值,在 C 中,赋值对象(即 var )必须是左值。cminus-f中,唯一的左值就是通过 var 的语法得到的,这个全局变量对我们后续函数补充有用。
Judge函数
为了减少重复的工作,增加了一个Judge
函数,用于判断两个变量的类型是否都为整数。因为在进行加减乘除时,要确保两个操作数的类型相同,如果不相同则整型值需要进行类型提升,转换成浮点数类型。这个函数在访问ASTSimpleExpression
简单表达式结点、ASTAdditiveExpression加减运算结点、ASTTerm乘除运算结点时用到了。
bool judge(IRBuilder *builder, Value **l_val_p, Value **r_val_p) // 用于判断赋值的左右两边是否为整数
{
bool is_int= false; //判断是否为整数
auto &l_val = *l_val_p;
auto &r_val = *r_val_p;
// 赋值时左右两边类型相同
if (l_val->get_type() == r_val->get_type()) is_int = l_val->get_type()->is_integer_type();
else{
// 浮点数和整型一起运算时,整型值需要进行类型提升,转换成浮点数类型,且运算结果也是浮点数类型
if (l_val->get_type()->is_integer_type()) l_val = builder->create_sitofp(l_val, FloatType);
else r_val = builder->create_sitofp(r_val, FloatType);
}
return is_int;
}
void CminusfBuilder::visit(ASTVar &node) 函数设计
设计中,最开始没有考虑到var可能为指针的情况,而只考虑了数组的情况,虽然这样也没有报错,但和同学交流后发现这样确实不稳妥,从严谨的角度上说,对于某个变量,类型为数组和指针的取值方法是不一样的。如果是数组,在使用gep时idx需要两个偏移量;而如果这个var是个指针,则只需要一个偏移量。如下所示,
void CminusfBuilder::visit(ASTVar &node) {
/*
var 可以是一个整型变量、浮点变量,或者一个取了下标的数组变量。
struct ASTVar: ASTFactor {
virtual void accept(ASTVisitor &) override final;
std::string id;
// nullptr if var is of int type
std::shared_ptr<ASTExpression> expression;
};
*/
auto var = scope.find(node.id); //var指向值的地址
assert(var != nullptr);
//判断值的类型
auto is_int = var->get_type()->get_pointer_element_type()->is_integer_type();
auto is_float = var->get_type()->get_pointer_element_type()->is_float_type();
auto is_ptr = var->get_type()->get_pointer_element_type()->is_pointer_type();
//判断是否为赋值的左部,如果是则返回地址,否则返回的是地址中的值。
bool should_return_lvalue = require_lvalue;
require_lvalue = false;
if (node.expression == nullptr){ //expression为nullptr,则为整型变量、浮点变量
if (should_return_lvalue) //如果var必须为左值,则取地址
{
node_val = var;
require_lvalue = false;
}
//否则,node_val存储var地址中的值
else
{
if (is_int || is_float) node_val = builder->create_load(var); //获取var的值
}
}
// 如果为数组或者指针
else{
node.expression->accept(*this);
auto val = node_val; //获取数组下标
Value *is_neg;
auto exceptBB = BasicBlock::create(module.get(), "", cur_fun);
auto contBB = BasicBlock::create(module.get(), "", cur_fun);
//数组下标必须为整数,如果不是则要进行类型转化
if (val->get_type()->is_float_type()) val = builder->create_fptosi(val, Int32Type);
//判断数组下标是否为非负数
is_neg = builder->create_icmp_lt(val, CONST_INT(0));
builder->create_cond_br(is_neg, exceptBB, contBB);
/*若val<0,一个负的下标会导致程序终止,需要调用框架中的内置函数neg_idx_except
(该内部函数会主动退出程序,只需要调用该函数即可),但是对于上界并不做检查。*/
builder->set_insert_point(exceptBB);
builder->create_call(static_cast<Function *>(scope.find("neg_idx_except")), {});
if (cur_fun->get_return_type()->is_void_type()) builder->create_void_ret();
else if (cur_fun->get_return_type()->is_float_type()) builder->create_ret(CONST_FP(0.));
else builder->create_ret(CONST_INT(0));
builder->set_insert_point(contBB); //val>=0
Value *node_ptr;
if (is_ptr) //如果这个var是个指针,则只需要一个偏移量。
{
auto array_load = builder->create_load(var); //把var中的值加载出来,即初始地址
node_ptr = builder->create_gep(array_load, {val}); //进行偏移
}
//对于数组,在使用gep时idx需要两个偏移量
else node_ptr = builder->create_gep(var, {CONST_INT(0), val});
if (should_return_lvalue) //如果var必须为左部,则取地址
{
node_val = node_ptr;
require_lvalue = false;
}
else node_val = builder->create_load(node_ptr); //否则,取node_ptr中的值
}
}
void CminusfBuilder::visit(ASTVarDeclaration &node) 函数设计
在这个函数设计的过程中,首先要确定变量的类型是整型还是浮点型,然后再判断其是声名一个变量还是一个数组,最后再判断其是声名的全局变量还是局部变量。需要注意的点时,在Cminusf
语法中,全局变量需要初始化为全 0,且记得通过语句 scope.push(node.id,value);
往当前作用域插入新的名字->值映射。
void CminusfBuilder::visit(ASTVarDeclaration &node) { //变量声名
/*
struct ASTVarDeclaration: ASTDeclaration {
virtual void accept(ASTVisitor &) override final;
CminusType type;
std::shared_ptr<ASTNum> num;
};
struct ASTDeclaration: ASTNode {
virtual void accept(ASTVisitor &) override;
CminusType type;
std::string id;
};
static GlobalVariable *create(std::string name, Module *m, Type* ty, bool is_const, Constant* init );
*/
Type *type=nullptr;
//在变量声明中,只有整型和浮点型可以使用
if(node.type==TYPE_INT) type=Int32Type;
else if(node.type==TYPE_FLOAT) type=FloatType;
//一个变量声明定义一个整型或者浮点型的变量,或者一个整型或浮点型的数组变量
//这里整型指的是32位有符号整型,浮点数是指32位浮点数
if(node.num==nullptr) //单个变量声名
{
if(scope.in_global()) // 判断当前是否在全局作用域内
{ //全局变量需要初始化为全 0
auto value=GlobalVariable::create(node.id,module.get(),type,false,ConstantZero::get(type, module.get()));
// 往当前作用域插入新的名字->值映射
scope.push(node.id,value);
}
else
{
//局部变量
auto value=builder->create_alloca(type);
// 往当前作用域插入新的名字->值映射
scope.push(node.id,value);
}
}
else //数组声名
{
auto *array_type=ArrayType::get(type, node.num->i_val); //参数依次是数组元素的类型,数组元素个数
if(scope.in_global()) // 判断当前是否在全局作用域内
{ //全局变量需要初始化为全 0
auto value=GlobalVariable::create(node.id,module.get(),array_type,false,ConstantZero::get(array_type, module.get()));
// 往当前作用域插入新的名字->值映射
scope.push(node.id,value);
}
else
{
//局部变量
auto value=builder->create_alloca(array_type);
// 往当前作用域插入新的名字->值映射
scope.push(node.id,value);
}
}
}
void CminusfBuilder::visit(ASTFunDeclaration &node) 函数设计
ASTFunDeclaration
访问函数的设计难度我觉得仅次于ASTVarDeclaration
访问函数的
设计。因为函数声名需要考虑的内容东西也很多,函数类型+函数名+参数列表+复合语句(局部声明与语句列表)。
- 首先需要确定返回值的类型,然后确定参数列表中各个参数的类型,最后才能确定函数的类型。
- 函数定义后,需要把全局变量中的
cur_fun
指向当前这个函数,并在创建函数块且执行builder->set_insert_point(BB);
后进入新的作用域。 - 在这个函数块中,我们需要获取函数的形参,给每个参数在内存中分配位置,并把值给对应的内存地址,然后加入当前作用域。
- 下一步就是访问函数中的复合语句,当访问结束后,函数将根据当前函数返回值的类型执行
ret
操作。 - 最后退出当前作用域
void CminusfBuilder::visit(ASTFunDeclaration &node) { //函数声名
/*
函数类型+函数名+参数列表+复合语句(局部声明与语句列表)
fun-declaration : type-specifier IDENTIFIER LPARENTHESE params RPARENTHESE compound-stmt
struct ASTFunDeclaration: ASTDeclaration {
virtual void accept(ASTVisitor &) override final;
std::vector<std::shared_ptr<ASTParam>> params;
std::shared_ptr<ASTCompoundStmt> compound_stmt;
};
struct ASTParam: ASTNode {
virtual void accept(ASTVisitor &) override final;
CminusType type;
std::string id;
// true if it is array param
bool isarray;
};
*/
Type *return_type; //返回值类型
std::vector<Type *> param_types;
//找函数返回值类型
if(node.type==TYPE_INT) return_type=Int32Type;
else if(node.type==TYPE_FLOAT) return_type=FloatType;
else return_type=Void;
for(auto &p :node.params){ //依次判断参数的类型
if(p->type==TYPE_INT){ //参数为整型
if(p->isarray) param_types.push_back(Int32_ptr); //参数为整型数组
else param_types.push_back(Int32Type); //整数
}
else{ //参数为浮点型
if(p->isarray) param_types.push_back(Float_ptr); //参数为浮点数组
else param_types.push_back(FloatType); //浮点数
}
}
//FunctionType *get(Type *result,std::vector<Type*> params);
//确定参数列表和返回值类型后,可确定函数
FunctionType *fun_type=FunctionType::get(return_type, param_types);
auto fun=Function::create(fun_type,node.id,module.get()); //声名函数,参数依次是:函数类型,名字, Module *parent
scope.push(node.id,fun); //加入当前作用域
cur_fun=fun;
auto BB=BasicBlock::create(module.get(), "entry", fun); //一个块
builder->set_insert_point(BB);
scope.enter(); //进入一个新的作用域
std::vector<Value *> args; // 获取函数的形参,通过Function中的iterator
for (auto arg = fun->arg_begin(); arg != fun->arg_end(); arg++) args.push_back(*arg);
int i=0; //参数
for(auto p :node.params) //给每个参数在内存中分配位置,并把值给对应的内存地址
{
if(p->type==TYPE_INT)
{
Value *i_type;
if(p->isarray) i_type = builder->create_alloca(Int32_ptr); //参数为整型数组
else i_type = builder->create_alloca(Int32Type); //整数
builder->create_store(args[i], i_type); //将参数的值存储到开辟的空间
scope.push(p->id, i_type); //加入当前作用域
}
else
{
Value *f_type;
if(p->isarray) f_type = builder->create_alloca(Float_ptr); //参数为浮点数组
else f_type = builder->create_alloca(FloatType); //浮点数
builder->create_store(args[i], f_type); //将参数的值存储到开辟的空间
scope.push(p->id, f_type); //加入当前作用域
}
i++;
}
node.compound_stmt->accept(*this); //复合语句的访问
//判断返回类型
if (cur_fun->get_return_type()->is_void_type()) builder->create_void_ret();
else if (cur_fun->get_return_type()->is_float_type()) builder->create_ret(CONST_FP(0.));
else builder->create_ret(CONST_INT(0));
scope.exit(); //退出当前作用域
}
实验结果
先在build
目录下依次执行cmake ..
,make -j
和sudo make install
,然后在tests/lab4 目录下运行./lab4_test.py
,结果如图所示,全部正确。
实验总结
这次实验整体来说还是有点点小复杂的,因为其涉及到的头文件很多,很难找到正确的切入点,且过程中还经常出现一些小问题不好找原因。需要认真仔细的看cminus-f
的语法与语义以及各个函数之间的联系才能比较顺利的完成这个实验。
通过这个实验,我对cminusf
语法语义和访问者模式有了更深入的理解,根据node对象具体类型的不同,编译器在对语法树自顶向下分析时会选择对应的visit
函数进行调用,从而较为高效的完成任务。以前写C++代码时,只知道不符合语法会报错,但并不知道计算机内部是如何实现这个过程的,完成这个实验的过程中我知道了答案。此外,本次实现需要阅读大量的.h头文件,所以我阅读理解C代码的能力有所提升,且养成了仔细严谨阅读文档、多思考的好习惯。