本编译器基本具备如下功能:
包含了基本要求的功能:表达式编译、常量变量、语句块作用域、if, while语句、函数调用、全局变量、数组。
还有如下等特点:速度慢、容易崩溃……/(ㄒoㄒ)/~~
整体设计:
编译器由4个主要模块组成:
sysy.l
以及sysy.y
分别进行词法分析和语法分析,构建抽象语法树(AST);ast
文件夹中的代码。实现AST结构,并使用该结构生成Koopa结构。builder
文件夹中的代码。读取Koopa结构,生成RISC-V代码。抽象语法树节点均继承自BaseAST
类,该类规定了一些可以实现的功能,方便使用多态调用。比如build_koopa_values
用于将该节点的语义转化为Koopa结构并返回;CalcValue
用于计算常量表达式的值。
BaseAST
中有3个static成员,作为全局信息:用于维护生成IR时需要的符号表symbol_list
;当前基本块的语句缓存区block_maintainer
;维护嵌套循环结构的loop_maintainer
class BaseAST
{
public:
static SymbolList symbol_list;
static BlockMaintainer block_maintainer;
static LoopMaintainer loop_maintainer;
virtual ~BaseAST() = default;
virtual std::string to_string() const;
virtual void *build_koopa_values() const;
virtual int CalcValue() const;
virtual void *koopa_leftvalue() const;
};
AST节点示例:
赋值语句的AST需要包含左值节点和右值表达式节点。其重写函数build_koopa_values
中,调用了子节点的函数,以获取他们的Koopa结构,并由此构建自己的Koopa结构(Store语句),再通过全局结构block_maintainer
将该语句添加进当前基本块。
class AssignmentAST : public BaseAST
{
public:
std::unique_ptr lval;
std::unique_ptr exp;
AssignmentAST(std::unique_ptr &_lval, std::unique_ptr &_exp);
void *build_koopa_values() const override
{
koopa_raw_value_data *res = new koopa_raw_value_data();
// .. some code to build koopa_raw_value_data
block_maintainer.AddInst(res);
return res;
}
};
在BlockMaintainer
中,需要存储当前正在构建的函数、基本块、指令缓冲区等Koopa结构信息。由于其为静态成员,任意AST节点都可以调用他来添加基本块、语句等。
当一个基本块完成时,该类的FinishCurrentBlock
函数负责清楚基本块末尾无法到达的语句,以及添加Jump、Return之类的语句防止基本块出错。
class BlockMaintainer
{
koopa_raw_function_t current_func;
std::vector current_insts_buf;
std::vector *basic_block_buf;
public:
void SetCurrentFunction(koopa_raw_function_t _cur_func);
void SetBasicBlockBuf(std::vector *_basic_block_buf);
void FinishCurrentBlock();
void AddNewBasicBlock(koopa_raw_basic_block_data_t *basic_block);
void AddInst(const void *inst);
};
嵌套的循环结构会造成大量基本块,出现复杂的跳转结构,为此实现了一个类来维护。
该类使用一个vector来构建一个栈,用于表示嵌套的循环结构。栈中元素存储对应循环的各个基本块信息,方便AST节点构建Koopa时使用。
struct KoopaWhile
{
koopa_raw_basic_block_data_t *while_entry;
koopa_raw_basic_block_data_t *while_body;
koopa_raw_basic_block_data_t *end_block;
};
class LoopMaintainer
{
std::vector loop_stk;
public:
void AddLoop(...)
{
KoopaWhile kw;
// ... 构建kw
loop_stk.push_back(kw);
}
KoopaWhile GetLoop() { return loop_stk[loop_stk.size() - 1]; }
void PopLoop() { loop_stk.pop_back(); }
};
RISC-V构建器中包含一个输出流output
,通过build
函数递归遍历Koopa结构,生成的RISC-V代码直接输出到output中。
构建器中包含一个Env
结构,用于存储Koopa指令到int的映射,代表该指令的计算结果存储在栈上的位置,RISC-V找到该结构并读取。
class RISCVBuilder
{
class Env
{
int total_size;
map addr;
public:
int cur;
bool has_call;
void NewEnv(int size, bool _has_call);
int GetTotalSize();
int GetAddr(koopa_raw_value_t kval);
} env;
ostream &output;
static int calc_func_size(koopa_raw_function_t kfunc, bool &has_call);
// ... some functions to calculate memory
static int calc_type_size(koopa_raw_type_t ty);
void traversal_raw_slice(const koopa_raw_slice_t *rs);
// ... some functions for every instructions
void gen_riscv_value_return(const koopa_raw_return_t *kret);
public:
RISCVBuilder(ostream &_out) : output(_out) {}
void build(const koopa_raw_program_t *raw);
};
符号表类里面定义了一个vector实现的栈,表示作用域的嵌套结构,新建作用域时在栈顶添加元素。
栈中每个元素为一个map,将字符串映射到Koopa结构,表示该作用域的符号表。
当需要查找符号时,从栈顶开始在map中查找,若没有找到,则进入栈中前一个元素继续查找,实现往父作用域的查找。
struct LValSymbol
{
enum SymbolType type;
void *number;
};
class SymbolList
{
std::vector> sym_stk;
public:
void NewEnv();
void AddSymbol(const std::string &name, LValSymbol koopa_item);
LValSymbol GetSymbol(const std::string &name)
{
LValSymbol res;
for (size_t i = sym_stk.size() - 1; i >= 0; i--)
if (sym_stk[i].count(name) != 0)
{
res = sym_stk[i][name];
break;
}
return res;
}
void DeleteEnv();
};
寄存器分配采用全部放在栈上的策略,仅在一些运算中途会使用寄存器。
首先Builder进入函数时,计算所有语句以及变量等占用的空间大小,在栈上分配足够的空间。
每个语句确保自己执行的内部寄存器不会冲突,执行完毕将语句的结构存在栈上指定位置,寄存器信息不再保存,可被其他语句复用。
每个语句仅使用t0t6寄存器,函数参数即返回值使用a0a7。
在语法分析器中,针对列表状的语句。=,如int x0,x1,x2....
任意多个变量。
在yacc中设计为如下形式:
VarDecl : BType VarDefList ';';
VarDefList : VarDef | VarDefList ',' VarDef;
VarDef : ..... {add_inst(...);}
并在全局开一个vector
对于VarDecl,VarDefList不再定义语法分析树节点,而是仅将VarDef生成的语法分析树节点添加到全局的vector中。然后上一级AST节点读取全局vector,将子节点添加进自己的结构。
这样设计有效简化了复杂的AST链状结构。
lab文档
该阶段仅仅按照文档完成简易功能。构建了AST的类结构,选择了直接生成Koopa结构内存形式的方式。
使用简单函数生成RISC-V,没有构建其他复杂的数据结构。
按照文档的语法规范,对每种表达式设计AST类。
由于Koopa没有逻辑与/或,只有按位与/或,所以这两个运算之前,添加不等于运算来将结果转化为1和0,再进行按位运算。
对于RISC-V代码,此时设计了栈的大小计算,将koopa语句与栈上地址映射,设计对应的类。
Koopa格式已经是简单的三地址语句,只需要读取栈上数据,使用固定语句计算,再存回栈上即可。
对于常量,在表达式的AST中添加了CalcValue功能用于求值。
因为变量的出现,完成了SymbolList类,并将其设为静态,作为全局信息管理。
由于之前把所有AST都视为BaseAST,使用多态调用build_koopa_value。在这里LValAST有时需要右值形态的Koopa,有时需要左值形态的Koopa,无法使用一个函数的多态来完成,出现了一定困难。于是在BaseAST中添加了新的虚函数用于处理左值。
对于RISC-V代码,仅在栈空间计算以及koopa与地址映射中添加了对变量的处理。
和Lv4一起完成的,在设计SymbolList时已经实现为栈结构,能够处理嵌套的作用域。
在语法分析阶段,建立了全局的语句缓存区栈
栈中每个元素为一个vector,存储该语句块中的语句
到了需要存储语句的AST时,从栈顶开始读取语句块内容,将其存入自己的结构中。
出现二义性时,我将IF exp
合并为IfExp : IF exp
,再将if语句定义为
Stmt : IfExp Stmt | IfExp Stmt ELSE {...} Stmt {...};
解决了问题。
对于If中的几个语句块,他们都存在了全局语句栈中。读取栈顶的两项即可获得If的语句块和Else的语句块。
由于基本块越来越多,在这个阶段设计了BlockMaintainer来专门维护各个基本块的语句缓存。
对于短路求值,直接将&&,||改为跳转形式,而不再使用按位与等运算。
因为嵌套的循环语句不方便找到需要跳转的块,设计了LoopMaintainer类来维护嵌套的循环,使用栈结构来存储每个循环的基本块信息。
这样break,continue能找到当前所在的循环,从而找到跳转位置。
这些跳转语句,直接对应RISC-V中的bnez和j命令,比较简单。
由于Koopa库能够自动处理名字重复问题,通过生成Koopa代码再重新读入回来,已经不存在标签重名的情况。
对于形参和实参都设计了专门的AST类实现,同时在语法分析阶段开了全局vector用来存储这些参数列表。
在函数定义FuncDefAST中读取列表构建AST,函数调用LVal中读取列表生成参数。
对于RISC-V阶段,增加了计算函数返回地址,传参在栈上存储的位置。以及调用函数将参数放入指定寄存器或栈上位置的功能。
设计InitValAST专门用于处理数组的初始化列表,并且分开处理了全局变量和局部变量的形式。
InitValAST首先调用预处理preprocess,根据指定的数组size将数据预处理成数组格式的数据。
然后根据是全局还是局部变量,生成aggerate的koopa内容或者读取某一位的值,在函数内部通过赋值初始化。
对于RISC-V,由于之前将Koopa对象映射到栈上位置,用的一直是Koopa语句对象本身的指针,他的ty表示的一定是一个整数指针(koopa语句返回值都是指针)。而目前因为数组参数的出现,出现了真正的指针,导致映射器无法区分是要读取指针本身还是指针指向的对象,于是只能对get_element_ptr和get_ptr进行特判处理。
get_element_ptr等语句,处理时会读取koopa对象中的ty以计算指针需要移动的大小,使用mul,add等语句计算得到指定位置的指针。
flex/bison
:编写sysy.l
和sysy.y
,完成词法分析和语法分析,通过表达式识别代码生成了语法分析树。使用了往年的测试数据,测试某些编译器崩溃的问题,有以下重点问题:
bnez
的跳转范围,必须修改为跳转到近处某个位置,使用j
命令跳转到远处。new char[len]
写成了new char(len)
,到处内存非法访问,到处出错。。。实现了一个编译器,非常有成就感。
加深理解了这门课程各个部分的内容。
通过实践学到了大软件开发的一些难点,最初的设计会如何影响到后续的扩展问题。
希望老师上课能讲得更有趣一些
完整代码下载地址:基于C++实现的一个SysY到RISC-V的编译器