北大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的內容了)

 

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