北大2022编译原理实践(C/C++)

这是今年新推出的实践方案,由往年的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)提供了例子,这里就不赘述了

在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的内容了)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章