北大编译实践踩坑

额外信息

Bison中百分号的作用:Bison中百分号的作用 - Gold_stein - 博客园

Lv0.环境配置

因为win11默认开启的VBS会导致在游戏等高负载场景显卡占用异常,但是win11又已经关闭了单独关闭vbs同时保留hyperv的通道,所以只能选择一劳永逸地将hyperv关闭。这样一来,windows宿主机就无法使用docker环境了,只能把docker安装在linux虚拟机当中,通过ssh来进行远程访问。

备忘:

1.这是 Koopa IR 里的规定, FunctionBasicBlockValue 的名字必须以 @ 或者 % 开头. 前者表示这是一个 “具名符号”, 后者表示这是一个 “临时符号”.

  • 这两者其实没有任何区别, 但我们通常用前者表示 SysY 里出现的符号, 用后者表示你的编译器在生成 IR 的时候生成的符号. 因为 main 是 SysY 里定义的, 所以这个函数叫 @main. 关于符号名称的细节见 Koopa IR 规范.

Lv1.main函数

Lv1.5测试

文档中给出的lex文件并没有处理块注释,需要自己加入定义。

起初我用的是\/\*[\s\S]*?\*\/  但无论如何也不能正确匹配,询问gpt后给出了非常冗杂的表达式:   \/\*([^*]|\*+[^/])*\*+\/  虽然简单,但多少有些反人类了。

直到最后也没有弄清楚\/\*[\s\S]*?\*\/ 为何报错,返璞归真使用了BlockComment   \/\*.*?\*\/的定义,还真跑起来了。

Lv2.

Lv2.2

我选择了dfs的写法,这样写的关键是搞清楚每个函数所接受参数的数据类型。

为了适应这里的输出方式,要把lv1里ast的dump函数从cout改成oss,格式化写入字符串。

Lv3.表达式

Lv3.1

关于算数右移和逻辑右移:

  1. 逻辑右移 (shr):逻辑右移是将所有位都向右移动,不考虑符号位。在逻辑右移中,最高位也是补零,不论它原来是什么。这意味着逻辑右移对于正数和无符号数的操作是一样的。例如,对于二进制数1101,逻辑右移一位后得到0110。
  2. 算术右移 (sar):算术右移是有符号整数右移操作,会考虑符号位。在算术右移中,最高位(符号位)保持不变,也就是说,如果原数是正数,最高位仍为0;如果原数是负数,最高位仍为1。这意味着算术右移会保持原数的符号。例如,对于二进制数1101,算术右移一位后得到1110,因为它是一个负数。
  3. 在baseAST头文件当中,一元运算符从右向左结合,所以这里要先访问子树再访问自身(如下代码)
  4. 在bison文件中,要注意字符串和字符变量的区别是很严格的,单个字符只能用单引号,不能用双引号,否则会导致syntax error
  • class UnaryExpAST : public BaseAST { public: std::unique_ptr<BaseAST> pexp; char uop; std::unique_ptr<BaseAST> uexp; void Dump(std::ostringstream &oss) const override { #ifdef DEBUG oss << "unaryexp "; #endif if (pexp == nullptr) { flag = 1; uexp->Dump(oss); #ifdef DEBUG oss << "uop " << uop << " and uexp "; #endif oss << "%" << cnt << " = "; if (uop == '!') { oss << "eq "; if (!cnt) oss << rootnum << ", 0"; else oss << "%" << cnt - 1 << ", 0"; } if (uop == '-') { oss << "sub 0, "; if (!cnt) oss << rootnum; else oss << "%" << cnt; } oss << "\n"; cnt++; } else pexp->Dump(oss); } };

Lv3.2

1.因为unordered_map和map没有提供koopa.h中各种数据类型对应的哈希函数,导致使用map不能有效地记录信息,为此,我们可以采用记忆化搜索的方案,在解析表达式的过程中记住每一步所使用的寄存器。

除此之外,我们还能采取一种取巧的办法:结构体由于数据类型的特殊性,不能够直接作为map的键值,但是指向它的指针本质上就是一个长整形,我们可以用指向其的指针取而代之,来作为我们map的键值,这样写比上面一种方法更加简单快捷。

2.cal是直接覆盖寄存器,不会新开寄存器,所以cal之后不需要now_using_reg++;

case KOOPA_RBO_SUB: 
        get_both(L, R, l_reg_idx, r_reg_idx);
        cout << "\tsub\t" << reg_names[now_using_reg - 1] << ", ";
        CalPrint(l_reg_idx, r_reg_idx);
        break;

3.常见问题:

在本次编译实践中,有很多地方会出现这样的情况:

有很多的if/switch的条件判断,每个条件都需要执行类似cnt++的操作,可以在每个判断语句块里单独写,也可以总写,千万注意不要两处都写上了

4.BaseAST的koopaIR输出问题:

       if (!mul_done && !add_done)
            {
                oss << root_mul << ", " << root_add << "\n";
            }
            else if (mul_done && !add_done)
            {
                oss << root_add << ", "
                    << "%" << new_mul_cnt - 1 << "\n";
            }
            else if (add_done && !mul_done)
            {
                oss << root_mul << ", "
                    << "%" << new_add_cnt - 1 << "\n";
            }
            else
                oss << "%" << new_add_cnt - 1 << ", "
                    << "%" << new_mul_cnt - 1 << "\n";
            cnt++;

第一种情况对应两边都没有进行额外计算,直接引入新操作数的情况。一开始想错了,写的判断条件是!cnt,但稍微复杂一点的表达式就能推翻这个错误判断。

例如:2*3+4*5

先处理4*5,此时cnt已经大于0了,但是在处理2*3时,依然属于第一种情况,所以必须用两个done是否都为假来执行该判断。

Lv3.3

历史遗留问题:

辨析make_unqiue<>()  和  unique_ptr<>();

例子:

auto comp_unit = make_unique<CompUnitAST>();
    comp_unit->func_def = unique_ptr<BaseAST>($1);

前者的相当于new ()   ,只不过得到的是一个智能指针,而非普通指针 ;   后者相当于强制转换类型,即把普通指针转换为智能指针。

tips:在类的成员函数中,只能调用同样为const的成员函数。

例如:

class LorExpAST : public BaseAST
{
public:
    void PrintOr(const std::string &a, const std::string &b, std::ostringstream &oss) const
    {
        oss << "%" << cnt++ << " = "
            << "or " << a << ", " << b << "\n";
        oss << "%" << cnt << " = "
            << "ne "
            << "%" << cnt - 1 << ", 0"
            << "\n";
        cnt++;
    }
    std::unique_ptr<BaseAST> lorexp, landexp;
    void Dump(std::ostringstream &oss) const override
    {
        if (lorexp == nullptr)
            landexp->Dump(oss);
        else
        {
            int old_and_cnt = cnt;
            landexp->Dump(oss);
            int root_and = rootnum;
            bool and_done = old_and_cnt != cnt;
            int and_cnt = cnt - 1;

            int old_or_cnt = cnt;
            lorexp->Dump(oss);
            int root_or = rootnum;
            bool or_done = old_or_cnt != cnt;
            int or_cnt = cnt - 1;

            std::string or_prd = or_done ? ("%" + std::to_string(or_cnt)) : std::to_string(root_or),
                        and_prd = and_done ? ("%" + std::to_string(and_cnt)) : std::to_string(root_and);
            PrintOr(or_prd, and_prd, oss);
        }
    }
};

这里的PrintOr函数必须定义成const,否则会报错。

重点: 引入这一节的新运算符时,我首先采取的策略是长度为一的>和<仍然采用单字符方式读取,其他的运算符长度为二,则新开一种token:

DoubleCharOp "||"|"&&" ……

这种方法是错误的,会导致shift/reduce conflicts   (规约冲突)

因为这一节引入的所有的表达式,都有一个统一的子结构:  x  op  y

如果所有长度为2的运算符都被归类为DoubleCharOp,那么就会导致程序在处理这种结构的时候,不知道是往下继续划分还是到此为止,造成冲突。

误区:   因为koopa只支持按位与和按位或,所以起初我在生成riscv时也用的是按位与andi和按位或ori,但是这两种运算的操作数有某种还没遇到的限制,会报错。

但是根据我已经编写的koopa规则,可以得知运算执行到这一步的时候,operands只可能是0或1,这时按位与和与、按位或和或是没有区别的,直接用and和or即可。

 

注意如果表达式当中两个数都是0,那么就会都用x0寄存器,就导致now_using_register就不会移动,接下来就会导致寄存器之间相互覆盖,因此,一旦在执行完get_both函数之后,发现这个now_using_register没有移动,就需要手动给他加上一。

Lv4

Lv4.1

这一章的ENBF出现了

ConstDecl     ::= "const" BType ConstDef {"," ConstDef} ";";

这种可以重复的形式,但是我们知道,实际上语法解释器是不支持用大括号来表示重复出现的,那我们要怎么编写,来实现这一功能呢?

可以用递归的形式来实现:

BlockItemList
 : BlockItem {
  auto v = new vector<unique_ptr<BaseAST>>();
  v->push_back(unique_ptr<BaseAST>($1));
  $$ = move(v);
 } | BlockItemList BlockItem {
  auto v = ($1);
  v->push_back(unique_ptr<BaseAST>($2));
  $$ = move(v);
 }
 ;

这样,我们就借助一个vector数组实现了重复功能。

起初,我试图用auto来遍历一个智能指针vector:

 

void ConstDeclCal() const override
    {
        for(auto i : constdef)
        {
            
        }
    }

 

这样子会报错,因为智能指针是不允许拷贝的,只能mv,这里只需要把i改为引用即可。

 Lv4.2

文档里说

在遇到 LVal 时, 你需要从符号表中查询这个符号的信息, 然后用查到的结果作为常量求值/IR 生成的结果. 注意, 如下情况属于语义错误:

  • 在进行常量求值时, 从符号表里查询到了变量而不是常量.
  • 在处理赋值语句时, 赋值语句左侧的 LVal 对应一个常量, 而不是变量.
  • 其他情况, 如符号重复定义, 或者符号未定义.


这里的第一点可能是指1.用变量作为数组的大小,但是目前还没有涉及到数组

2.常量表达式当中不能出现变量

 

如果需要在多个文件中使用同一个全局变量,那么应该只在main当中进行全局变量的定义,在头文件里用extern进行声明,避免内存的重复分配。

struct没有这种烦恼,因为定义一个struct只是“定义”一种结构,不涉及内存的分配。

 

历史遗留问题:

用于映射koopa中各个raw元素和相应寄存器(或内存)的map,键值如果用各种不同类型的指针的话,随着实验的规模愈发庞大,要做的特判也就越来越多,可以通过统一使用koopa_raw_value_t作为键值来省去这一麻烦,因为所有的指令最后都归结于value,可以在value的visit函数中执行这一操作,把koopa_raw_value_t作为参数传入到下一层函数当中。

脑瘫错误:

valuechart结构体的内部代码没有写完,导致之后自动补全不能正常生效,还以为是编译器设置出了问题,调试了半天。

 

关于const函数:传递可移动类型:qualifiers dropped in binding reference of type "std::__cxx11::string &" to initializer of type "const std::__cxx11::string"

为什么会这样报错:   在const函数中,调用了非const的引用,

例如:

#.hpp
class A
{
public:
    wstring& GetTitle() const;
private:
    wstring title;    
};
#.cpp
wstring& GetTitle() const
{
    return this->title;
}

如这里所示,在const函数gettitle中,调用了非const的成员title,而const函数是没有这个权限的。

 

如果一个结构体内部有返回本身数据类型的函数,可以模仿koopa.h这样写:

struct valuechart;

typedef valuechart *valuechart_t;

struct valuechart{
    ident_map chart;
    valuechart *father;
    int32_t depth = 1;
    valuechart() : father(nullptr) {}
    valuechart_t ident_search(const std::string &ident)
    {
        if(chart.count(ident)) return this;
        if(father == nullptr) return nullptr;
        return father->ident_search(ident);
    }
};

先定义一个空的,然后再完整写出来。

也可以这样:

#include <iostream>

// 结构体的声明
struct MyStruct {
    int data;

    // 成员函数的声明,返回类型为结构体自身类型
    MyStruct myFunction() const;

    // 成员函数的定义
    void printData() const {
        std::cout << "Data: " << data << std::endl;
    }
};

// 成员函数的定义
MyStruct MyStruct::myFunction() const {
    MyStruct result;
    result.data = data * 2;  // 做一些操作
    return result;
}

这种写法更适合类

 

Lv5

这个等级由于多了语句块和作用域的处理,所以新增了许多指向作用域的指针,带来了一些内存管理上的麻烦

尤其是处理新block这里:

auto rootchart = new valuechart();
            rootchart = nowchart;
            nowchart = new valuechart();
            nowchart->depth = rootchart->depth + 1;
            nowchart->father = rootchart;
            block->Dump();
            delete (nowchart);
            nowchart = rootchart;

因为rootchart需要来回赋值,所以不适合用unique_ptr;

又因为可能会产生死锁问题,所以不适合用shared_ptr;

最后发现还不如手写new,自己加个delete

另外,如果把rootchart定义成全局变量,也有可能在有多重作用域嵌套的情况下,在最后两层产生死锁问题,所以目前这种写法就是最优解。

 

Lv6

这个level新增了if-else,需要解决else的悬挂问题,解决办法:

把If语句分成两类,一类是已经完成if-else配对的,这样可以保证每个else都有清晰的从属;

另一类是未完成配对的,有机会加上else。

//! Stmt ::= UnmatchedStmt
//!        | MatchedStmt
//! 
//! UnmatchedStmt ::= "if" "(" Exp ")" MatchedStmt ["else" UnmatchedStmt]
//! 
//! MatchedStmt ::= LVal "=" Exp ";"
//!        | [Exp] ";"
//!        | Block
//!        | "if" "(" Exp ")" MatchedStmt "else" MatchedStmt
//!        | "return" [Exp] ";";

 

posted @ 2023-10-15 16:46  Gold_stein  阅读(250)  评论(0编辑  收藏  举报