【自研Fe编译器】三、(变量声明+栈分配)基于Flex + Yacc开发的高级语言编译器

  • 项目链接:https://github.com/FeliGame/FeCompiler
  • 本项目中的文法和AST数据结构设计参考了北京大学编译器实践项目pku-minic.github.io/online-doc/
  • 本博客会同步更新开发进度,欢迎各位交流、批评!

简介

Fe语言是一种语法类似C的高级语言,由于笔者将重点放在编译器的语义分析和代码优化上,因此目前阶段语法和C不会有什么不同。
该编译器会生成Koopa IR,并基于Koopa IR生成目标汇编代码(RISC-V)。

目前进度

可以实现变量的声明、定义、运算,并且通过栈统一维护用户定义变量(@开头)、参数、中间代码的临时变量(%开头)、返回值等。
运行PKU-Compiler自带的autotest脚本,Lv3和Lv4的全部测试通过。

解决问题

  • 加入RISC-V的rem指令以进行取模运算;
  • 为函数开辟栈空间时,需要事先知道指令中变量的个数。我采用了在分析IR前,预先扫描KoopaIR文本中的变量名种数,使用去重集合std::set维护,从而得到变量名个数,乘以4即为要开辟的栈空间。开辟栈的指令很简单:addi sp, sp, -(要开辟的栈空间字节数);相应地,在函数调用结束的ret指令前,要恢复栈顶地址(出栈):addi sp, sp, (要开辟的栈空间字节数)。
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字节
}
  • 在前面的版本中,RISCV编译器无法按需对寄存器进行精准定位,主要是通过reg_prev和reg_prev_prev来记录上次和上上次使用的寄存器名,在仅支持常量之间的二元运算的语法中,这样尚可使用,但在有了多种变量调用的情况下,不再可行,例如对于下面的Fe代码及生成的中间代码,不难发现%4的结果是%1 - %3所得,显然已经无法通过LRU的思想(reg_prev_prev - reg_prev)来实现了。
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
}

我的解决思路是:由于分析出IR的每个koopa_value变量和IR原文中的变量名一一对应,故可以制作一个哈希表,键是koopa_value的变量名,值是分配的栈空间地址。

// 变量->栈位置的映射
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的指令数,降低了目标代码效率。这里是笔者出于目前进度考虑、简化开发而为之,等到函数调用、条件跳转等基本功能都已实现后,再加入寄存器分配优化和指令数优化。

你可能感兴趣的:(c++,笔记,c语言,算法,数据结构,risc-v)