Fe语言是一种语法类似C的高级语言,由于笔者将重点放在编译器的语义分析和代码优化上,因此目前阶段语法和C不会有什么不同。
该编译器会生成Koopa IR,并基于Koopa IR生成目标汇编代码(RISC-V)。
可以实现变量的声明、定义、运算,并且通过栈统一维护用户定义变量(@开头)、参数、中间代码的临时变量(%开头)、返回值等。
运行PKU-Compiler自带的autotest脚本,Lv3和Lv4的全部测试通过。
void scanStackSize(string ir)
{
stringstream ss(ir);
string word;
set<string> identifiers; // 存储所有变量(不重复)
// 按空格切分ir
while (ss >> word)
{
if (word[0] == '@' || word[0] == '%')
{
// 去掉末尾的逗号
if (word.back() == ',')
{
word = word.substr(0, word.size() - 1);
}
identifiers.insert(word);
}
}
cerr << "stack size: " << identifiers.size() << endl;
stack_size = (identifiers.size() << 2); // 每个元素为4字节
}
int main() {
int x = 2, y = 3;
return x + 2 - y * 3;
}
fun @main(): i32 {
%entry:
@x = alloc i32
store 2, @x
@y = alloc i32
store 3, @y
%0 = load @x
%1 = add %0, 2
%2 = load @y
%3 = mul %2, 3
%4 = sub %1, %3
ret %4
}
我的解决思路是:由于
// 变量->栈位置的映射
unordered_map<string, int> id_map;
inline int getStackPos(const koopa_raw_value_t &value)
{
string value_name;
// %开头的变量没有value->name,使用value自身十六进制代号命名
if (value->name == nullptr)
{
stringstream ss;
ss << value;
// fout << "allocing" << ss.str();
value_name = ss.str();
} else value_name = value->name;
if (id_map.count(value_name))
{
// fout << "existing name: " << value_name << " " << id_map[value_name] << " ";
return id_map[value_name];
}
else
{
// fout << "adding name: " << value_name << " " << id_map[value_name] << " ";
max_stack_pos += 4;
id_map.insert(make_pair(value_name, max_stack_pos));
return max_stack_pos;
}
}
生成的RISC-V代码如下:
int main() {
int x = 2, y = 3;
return x + 2 - y * 3;
}
// Fe编译器生成的RISC-V代码
.text
.globl main
main:
addi sp, sp, -36 // 压栈
//@x = alloc i32
//store 2, @x
li t0, 2
sw t0, 0(sp)
//@y = alloc i32
//store 3, @y
li t0, 3
sw t0, 4(sp)
//%0 = load @x
lw t0, 0(sp)
sw t0, 8(sp)
//%1 = add %0, 2
lw t0, 8(sp)
li t1, 2
add t0, t0, t1
sw t0, 12(sp)
//%2 = load @y
lw t0, 4(sp)
sw t0, 16(sp)
//%3 = mul %2, 3
lw t0, 16(sp)
li t1, 3
mul t0, t0, t1
sw t0, 20(sp)
//%4 = sub %1, %3
lw t0, 12(sp)
lw t1, 20(sp)
sub t0, t0, t1
sw t0, 24(sp)
//ret %4
lw a0, 24(sp)
addi sp, sp, 36 // 出栈
ret
观察上方的RISC-V代码,不难发现这些代码操作寄存器基本都是t0, t1,很多寄存器都没用上(RISC-V支持t0-t7, a0-a8),一股脑放在栈中,大幅增加了s/l的指令数,降低了目标代码效率。这里是笔者出于目前进度考虑、简化开发而为之,等到函数调用、条件跳转等基本功能都已实现后,再加入寄存器分配优化和指令数优化。