这是今年新推出的实践方案,由往年的sysy->IR1->IR2->RISC V变成了sysy->Koopa->RISC V,通过增量的方式让整个实践过程更容易上手

所以先在这里简要记录一下整个实践过程

首先我们要始终跟随文档https://pku-minic.github.io/online-doc/#/,这篇文档的内容非常详细

那么环境安装的部分我们就先略过,直接开始正题

lv0:首先我们要注意的是我们要使用的是你自己的路径,比如我的电脑在输入指令

docker run compiler-dev ls -l /

时会报错,原因就是路径不对,实际上应当用的是

docker run maxxing/compiler-dev ls -l /

接下来所有的路径都要注意这点。

tips:这些指令是很长的,而且我们也没有必要把他们背下来,可如果每次去找又要花费不少时间,建议自己开一个.txt之类的文件存储常用的指令

那么我们就可以快乐地进入lv1

lv1:

进入lv1之后我们要处理的是最简单的

int main()
{
//可能有这样的注释,但是模板里已经帮你处理过了
/*
你需要自己处理这样的注释
仔细思考怎么处理,提示:.不能匹配换行符
*/
return 0;
}

我们观察下发的模板,发现我们实际上需要四个文件:sysy.l和sysy.y(用来进行词法分析、语法分析之类的),main.cpp(你的编译器从这里运行),以及你自己建立的AST.h(用来定义一些AST)

所谓AST,我们可以直观理解成语法结构,我们只需每次按照该部分的EBNF定义即可,比如文档中(lv1.3)提供了例子,这里就不赘述了

只需要处理一个问题:

#include "AST.h"

应该放在哪?

最野蛮的方法当然是——哪都放上

这时你make会报错,大概是什么redefinition之类的问题

怎么办?

其实编译器已经给你提示了:他会建议你在AST.h这个库里加入这句话:

#pragma once

然后问题就解决了

在lv1中,我们其实应当注意的问题是不要自己乱动东西,这是后面所有增量操作的基础——除了你新增加的功能以及为了实现新功能前面确实需要修改的内容外,你不应当改动前面你(或模板)已经正确实现的任何内容

举例:当我们在做解析的时候,原版(lv1.2提供,正确)可能是长成这个样子的:

Stmt
: RETURN Number ';' {
auto number = unique_ptr<string>($2);
$$ = new string("return " + *number + ";");
}
;

你需要修改他的功能,于是你类比这段代码(lv1.3提供,正确)

FuncDef
: FuncType IDENT '(' ')' Block {
auto ast = new FuncDefAST();
ast->func_type = unique_ptr<BaseAST>($1);
ast->ident = *unique_ptr<string>($2);
ast->block = unique_ptr<BaseAST>($5);
$$ = ast;
}
;

写出了这种东西

Stmt
: "return" Number ';'{
auto ast=new Stmt();
ast->num= $2;
$$=ast;
}
;

然后你觉得这很正确,因为EBNF就是这么说的呀?

CompUnit  ::= FuncDef;

FuncDef   ::= FuncType IDENT "(" ")" Block;
FuncType ::= "int"; Block ::= "{" Stmt "}";
Stmt ::= "return" Number ";";
Number ::= INT_CONST;

但是请注意!这样的字符串关键字是需要在.l文件里面进行声明的!如果你查看.l文件,会看到这样的内容:

"int"           { return INT; }
"return" { return RETURN; }

也就是说我们实际应该匹配的是RETURN,而不是"return"

这一点当你做到lv3或者lv4的时候会再次遇到,比如你想匹配一个const关键字,那么你应当先在.l文件里加上一行

"const"         { return CONST; }

然后就可以在.y文件里写类似这样的东西了

ConstDecl
: CONST INT MulConstDef ';'{
auto ast=new ConstDecl();
ast->const_decl=unique_ptr<BaseAST>($3);
$$=ast;
}
 ;

但是在一开始,显然你并没有对这些事情有充分的理解(本博客讲解的是一个小菜鸡做lab的心路历程,不建议巨佬食用),因此最好的方法就是不要动,反正我return的这个内容没有变,那我为什么要把他帮你写好的RETURN改成"return"呢?

那么你一阵瞎写,终于完成了这个.y文件,接下来我们按照编译文档上的指示,先

make

build/compiler -koopa hello.c -o hello.koopa

如果没有什么提示,那么我们就可以认为我们的解析过程是正确的了!

当然,如果有提示,一般来讲提示信息大概长这样:

compiler: /root/compiler/template/src/my.cpp:264: int main(int, const char **): Assertion `!ret' failed.
Aborted

这是啥?

观察我们的.y文件,我们不难发现我们还定义了一个报错函数

void yyerror(std::unique_ptr<BaseAST> &ast, const char *s) {
cerr << "error: " << s << endl;
}

那么如果出现错误,我们可以用这个报错函数帮我们获取错误信息,我们把报错函数修改成这样:

void yyerror(std::unique_ptr<BaseAST> &ast, const char *s) {

    extern int yylineno;    // defined and maintained in lex
extern char *yytext; // defined and maintained in lex
int len=strlen(yytext);
int i;
char buf[512]={0};
for (i=0;i<len;++i)
{
sprintf(buf,"%s%d ",buf,yytext[i]);
}
fprintf(stderr, "ERROR: %s at symbol '%s' on line %d\n", s, buf, yylineno); }

那么你看到的报错信息就会变成:

ERROR: syntax error at symbol '33 ' on line 1
compiler: /root/compiler/template/src/my.cpp:264: int main(int, const char **): Assertion `!ret' failed.
Aborted

好极了!第一行告诉我们在一行中出现了语法错误(syntax error),原因是它不能识别ascii码为33的字符!

那么这个错误有两个可能的原因,一个是我们的测试程序本身就有语法错误(这里所谓的语法错误,是指按我们当前体系设计不能识别的内容),比如如果我们把hello.c写成这个样子:

int main()
{
return !0;
}

按我们的认知来说这其实没错,但别忘了我们还在lv1,我们只能处理return 0,所以这样的语句就会产生上面的报错信息(!的ascii码为33)

另一种可能(也是可能性比较大的情况)就是我们的.y写错了,本应识别的东西没有识别,比如如果你把这个程序喂给了你在lv3中写的编译器,它还给你报上面的错,就说明你的.l,.y文件哪里写的出问题了

好,你通过不断地修改,终于让你的编译器能正确识别了(可喜可贺)

但可惜我们的编译过程还没有进行到一半

因为我们的编译过程应当是sysy->Koopa->RISC V,可是我们现在连Koopa都没有,我们只是得到了一堆数据结构

那么按照文档上的建议,我们只需在这些结构里面定义一个成员函数,通过成员函数直接输出Koopa即可

但是怎么直接输出Koopa呢?

这里我使用的是直接输出文本类型的Koopa,这样我们只需要对照Koopa的格式,在正确的地方输出正确的东西就可以,比如Koopa的格式是这样的:

fun @main(): i32 {  // main 函数的定义
%entry: // 入口基本块
ret 0 // return 0
}

首先是函数定义,那我们直接在自己的func_def AST里定义这样的函数:

void Dump() const override
{
std::cout << "fun ";
std::cout<<"@"<<ident<<"(): ";
func_type->Dump();
block->Dump();
}

接下来在函数类型的AST里定义这样的函数:

void Dump() const override
{
std::cout<<"i32"<<" ";
}

以此类推即可,然后加入一些文件读写,比如我们想把这个Koopa生成到一个叫whatever.txt的文本文件里,那么我们在main.cpp里加一个重定向:

assert(!ret);
freopen("whatever.txt","w",stdout);
ast->Dump();

这样不出意外的话,我们就会在whatever.txt里看到我们的Koopa内容了

其实在lv1中,我们就已经展示了我们在每次增量(增加新功能)的流程:首先根据EBNF修改AST.h来完成AST的定义,接下来根据EBNF完成.y文件的修改(有时可能需要修改.l文件匹配关键字),经过调试可以正确识别之后修改Dump函数生成Koopa,正确生成Koopa之后再去生成RISC V(当然这就是lv2的内容了)

 lv2:

接下来就是由Koopa IR生成RISC V了,lv2的文档里提供了一个模板,你只需定义这样的函数:

void parse_string(const char* str)
{
// 解析字符串 str, 得到 Koopa IR 程序
koopa_program_t program;
koopa_error_code_t ret = koopa_parse_from_string(str, &program);
assert(ret == KOOPA_EC_SUCCESS); // 确保解析时没有出错
// 创建一个 raw program builder, 用来构建 raw program
koopa_raw_program_builder_t builder = koopa_new_raw_program_builder();
// 将 Koopa IR 程序转换为 raw program
koopa_raw_program_t raw = koopa_build_raw_program(builder, program);
// 释放 Koopa IR 程序占用的内存
koopa_delete_program(program); // 处理 raw program
// 使用 for 循环遍历函数列表
for (size_t i = 0; i < raw.funcs.len; ++i) {
// 正常情况下, 列表中的元素就是函数, 我们只不过是在确认这个事实
// 当然, 你也可以基于 raw slice 的 kind, 实现一个通用的处理函数
assert(raw.funcs.kind == KOOPA_RSIK_FUNCTION);
// 获取当前函数
koopa_raw_function_t func = (koopa_raw_function_t) raw.funcs.buffer[i];
for (size_t j = 0; j < func->bbs.len; ++j) {
assert(func->bbs.kind == KOOPA_RSIK_BASIC_BLOCK);
koopa_raw_basic_block_t bb = func->bbs.buffer[j];
// 进一步处理当前基本块
// ...
koopa_raw_value_t value = ...;
// 示例程序中, 你得到的 value 一定是一条 return 指令
assert(value->kind.tag == KOOPA_RVT_RETURN);
// 于是我们可以按照处理 return 指令的方式处理这个 value
// return 指令中, value 代表返回值
koopa_raw_value_t ret_value = value->kind.data.ret.value;
// 示例程序中, ret_value 一定是一个 integer
assert(ret_value->kind.tag == KOOPA_RVT_INTEGER);
// 于是我们可以按照处理 integer 的方式处理 ret_value
// integer 中, value 代表整数的数值
int32_t int_val = ret_val->kind.data.integer.value;
// 示例程序中, 这个数值一定是 0
assert(int_val == 0); } // 进一步处理当前函数
// ...
} // 处理完成, 释放 raw program builder 占用的内存
// 注意, raw program 中所有的指针指向的内存均为 raw program builder 的内存
// 所以不要在 raw program builder 处理完毕之前释放 builder
koopa_delete_raw_program_builder(builder); }

这就是把2.1和2.2里提供的代码拼接起来得到的结果

那么...我们怎么用这个东西生成RISC V呢?

首先我们结合代码中的注释,看到它是按照层次:函数定义——基本块定义——基本块中的每一条指令

然后再参考文档中提供的RISCV示例,判断一下函数定义的时候我们需要输出什么样的格式,基本块定义的时候我们需要输出什么样的格式,return指令又是什么格式,在正确的位置对应输出即可

参考代码:(之所以是参考,是因为由于现在的编译器功能太少,很多格式在错误的地方输出其实也没有影响,当后面加入更多功能之后可能会需要调整格式的输出)

void parse_string(const char* str)
{
// 解析字符串 str, 得到 Koopa IR 程序
koopa_program_t program;
koopa_error_code_t ret = koopa_parse_from_string(str, &program);
assert(ret == KOOPA_EC_SUCCESS); // 确保解析时没有出错
// 创建一个 raw program builder, 用来构建 raw program
koopa_raw_program_builder_t builder = koopa_new_raw_program_builder();
// 将 Koopa IR 程序转换为 raw program
koopa_raw_program_t raw = koopa_build_raw_program(builder, program);
// 释放 Koopa IR 程序占用的内存
koopa_delete_program(program); cout<<" .text"<<endl; for (size_t i = 0; i < raw.funcs.len; ++i) {
// 正常情况下, 列表中的元素就是函数, 我们只不过是在确认这个事实
// 当然, 你也可以基于 raw slice 的 kind, 实现一个通用的处理函数
assert(raw.funcs.kind == KOOPA_RSIK_FUNCTION);
// 获取当前函数
koopa_raw_function_t func = (koopa_raw_function_t) raw.funcs.buffer[i]; cout<<" .globl "<<func->name+1<<endl;
cout<<func->name+1<<":"<<endl; for (size_t j = 0; j < func->bbs.len; ++j) {
assert(func->bbs.kind == KOOPA_RSIK_BASIC_BLOCK);
koopa_raw_basic_block_t bb = (koopa_raw_basic_block_t)func->bbs.buffer[j];
for (size_t k = 0; k < bb->insts.len; ++k){
koopa_raw_value_t value = (koopa_raw_value_t)bb->insts.buffer[k];
// 示例程序中, 你得到的 value 一定是一条 return 指令
assert(value->kind.tag == KOOPA_RVT_RETURN);
// 于是我们可以按照处理 return 指令的方式处理这个 value
// return 指令中, value 代表返回值
koopa_raw_value_t ret_value = value->kind.data.ret.value;
// 示例程序中, ret_value 一定是一个 integer
assert(ret_value->kind.tag == KOOPA_RVT_INTEGER);
// 于是我们可以按照处理 integer 的方式处理 ret_value
// integer 中, value 代表整数的数值
int32_t int_val = ret_value->kind.data.integer.value;
// 示例程序中, 这个数值一定是 0
//assert(int_val == 0);
cout<<" li "<<"a0 , "<<int_val<<endl;
cout<<" ret"<<endl;
}
// ...
}
// ...
} // 处理完成, 释放 raw program builder 占用的内存
// 注意, raw program 中所有的指针指向的内存均为 raw program builder 的内存
// 所以不要在 raw program builder 处理完毕之前释放 builder
koopa_delete_raw_program_builder(builder);
}

其中值得注意的是这条语句:

koopa_raw_value_t value = (koopa_raw_value_t)bb->insts.buffer[k];

这条语句怎么来的?

这里需要你自行查阅koopa.h,看看这些结构是怎么定义的(可能会很复杂,需要不断跳转),才能找到自己需要的东西,这一点很重要,因为后面有大量类似这样的内容。

同时还有这里要注意:

// 示例程序中, 这个数值一定是 0
//assert(int_val == 0);

我们把这个assert注释掉了,因为真实情况下main函数的返回值当然不一定(虽然原则上应当)是0,而且事实上大多数测试数据也不保证这一点,因此我们不需要这个assert

这些都处理好了,就可以进行简单的测试了:

autotest -koopa -s lv1 /root/compiler
autotest -riscv -s lv1 /root/compiler

第一条指令用来测试koopa,第二条指令用来测试riscv,你需要保证你的main函数能处理这样的指令:

compiler -riscv 输入文件 -o 输出文件
compiler -koopa 输入文件 -o 输出文件

文档建议你能够根据输入的-riscv和-koopa判断应当输出的格式,于是我们可以把main函数写成这个样子:

int main(int argc, const char *argv[])
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 解析命令行参数. 测试脚本/评测平台要求你的编译器能接收如下参数:
// compiler 模式 输入文件 -o 输出文件
assert(argc == 5);
auto mode = argv[1];
auto input = argv[2];
auto output = argv[4]; // 打开输入文件, 并且指定 lexer 在解析的时候读取这个文件
yyin = fopen(input, "r");
assert(yyin); // 调用 parser 函数, parser 函数会进一步调用 lexer 解析输入文件的
unique_ptr<BaseAST> ast;
auto ret = yyparse(ast);
assert(!ret);
if(mode[1]=='k')
{
freopen(output,"w",stdout);
Koopa_Dump();
cout<<endl;
ast->Dump();
return 0;
}
freopen("whatever.txt","w",stdout); Koopa_Dump();
cout<<endl;
ast->Dump(); FILE* ff=fopen("whatever.txt","r");
char *buf=(char *)malloc(10000000);
fread(buf, 1,10000000, ff);
/*freopen("temps.txt","w",stdout);
parse_string(buf,0);
M.clear();
array_size.clear();
deep=0;
now_array=0;*/
freopen(output,"w",stdout);
parse_string(buf,1); return 0;
}

(注释掉的部分与本环节无关)

做完这些我们就可以进行一波测试,通过了之后就可以高兴地进入

lv3:

这部分要求我们能处理一些表达式,但是不涉及到常量和变量的定义,因此我们需要处理的大概是形如这样的程序:

int main()
{
return 1+2-3*4/5%6+!7--8;//注意这里的--8应当被解释成-(-8)而不是--8(后者实际上也是不合法的)
}

那么我们还是一步一步来,首先是lv3.1要求你能处理一元表达式,这里我们支持的一元表达式实际上也只有+,-,!三种

那么首先观察一下EBNF:

Stmt        ::= "return" Exp ";";

Exp         ::= UnaryExp;
PrimaryExp ::= "(" Exp ")" | Number;
Number ::= INT_CONST;
UnaryExp ::= PrimaryExp | UnaryOp UnaryExp;
UnaryOp ::= "+" | "-" | "!";

嗯,看上去...|是什么鬼啊!

这里的|的规则表示它可以被解释成前者或者后者任意一个,文档中给出了两种处理方法,这里我采取的是第一种(因为代码比较直观好写)。

所谓为右侧每种情况都定义一个AST,就是直接利用C++的多态的特性,以PrimaryExp为例,我只定义:

class PrimaryExp:public BaseAST
{
public:
std::unique_ptr<BaseAST> p_exp;
void Dump()const override
{
p_exp->Dump();
}
};

而至于p_exp到底是什么呢?这由我解析的过程决定,我可以这样解析:

PrimaryExp
:'(' Exp ')'{
auto ast=new PrimaryExp();
ast->p_exp=unique_ptr<BaseAST>($2);
$$=ast;
}|NumExp {
auto ast=new PrimaryExp();
ast->p_exp=unique_ptr<BaseAST>($1);
$$=ast;
}
;

(这里同样展示了如何在.y文件中处理|)

利用多态的特性,无论p_exp是什么,我都可以正常调用Dump方法,调用到的就是对应类型的Dump方法了。

当然,为了格式的一致性,哪怕Number只是一个正常的整数,我们也不得不为其单独开一个AST,否则格式不一致难以处理(当然你也可以用union或者enum之类的进行分类,但这就是第二种策略了)

这种策略的缺点就是越到后期我们要开的AST的数量就越多,会带来比较大的代码量

那么类比上面的过程我们就可以完成AST的构造了

接下来就要生成Koopa

这里涉及到表达式运算,因此我们首先需要知道Koopa的一些特性(具体请查阅文档)

这里主要解决几个问题:

第一:单赋值问题怎么解决?

我的策略是使用一个全局变量标记当前使用到了哪个整数,每次增加这个整数,这样就能保证单赋值的特性了

也就是在AST.h里加一个这样的声明:

int now=0;

然后每次使用这个now作为临时符号即可,每次使用完了都要+1

然后你兴高采烈地去make,发现居然报错了,又报了什么redefinition之类的东西

这时你很困惑不解:我不是加了#pragma once了吗,怎么还报错

你知道应该是你定义的这个变量的问题,那么怎么办?

你想起了你学过的ics第七章linking部分的知识(或许有吧),决定听人家的话,定义这种变量时最好加一个static!

static int now=0;

这时你再make,发现你的程序可以正常运行了!

这是为什么?

我不知道...上面整个就是我的心路历程,我不知道为什么加个static他就对了,但是加static确实是有效的解决措施...

(2023.1.27增补:加入这个int now=0意味着now为强符号,这样在链接时由于不同的.c文件均include了这个.h文件(有些.c文件是中间生成的),会导致多个同名强符号报错。加入static之后会导致不同的.c文件不能共享这个变量,这一点在这个lab里面没问题,因为我们真正使用这个变量的只有一个.c文件,但是在其他场景下会出现严重的问题,因此更加推荐的写法是在.h文件里使用extern int now;,同时额外增加一个AST.c文件定义int now=0)

那么总结一下:如何生成Koopa?比如我们想计算--6这个表达式,那么我们在计算过程中我们Dump的过程应该是这样:

首先我们会把--6解释成整个表达式,在这里调用Dump时,第一个‘-’会被解释成UnaryOp,而后面的-6会被解释成下一个表达式,那么我们希望得知后面那个表达式的标号是%x=....,这样整个表达式就可以被输出成%x+1=sub 0, %x,因此我们需要先对后面那个表达式调用Dump,调用过Dump之后此时的now记录的就是后面表达式的标号(这是由我们输出的过程决定的),因此我们对这个部分的输出Koopa的手段即为:

class SinExp:public BaseAST
{
public:
char una_op;
std::unique_ptr<BaseAST> una_exp;
void Dump()const override
{
una_exp->Dump();
if(una_op=='-')
{
std::cout<<'%'<<nowww<<"= "<<"sub 0,"<<'%'<<nowww-1<<std::endl;
++nowww;
}
}
};

(请大家忽略我的变量名,这只是一个例子)

那么在这个基础之上,想要生成RISCV其实并不太容易,因为RISCV涉及到寄存器的分配,在Lv3里面这个问题体现的并不明显,这里我们可以假定所有的寄存器都是够的,然后随便使用寄存器(当然要遵循他要求的规则,比如最后要把返回值放在寄存器a0里面之类的)

那么我们就进入到了Lv3.2,我们要能够处理二元表达式,语法规范是:

Exp         ::= AddExp;
PrimaryExp ::= ...;
Number ::= ...;
UnaryExp ::= ...;
UnaryOp ::= ...;
MulExp ::= UnaryExp | MulExp ("*" | "/" | "%") UnaryExp;
AddExp ::= MulExp | AddExp ("+" | "-") MulExp;

那么同理修改一下AST.h就可以实现识别了

而在Dump生成Koopa的时候也和上面基本同理,比如对一个二元的加法表达式(-7)+(-6),我们会把它解释(-7)是AddExp,(-6)是MulExp,中间是'+',然后我们分别Dump这两个表达式得到的标号为x1,x2,然后最后得到了%x3=add %x1, %x2

但是这里会遇到一个问题,就是如果我们有这样的表达式6+5,前后两个都只是常数,这两个数不会被解释成标号,那么我们一种策略是设法判断参与表达式运算的是另一个表达式还是常数,然后分类处理,但这样做太复杂了,我在这里倾向于采用另一种方法:

事实上一个常数c可以被解释成一个表达式%x=add 0, c,因此即使参与运算的是常数,我们也新增一条形如上面的表达式,这样我们就把常数与表达式统一成了表达式,这样就可以解决这个问题,虽然这样可能会带来的另一个问题是我们生成的Koopa中有大量无用的语句,但是在初期这简化了我们代码的难度。

class MultiExp:public BaseAST
{
public:
std::unique_ptr<BaseAST> mu_exp;
char op;
std::unique_ptr<BaseAST> un_exp;
void Dump()override
{
mu_exp->Dump();
int now1=nowww-1;
un_exp->Dump();
int now2=nowww-1;
if(op=='*')
{
std::cout<<"\t"<<'%'<<nowww<<"= mul "<<'%'<<now1<<", %"<<now2<<std::endl;
++nowww;
}else if(op=='/')
{
std::cout<<"\t"<<'%'<<nowww<<"= div "<<'%'<<now1<<", %"<<now2<<std::endl;
++nowww;
}else
{
std::cout<<"\t"<<'%'<<nowww<<"= mod "<<'%'<<now1<<", %"<<now2<<std::endl;
++nowww;
}
}
int Calc() override
{
if(op=='*')
return (mu_exp->Calc())*(un_exp->Calc());
else if(op=='/')
return (mu_exp->Calc())/(un_exp->Calc());
else
return (mu_exp->Calc())%(un_exp->Calc());
}
};

这是乘法表达式的一个例子,我们模仿上述过程正常生成Koopa即可

对于3.3的比较和逻辑表达式,首先要进行一些词法分析,因为比较和逻辑表达式里有一些符号需要特殊处理,我们这样写:

"||"            { return LOR; }
"&&" { return LAND; }
"==" { return EQ; }
"!=" { return NEQ; }
">=" { return GEQ; }
"<=" { return LEQ; }

处理Koopa如何支持逻辑运算:一个逻辑表达式a||b等价于!(a==0)|!(b==0),用类似这样的手法去处理即可

需要说明的是,我们要严格按照EBNF的说明设计AST,因为这里有一个运算顺序的问题,如果AST设计的不好那么运算顺序会出现错误!

可以简单思考一下文档里给出的运算顺序问题:我们认为加法表达式中的一部分可以是一个乘法表达式,这样就意味着对于加法和乘法同时存在的表达式,我们会将其解释成一个加法表达式,参与运算的一个分量是一个乘法而非一个运算分量为加法的乘法表达式,这样才能保证运算顺序的合理性。

同样,由于括号是最基本的primaryexp,因此括号的优先级会保证为最高,这样就解决了优先级的问题。

然后是RISCV如何支持大于等于和小于等于:我们只需判断是否大于(小于)和是否等于,然后二者或起来即可

在这里解析时,我们会发现取出的语句类型不再是KOOPA_RVT_RETURN了,而是KOOPA_RVT_BINARY,因此我们要查看koopa.h,寻找KOOPA_RVT_BINARY相关的操作

而从KOOPA生成RISCV时,需要注意到所有的表达式都只支持寄存器操作,因此我们要首先找到两个操作数的寄存器,我们使用这样的函数:

void slice_value(koopa_raw_value_t l,koopa_raw_value_t r,int &lreg,int &rreg,int &noww)
{ if(l->kind.tag==KOOPA_RVT_INTEGER)
{
if(l->kind.data.integer.value==0)
{
lreg=-1;
}else
{
cout<<" li t"<<noww++<<","<<l->kind.data.integer.value<<endl;
lreg=noww-1;
}
}else lreg=M[(ull)l];
if(r->kind.tag==KOOPA_RVT_INTEGER)
{
if(r->kind.data.integer.value==0)rreg=-1;
else
{
cout<<" li t"<<noww++<<","<<l->kind.data.integer.value<<endl;
rreg=noww-1;
}
}else rreg=M[(ull)r];
}

这个函数的逻辑是如果操作数是一个integer,我们就把其移到一个寄存器里,而如果操作数是一个运算表达式,我们就找到存储其结果的寄存器参与下面的运算,最后得到两个操作数所在的寄存器lreg和rreg

而特别地,我们知道寄存器x0总是存储着0,所以我们没必要把零加载到一个寄存器里,在输出寄存器时我们这样写:

void print(int lreg,int rreg)
{
if(lreg==-1)
cout<<"x0";
else
cout<<"t"<<lreg; if(rreg==-1)
cout<<", x0"<<endl;
else
cout<<", t"<<rreg<<endl;
}

而进行完运算后,我们再把结果存储到一个寄存器里,就像这样:

else if(exp.op==7)//减法
{
slice_value(l, r, lreg, rreg, noww);
cout<<" sub t"<<noww++<<", ";
print(lreg,rreg);
M[(ull)value]=noww-1;
}

这里使用一个unordered_map将一条指令与存储其结果的寄存器对应起来,注意我们假设寄存器数量充足,因此我们只需一直增加寄存器的标号即可。

lv4:

现在我们要支持变量了!

我们还是从lv4.1开始,考虑常量的定义

那么由于多了一个关键字,我们首先应该在.l文件里加入一个

"const"         { return CONST; }

然后观察一下EBNF,有:

Decl          ::= ConstDecl;
ConstDecl ::= "const" BType ConstDef {"," ConstDef} ";";
BType ::= "int";
ConstDef ::= IDENT "=" ConstInitVal;
ConstInitVal ::= ConstExp; Block ::= "{" {BlockItem} "}";
BlockItem ::= Decl | Stmt; LVal ::= IDENT;
PrimaryExp ::= "(" Exp ")" | LVal | Number; ConstExp ::= Exp;

我们的main函数里终于有除了return以外的第二条语句,所以我们要首先修改一下块里面的内容:

BlockItem
: MulBlockItem{
auto ast=new BlockItem();
ast->block_item=unique_ptr<BaseAST>($1);
$$=ast;
}|SinBlockItem{
auto ast=new BlockItem();
ast->block_item=unique_ptr<BaseAST>($1);
$$=ast;
}
;

即一个块里可以有一条语句或很多条语句。

而对于一条语句,这里只可能是一条指令或者一个声明,因此:

SinBlockItem
: Stmt {
auto ast=new SinBlockItem();
ast->sin_block_item=unique_ptr<BaseAST>($1);
$$=ast;
}|Decl {
auto ast=new SinBlockItem();
ast->sin_block_item=unique_ptr<BaseAST>($1);
$$=ast;
}
;

对于很多条语句,一定以一条语句开头,于是:

MulBlockItem
: SinBlockItem BlockItem{
auto ast=new MulBlockItem();
ast->sin_item=unique_ptr<BaseAST>($1);
ast->mul_item=unique_ptr<BaseAST>($2);
$$=ast;
}
;

而一条语句可以是一个return语句或者一个赋值语句(当然lv4.1没有赋值,因为全是常量),因此我们这样写:

Stmt
: RETURN Exp ';'{
auto ast=new Stmt();
ast->exp= unique_ptr<BaseAST>($2);
ast->typ=0;
$$=ast;
}|LeVal '=' Exp ';'{
auto ast=new Stmt();
ast->lval=unique_ptr<BaseAST>($1);
ast->exp=unique_ptr<BaseAST>($3);
ast->typ=1;
$$=ast;
}
;

一个声明可以是一个常量声明或一个变量声明(当然lv4.1没有变量),因此:

Decl
: ConstDecl {
auto ast=new Decl();
ast->decl=unique_ptr<BaseAST>($1);
$$=ast;
}|VarDecl {
auto ast=new Decl();
ast->decl=unique_ptr<BaseAST>($1);
$$=ast;
}
;

前面东西都很简单,但是...这个重复怎么解决啊!

这个重复要求我们能够识别这样的东西:

const int a=1,b=2,c=3,d=4;//even more

文档建议我们使用vector之类的结构来处理,这当然也可行,但是我起初没有找到合理的方法,所以...

我采用了另一种更诡异的方法:首先我们把整个ConstDecl解释成一个大的整体,对这个整体而言,其有两种可能:有一个声明和有两个或两个以上个声明

那么也就是这样:

ConstDecl
: CONST INT MulConstDef ';'{
auto ast=new ConstDecl();
ast->const_decl=unique_ptr<BaseAST>($3);
$$=ast;
}|CONST INT ConstDef ';'
{
auto ast=new ConstDecl();
ast->const_decl=unique_ptr<BaseAST>($3);
$$=ast;
}
;

而对于只有一个声明的情况,是很简单的:

ConstDef
: IDENT '=' ConstInitVal{
auto ast=new ConstDef();
ast->IDENT=*unique_ptr<string>($1);
ast->const_init_val=unique_ptr<BaseAST>($3);
$$=ast;
}
;

但是对于有多个声明的情况,其一定会以一个常量声明为开头,后面跟上其他的常量声明,也就是:

MulConstDef
: ConstDef MulConstDecl{
auto ast=new MulConstDef();
ast->const_def=unique_ptr<BaseAST>($1);
ast->mul_const_dcl=unique_ptr<BaseAST>($2);
$$=ast;
}
;

而后面跟着的常量声明,一定以一个逗号开头,后面跟着一个或多个常量声明,这样就是一个递归的过程:

MulConstDecl
: ',' MulConstDef{
auto ast=new MulConstDecl();
ast->mul_const_def=unique_ptr<BaseAST>($2);
$$=ast;
}|',' ConstDef {
auto ast=new MulConstDecl();
ast->mul_const_def=unique_ptr<BaseAST>($2);
$$=ast;
}
;

这样我们就识别出了重复的常量,而我们后面要解释类似的东西也都使用这样的方法。

接下来我们要解决两个问题:

第一,我们如何“识别”一个符号?

我们需要构建一个叫“符号表”的结构,用来存储我们定义的符号名、其值(如果是常量)、其类型等等,而这里我们推荐使用C++的unordered_map,因为这个结构不内置排序,所以效率相较于map要高一些。

比如我们构造一个这样的unordered_map:

static std::unordered_map<std::string, int> const_val;

这个表把一个常量名映射到其值。

第二,如何在编译期实现对常量的求值?

首先我们要解决的是表达式里多了一个Lval,这个非终结符识别了一个符号名,在常量声明中这个符号只会对应一个前面已经声明好的常量,因此我们只需要用上述符号表返回这个常量的值即可。

受到这里的启发,我们在所有的表达式相关类里加入一个函数int Calc(),这个函数可以计算后面表达式的值,而计算方法则是很显然的,以加法表达式为例:

class MuladdExp:public BaseAST
{
public:
std::unique_ptr<BaseAST> ad_exp;
char op;
std::unique_ptr<BaseAST> mult_exp;
void Dump()const override
{
//Dump函数的内容
}
int Calc() const override
{
if(op=='+')
return (ad_exp->Calc())+(mult_exp->Calc());
else
return (ad_exp->Calc())-(mult_exp->Calc());
}
};

直接用递归方法逐个计算表达式的值即可。

综上所述,声明一个常量时不需要真正输出什么东西,但是需要在后台计算好常量的值并保存好其类型,于是有:

class ConstDef:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> const_init_val;
int Calc()const override
{
const_val[IDENT]=const_init_val->Calc();
return const_val[IDENT];
}
void Dump()const override
{
var_type[IDENT]=0;
Calc();
//std::cout<<IDENT<<std::endl;
//const_init_val->Dump();
} };

而对于多个声明,简单地逐个处理即可:

class MulConstDef:public BaseAST
{
public:
std::unique_ptr<BaseAST> const_def;
std::unique_ptr<BaseAST> mul_const_dcl;
void Dump()const override
{
const_def->Dump();
mul_const_dcl->Dump();
}
int Calc() const override{ return 3; }
};

由于编译器认为常量和一个数值没有区别,因此生成Koopa和RISCV都与之前没有区别,具体地,比如我们有下面的程序:

int main()
{
const int a=0;
return a;
}

那我们希望生成的Koopa是

fun @main(): i32 {
%entry:
ret 0
}

但是实际上我们会把return后面的a解释成一个Exp,因此我们在Dump的时候对于一个常量我们要直接输出其值,当然为了一致性,我们输出的实际上是其值加0这样一个表达式,即:

class Lval:public BaseAST
{
public:
std::string IDENT;
void Dump()const override
{
std::cout<<"%"<<nowww<<"= add "<<"0 ,"<<const_val[IDENT]<<std::endl;
nowww++;
}
int Calc() const override
{
//Calc相关内容
}
};

这样我们就处理好了常量。

接下来我们考察如何处理变量:

对于变量,和常量的区别只是在于变量可以没有初值以及变量可以被重新赋值。

事实上我们对于常量的认识就是一个数,它实际上就是其值的另一个名字,我们不需要真正在程序的内存中保留一个地方来存储这个常量对应的值,我们只需要在编译时记住这个常量的值然后每次调用时直接用其值去替换这个常量名字即可。

但是对于变量而言,其值是可以随着程序的运行而改变的,因此我们需要在程序中为变量分配一个内存地址用来存储这个变量的值,这也是我们编译器要做的工作。

识别变量的方法其实和识别常量的方法是类似的,区别在于变量不需要能够计算出(实际上也可能不能计算出)初值,同时变量可能没有初值!

那么为了识别所有的情况,我们这样写:一个变量的声明一定是int开头后跟一些变量名和(可能有的)初值,那么:

VarDecl
: INT VarDef ';' {
auto ast=new VarDecl();
ast->var_decl=unique_ptr<BaseAST>($2);
$$=ast;
}
;

而一个变量定义可能包含单个变量或多个变量,也就是:

VarDef
: SinVarDef {
auto ast=new VarDef();
ast->var_def=unique_ptr<BaseAST>($1);
$$=ast;
}|MulVarDef {
auto ast=new VarDef();
ast->var_def=unique_ptr<BaseAST>($1);
$$=ast;
}
;

单个变量的定义可能只声明了变量名或同时声明了变量名和初值,因此:

SinVarDef
: SinVarName {
auto ast=new SinVarDef();
ast->sin_var_def=unique_ptr<BaseAST>($1);
$$=ast;
}|MulVarName {
auto ast=new SinVarDef();
ast->sin_var_def=unique_ptr<BaseAST>($1);
$$=ast;
}
;

如果只声明了变量名的话:

SinVarName
: IDENT {
auto ast=new SinVarName();
ast->IDENT=*unique_ptr<string>($1);
$$=ast;
}
;

而如果同时声明了变量名和初值,我们知道初值一定是一个表达式,因此:

MulVarName
: IDENT '=' InitVal {
auto ast=new MulVarName();
ast->IDENT=*unique_ptr<string>($1);
ast->init_val=unique_ptr<BaseAST>($3);
$$=ast;
}
; InitVal
: Exp {
auto ast=new InitVal();
ast->init_exp=unique_ptr<BaseAST>($1);
$$=ast;
}
;

最后,对于一次性声明了多个变量的情况:

MulVarDef
: SinVarDef ',' VarDef {
auto ast=new MulVarDef();
ast->sin_var=unique_ptr<BaseAST>($1);
ast->mul_var=unique_ptr<BaseAST>($3);
$$=ast;
}
;

而变量定义的Koopa怎么生成呢?上面说过一个变量是需要在内存中有自己的位置的,因此这里需要配合alloc,load,store语句使用。

在声明一个变量时,我们要alloc一个东西:

class SinVarName:public BaseAST
{
public:
std::string IDENT;
void Dump()const override
{
std::cout<<"@"<<IDENT<<" = alloc i32"<<std::endl;
var_type[IDENT]=1;
const_val[IDENT]=0;
}
int Calc()const override
{
return 0;
}
};

即alloc一个32位int

而如果这个变量有初值,我们就要计算出这个初值然后存储到这个变量里,也即:

class MulVarName:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> init_val;
void Dump()const override
{
std::cout<<"@"<<IDENT<<" = alloc i32"<<std::endl;
//const_val[IDENT]=init_val->Calc();
var_type[IDENT]=1;
init_val->Dump();
std::cout<<"store %"<<nowww-1<<", @"<<IDENT<<std::endl;
}
int Calc()const override
{
return const_val[IDENT];
}
};

注意这里的定义同样维护了一个符号表:

static std::unordered_map<std::string, int> var_type;//确定某一变量的类型(常量 or 变量?)

这样就解决了定义的问题,但是这里同样需要支持变量的赋值。

而变量的赋值意味着左边是一个变量名,右边是一个表达式,于是:

Stmt
: RETURN Exp ';'{
auto ast=new Stmt();
ast->exp= unique_ptr<BaseAST>($2);
ast->typ=0;
$$=ast;
}|LeVal '=' Exp ';'{
auto ast=new Stmt();
ast->lval=unique_ptr<BaseAST>($1);
ast->exp=unique_ptr<BaseAST>($3);
ast->typ=1;
$$=ast;
}
;

这里使用的非终结符是LeVal而并不是Lval,因为我们注意到等号左边和右边出现的变量的行为是不一样的,我们要把等号右边的变量的值加载出来好参与运算,而要把运算的结果存储到等号左边的变量里,因此我们要对二者有所区分,其中右侧需要把变量的值加载出来,需要用到load指令,也就是:

class Lval:public BaseAST
{
public:
std::string IDENT;
void Dump()const override
{
if(var_type[IDENT]==0)
std::cout<<"%"<<nowww<<"= add "<<"0 ,"<<const_val[IDENT]<<std::endl;
else
std::cout<<"%"<<nowww<<"= load "<<"@"<<IDENT<<std::endl;
nowww++;
}
int Calc() const override
{
return const_val[IDENT];
}
};

如上述代码所示:当一个符号出现在等号右侧时,我们要将其值加载到一个临时符号里面以便于参与后续的运算。

(这里隐含了一种代码生成的假设:由于我们只处理所有合法的sysy程序,因此如果我需要调用Lval的Calc函数,说明此时的符号一定是一个常量,因此我可以直接在const_val中去寻找而不必担忧出现错误)

class LeVal:public BaseAST
{
public:
std::string IDENT;
void Dump()const override
{
//assert(var_type[IDENT]!=0);
std::cout<<"store %"<<nowww-1<<", @"<<IDENT<<std::endl; }
int Calc()const override
{
return 0;
}
};

但是对于等号左边的变量,我们只需要把运算结果存在其中就可以了,这样还是用到store指令。

同时,对于一条语句,现在有了两种可能性,于是我们应当这样处理:

class Stmt:public BaseAST
{
public:
std::unique_ptr<BaseAST> lval;
std::unique_ptr<BaseAST> exp;
int typ;
void Dump() const override
{
if(typ==0)//return
{
exp->Dump();
std::cout << "ret " <<'%'<<nowww-1<<std::endl;
}else //赋值
{
exp->Dump();
lval->Dump();
}
}
int Calc() const override{ return 6; }
//~Stmt(){ return 0; }
};

注意到赋值时我们先输出等号右侧的表达式,再把其存到等号左边的变量里即可。

这样我们就基本解决了常量和变量的koopa,而在生成对应riscv时,我们需要多处理的是load,store和alloc三条指令。

那么我们要解决的其实是一个问题——对于我们声明的每个变量,我们都需要找到一个位置用于存储这个变量的值,我们可以把这个变量的值关联到一个寄存器上(正如我们前三个lv所做的那样),但是随着我们使用的变量的增加,寄存器的数量总是不够的。

另外的问题就是寄存器本身也区分caller saved和callee saved,也就是说我们不能胡乱破坏寄存器的值,寄存器的值在破坏之前也要保存好(除非我们肯定这个值不会被复用,但一般我们都保证不了这一点。)

于是如何进行寄存器的分配就成了相当困难的一个问题,于是文档中给出了一个解决方案——索性不做寄存器分配了罢!我们把所有需要储存起来留给别的指令调用的值都存储到内存里,等到需要用的时候就从内存里随用随读到寄存器里就好!

当然,这样做是相当浪费的,但是这样做能大幅度减小工作量,因此...我们还是选择了这种方法。

而内存是如何布局的呢?想象内存是一个巨大的数组,我们使用的部分是一个叫做运行时栈的结构,有一个栈指针寄存器sp存储当前栈顶所在地址,栈顶向下增长(即sp减小),每个函数能使用的部分是从上一个函数的栈顶sp向下增长出来的。

因此我们每遇到一个函数,首先要把栈顶向下增长用来给这个函数分配所需要的内存空间,但是具体分配多少空间呢?

这个分配多少空间是可以计算的,但是这个计算过程暂且留到后面,对于绝大多数内容我们开一个固定大小的栈帧就够了,这里先选择256

for (size_t i = 0; i < raw.funcs.len; ++i)
{
// 正常情况下, 列表中的元素就是函数, 我们只不过是在确认这个事实
// 当然, 你也可以基于 raw slice 的 kind, 实现一个通用的处理函数 assert(raw.funcs.kind == KOOPA_RSIK_FUNCTION);
// 获取当前函数
koopa_raw_function_t func = (koopa_raw_function_t) raw.funcs.buffer[i];
cout<<" .globl "<<func->name+1<<endl;
cout<<func->name+1<<":"<<endl;
cout<<" addi sp, sp, -256"<<endl; //......
cout<<" addi sp, sp, 256"<<endl;
}

如上述代码所示,把栈帧向下增长了256个字节(当然,不要忘记在处理完这个函数之后把栈帧恢复了啊!)

而对于所有的结果,我们都设法将其存放在栈上,这样以减法指令为例:我们计算了两个数相减的结果,然后把这个结果存储在栈上,那么我们要记录下这条指令的结果存放在了栈中这个位置上,因此我们还需要一个unordered_map用来维护这样的映射(即把一条指令映射到一个内存偏移量),然后修改内存偏移量用来存储后面的结果。

else if(exp.op==7)//减法
{
slice_value(l, r, lreg, rreg);
cout<<" sub t2"<<", ";
print(lreg,rreg);
cout<<" sw t2"<<", "<<st<<"(sp)"<<endl;
M[(ull)value]=st;
st+=4;
}

而我们怎么获取先前保存的结果呢?我们维护了每条指令的执行结果到内存偏移量的映射,这样我们可以直接根据指令在内存中找到其结果对应的位置,以算术表达式为例:处理算术表达式时我们需要将两个运算分量分别放到寄存器里,那么我们就要找到其对应的内存位置然后load出来,也就是这样:

void slice_value(koopa_raw_value_t l,koopa_raw_value_t r,int &lreg,int &rreg)
{
if(l->kind.tag==KOOPA_RVT_INTEGER)
{
if(l->kind.data.integer.value==0)
{
lreg=-1;
}else
{
cout<<" li t0"<<","<<l->kind.data.integer.value<<endl;
lreg=0;
}
}else
{
cout<<" lw t0, "<<M[(ull)l]<<"(sp)"<<endl;
lreg=0;
}
if(r->kind.tag==KOOPA_RVT_INTEGER)
{
if(r->kind.data.integer.value==0)rreg=-1;
else
{
cout<<" li t1"<<","<<l->kind.data.integer.value<<endl;
rreg=1;
}
}else
{
cout<<" lw t1, "<<M[(ull)r]<<"(sp)"<<endl;
rreg=1;
}
}

类似地,对于一个alloc指令,实际上就是在申请一块内存空间用来存放一个变量,在lv4中暂时不需要对其单独处理,我们把它和store指令放到一起去处理。

对于store指令,我们阅读koopa.h可以发现我们其有两个组成部分:要store进去的value和store的目的,要store进去的value可能是一个立即数,那我们就要把这个立即数读入寄存器里,也可能是一个运算结果,而这个运算结果根据我们刚才所说一定被存放在了栈的某个位置上,那么我们就要把它读到一个寄存器里。而要store的目的也有两种可能,一种可能是我们已经为其分配了一个空间,那么我们同样可以直接从映射中读出来,而另一种可能则是还没有为其分配空间,那我们就先为其分配一个空间即可。最后我们把值存到分配的这个空间中去即可。

void solve_store(koopa_raw_value_t value,int &st)
{
koopa_raw_store_t sto=value->kind.data.store;
koopa_raw_value_t sto_value=sto.value;
koopa_raw_value_t sto_dest=sto.dest;
if(sto_value->kind.tag==KOOPA_RVT_INTEGER)
{
cout<<" li t0, "<<sto_value->kind.data.integer.value<<endl;
}else
{
cout<<" lw t0, "<<M[(ull)sto_value]<<"(sp)"<<endl;
}
if(M.find((ull)sto_dest)==M.end())
{
M[(ull)sto_dest]=st;
st+=4;
}
cout<<" sw t0, "<<M[(ull)sto_dest]<<"(sp)"<<endl;
}

而有了上面的说法之后,处理load指令就变得容易了——load指令并不需要真正意义上处理什么,只需要记录下这条指令的结果就存储在被加载的对象的内存位置上即可,这样我们如果后面需要用到load出来的值我们就直接到被加载的对象的内存位置上找总是对的。

void solve_load(koopa_raw_value_t value,int &st)
{
M[(ull)value]=M[(ull)(value->kind.data.load.src)];
}

这样我们就解决了lv4的问题,然后我们就进入了lv5

lv5:

lv5要求我们支持语句块和作用域的处理,所谓语句块大概是这样的东西:

int main()
{
int a=1;
{
int a=2;
return a;//这里返回值应为2
}
}

每个大括号会新引出一个语句块,在这个语句块中变量是可以与语句块外层的变量重名的

当然了,语句块里的变量也可以直接调用语句块外层的变量,但是不能调用与其不相交语句块或其内层语句块的变量,或者说一个变量的作用域就是从其被声明开始到这个语句块结束为止,而使用一个变量时优先使用“最深”的那个变量。(大概说明白了?)

那么我们首先就要能够识别这些语句块,这样我们发现一个stmt其实可以是return、赋值、单纯的运算表达式(当然这没什么用就是了)、单纯的分号(也没什么用)和一个语句块!

于是我们要这样识别:

Stmt
: RETURN Exp ';'{
auto ast=new Stmt();
ast->exp= unique_ptr<BaseAST>($2);
ast->typ=0;
$$=ast;
}|LeVal '=' Exp ';'{
auto ast=new Stmt();
ast->lval=unique_ptr<BaseAST>($1);
ast->exp=unique_ptr<BaseAST>($3);
ast->typ=1;
$$=ast;
}|Block {
auto ast=new Stmt();
ast->exp=unique_ptr<BaseAST>($1);
ast->typ=3;
$$=ast;
}|Exp';'{
auto ast=new Stmt();
ast->exp=unique_ptr<BaseAST>($1);
ast->typ=2;
$$=ast;
}|';'
{
auto ast=new Stmt();
ast->typ=5;
$$=ast;
}
;

而同样,一个语句块里也可以什么都没有,于是我们要加入这样的识别:

Block
: '{' BlockItem '}' {
auto ast=new Block();
ast->block = unique_ptr<BaseAST>($2);
ast->typ=0;
$$=ast;
}|'{' '}'
{
auto ast=new Block();
ast->typ=1;
$$=ast;
}
;

接下来我们考虑如何解决重名变量的识别问题,首先我们观察一下这些语句块的结构:

{
{
{
//...
}
}
{
//...
}
}

我们把语句块抽象成这个样子,可以看到这其实是一个嵌套括号序列的结构,比如上面的结构大概是这个样子:((())())

而这样的括号序列实际上又是一种树形结构,比如如果我们对这个结构编号,大概就是这样的一棵树:

1有两个子节点2和4,2有一个子节点3

据此,我们可以设法维护下这个树结构,然后用一个全局变量记录下我们当前位于哪个语句块中,那么如果我们离开了当前语句块,我们一定就走向了当前语句块的一个父节点,而如果我们新进入了一个语句块,那么我们就为当前节点生成一个子节点即可!

(当然,在同一时刻要被使用到的实际上只会是从根节点到某个节点的一条链而不需要整个树结构)

而对于一个变量的声明,我们为其附加一个其所属的语句块的编号,然后在使用这个变量的时候沿着当前所在语句块编号向根节点在符号表中查询直到查找到定义即可。

举个例子:

int main()
{
int a=1;
{
return a;
}
}

我们在定义int a=1时,我们认为当前正位于语句块1,这样我们在编译器中把变量a叫做a1

而我们试图执行return a时,我们发现自己正处于语句块2,那么我们首先尝试在符号表中查找a2,发现找不到,于是我们查看当前语句块的父节点,发现是语句块1,那么我们尝试在符号表中查找a1,发现查到了,于是我们使用变量a1即可

这样我们在维护的时候就要这样维护:

class Block:public BaseAST
{
public:
std::unique_ptr<BaseAST> block;
int typ;
void Dump() override
{
if(typ==0)
{
dep++;
f[dep]=nowdep;
nowdep=dep;
block->Dump();
nowdep=f[nowdep];
}
}
int Calc() override{ return 10; }
//~Block(){ return 0; }
};

而在定义的时候要这样定义(以有初值的变量定义为例,其余类似):

class MulVarName:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> init_val;
void Dump()override
{
IDENT=IDENT+std::to_string(nowdep);
std::cout<<"@"<<IDENT<<" = alloc i32"<<std::endl;
//const_val[IDENT]=init_val->Calc();
var_type[IDENT]=1;
init_val->Dump();
std::cout<<"store %"<<nowww-1<<", @"<<IDENT<<std::endl;
}
int Calc()override
{
return const_val[IDENT];
}
};

在查询的时候只需这样查询:

class Lval:public BaseAST
{
public:
std::string IDENT;
void Dump()override
{
int tempdep=nowdep;
while(var_type.find(IDENT+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep];
IDENT=IDENT+std::to_string(tempdep);
if(var_type[IDENT]==0)
std::cout<<"%"<<nowww<<"= add "<<"0 ,"<<const_val[IDENT]<<std::endl;
else
std::cout<<"%"<<nowww<<"= load "<<"@"<<IDENT<<std::endl;
nowww++;
}
int Calc() override
{
int tempdep=nowdep;
while(var_type.find(IDENT+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep];
IDENT=IDENT+std::to_string(tempdep);
return const_val[IDENT];
}
};

当然,现实情况下这样的做法必然是有问题的,举个例子:如果我们在语句块1中定义了一个变量a1,而在语句块11中定义了一个变量a,这样就会导致重名(而实际上本没有重名!)

为了解决这个问题,我们实际上在编译器中对于位于语句块x中的变量a,后面会起名字COMPILER__a_x,这样我们能清楚地找出该变量所在的语句块(也能满足某种显得“专业”的恶趣味)。

这样我们就解决了lv5的问题

lv6:

这部分我们要处理if/else,那么首先观察一下语法规范:

Stmt ::= ...
| ...
| ...
| "if" "(" Exp ")" Stmt ["else" Stmt]
| ...;

那这里多了两个关键字if和else,因此我们首先要更改词法分析:

"if"            { return IF; }
"else" { return ELSE; }

如何识别呢?和上文类似地:if语句有两种可能:单纯的if和if-else配套语句。

IfStmt
: SinIfStmt {
auto ast=new IfStmt();
ast->if_stm=unique_ptr<BaseAST>($1);
$$=ast;
}|MulIfStmt {
auto ast=new IfStmt();
ast->if_stm=unique_ptr<BaseAST>($1);
$$=ast;
}
;

单纯的if就是这样:

SinIfStmt
: IF '(' Exp ')' Stmt {
auto ast=new SinIfStmt();
ast->if_exp=unique_ptr<BaseAST>($3);
ast->if_stmt=unique_ptr<BaseAST>($5);
$$=ast;
}
;

而if-else配套使用就是这样:

MulIfStmt
: IF '(' Exp ')' Stmt ELSE Stmt {
auto ast=new MulIfStmt();
ast->if_exp=unique_ptr<BaseAST>($3);
ast->if_stmt=unique_ptr<BaseAST>($5);
ast->el_stmt=unique_ptr<BaseAST>($7);
$$=ast;
}
;

当然,这样的文法是有二义性的——比如如下的代码if exp1 stmt1 if exp2 stmt2 else stmt3

那么按照上面的文法,我们既可以解释成第一个if是一个单独的if,里面有一个if-else语句,也可以解释成第一个if与后面的else匹配,而第二个if是第一个if里面的一个语句。

但是这个问题语法分析工具会自动帮我们处理成匹配最近的if-else,因此这里就不作处理了。

而接下来,我们就需要生成if对应的koopa,这里我们需要用到跳转指令,跳转指令格式形如br %x,label1,label2表示按照%x是否为真,若为真跳转到label1,否则跳转到label2

那么怎么生成呢?我们要先对exp进行求值,假设求值的结果放在了%x里,然后我们生成分支语句,接下来生成一个标号表示为真的情况,接下来输出为真的语句,接下来生成一个标号表示为假的情况,最后输出为假的语句。

而对于if/else配套的语句,我们在生成if为真的语句之后,不能继续去执行if为假的语句,因此我们要在最后生成一条跳转指令跳转到if结束的位置,因此我们在这里还需要生成一个if结束的标号来实现跳转。

上面的是一个基本的思想,落实到具体实现,我们要解决一个问题:我们在生成具体的代码之前就已经生成了标号,那么我们就要保证这个标号的正确性,因此一个最直接的方法就是我们使用一套统一的标准进行标号,然后在输出代码时记录好标号即可。

接下来的一个问题就是——koopa中的一个基本块必须只能以一个ret/jump/branch语句结束,而不能有多个,因此如果出现这样的情况:

int main()
{
if(1)
{
return 1;
}else
{
return 2;
}
return 0;
}

在if为真的部分中已经出现了一个ret,此时我们不应当再生成一个跳转语句跳转到if结束了。

再比如一个更简单的例子:

int main()
{
return 0;
return 1;
}

我们只应当生成一个ret语句而不应当生成多个,那么怎么保证这一点呢?

其实很简单,我们用一个计数变量记录当前所处的基本块,如果当前基本块已经生成了这样的跳转语句,那么就不继续生成后面的语句了,这样还可以节约一些资源。

而目前我们认为进入main函数会生成一个新的基本块,进入if的时候,进入else的时候,离开if/else的时候都需要新生成基本块,然后在基本块中如果已经生成过ret之类的指令,那么就不生成别的跳转指令了。

class SinIfStmt:public BaseAST
{
public:
std::unique_ptr<BaseAST> if_exp;
std::unique_ptr<BaseAST> if_stmt;
void Dump()override
{
if(be_end_bl[nowbl])return;
if_cnt++;
int now_if=if_cnt; if_exp->Dump(); std::cout<<"\tbr %"<<nowww-1<<", %then"<<now_if<<", %end"<<now_if<<std::endl;
std::cout<<std::endl; std::cout<<"%then"<<now_if<<":"<<std::endl; bl_dep++;
nowbl=bl_dep; if_stmt->Dump(); if(!be_end_bl[nowbl])std::cout<<"\tjump %end"<<now_if<<std::endl; std::cout<<std::endl;
std::cout<<"%end"<<now_if<<":"<<std::endl;
bl_dep++;
nowbl=bl_dep;
}
int Calc()override
{
return 20;
}
};

对于if/else嵌套的情况,我们使用类似的方法:

class MulIfStmt:public BaseAST
{
public:
std::unique_ptr<BaseAST> if_exp;
std::unique_ptr<BaseAST> if_stmt;
std::unique_ptr<BaseAST> el_stmt;
void Dump()override
{
if(be_end_bl[nowbl])return;
if_cnt++;
int now_if=if_cnt; if_exp->Dump();
std::cout<<"\tbr %"<<nowww-1<<", %then"<<now_if<<", %else"<<now_if<<std::endl;
std::cout<<std::endl;
std::cout<<"%then"<<now_if<<":"<<std::endl; bl_dep++;
nowbl=bl_dep; if_stmt->Dump();
if(!be_end_bl[nowbl])std::cout<<"\tjump %end"<<now_if<<std::endl; bl_dep++;
nowbl=bl_dep; std::cout<<std::endl;
std::cout<<"%else"<<now_if<<":"<<std::endl; el_stmt->Dump(); if(!be_end_bl[nowbl])std::cout<<"\tjump %end"<<now_if<<std::endl;
std::cout<<std::endl;
std::cout<<"%end"<<now_if<<":"<<std::endl;
bl_dep++;
nowbl=bl_dep;
}
int Calc()override
{
return 21;
}
};

同样在生成指令的时候,我们要先检查这个基本块内是否已经生成过ret之类的指令,如果生成过那么就不应该再生成后面的指令了。

class SinBlockItem:public BaseAST
{
public:
std::unique_ptr<BaseAST> sin_block_item;
void Dump()override
{
if(be_end_bl[nowbl])return;
sin_block_item->Dump();
}
int Calc() override{ return 7; }
};

这样if的问题就基本解决了,而使用处理if的方法可以实现短路求值

所谓短路求值,是指在&&和||这样的逻辑表达式中,我们先对第一个运算分量进行求值,对于&&而言,如果第一个运算分量为假,那么我们就不对第二个分量求值直接返回假,同样对于||而言,如果第一个运算分量为真,那么我们就不对第二个运算分量求值直接返回真。

这样做的正确性是显然的,而且这种要求是很有用的,比如我想对一个指针求值,我可以这样写:

if(p&&(*p)==1)
return 1;
else
return 0;

如果不是短路求值,上面对指针的求值可能会带来错误——即使已经判定了为空指针,也会试图对其求值。

因此我们规定短路求值是逻辑运算的要求,而在这里我们就可以用if的方法来实现,文档中给了说明:

短路求值 lhs || rhs 本质上做了这个操作:

int result = 1;
if (lhs == 0) {
result = rhs != 0;
}
// 表达式的结果即是 result

那么我们在求值时类比这个过程即可,比如处理逻辑与就像这样:

class MulAndExp:public BaseAST
{
public:
std::unique_ptr<BaseAST> and_exp;
std::string op;
std::unique_ptr<BaseAST> e_exp;
void Dump()override
{
and_exp->Dump();
int now1=nowww-1;
int temp=nowww;
std::cout<<"\t@result_"<<temp<<" = alloc i32"<<std::endl;
std::cout<<"\t%"<<nowww<<"= ne 0, %"<<now1<<std::endl;
std::cout<<"\tstore %"<<now1<<", @result_"<<temp<<std::endl;
nowww++; if_cnt++;
int now_if=if_cnt; std::cout<<"\tbr %"<<now1<<", %then"<<now_if<<", %end"<<now_if<<std::endl;
std::cout<<std::endl;
std::cout<<"%then"<<now_if<<":"<<std::endl;
e_exp->Dump();
int now2=nowww-1;
std::cout<<"\t%"<<nowww<<"= ne 0, %"<<now2<<std::endl;
nowww++;
std::cout<<"\tstore "<<'%'<<nowww-1<<", @result_"<<temp<<std::endl;
std::cout<<"\tjump %end"<<now_if<<std::endl; std::cout<<std::endl;
std::cout<<"%end"<<now_if<<":"<<std::endl;
std::cout<<"\t%"<<nowww<<"= load @result_"<<temp<<std::endl;
nowww++;
}
int Calc() override
{
return (and_exp->Calc())&&(e_exp->Calc());
}
};

对于逻辑或也是同理的。

接下来我们来生成RISCV:生成RISCV时我们需要处理的一些别的问题

第一,我们现在会有很多个基本块,那么我们在循环的时候就要逐个输出这些基本块,因此我们需要一个循环:

for (size_t j = 0; j < func->bbs.len; ++j)
{
assert(func->bbs.kind == KOOPA_RSIK_BASIC_BLOCK); koopa_raw_basic_block_t bb = (koopa_raw_basic_block_t)func->bbs.buffer[j];
if(bb->name)cout<<endl<<bb->name+1<<":"<<endl;
for (size_t k = 0; k < bb->insts.len; ++k)
{
koopa_raw_value_t value = (koopa_raw_value_t)bb->insts.buffer[k];
if(value->kind.tag == KOOPA_RVT_RETURN)
{
solve_return(value,st,i,typ);
}else if(value->kind.tag==KOOPA_RVT_BINARY)
{
solve_binary(value,st);
}else if(value->kind.tag==KOOPA_RVT_LOAD)
{
solve_load(value,st);
}else if(value->kind.tag==KOOPA_RVT_STORE)
{
solve_store(value,st,i,typ);
}else if(value->kind.tag==KOOPA_RVT_BRANCH)
{
solve_branch(value,st);
}else if(value->kind.tag==KOOPA_RVT_JUMP)
{
solve_jump(value,st);
}//后面的部分与lv6无关
/*else if(value->kind.tag==KOOPA_RVT_CALL)
{
solve_call(value,st);
}else if(value->kind.tag==KOOPA_RVT_GET_ELEM_PTR)
{
solve_get_element_ptr(value,st);
}else if(value->kind.tag==KOOPA_RVT_ALLOC)
{
solve_alloc(value,st);
}else if(value->kind.tag==KOOPA_RVT_GET_PTR)
{
solve_get_ptr(value,st);
}*/
}
}

(通过观察koopa.h可以找到一个基本块的名字)

接下来我们就可以处理branch语句了,在RISCV中没有能直接按真假进行双目标跳转的语句,因此我们把一个branch拆成两部分,即为真的时候跳转到哪和为假的时候跳转到哪。

而koopa.h中是如何看待branch语句的呢?其由三部分组成——判断条件(是一个之前应当已经计算出结果的表达式),为真的时候要跳转到的目标和为假的时候要跳转到的目标。

当然了,还有一个诡异的问题——由于某种原因,在RISCV中的分支语句的跳转范围是很小的,因此如果分支指令和跳转目标离得太远,我们就无法正常跳转

因此一个取巧的方法是——条件跳转很短,但是无条件跳转很长啊!因此我们先用条件跳转完成分支的过程,然后在分支之后用无条件跳转跳转到真实的语句。

那么无条件跳转语句在RISCV中怎么处理呢?其实更加容易——无条件跳转只需要一个跳转目标就可以跳过去了。

void solve_branch(koopa_raw_value_t value,int &st)
{
koopa_raw_branch_t bran=value->kind.data.branch;
cout<<" li t4, "<<M[(ull)bran.cond]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t3, (t4)"<<endl;
cout<<" beqz t3, "<<bran.false_bb->name+1<<bran.false_bb->name+1<<endl;
cout<<" bnez t3, "<<bran.true_bb->name+1<<bran.true_bb->name+1<<endl;
cout<<bran.false_bb->name+1<<bran.false_bb->name+1<<":"<<endl;
cout<<" j "<<bran.false_bb->name+1<<endl;
cout<<bran.true_bb->name+1<<bran.true_bb->name+1<<":"<<endl;
cout<<" j "<<bran.true_bb->name+1<<endl;
cout<<endl;
} void solve_jump(koopa_raw_value_t value,int &st)
{
koopa_raw_jump_t jum=value->kind.data.jump;
cout<<" j "<<jum.target->name+1<<endl;
cout<<endl;
}

这样lv6所有的问题就都解决了。

lv7:

lv7要求我们能够处理while循环以及对应的break和continue,语法规范如下:

Stmt ::= ...
| ...
| ...
| ...
| "while" "(" Exp ")" Stmt
    | "break" ";"   
    | "continue" ";"
| ...;

首先我们在词法分析过程中增加while,break和continue关键字

"while"         { return WHILE; }
"break" { return BREAK; }
"continue" { return CONTINUE; }

接下来识别while本身是很容易的:

WhileStmt
: WHILE '(' Exp ')' Stmt {
auto ast=new WhileStmt();
ast->while_exp=unique_ptr<BaseAST>($3);
ast->while_stmt=unique_ptr<BaseAST>($5);
$$=ast;
}
;

而识别break和continue也是很容易的:

ConWhile
: CONTINUE ';' {
auto ast=new ConWhile();
ast->str="continue";
$$=ast;
}|BREAK ';' {
auto ast=new ConWhile();
ast->str="break";
$$=ast;
}
;

而对while的处理与if很类似,无非就是在while的判断部分我们要生成一个标号,在循环结束时和循环开始前跳转到这个标号,同时在循环体之前和循环结束后生成一个新的标号,这样判断结尾就可以生成一个分支语句决定跳转到循环循环体还是循环末尾

同样,与if的处理相似,为了在生成循环体之前确定好标号,我们用统一的规则进行标号。

而对于break和continue的处理,break就生成一条跳转到循环结束的标号的语句,而continue就生成一条跳转到循环判断的语句即可,需要注意的是break和continue的出现意味着这个基本块已经结束,所以我们要做好标记,防止在一个基本块里出现多个ret/branch/jump这样的指令

另外,由于我们需要在break和continue时获取当前循环的标号,因此我们把循环的标号设计成全局变量,但这就意味着其会随着循环的嵌套而改变,那么为了维护这个问题我们记录一个改变的路径,当结束一个循环时回退到上一个循环的标号,这样保证在break和continue时获取的循环标号总是正确的。

class WhileStmt:public BaseAST
{
public:
std::unique_ptr<BaseAST> while_exp;
std::unique_ptr<BaseAST> while_stmt;
void Dump()override
{
wh_cnt++;
whf[wh_cnt]=now_wh;
now_wh=wh_cnt; if(!be_end_bl[nowbl])std::cout<<"jump %whilecheck"<<now_wh<<std::endl;
std::cout<<std::endl;
std::cout<<"%whilecheck"<<now_wh<<":"<<std::endl; while_exp->Dump();
if(!be_end_bl[nowbl])std::cout<<"\tbr %"<<nowww-1<<", %whilethen"<<now_wh<<", %endwhile"<<now_wh<<std::endl;
std::cout<<std::endl;
std::cout<<"%whilethen"<<now_wh<<":"<<std::endl; bl_dep++;
nowbl=bl_dep; while_stmt->Dump();
if(!be_end_bl[nowbl])std::cout<<"\tjump %whilecheck"<<now_wh<<std::endl; std::cout<<std::endl;
std::cout<<"%endwhile"<<now_wh<<":"<<std::endl;
now_wh=whf[now_wh];
bl_dep++;
nowbl=bl_dep;
}
int Calc()override
{
return 22;
}
}; class ConWhile:public BaseAST
{
public:
std::string str;
void Dump()override
{
if(str=="break")
{
std::cout<<"\tjump %endwhile"<<now_wh<<std::endl;
be_end_bl[nowbl]=1; }else
{
std::cout<<"\tjump %whilecheck"<<now_wh<<std::endl;
be_end_bl[nowbl]=1;
}
}
int Calc()override
{
return 23;
}
};

而lv7并没有使用新的指令,因此RISCV不需要重新生成,这样lv7就结束了。

lv8:

lv8要求我们能处理函数和全局变量,首先解决一下函数:

CompUnit    ::= [CompUnit] FuncDef;

FuncDef     ::= FuncType IDENT "(" [FuncFParams] ")" Block;
FuncType ::= "void" | "int";
FuncFParams ::= FuncFParam {"," FuncFParam};
FuncFParam ::= BType IDENT; UnaryExp ::= ...
| IDENT "(" [FuncRParams] ")"
| ...;
FuncRParams ::= Exp {"," Exp};

我们发现要处理的主要是:函数的定义,函数的参数和函数的调用。

首先函数的定义是允许返回值为void的,因此我们需要先做词法分析:

"void"          { return VOID; }

接下来对于函数的识别其实main函数已经帮我们做过了,但是我们写的识别只能识别返回值为int、没有参数的情况,我们对其进行拓展,就能得到:

FuncDef
: INT IDENT '(' ')' Block {
auto ast = new FuncDef();
ast->func_type = "int";
ast->ident = *unique_ptr<string>($2);
ast->block = unique_ptr<BaseAST>($5);
$$ = ast;
}|INT IDENT '(' FuncParas ')' Block {
auto ast = new FuncDef();
ast->func_type = "int";
ast->ident = *unique_ptr<string>($2);
ast->func_para=unique_ptr<BaseAST>($4);
ast->block = unique_ptr<BaseAST>($6);
$$ = ast;
}|VOID IDENT '(' ')' Block {
auto ast = new FuncDef();
ast->func_type = "void";
ast->ident = *unique_ptr<string>($2);
ast->block = unique_ptr<BaseAST>($5);
$$ = ast;
}|VOID IDENT '(' FuncParas ')' Block {
auto ast = new FuncDef();
ast->func_type = "void";
ast->ident = *unique_ptr<string>($2);
ast->func_para=unique_ptr<BaseAST>($4);
ast->block = unique_ptr<BaseAST>($6);
$$ = ast;
}
;

(这里就使用了非常简单粗暴的分类讨论来解决了)

而对于一个有参数的函数,类似我们上面的处理,我们知道其可能有一个参数或很多个参数

FuncParas
: SinFuncPara {
auto ast=new FuncParas();
ast->func_para=unique_ptr<BaseAST>($1);
$$=ast;
}|MulFuncPara {
auto ast=new FuncParas();
ast->func_para=unique_ptr<BaseAST>($1);
$$=ast;
}|ArrayFuncPara {
auto ast=new FuncParas();
ast->func_para=unique_ptr<BaseAST>($1);
$$=ast;
}
;

(最后一个为数组参数,留到lv9详细说明)

而如果至右一个参数,那么一定是一个int跟着参数名,也就是:

SinFuncPara
: INT IDENT {
auto ast=new SinFuncPara();
ast->IDENT=*unique_ptr<string>($2);
$$=ast;
}
;

而如果有很多个参数,一定是以一个参数开头,然后后面再跟上一个或一些参数,也就是:

MulFuncPara
: SinFuncPara ',' FuncParas {
auto ast=new MulFuncPara();
ast->sin_func_para=unique_ptr<BaseAST>($1);
ast->mul_func_para=unique_ptr<BaseAST>($3);
$$=ast;
}|ArrayFuncPara ',' FuncParas {
auto ast=new MulFuncPara();
ast->sin_func_para=unique_ptr<BaseAST>($1);
ast->mul_func_para=unique_ptr<BaseAST>($3);
$$=ast;
}
;

(同样暂时忽略数组参数)

这样我们就识别好了所有的函数参数

而关于函数调用,实际上函数调用是一种表达式,因此要在Unaryexp里加入这个表达式,也就是:

UnaryExp
: PrimaryExp{
auto ast=new UnaryExp();
ast->un_exp=unique_ptr<BaseAST>($1);
$$=ast;
}|SinExp
{
auto ast=new UnaryExp();
ast->un_exp=unique_ptr<BaseAST>($1);
$$=ast;
}|FuncExp
{
auto ast=new UnaryExp();
ast->un_exp=unique_ptr<BaseAST>($1);
$$=ast;
}
;

而函数调用有两种可能——有参数和没有参数,也就是这样:

FuncExp
: IDENT '(' ')'{
auto ast=new FuncExp();
ast->IDENT=*unique_ptr<string>($1);
ast->typ=0;
$$=ast;
}|IDENT '(' CallPara ')'{
auto ast=new FuncExp();
ast->IDENT=*unique_ptr<string>($1);
ast->call_para=unique_ptr<BaseAST>($3);
ast->typ=1;
$$=ast;
}
;

而有参数的函数调用识别参数的过程与上述函数定义的过程类似,这里不再赘述。

接下来看一下全局变量:

CompUnit ::= [CompUnit] (Decl | FuncDef);

发现这里全局变量实际上和函数定义是同层的,我们这样识别就好:

SinCompUnit
: GloDecl {
auto ast=new SinCompUnit();
ast->func_def=unique_ptr<BaseAST>($1);
$$=ast;
}|FuncDef {
auto ast=new SinCompUnit();
ast->func_def=unique_ptr<BaseAST>($1);
$$=ast;
}
;

而全局变量是如何定义的呢?我们这样认为:

GloDecl
: Decl {
auto ast=new GloDecl();
ast->glo_decl=unique_ptr<BaseAST>($1);
$$=ast;
}
;

(因为全局变量从一定程度上来说和普通的局部变量是类似的)

这样我们完成了语法分析,接下来来生成koopa:

首先对于一般的函数定义,我们与main函数的定义其实是一致的,但是值得注意的是,我们要处理好函数参数的问题,文档建议我们把所有的参数都读出来然后存到另一个地方去而不是直接使用,这样做能便于目标代码的生成,因此我们遵循文档的指示,在进入函数之后首先把所有的参数复制一份,也就是这样:

class SinFuncPara:public BaseAST
{
public:
std::string IDENT;
void Dump()override
{
std::cout<<"@"<<IDENT<<":i32";
}
int Calc()override
{
return 25;
}
void Show()override
{
std::cout<<" @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<"= alloc i32"<<std::endl;
std::cout<<" store @"<<IDENT<<", @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<std::endl;
var_type["COMPILER__"+IDENT+"_"+std::to_string(nowdep)]=1;
}
};

(实际上大概就是干了这样的事情——我们在生成函数体之前先定义了一堆和参数重名的变量,然后把这堆参数的值存到这些变量里,以后就直接调用这些变量就行了)

接下来,一个函数名同样是一个符号,我们要在符号表里加入其名字,同时要记录其返回值,最后生成函数体的内容即可。

需要特别注意的是,对于void函数本身没有返回值,因此我可以写一个单纯的return,那么我们还要在语法分析中识别这个return,同时在函数中有些需要return的地方会省略掉return,这些地方都需要我们补充好return在里面。

class FuncDef:public BaseAST
{
public:
std::string func_type;
std::string ident;
std::unique_ptr<BaseAST> func_para;
std::unique_ptr<BaseAST> block;
void Dump() override
{
std::cout << "fun ";
std::cout<<"@"<<ident<<"( ";
if(func_para)func_para->Dump();
std::cout<<")";
if(func_type=="int")std::cout<<": i32";
std::cout << "{ "<<std::endl;
std::cout<<"%entry"<<bl_dep<<":"<<std::endl; dep++;
f[dep]=nowdep;
nowdep=dep; if(func_para)func_para->Show(); bl_dep++;
nowbl=bl_dep;
func_ret[ident]=(func_type=="int"); block->Dump(); if(!be_end_bl[nowbl])
{
if(func_ret[ident])std::cout<<" ret 0"<<std::endl;
else std::cout<<" ret"<<std::endl;
} nowdep=f[nowdep];
std::cout << "}"<<std::endl;
}
int Calc() override{ return 12; }
};

处理完函数的定义和参数,接下来就是函数的调用,调用函数在koopa中使用的是call指令,而传递的参数直接放在了括号里面,那么我们在生成koopa的时候就类似地生成即可。

class FuncExp:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> call_para;
int typ;
void Dump()override
{
if(typ)
{
be_func_para=1;
std::vector<int> paras=call_para->Para();
if(IDENT=="getint"||IDENT=="getch"||IDENT=="getarray"||func_ret[IDENT])
{
std::cout<<" %"<<nowww<<"=call @"<<IDENT<<"(";
nowww++;
}else std::cout<<" call @"<<IDENT<<"(";
for(auto it=paras.begin();it!=paras.end();it++)
{
if(it!=paras.begin())std::cout<<',';
std::cout<<"%"<<*it;
}
std::cout<<')'<<std::endl;
be_func_para=0;
}else
{
if(IDENT=="getint"||IDENT=="getch"||IDENT=="getarray"||func_ret[IDENT])
{
std::cout<<" %"<<nowww<<"=call @"<<IDENT<<"()"<<std::endl;
nowww++;
}else std::cout<<" call @"<<IDENT<<"()"<<std::endl;
}
}
int Calc()override
{
return 27;
}
};

如上述代码所示,在调用一个函数的时候,首先我们需要知道其是否有参数,如果其有参数的话那么要先计算出其参数,也就是这样:

class CallPara:public BaseAST
{
public:
std::unique_ptr<BaseAST> call_para;
void Dump()override
{
call_para->Dump();
}
int Calc()override
{
return 28;
}
std::vector<int> Para()override
{
return call_para->Para();
}
};

这里新定义了一个Para函数,返回值为一个vector,用来存储所有参数所在的临时符号,利用这个函数就可以计算出所有的参数了,具体过程大致如下:

class SinCallPara:public BaseAST
{
public:
std::unique_ptr<BaseAST>para_exp;
void Dump()override
{
para_exp->Dump();
}
int Calc()override
{
return 29;
}
std::vector<int> Para()override
{
std::vector<int> ret;
Dump();
ret.push_back(nowww-1);
return ret;
}
}; class MulCallPara:public BaseAST
{
public:
std::unique_ptr<BaseAST> sin_call_para;
std::unique_ptr<BaseAST> mul_call_para;
void Dump()override
{
sin_call_para->Dump();
mul_call_para->Dump();
}
int Calc()override
{
return 30;
}
std::vector<int> Para()override
{
std::vector <int> ret;
std::vector <int> re1=sin_call_para->Para();
std::vector <int> re2=mul_call_para->Para();
for(auto it=re1.begin();it!=re1.end();it++)ret.push_back(*it);
for(auto it=re2.begin();it!=re2.end();it++)ret.push_back(*it);
return ret;
}
};

而在koopa之中,需要特意注意的就是库函数了,我们需要在前面加上库函数的声明,然后在调用的时候判断是否是库函数,如果是库函数就按照对应的库函数的要求去处理即可,这里不赘述。

接下来是koopa中生成全局变量,全局的常量其实没什么区别,重要的是全局的变量需要用到全局的分配指令,除此之外还有初值,这里初值的计算其实和const初值的计算是一样的——这里保证初值能计算出来,因此按照文档的说明生成即可,由于我没有为全局变量的声明额外设计一些AST,因此我使用了一个全局标记用来判断当前正在声明的变量是局部还是全局,如果是全局就按照全局的方法生成,就像这样:

class SinVarName:public BaseAST
{
public:
std::string IDENT;
void Dump()override
{
if(!glo_var)
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"\t@"<<IDENT<<" = alloc i32"<<std::endl;
var_type[IDENT]=1;
const_val[IDENT]=0;
}else
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"global @"<<IDENT<<" = alloc i32, 0"<<std::endl;
var_type[IDENT]=1;
const_val[IDENT]=0;
}
}
int Calc()override
{
return 0;
}
}; class MulVarName:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> init_val;
void Dump()override
{
if(!glo_var)
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"\t@"<<IDENT<<" = alloc i32"<<std::endl; var_type[IDENT]=1;
init_val->Dump();
std::cout<<"\tstore %"<<nowww-1<<", @"<<IDENT<<std::endl;
}else
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
var_type[IDENT]=1;
const_val[IDENT]=init_val->Calc();
std::cout<<"global @"<<IDENT<<" = alloc i32, "<<const_val[IDENT]<<std::endl;
}
}
int Calc()override
{
return const_val[IDENT];
}
};

(这里特别地,全局变量在声明后可以在任何一个地方被访问,因此全局变量所在的语句块应当设为0,这个0是所有语句块的根节点)

这样全局变量的问题也就解决了。

而在生成RISCV时,对于函数定义并没有新的语义,但是在函数调用时,约定使用8个寄存器存储参数,超过8个的参数则存储在栈上,因此我们在调用函数之前要首先把这些参数处理好,就像这样:

void solve_call(koopa_raw_value_t value,int &st)
{
koopa_raw_function_t func=value->kind.data.call.callee;
koopa_raw_slice_t args=value->kind.data.call.args;
int nowst=0;
for(int i=1;i<=args.len;i++)
{
if(M.find((ull)(args.buffer[i-1]))!=M.end())
{
if(i<=8)
{
cout<<" li t4, "<<M[(ull)(args.buffer[i-1])]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw a"<<i-1<<", (t4)"<<endl;
}else
{
cout<<" li t4, "<<M[(ull)args.buffer[i-1]]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
cout<<" li t4, "<<nowst<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
nowst+=4;
}
} }
cout<<" call "<<func->name+1<<endl;
M[(ull)value]=st;
st+=4;
cout<<" li t4, "<<M[(ull)value]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw a0, (t4)"<<endl;
}

具体来讲,这些参数是怎么存储的呢?首先前8个参数按顺序存储在a0~a7之中,那么我们就先把这些参数存储在的栈上的位置读出来,然后把这个位置上的东西存到对应的寄存器里即可。

而对于超过8个的参数,我们从栈顶向上逐个存储,也即sp所在的位置存储第9个参数、sp+4的位置存储第10个参数...因此我们要在栈顶保留一些位置用于存储函数调用的参数。

而如上文所说,在函数的第一步我们要把函数参数读到局部,而读取函数参数的过程就是与上述存储的过程相反,我们到存储好参数的地方读取即可。

void solve_store(koopa_raw_value_t value,int &st,int pos,int typ)
{
koopa_raw_store_t sto=value->kind.data.store;
koopa_raw_value_t sto_value=sto.value;
koopa_raw_value_t sto_dest=sto.dest;
if(sto_value->kind.tag==KOOPA_RVT_INTEGER)
{
cout<<" li t0, "<<sto_value->kind.data.integer.value<<endl; }else if(sto_value->kind.tag==KOOPA_RVT_FUNC_ARG_REF)
{
koopa_raw_func_arg_ref_t arg=sto_value->kind.data.func_arg_ref;
if(arg.index<8)
cout<<" mv t0, a"<<arg.index<<endl;
else
{
if(typ)cout<<" li t4, "<<stack_size[pos]+(arg.index-8)*4<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
}
}else
{
if(sto_value->kind.tag==KOOPA_RVT_LOAD)solve_load(sto_value,st);
cout<<" li t4, "<<M[(ull)sto_value]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
}
if(sto_dest->kind.tag==KOOPA_RVT_GLOBAL_ALLOC)
{
cout<<" la t1, "<<sto_dest->name+1<<endl;
cout<<" sw t0, 0(t1)"<<endl;
}else if(sto_dest->kind.tag==KOOPA_RVT_GET_ELEM_PTR)
{
cout<<" li t4, "<<M[(ull)sto_dest]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, (t4)"<<endl;
cout<<" sw t0, 0(t1)"<<endl;
}else if(sto_dest->kind.tag==KOOPA_RVT_GET_PTR)
{
cout<<" li t4, "<<M[(ull)sto_dest]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, (t4)"<<endl;
cout<<" sw t0, 0(t1)"<<endl;
}else
{
cout<<" li t4, "<<M[(ull)sto_dest]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
}
}

最后,函数的返回值总是保存在a0寄存器中,因此我们有责任保护好这个寄存器,通常在函数调用之前把这个寄存器的值保存在这个函数栈地址最高处,函数调用后再将其恢复即可,这一点是在函数体生成之初就做的事情。

而在函数调用返回时则要恢复这个寄存器的值,因此处理return变成了这样

void solve_return(koopa_raw_value_t value,int &st,int pos,int typ)
{
koopa_raw_value_t ret_value = value->kind.data.ret.value;
if(!ret_value)
{
cout<<" li a0, 0"<<endl;
if(typ)cout<<" li t4, "<<stack_size[pos]-4<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw ra, (t4)"<<endl;
if(typ)cout<<" li t4, "<<stack_size[pos]<<endl;
cout<<" add sp, sp, t4"<<endl;
cout<<" ret"<<endl;
return;
}else if(ret_value->kind.tag == KOOPA_RVT_INTEGER)
{
int32_t int_val = ret_value->kind.data.integer.value;
cout<<" li "<<"a0 , "<<int_val<<endl;
if(typ)cout<<" li t4, "<<stack_size[pos]-4<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw ra, (t4)"<<endl;
if(typ)cout<<" li t4, "<<stack_size[pos]<<endl;
cout<<" add sp, sp, t4"<<endl;
cout<<" ret"<<endl;
}else
{
if(ret_value->kind.tag==KOOPA_RVT_LOAD)solve_load(ret_value,st);
cout<<" li t4, "<<M[(ull)ret_value]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw "<<"a0 , (t4)"<<endl;
if(typ)cout<<" li t4, "<<stack_size[pos]-4<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw ra, (t4)"<<endl;
if(typ)cout<<" li t4, "<<stack_size[pos]<<endl;
cout<<" add sp, sp, t4"<<endl;
cout<<" ret"<<endl;
}
}

一般地,如果没有返回值我们就认为返回0,把0放在存储返回值的寄存器a0中即可。

这里同时展现了一点——return意味着函数的结束,因此在遇到return时我们必须生成一些恢复指令——恢复栈帧、恢复寄存器....

最后要处理的是全局变量,RISCV中的全局变量与局部变量截然不同,全局变量并不存储在栈上,而是存储在.data字段中,因此我们要先生成好全局变量:

if(raw.values.len)
{
cout<<" .data"<<endl;
for(size_t i=0;i<raw.values.len;++i)
{
koopa_raw_value_t data=(koopa_raw_value_t)raw.values.buffer[i];
cout<<" .globl "<<data->name+1<<endl;
cout<<data->name+1<<":"<<endl;
if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_INTEGER)
{
cout<<" .word "<<data->kind.data.global_alloc.init->kind.data.integer.value<<endl;
cout<<endl;
}else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_AGGREGATE)
{
koopa_raw_value_t val=data->kind.data.global_alloc.init;
//for(int i=0;i<elems.len;i++)
//{
//koopa_raw_value_t val=(koopa_raw_value_t)elems.buffer[i];
int a=1;
solve_global_array(val,data,a,1);
//}
cout<<endl;
}else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_ZERO_INIT)
{
koopa_raw_type_t value=data->ty->data.pointer.base;
int siz=4;
while(value->tag==KOOPA_RTT_ARRAY)
{
array_size[(ull)data].push_back(value->data.array.len);
siz*=value->data.array.len;
value=value->data.array.base;
}
cout<<" .zero "<<siz<<endl;
}
}
}

对lv8而言,这里的全局变量只有int类型的,暂时忽略后两种情况,我们发现int情况的处理还是并不复杂的——首先在所有全局变量之前生成一个.data标号,然后对每个变量生成一个.globl加名字表示一个全局变量,最后跟一个.word(即这个变量占了一个字长的空间)和初值,这样就解决了所有的全局变量。

而在访问全局变量的时候,由于全局变量并不存储在栈上,因此在load时我们要首先load address(即加载出其地址),然后再将其地址上的值存储到栈上便于后面调用即可。

void solve_load(koopa_raw_value_t value,int &st)
{
if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC)
{
cout<<" la t0, "<<value->kind.data.load.src->name+1<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_ELEM_PTR)
{
cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_PTR)
{
cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else
{
if(array_size.find((ull)(value->kind.data.load.src))!=array_size.end())
array_size[(ull)value]=array_size[(ull)(value->kind.data.load.src)];
M[(ull)value]=M[(ull)(value->kind.data.load.src)];
}
}

store指令与之类似,如果想修改一个全局变量的值,我们要先读出其地址,然后将修改后的值放在其地址上即可,代码上面已经给出,这里不再赘述。

这样lv8就解决了。

lv9:

lv9要求我们实现对数组的支持,这部分的工作量是相当巨大的

首先,由于一维数组与多维数组没有特别本质的区别,因此我们直接考虑多维数组。其EBNF如下:

ConstDef      ::= IDENT {"[" ConstExp "]"} "=" ConstInitVal;
ConstInitVal ::= ConstExp | "{" [ConstInitVal {"," ConstInitVal}] "}";
VarDef ::= IDENT {"[" ConstExp "]"}
| IDENT {"[" ConstExp "]"} "=" InitVal;
InitVal ::= Exp | "{" [InitVal {"," InitVal}] "}"; LVal ::= IDENT {"[" Exp "]"};

看上去就很可怕...

我们一个一个解决,首先是数组的定义,数组的定义一定是一个int后面跟着数组名,然后是数组的维度,然后是可能有的初始值,以常量数组为例:

ConstArrayDef
: IDENT ArraySize '=' ConstArrayVal {
auto ast=new ConstArrayDef();
ast->IDENT=*unique_ptr<string>($1);
ast->siz=unique_ptr<BaseAST>($2);
ast->const_array_val=unique_ptr<BaseAST>($4);
$$=ast;
}
;

而数组的维度可能是一维也可能是多维,每一维都是一对中括号中间夹一个常量表达式(定义必须是常量表达式),也就是:

ArraySize
: '[' ConstExp ']'{
auto ast=new ArraySize();
ast->array_size=unique_ptr<BaseAST>($2);
$$=ast;
}|MulArraySize {
auto ast=new ArraySize();
ast->array_size=unique_ptr<BaseAST>($1);
$$=ast;
}
; MulArraySize
: '[' ConstExp ']' ArraySize{
auto ast=new MulArraySize();
ast->sin_array_size=unique_ptr<BaseAST>($2);
ast->mul_array_size=unique_ptr<BaseAST>($4);
$$=ast;
}
;

数组参数识别好了之后,接下来就要识别数组初值,数组初值是用大括号括起来的一些表达式或更多的大括号(常量数组初值必须是常量表达式)或者空的大括号,也就是这样:

ConstArrayVal
: '{' ConstArrVal '}'{
auto ast=new ConstArrayVal();
ast->const_array_val=unique_ptr<BaseAST>($2);
$$=ast;
}|'{' '}' {
auto ast=new ConstArrayVal();
$$=ast;
}
; ConstArrVal
: ConstInitVal{
auto ast=new ConstArrVal();
ast->const_arr_val=unique_ptr<BaseAST>($1);
$$=ast;
}|MulConArrVal {
auto ast=new ConstArrVal();
ast->const_arr_val=unique_ptr<BaseAST>($1);
$$=ast;
}|ConstArrayVal {
auto ast=new ConstArrVal();
ast->const_arr_val=unique_ptr<BaseAST>($1);
$$=ast;
}
; MulConArrVal
: ConstInitVal ',' ConstArrVal {
auto ast=new MulConArrVal();
ast->sin_con_arr_val=unique_ptr<BaseAST>($1);
ast->mul_con_arr_val=unique_ptr<BaseAST>($3);
$$=ast;
}|ConstArrayVal ',' ConstArrVal {
auto ast=new MulConArrVal();
ast->sin_con_arr_val=unique_ptr<BaseAST>($1);
ast->mul_con_arr_val=unique_ptr<BaseAST>($3);
$$=ast;
}
;

而变量数组与常量数组的识别类似,这里不再赘述,接下来我们考察数组的访问,数组的访问实际上是Lval的一种,为了与别的Lval相区别,我们这样识别:

AllLval
: Lval {
auto ast=new AllLval();
ast->all_lval=unique_ptr<BaseAST>($1);
$$=ast;
}|ArrLval {
auto ast=new AllLval();
ast->all_lval=unique_ptr<BaseAST>($1);
$$=ast;
}
;

即Lval可能有其他的Lval和数组的Lval

而数组的Lval长什么样子呢?一定是数组名跟着数组维度信息,也就是这样:

ArrLval
: IDENT ArrPara {
auto ast=new ArrLval();
ast->IDENT=*unique_ptr<string>($1);
ast->pos_exp=unique_ptr<BaseAST>($2);
$$=ast;
}
;

而数组的维度信息是中括号夹着表达式,也就是这样:

ArrPara
: '[' Exp ']' {
auto ast=new ArrPara();
ast->arr_para=unique_ptr<BaseAST>($2);
$$=ast;
}|MulArrPara {
auto ast=new ArrPara();
ast->arr_para=unique_ptr<BaseAST>($1);
$$=ast;
}
; MulArrPara
: '[' Exp ']' ArrPara {
auto ast=new MulArrPara();
ast->sin_arr_para=unique_ptr<BaseAST>($2);
ast->mul_arr_para=unique_ptr<BaseAST>($4);
$$=ast;
}
;

这样就实现了数组访问的识别。

其实识别本身难度并不大,但是如何处理就变得很困难了,首先我们考察数组的定义

在koopa中要定义一个数组,仍然要alloc,但alloc的不再是一个int而是一个数组,怎么定义数组呢?

先考虑一维的情况,以int a[5]为例,表示5个int构成的数组,那么alloc的时候应当是@a = alloc [i32,5]

再考虑一个数组int a[3][4],这个数组的含义是3个int[4]构成的数组,也就是说我们要这样做:@a = alloc [[i32,4],3]

那么于是我们在定义的时候首先识别出所有的维度,然后从后向前生成即可。

接下来考虑数组的初值,其实这里是最困难的地方,我们按照文档的进行识别,首先遇到常数就认为填充在最后一维,然后按照对齐的标准处理初始化列表。

举个例子:如果我们给出这样的初始化列表:

int a[2][2][2]={1,2,3,4,{{5}}}

对于1,2填充好了int a[0][0][0~1]

3,4填充好了int a[0][1][0~1]

那么最后一个初始化列表{{5}}应该对应于int a[1][0~1][0~1]

而这个初始化列表中只有一个元素{5},这也是一个初始化列表,这个初始化列表应当对应于int a[1][0][0~1]

也就是说,一个初始化列表究竟初始化的是什么数组呢?是由两部分决定的——这个初始化列表的大括号究竟在第几层(即大括号深度)和这个初始化列表前面的对齐情况。

因此我们使用一个返回值为vector的函数来处理所有前面已经有的返回值,根据前面填充好的数量和当前大括号深度来决定这个初始化列表初始化的是哪个数组,如果当前初始化列表中的元素不够则需要补0对齐

大概就是这样:

class ConstArrayVal:public BaseAST
{
public:
std::unique_ptr<BaseAST> const_array_val;
void Dump()override
{
const_array_val->Dump();
}
int Calc()override
{
return 36;
}
std::vector<int> Para()override
{
brace_dep++;
std::vector<int> ret;
int temp=filled_sum; if(const_array_val)ret=const_array_val->Para();
int siz=1;
brace_dep--;
int las=1,i;
for(i=array_dim.size()-1;i>=0;i--)
{
las*=array_dim[i];
if(temp%las)break;
}
for(int k=std::max(i+1,brace_dep);k<array_dim.size();k++)siz*=array_dim[k];
while(ret.size()<siz)ret.push_back(0),filled_sum++;
return ret;
}
};

这样的话对于多个初值的情况就很容易处理了:

class MulConArrVal:public BaseAST
{
public:
std::unique_ptr<BaseAST> sin_con_arr_val;
std::unique_ptr<BaseAST> mul_con_arr_val;
void Dump()override
{
return;
}
int Calc()override
{
return 37;
}
std::vector <int> Para()override
{
std::vector <int> ret; std::vector <int> v1=sin_con_arr_val->Para();
std::vector <int> v2=mul_con_arr_val->Para(); for(auto it=v1.begin();it!=v1.end();it++)ret.push_back((*it));
for(auto it=v2.begin();it!=v2.end();it++)ret.push_back((*it)); return ret;
}
};

但是对于全局的数组,我们要生成一个初始化列表,当我们已经生成好了所有初值之后,剩下的部分就容易了

总结一下,常量数组的声明大致有如下流程——首先计算出数组每个维度的参数放在一个vector里,然后根据这个维度以及是否是全局变量生成一个alloc声明,然后处理出所有的初值,对于全局变量而言生成一个初始化列表(对于一个空大括号作为初值的情况,生成一个zeroinit作为初值),而作为局部变量,则计算出所有初值然后把所有的初值store进去。

而怎么store呢?这其实涉及到了数组访问问题——以一维数组为例,对于一个int a[5],我们想要访问a[1],那么我们要怎么做呢?我们要首先获取一个指向a[1]的指针,因此我们要先写:

%x=getelemptr @a,1

然后我们想要读取出a[1]的值,我们还要:

%x+1=load %x

这样就读出了a[1]的值,如果想要把值存进去,我们就要这样写:

store %n,%x

最后我们在数组符号表中存储好数组的信息即可。

因此整体常量数组的定义大概就这样:

class ConstArrayDef:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> siz;
std::unique_ptr<BaseAST> const_array_val;
std::vector<int> size;
void Dump()override
{
size=siz->Para();
array_dim=size;
filled_sum=0;
if(!glo_var)
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"\t@"<<IDENT<<" = alloc ";
for(auto it=size.begin();it!=size.end();it++)
{
std::cout<<"[";
}
std::cout<<"i32";
for(int it=size.size()-1;it>=0;it--)
{
std::cout<<", "<<size[it]<<"]";
}
std::cout<<std::endl;
var_type[IDENT]=2;
}else
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"global @"<<IDENT<<" = alloc ";
for(auto it=size.begin();it!=size.end();it++)
{
std::cout<<"[";
}
std::cout<<"i32";
for(int it=size.size()-1;it>=0;it--)
{
std::cout<<", "<<size[it]<<"]";
}
std::cout<<", ";
var_type[IDENT]=2;
}
if(glo_var)
{
if(!const_array_val)
{
std::cout<<"zeroinit"<<std::endl;
return;
}
std::vector<int> con_init_val=const_array_val->Para();
auto it=con_init_val.begin();
for(int i=0;i<size.size();i++)std::cout<<"{";
std::vector<int> v;
for(int i=size.size()-1;i>=0;i--)
{
if(i==size.size()-1)v.push_back(size[i]);
else v.push_back(size[i]*v[v.size()-1]);
}
int p=0;
while(it!=con_init_val.end())
{
std::cout<<(*it);
p++;
int flag=0;
for(int i=0;i<v.size();i++)
{
if(p%v[i]==0)
{
flag++;
std::cout<<"}";
}else break;
}
it++;
if(it!=con_init_val.end())
{
std::cout<<", ";
if(flag)
{
for(int i=0;i<flag;i++)std::cout<<"{";
}
}
}
std::cout<<std::endl;
}else
{
std::vector<int> con_init_val=const_array_val->Para(); int pos=0;
std::vector<int> v;
int rsize=1;
for(auto it=size.begin();it!=size.end();it++)rsize*=(*it);
for(auto it=size.begin();it!=size.end();it++)rsize/=(*it),v.push_back(rsize);
std::cout<<"\t%"<<nowww<<"= add 0, 0"<<std::endl;
int reg0=nowww;
nowww++;
for(auto it=con_init_val.begin();it!=con_init_val.end();it++,pos++)
{
int temp=pos;
for(auto i=v.begin();i!=v.end();i++)
{
if(i==v.begin())
std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", "<<temp/(*i)<<std::endl;
else
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", "<<temp/(*i)<<std::endl;
nowww++,temp%=(*i);
}
if((*it)!=0)
std::cout<<"\tstore %"<<(*it)<<", %"<<nowww-1<<std::endl;
else
std::cout<<"\tstore %"<<reg0<<", %"<<nowww-1<<std::endl;
}
}
array_siz[IDENT]=size.size();
}
int Calc()override
{
return 35;
} };

对于变量数组,在全局的情况是一致的,但是在局部的情况我们不能保证其初始化的值是一定可以在编译期被计算出来的,因此我们要生成计算其值的表达式,然后把这个表达式对应的标号store进去

因此,对于在局部的变量数组,我们在计算初始值时需要存进vector里的就不再是一个个真实的初始值了,而是计算每个初始值的表达式标号了。

当然,变量数组是可以没有初值的,因此我们同样要区分初始化了的数组和没有初始化的数组,这里的处理和一般的变量就类似了。

因此变量数组的整个过程如下:

class SinNameVarArrDef:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> siz;
std::vector<int> size;
void Dump()override
{
size=siz->Para();
if(!glo_var)
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"\t@"<<IDENT<<" = alloc";
for(auto it=size.begin();it!=size.end();it++)
{
std::cout<<"[";
}
std::cout<<"i32";
for(int it=size.size()-1;it>=0;it--)
{
std::cout<<", "<<size[it]<<"]";
}
std::cout<<std::endl;
var_type[IDENT]=2; }else
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"global @"<<IDENT<<" = alloc ";
for(auto it=size.begin();it!=size.end();it++)
{
std::cout<<"[";
}
std::cout<<"i32";
for(int it=size.size()-1;it>=0;it--)
{
std::cout<<", "<<size[it]<<"]";
}
std::cout<<", zeroinit"<<std::endl; var_type[IDENT]=2;
}
array_siz[IDENT]=size.size();
}
int Calc()override
{
return 39;
}
}; class MulNameVarArrDef:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> siz;
std::unique_ptr<BaseAST> init_val;
std::vector<int> size;
void Dump()override
{
filled_sum=0;
size=siz->Para();
array_dim=size;
if(!glo_var)
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"\t@"<<IDENT<<" = alloc ";
for(auto it=size.begin();it!=size.end();it++)
{
std::cout<<"[";
}
std::cout<<"i32";
for(int it=size.size()-1;it>=0;it--)
{
std::cout<<", "<<size[it]<<"]";
}
std::cout<<std::endl;
var_type[IDENT]=2;
}else
{
IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep);
std::cout<<"global @"<<IDENT<<" = alloc ";
for(auto it=size.begin();it!=size.end();it++)
{
std::cout<<"[";
}
std::cout<<"i32";
for(int it=size.size()-1;it>=0;it--)
{
std::cout<<", "<<size[it]<<"]";
}
std::cout<<", ";
var_type[IDENT]=2;
}
if(glo_var)
{
filled_sum=0;
std::vector<int> con_init_val=init_val->Para();
auto it=con_init_val.begin();
for(int i=0;i<size.size();i++)std::cout<<"{";
std::vector<int> v;
for(int i=size.size()-1;i>=0;i--)
{
if(i==size.size()-1)v.push_back(size[i]);
else v.push_back(size[i]*v[v.size()-1]);
}
int p=0;
while(it!=con_init_val.end())
{
std::cout<<(*it);
p++;
int flag=0;
for(int i=0;i<v.size();i++)
{
if(p%v[i]==0)
{
flag++;
std::cout<<"}";
}else break;
}
it++;
if(it!=con_init_val.end())
{
std::cout<<", ";
if(flag)
{
for(int i=0;i<flag;i++)std::cout<<"{";
}
}
}
std::cout<<std::endl;
}else
{
std::vector<int> con_init_val=init_val->Para();
int pos=0;
std::vector<int> v;
int rsize=1;
for(auto it=size.begin();it!=size.end();it++)rsize*=(*it);
for(auto it=size.begin();it!=size.end();it++)rsize/=(*it),v.push_back(rsize);
std::cout<<"\t%"<<nowww<<"= add 0, 0"<<std::endl;
int reg0=nowww;
nowww++;
for(auto it=con_init_val.begin();it!=con_init_val.end();it++,pos++)
{
int temp=pos;
for(auto i=v.begin();i!=v.end();i++)
{
if(i==v.begin())
std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", "<<temp/(*i)<<std::endl;
else
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", "<<temp/(*i)<<std::endl;
nowww++,temp%=(*i);
}
if((*it)!=0)
std::cout<<"\tstore %"<<(*it)<<", %"<<nowww-1<<std::endl;
else
std::cout<<"\tstore %"<<reg0<<", %"<<nowww-1<<std::endl;
}
}
array_siz[IDENT]=size.size();
}
int Calc()override
{
return 40;
}
};

这样数组定义的问题就基本解决了,同时我们也解决了数组访问的问题,这样我们就可以完成数组参与运算的过程:

class ArrLval:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> pos_exp;
void Dump()override
{
int tempdep=nowdep;
while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep];
IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep);
std::vector<int> pos=pos_exp->Para();
for(auto it=pos.begin();it!=pos.end();it++)
{
if(it==pos.begin())
{
if(var_type[IDENT]==3)
{
std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl;
nowww++;
std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
}else
std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl;
}
else
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
nowww++;
}
if(!be_func_para||pos.size()==array_siz[IDENT])
{
std::cout<<"\t%"<<nowww<<"= load %"<<nowww-1<<std::endl;
nowww++;
}else
{
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", 0"<<std::endl;
nowww++;
}
}
int Calc()override
{
return 43;
}
}; class ArrLeval:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> pos_exp;
void Dump()override
{
int tempdep=nowdep;
while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep];
IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep);
int now=nowww-1;
std::vector<int> pos=pos_exp->Para();
for(auto it=pos.begin();it!=pos.end();it++)
{
if(it==pos.begin())
{
if(var_type[IDENT]==3)
{
std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl;
nowww++;
std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
}else
std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl;
}else
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
nowww++;
}
std::cout<<"\tstore %"<<now<<", %"<<nowww-1<<std::endl;
nowww++;
}
int Calc()override
{
return 44;
}
};

其中需要注意的就是这时如何处理数组的维度信息,其实处理方式与数组的声明是类似的:

class ArrPara:public BaseAST
{
public:
std::unique_ptr<BaseAST> arr_para;
void Dump()override
{
arr_para->Dump();
}
int Calc()override
{
return 49;
}
std::vector<int> Para()override
{
return arr_para->Para();
}
}; class MulArrPara:public BaseAST
{
public:
std::unique_ptr<BaseAST> sin_arr_para;
std::unique_ptr<BaseAST> mul_arr_para;
void Dump()override
{
sin_arr_para->Dump();
mul_arr_para->Dump();
}
int Calc()override
{
return 50;
}
std::vector<int> Para()override
{
std::vector<int> ret;
std::vector<int> v1=sin_arr_para->Para();
std::vector<int> v2=mul_arr_para->Para(); for(auto it=v1.begin();it!=v1.end();it++)ret.push_back((*it));
for(auto it=v2.begin();it!=v2.end();it++)ret.push_back((*it)); return ret;
}
};

这样我们就完成了基本的数组操作。

接下来放到RISCV里,同样,我们要解决数组如何定义、如何初始化以及如何访问

全局数组的定义其实与全局变量是一致的,只需在.data字段生成其名字即可,而最简单的初始化方法就是读取我们在koopa中生成的初始化列表,然后按顺序逐个生成初始元素即可,同时在这一过程中我们也可以记录下这个数组各个维度的信息

(在处理数组时有一点值得注意——由于不同的定义和访问顺序的混用,如果我们使用类似vector这样的结构来保存数组的维度信息,那么我们一定要搞清楚vector中究竟是以什么样的顺序存放的元素信息!)

但是这样简单的做法其实有一点小问题——假设我在全局开了一个const int a[100000]={}的数组,那我就要生成十万个.word 0这样的语句

这样做简直是不可接受的浪费,为了避免这样的问题,我们观察到全局变量的初始化方法中有一个zeroinit,正如我们上文所说,我们在生成koopa时对于这样全零的全局数组我们直接用zeroinit初始化,然后用zeroinit对应的方法去初始化即可。

那么是怎样初始化呢?zeroinit本质是一条指令,通过在koopa.h中研究这条指令我们可以找到这个数组的情况,然后在下面生成.zero 与数组大小即可。

因此处理全局数组的初始化大致如下:

void solve_global_array(koopa_raw_value_t value,koopa_raw_value_t ori,int &flag,int dep)
{
if(value->kind.tag==KOOPA_RVT_INTEGER)
{
cout<<" .word "<<value->kind.data.integer.value<<endl;
return;
}else
{
koopa_raw_slice_t elems=value->kind.data.aggregate.elems;
if(flag==dep)
{
array_size[(ull)ori].push_back(elems.len);
flag++;
}
for(int i=0;i<elems.len;i++)
{
koopa_raw_value_t val=(koopa_raw_value_t)elems.buffer[i];
solve_global_array(val,ori,flag,dep+1);
}
return;
}
} if(raw.values.len)
{
cout<<" .data"<<endl;
for(size_t i=0;i<raw.values.len;++i)
{
koopa_raw_value_t data=(koopa_raw_value_t)raw.values.buffer[i];
cout<<" .globl "<<data->name+1<<endl;
cout<<data->name+1<<":"<<endl;
if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_INTEGER)
{
cout<<" .word "<<data->kind.data.global_alloc.init->kind.data.integer.value<<endl;
cout<<endl;
}else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_AGGREGATE)
{
koopa_raw_value_t val=data->kind.data.global_alloc.init;
int a=1;
solve_global_array(val,data,a,1);
cout<<endl;
}else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_ZERO_INIT)
{
koopa_raw_type_t value=data->ty->data.pointer.base;
int siz=4;
while(value->tag==KOOPA_RTT_ARRAY)
{
array_size[(ull)data].push_back(value->data.array.len);
siz*=value->data.array.len;
value=value->data.array.base;
}
cout<<" .zero "<<siz<<endl;
}
}
}

而对于局部的数组分配,就不能在像之前局部变量那样任性地等到使用的时候再去alloc了,我们要认真地alloc出来一块区域,同时我们要维护好数组的大小,这里使用一个从指令映射到vector的map,其中vector即为这个数组的维度信息

void solve_alloc(koopa_raw_value_t value,int &st)
{
if(value->ty->data.pointer.base->tag==KOOPA_RTT_INT32)M[(ull)value]=st,st+=4;
else if(value->ty->data.pointer.base->tag==KOOPA_RTT_ARRAY)
{
koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base;
array_size[(ull)value].push_back(base->data.array.len);
int siz=base->data.array.len*4;
while(base->data.array.base->tag!=KOOPA_RTT_INT32)
{
base=(koopa_raw_type_kind_t*)base->data.array.base;
array_size[(ull)value].push_back(base->data.array.len);
siz*=base->data.array.len;
}
M[(ull)value]=st;
st+=siz;
}else if(value->ty->data.pointer.base->tag==KOOPA_RTT_POINTER)
{
koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base;
array_size[(ull)value].push_back(1);
while(base->data.array.base->tag!=KOOPA_RTT_INT32)
{
base=(koopa_raw_type_kind_t*)base->data.array.base;
array_size[(ull)value].push_back(base->data.array.len);
}
M[(ull)value]=st;
st+=4;
}
}

这样对于数组的定义其实已经解决了,接下来只需要解决最复杂的getelemptr即可

这个指令究竟在干什么?

其实这个指令就是在寻址,只不过是过程相对复杂的寻址。

怎么说?

比如getelemptr @a,1,我们实际上想要获得的就是指针a+1,而这个指针是怎么生成的?

那我们首先就要找到a在哪,它如果是个全局数组,我们就要直接load address,如果是个放在栈上的局部数组,那么我们就要根据我们alloc的结果找到其在栈上的什么位置。

接下来,a+1这个指针是什么?

以int a[2][3]为例,其表示一个由两个长度为3的int数组组成的数组,因此a+1表示的是第二个长度为3的int数组!

也就是说,我们要计算出真正的偏移量,这个偏移量需要依赖于数组的信息计算,因此当我们在对数组进行getelemptr时,我们还要在全局保存一个now_array用来记录当前处理的是哪个数组,然后按照当前数组的信息计算出偏移量,然后把基地址(即a的地址)加上这个偏移量(即+1)即是我们getelemptr出来的结果,然后按照习惯,我们把这个结果存储到栈上。

同样,如果是getelemptr %x,1这样的指令,我们还是先读出%x的值,这个值就是基地址,然后在这个基地址的基础上去加偏移量就好。

但是这样就又要面对一个问题了——单看这条指令,我们是根本不知道该加多少偏移量的!

但是koopa也是我们自己生成的,在生成koopa的时候我们知道我们访问一个数组元素的getelemptr指令是连续的,因此当我们遇到第一个getelemptr指令时我们就开始记录当前处理了前几维,然后我们当前处理所需的偏移量就是剩下的维度了。

void solve_get_element_ptr(koopa_raw_value_t value,int &st)
{
koopa_raw_value_t src=value->kind.data.get_elem_ptr.src;
if(src->kind.tag==KOOPA_RVT_ALLOC)
{
now_array=(ull)src;
deep=1;
cout<<" li t4, "<<M[(ull)src]<<endl;
cout<<" add t0, sp, t4"<<endl;
if(value->kind.data.get_elem_ptr.index->kind.tag==KOOPA_RVT_INTEGER)
cout<<" li t1, "<<value->kind.data.get_elem_ptr.index->kind.data.integer.value<<endl;
else
{
cout<<" li t4, "<<M[(ull)value->kind.data.get_elem_ptr.index]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, "<<"(t4)"<<endl;
}
int p=4;
for(int i=deep;i<array_size[now_array].size();i++)p*=array_size[now_array][i];
cout<<" li t2, "<<p<<endl;
cout<<" mul t1, t1, t2"<<endl;
cout<<" add t0, t0, t1"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, "<<"(t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else if(src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC)
{
now_array=(ull)src;
deep=1;
cout<<" la t0, "<<value->kind.data.get_elem_ptr.src->name+1<<endl;
if(value->kind.data.get_elem_ptr.index->kind.tag==KOOPA_RVT_INTEGER)
cout<<" li t1, "<<value->kind.data.get_elem_ptr.index->kind.data.integer.value<<endl;
else
{
cout<<" li t4, "<<M[(ull)value->kind.data.get_elem_ptr.index]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, "<<"(t4)"<<endl;
}
int p=4;
for(int i=deep;i<array_size[now_array].size();i++)
{
p*=array_size[now_array][i];
}
//cout<<"??"<<array_size[now_array].size()<<endl;
cout<<" li t2, "<<p<<endl;
cout<<" mul t1, t1, t2"<<endl;
cout<<" add t0, t0, t1"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, "<<"(t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else if(src->kind.tag==KOOPA_RVT_GET_ELEM_PTR||src->kind.tag==KOOPA_RVT_GET_PTR)
{
cout<<" li t4, "<<M[(ull)src]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
if(value->kind.data.get_elem_ptr.index->kind.tag==KOOPA_RVT_INTEGER)
cout<<" li t1, "<<value->kind.data.get_elem_ptr.index->kind.data.integer.value<<endl;
else
{
cout<<" li t4, "<<M[(ull)value->kind.data.get_elem_ptr.index]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, "<<"(t4)"<<endl;
}
deep++;
int p=4;
for(int i=deep;i<array_size[now_array].size();i++)p*=array_size[now_array][i];
cout<<" li t2, "<<p<<endl;
cout<<" mul t1, t1, t2"<<endl;
cout<<" add t0, t0, t1"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, "<<"(t4)"<<endl;
M[(ull)value]=st;
st+=4;
}
}

过程大概是这样。

这样我们还要修改一下load指令,在取出了我们想要的元素的地址之后,我们将其load出来的过程实际就是读出这个地址,然后取出这个地址上的值即可。

void solve_load(koopa_raw_value_t value,int &st)
{
if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC)
{
cout<<" la t0, "<<value->kind.data.load.src->name+1<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_ELEM_PTR)
{
cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_PTR)
{
cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else
{
if(array_size.find((ull)(value->kind.data.load.src))!=array_size.end())
array_size[(ull)value]=array_size[(ull)(value->kind.data.load.src)];
M[(ull)value]=M[(ull)(value->kind.data.load.src)];
}
}

同样,对于store指令,由于我们已经计算出了要存储的位置的地址,因此我们直接读出这个地址,然后把值保存进去即可。

void solve_store(koopa_raw_value_t value,int &st,int pos,int typ)
{
koopa_raw_store_t sto=value->kind.data.store;
koopa_raw_value_t sto_value=sto.value;
koopa_raw_value_t sto_dest=sto.dest;
if(sto_value->kind.tag==KOOPA_RVT_INTEGER)
{
cout<<" li t0, "<<sto_value->kind.data.integer.value<<endl; }else if(sto_value->kind.tag==KOOPA_RVT_FUNC_ARG_REF)
{
koopa_raw_func_arg_ref_t arg=sto_value->kind.data.func_arg_ref;
if(arg.index<8)
cout<<" mv t0, a"<<arg.index<<endl;
else
{
if(typ)cout<<" li t4, "<<stack_size[pos]+(arg.index-8)*4<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
}
}else
{
if(sto_value->kind.tag==KOOPA_RVT_LOAD)solve_load(sto_value,st);
cout<<" li t4, "<<M[(ull)sto_value]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
}
if(sto_dest->kind.tag==KOOPA_RVT_GLOBAL_ALLOC)
{
cout<<" la t1, "<<sto_dest->name+1<<endl;
cout<<" sw t0, 0(t1)"<<endl;
}else if(sto_dest->kind.tag==KOOPA_RVT_GET_ELEM_PTR)
{
cout<<" li t4, "<<M[(ull)sto_dest]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, (t4)"<<endl;
cout<<" sw t0, 0(t1)"<<endl;
}else if(sto_dest->kind.tag==KOOPA_RVT_GET_PTR)
{
cout<<" li t4, "<<M[(ull)sto_dest]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, (t4)"<<endl;
cout<<" sw t0, 0(t1)"<<endl;
}else
{
cout<<" li t4, "<<M[(ull)sto_dest]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
}
}

这样数组的基本操作就完成了。

接下来就是必须功能的最后一步——处理数组参数。

FuncFParam ::= BType IDENT ["[" "]" {"[" ConstExp "]"}];

那首先我们就要搞清楚数组参数长什么样,我们发现数组参数大概是长这个样子的:int a[][2][3]

那么这到底是个什么玩意呢?

这实际上给出的是一个指针,这个指针指向了一个int [2][3]类型的数组。

因此在处理数组参数时,我们首先要搞清楚一件事——这里的数组是可以部分解引用的。

举个例子,我们有一个数组int a[4][2][3],那么对于上面那个数组参数,我们可以把a[0],a[1]这样的东西扔进去,a[0],a[1]代表的是指向int [2][3]类型的一个指针

那么我们还是从语法分析开始,这个玩意怎么识别呢?

其实也很容易:

ArrayFuncPara
: INT IDENT ArrayParaSize {
auto ast=new ArrayFuncPara();
ast->IDENT=*unique_ptr<string>($2);
ast->siz=unique_ptr<BaseAST>($3);
$$=ast;
}
;

即int数组名加上数组维度,而数组维度长什么样呢?

ArrayParaSize
: '[' ']' {
auto ast=new ArrayParaSize();
$$=ast;
}|'[' ']' ArraySize{
auto ast=new ArrayParaSize();
ast->array_para_size=unique_ptr<BaseAST>($3);
$$=ast;
}
;

也即可以是int a[]这样的,也可以是int a[][2][3]这样的(即抛去第一个空的中括号,后面与声明数组的维度识别方法是一样的)

那么怎么处理这个数组参数呢?首先我们和普通的数组一样搞清楚数组的维度,然后我们说数组参数本质是一个指针,那么对于int a[]这样的参数,我们实际上就是*i32,而对于int a[][2]这样的参数,就是*[i32,2]

同样,我们还是要把这个参数的值保存到局部,那么局部我们alloc一个和参数类型相同的变量用于存储即可。

class ArrayFuncPara:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> siz;
std::vector<int> size;
void Dump()override
{
size=siz->Para();
if(size.size()==0)
{
std::cout<<"@"<<IDENT<<": *i32";
}else
{
std::cout<<"@"<<IDENT<<": *";
for(int i=0;i<size.size();i++)std::cout<<"[";
std::cout<<"i32, ";
for(int i=size.size()-1;i>=0;i--)
{
std::cout<<size[i]<<"]";
if(i!=0)std::cout<<",";
}
}
}
int Calc()override
{
return 51;
}
void Show()override
{
std::cout<<" @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<"= alloc ";
std::cout<<"*";
for(int i=0;i<size.size();i++)std::cout<<"[";
if(size.size())std::cout<<"i32, ";
else std::cout<<"i32";
for(int i=size.size()-1;i>=0;i--)
{
std::cout<<size[i]<<"]";
if(i!=0)std::cout<<",";
}
std::cout<<std::endl;
std::cout<<" store @"<<IDENT<<", @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<std::endl;
var_type["COMPILER__"+IDENT+"_"+std::to_string(nowdep)]=3;
array_siz["COMPILER__"+IDENT+"_"+std::to_string(nowdep)]=size.size()+1;
}
};

这里特别把数组参数的类型作出特别标记与其他数组相区别,这样数组参数的展示就完成了,而数组参数是如何被访问的?

如果我们的数组参数是int a[],现在想访问a[3],我们要怎么做呢?

在这里我们详细解释一下getelemptr到底干了什么:getelemptr的作用是对于一个类型为*[T,N]的指针,使用后会变成*T类型的指针

这是什么意思?

首先我们要清楚一点:在koopa中alloc T语句的作用是生成一个类型为*T的指针,而load *T指令的作用则是获得类型为T的结果

也就是说,当我们生成一个数组时,如果我们写了@a=alloc [i32,10],我们实际上生成的东西是一个类型为*[i32,10]的指针

而getelemptr干了什么呢?执行一次这个指令,我们就获得了一个类型为*i32的指针

这样再load一次,我们就可以获得i32的值,这样做是符合逻辑的。

但是,在生成数组参数的时候,事情并不是这样的,数组参数并不把int a[]解释成一个数组,而是解释成一个指针,因此我们要做的是指针运算。

也就是说,这里的a的类型是*i32,如果我们对其使用getelemptr,我们就会遇到错误——这是直观的,因为getelemptr处理的类型应当是*[T,N]

因此我们应当使用的是getptr,getptr干了什么呢?它会把一个*T类型的指针变成一个*T类型的指针。

这就让人十分舒适了,现在如果我们想访问a[3],我们直接getptr @a,3,这样得到的还是*i32,然后把这个值load出来就得到了a[3]

看上去很美好,只是有一个细节需要注意——我们说过alloc会生成一个类型为要分配类型的指针

也就是说,如果我们写了这样的东西:@a = alloc *i32,我们得到的a的类型实际是**i32!

那这其实本身也很好解决——我们只需要先load一次得到的就是*i32了,然后正常像上面一样getptr即可

而对于多维数组也是一样的,比如对于int a[][2],首先执行@a = alloc *[i32,2],这样得到的a是**[i32,2]类型的,然后load一次,得到的就是*[i32,2]类型的,但是当前的指针指向的是a[0]这个数组,那么如果想访问a[1]这个数组,我们就要执行getptr @a,1,这样得到的仍然是一个*[i32,2]类型的指针,但是指向的东西变成了另一个数组,然后我们就可以开心地按数组进行访问了。

那么总结一下,在使用数组参数时,我们首先应该进行一个load,然后进行一个getptr,最后视情况决定是否要进行更多的getelemptr

而在进行参数传递时,如果我们想把数组a传递给int a[]为参数的函数f,那么我们非常自然地会这样写:f(a)

那么我们到底传进去了个什么?我们要知道,a是一个*[i32,N],而我们要的是*i32,因此我们需要做一次getelemptr @a ,0才能获得要传入的参数

这里的原理简单解释为一个数组的数组名可以看做指向数组第一个元素的指针,而我们传递给函数的必须是一个指针而非数组,因此我们要先获取这个指针,然后再传递给函数,对于多维数组的处理也是类似的。

那么最后,数组参与运算(包括函数调用和函数参数)的总流程如下:

class ArrLval:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> pos_exp;
void Dump()override
{
int tempdep=nowdep;
while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep];
IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep);
std::vector<int> pos=pos_exp->Para();
for(auto it=pos.begin();it!=pos.end();it++)
{
if(it==pos.begin())
{
if(var_type[IDENT]==3)
{
std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl;
nowww++;
std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
}else
std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl;
}
else
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
nowww++;
}
if(!be_func_para||pos.size()==array_siz[IDENT])
{
std::cout<<"\t%"<<nowww<<"= load %"<<nowww-1<<std::endl;
nowww++;
}else
{
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", 0"<<std::endl;
nowww++;
}
}
int Calc()override
{
return 43;
}
}; class ArrLeval:public BaseAST
{
public:
std::string IDENT;
std::unique_ptr<BaseAST> pos_exp;
void Dump()override
{
int tempdep=nowdep;
while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep];
IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep);
int now=nowww-1;
std::vector<int> pos=pos_exp->Para();
for(auto it=pos.begin();it!=pos.end();it++)
{
if(it==pos.begin())
{
if(var_type[IDENT]==3)
{
std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl;
nowww++;
std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
}else
std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl;
}else
std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl;
nowww++;
}
std::cout<<"\tstore %"<<now<<", %"<<nowww-1<<std::endl;
nowww++;
}
int Calc()override
{
return 44;
}
};

这件事情说清楚了以后,别的事情其实就比较好说了——由于getptr和getelemptr在汇编层面的行为基本是一致的,因此直接类似getelemptr生成代码就好,

void solve_get_ptr(koopa_raw_value_t value,int &st)
{
koopa_raw_value_t src=value->kind.data.get_ptr.src;
now_array=(ull)src;
deep=1;
cout<<" li t4, "<<M[(ull)src]<<endl;
cout<<" add t4, sp, t4"<<endl;
cout<<" lw t0, (t4)"<<endl;
if(value->kind.data.get_ptr.index->kind.tag==KOOPA_RVT_INTEGER)
cout<<" li t1, "<<value->kind.data.get_ptr.index->kind.data.integer.value<<endl;
else
{
cout<<" li t4, "<<M[(ull)value->kind.data.get_ptr.index]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t1, "<<"(t4)"<<endl;
}
int p=4;
for(int i=deep;i<array_size[now_array].size();i++)p*=array_size[now_array][i];
cout<<" li t2, "<<p<<endl;
cout<<" mul t1, t1, t2"<<endl;
cout<<" add t0, t0, t1"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, "<<"(t4)"<<endl;
M[(ull)value]=st;
st+=4;
}

另外一些细节比如alloc的此时我们alloc的东西可能是一个指针,因此我们需要为alloc添加处理指针的情况,我们可以把一个指针看做第一维为1的数组(其实这个第一维大小主要是用来填充,但是为了计算数组大小的准确性我们用1来填充),就是这样:

void solve_alloc(koopa_raw_value_t value,int &st)
{
if(value->ty->data.pointer.base->tag==KOOPA_RTT_INT32)M[(ull)value]=st,st+=4;
else if(value->ty->data.pointer.base->tag==KOOPA_RTT_ARRAY)
{
koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base;
array_size[(ull)value].push_back(base->data.array.len);
int siz=base->data.array.len*4;
while(base->data.array.base->tag!=KOOPA_RTT_INT32)
{
base=(koopa_raw_type_kind_t*)base->data.array.base;
array_size[(ull)value].push_back(base->data.array.len);
siz*=base->data.array.len;
}
M[(ull)value]=st;
st+=siz;
}else if(value->ty->data.pointer.base->tag==KOOPA_RTT_POINTER)
{
koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base;
array_size[(ull)value].push_back(1);
while(base->data.array.base->tag!=KOOPA_RTT_INT32)
{
base=(koopa_raw_type_kind_t*)base->data.array.base;
array_size[(ull)value].push_back(base->data.array.len);
}
M[(ull)value]=st;
st+=4;
}
}

除此之外,在这里load **[T,N]的时候,我们还要把数组大小记录好,因为我们后面访问数组的指令依赖的是这个load指令load出来的数组,因此我们还要维护这一点。

void solve_load(koopa_raw_value_t value,int &st)
{
if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC)
{
cout<<" la t0, "<<value->kind.data.load.src->name+1<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_ELEM_PTR)
{
cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_PTR)
{
cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" lw t0, (t4)"<<endl;
cout<<" lw t0, 0(t0)"<<endl;
cout<<" li t4, "<<st<<endl;
cout<<" add t4, t4, sp"<<endl;
cout<<" sw t0, (t4)"<<endl;
M[(ull)value]=st;
st+=4;
}else
{
if(array_size.find((ull)(value->kind.data.load.src))!=array_size.end())
array_size[(ull)value]=array_size[(ull)(value->kind.data.load.src)];
M[(ull)value]=M[(ull)(value->kind.data.load.src)];
}
}

这样,这个编译器大体的功能就完成了。

当然,单纯这样的话其实是不够的——我们还需要处理一些栈的问题。首先,随着我们代码变得复杂,栈的空间会变得越来越大,但是在表达式中能出现的立即数大小是受限的,因此我们需要先将修改栈指针的立即数加载到寄存器内,然后用这个寄存器与栈指针进行计算。

除此之外,如果对所有函数使用固定大小的栈帧,那么我们就必须使用一个非常巨大的栈帧,但是这样的话递归函数递归几层栈空间就不够了,因此我们必须根据实际需要生成栈帧大小,有一种非常取巧的方法解决这个问题——我们先生成一次目标代码,这样我们就能确定出来每个函数所需的栈帧大小,我们把这些栈帧大小存在一个vector里,然后用计算好的栈帧大小重新生成所有的目标代码即可。

当然,这样做是相当麻烦的,因为相当于我们执行了两次目标代码生成,这一点是后期可以优化的。

北大2022编译原理实践(C/C++)-sysy 编译器构建的更多相关文章

  1. C#基础--.net平台的重要组成部分以及.net程序简单的编译原理

    .net平台的组成只要有两部分   FCL:框架类库    CLR:公共语言运行时 .net程序简单的编译原理 1.0:使用C#编译器(csc.exe) 将C#源代码编译成程序集+{编译之前:会检查C ...

  2. 跟vczh看实例学编译原理——三:Tinymoe与无歧义语法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe.   看了前面的三篇文章,大家应该基本对Tinymoe的代码有一个初步的感觉了.在正确分析"print ...

  3. Atitit.编译原理与概论

    Atitit.编译原理与概论 编译原理 词法分析 Ast构建,语法分析 语意分析 6 数据结构  1. ▪ 记号 2. ▪ 语法树 3. ▪ 符号表 4. ▪ 常数表 5. ▪ 中间代码 1. ▪ 临 ...

  4. Compiler Theory(编译原理)、词法/语法/AST/中间代码优化在Webshell检测上的应用

    catalog . 引论 . 构建一个编译器的相关科学 . 程序设计语言基础 . 一个简单的语法制导翻译器 . 简单表达式的翻译器(源代码示例) . 词法分析 . 生成中间代码 . 词法分析器的实现 ...

  5. 学了编译原理能否用 Java 写一个编译器或解释器?

    16 个回答 默认排序​ RednaxelaFX JavaScript.编译原理.编程 等 7 个话题的优秀回答者 282 人赞同了该回答 能.我一开始学编译原理的时候就是用Java写了好多小编译器和 ...

  6. 第二章 Javac编译原理

    注:本文主要记录自<深入分析java web技术内幕>"第四章 javac编译原理" 1.javac作用 将*.java源代码文件转化为*.class文件 2.编译流程 ...

  7. 编译原理_P1004

    龙书相关知识点总结 //*************************引论***********************************// 1. 编译器(compiler):从一中语言( ...

  8. 编译原理-词法分析05-正则表达式到DFA-01

    编译原理-词法分析05-正则表达式到DFA 要经历 正则表达式 --> NFA --> DFA 的过程. 0. 术语 Thompson构造Thompson Construction 利用ε ...

  9. 跟vczh看实例学编译原理——二:实现Tinymoe的词法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe.   实现Tinymoe的第一步自然是一个词法分析器.词法分析其所作的事情很简单,就是把一份代码分割成若干个tok ...

  10. 跟vczh看实例学编译原理——一:Tinymoe的设计哲学

    自从<序>胡扯了快一个月之后,终于迎来了正片.之所以系列文章叫<看实例学编译原理>,是因为整个系列会通过带大家一步一步实现Tinymoe的过程,来介绍编译原理的一些知识点. 但 ...

随机推荐

  1. 南大ics-pa/PA0过程及感想

    实验教程地址:https://nju-projectn.github.io/ics-pa-gitbook/ics2022/index.html 一.Ubuntu安装 在清华大学镜像站下载了Ubuntu ...

  2. windows系统mysql8.0.20.0踩坑(-)

    首先, 下载mysql-installer-web-community-8.0.20.0.msi 一步一步安装下来,一切看起来如此美好 cmd运行mysqld --install 但发现net sta ...

  3. 在百度云服务器上部署Django网站的经历

    前言: 前段时间,利用Django为单位制作了一个小型的内部考勤系统,本想放到单位内部的服务器上,考虑到运行的稳定.安全防护等问题,最终决定把网站部署到百度云服务器上,事先也在网上查找了一些资料,但过 ...

  4. PostgreSQL-14 安装配置-wsl_v1_ubuntu22.04

    环境准备 pgAdmin: Cisco2022 postgrep数据库: postgres: Postgres_2023 install https://learn.microsoft.com/en- ...

  5. MATLAB基础—基础认识

    数建-MATLAB(基础认识) 一些基本使用 clear all :清除Workspace中的所有变量 clc: 清除Command Window中的所有命令 注释:%%(空空格)多行注释 或 % 单 ...

  6. 使用viper读取配置文件

    配置文件config.yml mysql: type: mysql dsn: "user:pass@tcp(localhost:30306)/db_name?charset=utf8& ...

  7. TCP通信实现两个主机之间的信息交互

    TCP通信概述TCP协议用来控制两个网络设备之间的点对点通信,两端设备按作用分为客户端和服务端.服务端为客户端提供服务,通常等待客户端的请求信息,有客户端请求到达之后,及时提供服务和返回响应消息:客户 ...

  8. 【阿里云ACP】-03(数据库RDS)

    OSS快速使用入门:创建Bucket 1.用户创建一个Bucket时,可以根据费用单价.请求来源分布.响应延迟等方面的考虑,为该bucket选择所在的数据中心 阿里云所有数据中心都提供OSS公众服务 ...

  9. 面试之AQS

    https://blog.csdn.net/wwwzhouzy/article/details/119702170 https://copyfuture.com/blogs-details/20200 ...

  10. Tunnel

    Tunnel既不是给https用的,也不是给代理用的,是给https代理用的 之所以以前老觉得Https也有一个tunnel,是因为每次看https请求,fiddler本身就是http代理,本来就会有 ...