這是今年新推出的實踐方案,由往年的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)提供了例子,這里就不贅述了
只需要處理一個問題:
#include "AST.h"
應該放在哪?
最野蠻的方法當然是——哪都放上
這時你make會報錯,大概是什么redefinition之類的問題
怎么辦?
其實編譯器已經給你提示了:他會建議你在AST.h這個庫里加入這句話:
#pragma once
然后問題就解決了
在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的內容了)
lv2:
接下來就是由Koopa IR生成RISC V了,lv2的文檔里提供了一個模板,你只需定義這樣的函數:
void parse_string(const char* str) { // 解析字符串 str, 得到 Koopa IR 程序 koopa_program_t program; koopa_error_code_t ret = koopa_parse_from_string(str, &program); assert(ret == KOOPA_EC_SUCCESS); // 確保解析時沒有出錯 // 創建一個 raw program builder, 用來構建 raw program koopa_raw_program_builder_t builder = koopa_new_raw_program_builder(); // 將 Koopa IR 程序轉換為 raw program koopa_raw_program_t raw = koopa_build_raw_program(builder, program); // 釋放 Koopa IR 程序占用的內存 koopa_delete_program(program); // 處理 raw program // 使用 for 循環遍歷函數列表 for (size_t i = 0; i < raw.funcs.len; ++i) { // 正常情況下, 列表中的元素就是函數, 我們只不過是在確認這個事實 // 當然, 你也可以基於 raw slice 的 kind, 實現一個通用的處理函數 assert(raw.funcs.kind == KOOPA_RSIK_FUNCTION); // 獲取當前函數 koopa_raw_function_t func = (koopa_raw_function_t) raw.funcs.buffer[i]; for (size_t j = 0; j < func->bbs.len; ++j) { assert(func->bbs.kind == KOOPA_RSIK_BASIC_BLOCK); koopa_raw_basic_block_t bb = func->bbs.buffer[j]; // 進一步處理當前基本塊 // ... koopa_raw_value_t value = ...; // 示例程序中, 你得到的 value 一定是一條 return 指令 assert(value->kind.tag == KOOPA_RVT_RETURN); // 於是我們可以按照處理 return 指令的方式處理這個 value // return 指令中, value 代表返回值 koopa_raw_value_t ret_value = value->kind.data.ret.value; // 示例程序中, ret_value 一定是一個 integer assert(ret_value->kind.tag == KOOPA_RVT_INTEGER); // 於是我們可以按照處理 integer 的方式處理 ret_value // integer 中, value 代表整數的數值 int32_t int_val = ret_val->kind.data.integer.value; // 示例程序中, 這個數值一定是 0 assert(int_val == 0); } // 進一步處理當前函數 // ... } // 處理完成, 釋放 raw program builder 占用的內存 // 注意, raw program 中所有的指針指向的內存均為 raw program builder 的內存 // 所以不要在 raw program builder 處理完畢之前釋放 builder koopa_delete_raw_program_builder(builder); }
這就是把2.1和2.2里提供的代碼拼接起來得到的結果
那么...我們怎么用這個東西生成RISC V呢?
首先我們結合代碼中的注釋,看到它是按照層次:函數定義——基本塊定義——基本塊中的每一條指令
然后再參考文檔中提供的RISCV示例,判斷一下函數定義的時候我們需要輸出什么樣的格式,基本塊定義的時候我們需要輸出什么樣的格式,return指令又是什么格式,在正確的位置對應輸出即可
參考代碼:(之所以是參考,是因為由於現在的編譯器功能太少,很多格式在錯誤的地方輸出其實也沒有影響,當后面加入更多功能之后可能會需要調整格式的輸出)
void parse_string(const char* str) { // 解析字符串 str, 得到 Koopa IR 程序 koopa_program_t program; koopa_error_code_t ret = koopa_parse_from_string(str, &program); assert(ret == KOOPA_EC_SUCCESS); // 確保解析時沒有出錯 // 創建一個 raw program builder, 用來構建 raw program koopa_raw_program_builder_t builder = koopa_new_raw_program_builder(); // 將 Koopa IR 程序轉換為 raw program koopa_raw_program_t raw = koopa_build_raw_program(builder, program); // 釋放 Koopa IR 程序占用的內存 koopa_delete_program(program); cout<<" .text"<<endl; for (size_t i = 0; i < raw.funcs.len; ++i) { // 正常情況下, 列表中的元素就是函數, 我們只不過是在確認這個事實 // 當然, 你也可以基於 raw slice 的 kind, 實現一個通用的處理函數 assert(raw.funcs.kind == KOOPA_RSIK_FUNCTION); // 獲取當前函數 koopa_raw_function_t func = (koopa_raw_function_t) raw.funcs.buffer[i]; cout<<" .globl "<<func->name+1<<endl; cout<<func->name+1<<":"<<endl; for (size_t j = 0; j < func->bbs.len; ++j) { assert(func->bbs.kind == KOOPA_RSIK_BASIC_BLOCK); koopa_raw_basic_block_t bb = (koopa_raw_basic_block_t)func->bbs.buffer[j]; for (size_t k = 0; k < bb->insts.len; ++k){ koopa_raw_value_t value = (koopa_raw_value_t)bb->insts.buffer[k]; // 示例程序中, 你得到的 value 一定是一條 return 指令 assert(value->kind.tag == KOOPA_RVT_RETURN); // 於是我們可以按照處理 return 指令的方式處理這個 value // return 指令中, value 代表返回值 koopa_raw_value_t ret_value = value->kind.data.ret.value; // 示例程序中, ret_value 一定是一個 integer assert(ret_value->kind.tag == KOOPA_RVT_INTEGER); // 於是我們可以按照處理 integer 的方式處理 ret_value // integer 中, value 代表整數的數值 int32_t int_val = ret_value->kind.data.integer.value; // 示例程序中, 這個數值一定是 0 //assert(int_val == 0); cout<<" li "<<"a0 , "<<int_val<<endl; cout<<" ret"<<endl; } // ... } // ... } // 處理完成, 釋放 raw program builder 占用的內存 // 注意, raw program 中所有的指針指向的內存均為 raw program builder 的內存 // 所以不要在 raw program builder 處理完畢之前釋放 builder koopa_delete_raw_program_builder(builder); }
其中值得注意的是這條語句:
koopa_raw_value_t value = (koopa_raw_value_t)bb->insts.buffer[k];
這條語句怎么來的?
這里需要你自行查閱koopa.h,看看這些結構是怎么定義的(可能會很復雜,需要不斷跳轉),才能找到自己需要的東西,這一點很重要,因為后面有大量類似這樣的內容。
同時還有這里要注意:
// 示例程序中, 這個數值一定是 0 //assert(int_val == 0);
我們把這個assert注釋掉了,因為真實情況下main函數的返回值當然不一定(雖然原則上應當)是0,而且事實上大多數測試數據也不保證這一點,因此我們不需要這個assert
這些都處理好了,就可以進行簡單的測試了:
autotest -koopa -s lv1 /root/compiler
autotest -riscv -s lv1 /root/compiler
第一條指令用來測試koopa,第二條指令用來測試riscv,你需要保證你的main函數能處理這樣的指令:
compiler -riscv 輸入文件 -o 輸出文件
compiler -koopa 輸入文件 -o 輸出文件
文檔建議你能夠根據輸入的-riscv和-koopa判斷應當輸出的格式,於是我們可以把main函數寫成這個樣子:
int main(int argc, const char *argv[]) { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); // 解析命令行參數. 測試腳本/評測平台要求你的編譯器能接收如下參數: // compiler 模式 輸入文件 -o 輸出文件 assert(argc == 5); auto mode = argv[1]; auto input = argv[2]; auto output = argv[4]; // 打開輸入文件, 並且指定 lexer 在解析的時候讀取這個文件 yyin = fopen(input, "r"); assert(yyin); // 調用 parser 函數, parser 函數會進一步調用 lexer 解析輸入文件的 unique_ptr<BaseAST> ast; auto ret = yyparse(ast); assert(!ret); if(mode[1]=='k') { freopen(output,"w",stdout); Koopa_Dump(); cout<<endl; ast->Dump(); return 0; } freopen("whatever.txt","w",stdout); Koopa_Dump(); cout<<endl; ast->Dump(); FILE* ff=fopen("whatever.txt","r"); char *buf=(char *)malloc(10000000); fread(buf, 1,10000000, ff); /*freopen("temps.txt","w",stdout); parse_string(buf,0); M.clear(); array_size.clear(); deep=0; now_array=0;*/ freopen(output,"w",stdout); parse_string(buf,1); return 0; }
(注釋掉的部分與本環節無關)
做完這些我們就可以進行一波測試,通過了之后就可以高興地進入
lv3:
這部分要求我們能處理一些表達式,但是不涉及到常量和變量的定義,因此我們需要處理的大概是形如這樣的程序:
int main() { return 1+2-3*4/5%6+!7--8;//注意這里的--8應當被解釋成-(-8)而不是--8(后者實際上也是不合法的) }
那么我們還是一步一步來,首先是lv3.1要求你能處理一元表達式,這里我們支持的一元表達式實際上也只有+,-,!三種
那么首先觀察一下EBNF:
Stmt ::= "return" Exp ";"; Exp ::= UnaryExp; PrimaryExp ::= "(" Exp ")" | Number; Number ::= INT_CONST; UnaryExp ::= PrimaryExp | UnaryOp UnaryExp; UnaryOp ::= "+" | "-" | "!";
嗯,看上去...|是什么鬼啊!
這里的|的規則表示它可以被解釋成前者或者后者任意一個,文檔中給出了兩種處理方法,這里我采取的是第一種(因為代碼比較直觀好寫)。
所謂為右側每種情況都定義一個AST,就是直接利用C++的多態的特性,以PrimaryExp為例,我只定義:
class PrimaryExp:public BaseAST { public: std::unique_ptr<BaseAST> p_exp; void Dump()const override { p_exp->Dump(); } };
而至於p_exp到底是什么呢?這由我解析的過程決定,我可以這樣解析:
PrimaryExp :'(' Exp ')'{ auto ast=new PrimaryExp(); ast->p_exp=unique_ptr<BaseAST>($2); $$=ast; }|NumExp { auto ast=new PrimaryExp(); ast->p_exp=unique_ptr<BaseAST>($1); $$=ast; } ;
(這里同樣展示了如何在.y文件中處理|)
利用多態的特性,無論p_exp是什么,我都可以正常調用Dump方法,調用到的就是對應類型的Dump方法了。
當然,為了格式的一致性,哪怕Number只是一個正常的整數,我們也不得不為其單獨開一個AST,否則格式不一致難以處理(當然你也可以用union或者enum之類的進行分類,但這就是第二種策略了)
這種策略的缺點就是越到后期我們要開的AST的數量就越多,會帶來比較大的代碼量
那么類比上面的過程我們就可以完成AST的構造了
接下來就要生成Koopa
這里涉及到表達式運算,因此我們首先需要知道Koopa的一些特性(具體請查閱文檔)
這里主要解決幾個問題:
第一:單賦值問題怎么解決?
我的策略是使用一個全局變量標記當前使用到了哪個整數,每次增加這個整數,這樣就能保證單賦值的特性了
也就是在AST.h里加一個這樣的聲明:
int now=0;
然后每次使用這個now作為臨時符號即可,每次使用完了都要+1
然后你興高采烈地去make,發現居然報錯了,又報了什么redefinition之類的東西
這時你很困惑不解:我不是加了#pragma once了嗎,怎么還報錯
你知道應該是你定義的這個變量的問題,那么怎么辦?
你想起了你學過的ics第七章linking部分的知識(或許有吧),決定聽人家的話,定義這種變量時最好加一個static!
static int now=0;
這時你再make,發現你的程序可以正常運行了!
這是為什么?
我不知道...上面整個就是我的心路歷程,我不知道為什么加個static他就對了,但是加static確實是有效的解決措施...
(2023.1.27增補:加入這個int now=0意味着now為強符號,這樣在鏈接時由於不同的.c文件均include了這個.h文件(有些.c文件是中間生成的),會導致多個同名強符號報錯。加入static之后會導致不同的.c文件不能共享這個變量,這一點在這個lab里面沒問題,因為我們真正使用這個變量的只有一個.c文件,但是在其他場景下會出現嚴重的問題,因此更加推薦的寫法是在.h文件里使用extern int now;,同時額外增加一個AST.c文件定義int now=0)
那么總結一下:如何生成Koopa?比如我們想計算--6這個表達式,那么我們在計算過程中我們Dump的過程應該是這樣:
首先我們會把--6解釋成整個表達式,在這里調用Dump時,第一個‘-’會被解釋成UnaryOp,而后面的-6會被解釋成下一個表達式,那么我們希望得知后面那個表達式的標號是%x=....,這樣整個表達式就可以被輸出成%x+1=sub 0, %x,因此我們需要先對后面那個表達式調用Dump,調用過Dump之后此時的now記錄的就是后面表達式的標號(這是由我們輸出的過程決定的),因此我們對這個部分的輸出Koopa的手段即為:
class SinExp:public BaseAST { public: char una_op; std::unique_ptr<BaseAST> una_exp; void Dump()const override { una_exp->Dump(); if(una_op=='-') { std::cout<<'%'<<nowww<<"= "<<"sub 0,"<<'%'<<nowww-1<<std::endl; ++nowww; } } };
(請大家忽略我的變量名,這只是一個例子)
那么在這個基礎之上,想要生成RISCV其實並不太容易,因為RISCV涉及到寄存器的分配,在Lv3里面這個問題體現的並不明顯,這里我們可以假定所有的寄存器都是夠的,然后隨便使用寄存器(當然要遵循他要求的規則,比如最后要把返回值放在寄存器a0里面之類的)
那么我們就進入到了Lv3.2,我們要能夠處理二元表達式,語法規范是:
Exp ::= AddExp; PrimaryExp ::= ...; Number ::= ...; UnaryExp ::= ...; UnaryOp ::= ...; MulExp ::= UnaryExp | MulExp ("*" | "/" | "%") UnaryExp; AddExp ::= MulExp | AddExp ("+" | "-") MulExp;
那么同理修改一下AST.h就可以實現識別了
而在Dump生成Koopa的時候也和上面基本同理,比如對一個二元的加法表達式(-7)+(-6),我們會把它解釋(-7)是AddExp,(-6)是MulExp,中間是'+',然后我們分別Dump這兩個表達式得到的標號為x1,x2,然后最后得到了%x3=add %x1, %x2
但是這里會遇到一個問題,就是如果我們有這樣的表達式6+5,前后兩個都只是常數,這兩個數不會被解釋成標號,那么我們一種策略是設法判斷參與表達式運算的是另一個表達式還是常數,然后分類處理,但這樣做太復雜了,我在這里傾向於采用另一種方法:
事實上一個常數c可以被解釋成一個表達式%x=add 0, c,因此即使參與運算的是常數,我們也新增一條形如上面的表達式,這樣我們就把常數與表達式統一成了表達式,這樣就可以解決這個問題,雖然這樣可能會帶來的另一個問題是我們生成的Koopa中有大量無用的語句,但是在初期這簡化了我們代碼的難度。
class MultiExp:public BaseAST { public: std::unique_ptr<BaseAST> mu_exp; char op; std::unique_ptr<BaseAST> un_exp; void Dump()override { mu_exp->Dump(); int now1=nowww-1; un_exp->Dump(); int now2=nowww-1; if(op=='*') { std::cout<<"\t"<<'%'<<nowww<<"= mul "<<'%'<<now1<<", %"<<now2<<std::endl; ++nowww; }else if(op=='/') { std::cout<<"\t"<<'%'<<nowww<<"= div "<<'%'<<now1<<", %"<<now2<<std::endl; ++nowww; }else { std::cout<<"\t"<<'%'<<nowww<<"= mod "<<'%'<<now1<<", %"<<now2<<std::endl; ++nowww; } } int Calc() override { if(op=='*') return (mu_exp->Calc())*(un_exp->Calc()); else if(op=='/') return (mu_exp->Calc())/(un_exp->Calc()); else return (mu_exp->Calc())%(un_exp->Calc()); } };
這是乘法表達式的一個例子,我們模仿上述過程正常生成Koopa即可
對於3.3的比較和邏輯表達式,首先要進行一些詞法分析,因為比較和邏輯表達式里有一些符號需要特殊處理,我們這樣寫:
"||" { return LOR; } "&&" { return LAND; } "==" { return EQ; } "!=" { return NEQ; } ">=" { return GEQ; } "<=" { return LEQ; }
處理Koopa如何支持邏輯運算:一個邏輯表達式a||b等價於!(a==0)|!(b==0),用類似這樣的手法去處理即可
需要說明的是,我們要嚴格按照EBNF的說明設計AST,因為這里有一個運算順序的問題,如果AST設計的不好那么運算順序會出現錯誤!
可以簡單思考一下文檔里給出的運算順序問題:我們認為加法表達式中的一部分可以是一個乘法表達式,這樣就意味着對於加法和乘法同時存在的表達式,我們會將其解釋成一個加法表達式,參與運算的一個分量是一個乘法而非一個運算分量為加法的乘法表達式,這樣才能保證運算順序的合理性。
同樣,由於括號是最基本的primaryexp,因此括號的優先級會保證為最高,這樣就解決了優先級的問題。
然后是RISCV如何支持大於等於和小於等於:我們只需判斷是否大於(小於)和是否等於,然后二者或起來即可
在這里解析時,我們會發現取出的語句類型不再是KOOPA_RVT_RETURN了,而是KOOPA_RVT_BINARY,因此我們要查看koopa.h,尋找KOOPA_RVT_BINARY相關的操作
而從KOOPA生成RISCV時,需要注意到所有的表達式都只支持寄存器操作,因此我們要首先找到兩個操作數的寄存器,我們使用這樣的函數:
void slice_value(koopa_raw_value_t l,koopa_raw_value_t r,int &lreg,int &rreg,int &noww) { if(l->kind.tag==KOOPA_RVT_INTEGER) { if(l->kind.data.integer.value==0) { lreg=-1; }else { cout<<" li t"<<noww++<<","<<l->kind.data.integer.value<<endl; lreg=noww-1; } }else lreg=M[(ull)l]; if(r->kind.tag==KOOPA_RVT_INTEGER) { if(r->kind.data.integer.value==0)rreg=-1; else { cout<<" li t"<<noww++<<","<<l->kind.data.integer.value<<endl; rreg=noww-1; } }else rreg=M[(ull)r]; }
這個函數的邏輯是如果操作數是一個integer,我們就把其移到一個寄存器里,而如果操作數是一個運算表達式,我們就找到存儲其結果的寄存器參與下面的運算,最后得到兩個操作數所在的寄存器lreg和rreg
而特別地,我們知道寄存器x0總是存儲着0,所以我們沒必要把零加載到一個寄存器里,在輸出寄存器時我們這樣寫:
void print(int lreg,int rreg) { if(lreg==-1) cout<<"x0"; else cout<<"t"<<lreg; if(rreg==-1) cout<<", x0"<<endl; else cout<<", t"<<rreg<<endl; }
而進行完運算后,我們再把結果存儲到一個寄存器里,就像這樣:
else if(exp.op==7)//減法 { slice_value(l, r, lreg, rreg, noww); cout<<" sub t"<<noww++<<", "; print(lreg,rreg); M[(ull)value]=noww-1; }
這里使用一個unordered_map將一條指令與存儲其結果的寄存器對應起來,注意我們假設寄存器數量充足,因此我們只需一直增加寄存器的標號即可。
lv4:
現在我們要支持變量了!
我們還是從lv4.1開始,考慮常量的定義
那么由於多了一個關鍵字,我們首先應該在.l文件里加入一個
"const" { return CONST; }
然后觀察一下EBNF,有:
Decl ::= ConstDecl; ConstDecl ::= "const" BType ConstDef {"," ConstDef} ";"; BType ::= "int"; ConstDef ::= IDENT "=" ConstInitVal; ConstInitVal ::= ConstExp; Block ::= "{" {BlockItem} "}"; BlockItem ::= Decl | Stmt; LVal ::= IDENT; PrimaryExp ::= "(" Exp ")" | LVal | Number; ConstExp ::= Exp;
我們的main函數里終於有除了return以外的第二條語句,所以我們要首先修改一下塊里面的內容:
BlockItem : MulBlockItem{ auto ast=new BlockItem(); ast->block_item=unique_ptr<BaseAST>($1); $$=ast; }|SinBlockItem{ auto ast=new BlockItem(); ast->block_item=unique_ptr<BaseAST>($1); $$=ast; } ;
即一個塊里可以有一條語句或很多條語句。
而對於一條語句,這里只可能是一條指令或者一個聲明,因此:
SinBlockItem : Stmt { auto ast=new SinBlockItem(); ast->sin_block_item=unique_ptr<BaseAST>($1); $$=ast; }|Decl { auto ast=new SinBlockItem(); ast->sin_block_item=unique_ptr<BaseAST>($1); $$=ast; } ;
對於很多條語句,一定以一條語句開頭,於是:
MulBlockItem : SinBlockItem BlockItem{ auto ast=new MulBlockItem(); ast->sin_item=unique_ptr<BaseAST>($1); ast->mul_item=unique_ptr<BaseAST>($2); $$=ast; } ;
而一條語句可以是一個return語句或者一個賦值語句(當然lv4.1沒有賦值,因為全是常量),因此我們這樣寫:
Stmt : RETURN Exp ';'{ auto ast=new Stmt(); ast->exp= unique_ptr<BaseAST>($2); ast->typ=0; $$=ast; }|LeVal '=' Exp ';'{ auto ast=new Stmt(); ast->lval=unique_ptr<BaseAST>($1); ast->exp=unique_ptr<BaseAST>($3); ast->typ=1; $$=ast; } ;
一個聲明可以是一個常量聲明或一個變量聲明(當然lv4.1沒有變量),因此:
Decl : ConstDecl { auto ast=new Decl(); ast->decl=unique_ptr<BaseAST>($1); $$=ast; }|VarDecl { auto ast=new Decl(); ast->decl=unique_ptr<BaseAST>($1); $$=ast; } ;
前面東西都很簡單,但是...這個重復怎么解決啊!
這個重復要求我們能夠識別這樣的東西:
const int a=1,b=2,c=3,d=4;//even more
文檔建議我們使用vector之類的結構來處理,這當然也可行,但是我起初沒有找到合理的方法,所以...
我采用了另一種更詭異的方法:首先我們把整個ConstDecl解釋成一個大的整體,對這個整體而言,其有兩種可能:有一個聲明和有兩個或兩個以上個聲明
那么也就是這樣:
ConstDecl : CONST INT MulConstDef ';'{ auto ast=new ConstDecl(); ast->const_decl=unique_ptr<BaseAST>($3); $$=ast; }|CONST INT ConstDef ';' { auto ast=new ConstDecl(); ast->const_decl=unique_ptr<BaseAST>($3); $$=ast; } ;
而對於只有一個聲明的情況,是很簡單的:
ConstDef : IDENT '=' ConstInitVal{ auto ast=new ConstDef(); ast->IDENT=*unique_ptr<string>($1); ast->const_init_val=unique_ptr<BaseAST>($3); $$=ast; } ;
但是對於有多個聲明的情況,其一定會以一個常量聲明為開頭,后面跟上其他的常量聲明,也就是:
MulConstDef : ConstDef MulConstDecl{ auto ast=new MulConstDef(); ast->const_def=unique_ptr<BaseAST>($1); ast->mul_const_dcl=unique_ptr<BaseAST>($2); $$=ast; }
;
而后面跟着的常量聲明,一定以一個逗號開頭,后面跟着一個或多個常量聲明,這樣就是一個遞歸的過程:
MulConstDecl : ',' MulConstDef{ auto ast=new MulConstDecl(); ast->mul_const_def=unique_ptr<BaseAST>($2); $$=ast; }|',' ConstDef { auto ast=new MulConstDecl(); ast->mul_const_def=unique_ptr<BaseAST>($2); $$=ast; } ;
這樣我們就識別出了重復的常量,而我們后面要解釋類似的東西也都使用這樣的方法。
接下來我們要解決兩個問題:
第一,我們如何“識別”一個符號?
我們需要構建一個叫“符號表”的結構,用來存儲我們定義的符號名、其值(如果是常量)、其類型等等,而這里我們推薦使用C++的unordered_map,因為這個結構不內置排序,所以效率相較於map要高一些。
比如我們構造一個這樣的unordered_map:
static std::unordered_map<std::string, int> const_val;
這個表把一個常量名映射到其值。
第二,如何在編譯期實現對常量的求值?
首先我們要解決的是表達式里多了一個Lval,這個非終結符識別了一個符號名,在常量聲明中這個符號只會對應一個前面已經聲明好的常量,因此我們只需要用上述符號表返回這個常量的值即可。
受到這里的啟發,我們在所有的表達式相關類里加入一個函數int Calc(),這個函數可以計算后面表達式的值,而計算方法則是很顯然的,以加法表達式為例:
class MuladdExp:public BaseAST { public: std::unique_ptr<BaseAST> ad_exp; char op; std::unique_ptr<BaseAST> mult_exp; void Dump()const override { //Dump函數的內容 } int Calc() const override { if(op=='+') return (ad_exp->Calc())+(mult_exp->Calc()); else return (ad_exp->Calc())-(mult_exp->Calc()); } };
直接用遞歸方法逐個計算表達式的值即可。
綜上所述,聲明一個常量時不需要真正輸出什么東西,但是需要在后台計算好常量的值並保存好其類型,於是有:
class ConstDef:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> const_init_val; int Calc()const override { const_val[IDENT]=const_init_val->Calc(); return const_val[IDENT]; } void Dump()const override { var_type[IDENT]=0; Calc(); //std::cout<<IDENT<<std::endl; //const_init_val->Dump(); } };
而對於多個聲明,簡單地逐個處理即可:
class MulConstDef:public BaseAST { public: std::unique_ptr<BaseAST> const_def; std::unique_ptr<BaseAST> mul_const_dcl; void Dump()const override { const_def->Dump(); mul_const_dcl->Dump(); } int Calc() const override{ return 3; } };
由於編譯器認為常量和一個數值沒有區別,因此生成Koopa和RISCV都與之前沒有區別,具體地,比如我們有下面的程序:
int main() { const int a=0; return a; }
那我們希望生成的Koopa是
fun @main(): i32 { %entry: ret 0 }
但是實際上我們會把return后面的a解釋成一個Exp,因此我們在Dump的時候對於一個常量我們要直接輸出其值,當然為了一致性,我們輸出的實際上是其值加0這樣一個表達式,即:
class Lval:public BaseAST { public: std::string IDENT; void Dump()const override { std::cout<<"%"<<nowww<<"= add "<<"0 ,"<<const_val[IDENT]<<std::endl; nowww++; } int Calc() const override { //Calc相關內容 } };
這樣我們就處理好了常量。
接下來我們考察如何處理變量:
對於變量,和常量的區別只是在於變量可以沒有初值以及變量可以被重新賦值。
事實上我們對於常量的認識就是一個數,它實際上就是其值的另一個名字,我們不需要真正在程序的內存中保留一個地方來存儲這個常量對應的值,我們只需要在編譯時記住這個常量的值然后每次調用時直接用其值去替換這個常量名字即可。
但是對於變量而言,其值是可以隨着程序的運行而改變的,因此我們需要在程序中為變量分配一個內存地址用來存儲這個變量的值,這也是我們編譯器要做的工作。
識別變量的方法其實和識別常量的方法是類似的,區別在於變量不需要能夠計算出(實際上也可能不能計算出)初值,同時變量可能沒有初值!
那么為了識別所有的情況,我們這樣寫:一個變量的聲明一定是int開頭后跟一些變量名和(可能有的)初值,那么:
VarDecl : INT VarDef ';' { auto ast=new VarDecl(); ast->var_decl=unique_ptr<BaseAST>($2); $$=ast; } ;
而一個變量定義可能包含單個變量或多個變量,也就是:
VarDef : SinVarDef { auto ast=new VarDef(); ast->var_def=unique_ptr<BaseAST>($1); $$=ast; }|MulVarDef { auto ast=new VarDef(); ast->var_def=unique_ptr<BaseAST>($1); $$=ast; } ;
單個變量的定義可能只聲明了變量名或同時聲明了變量名和初值,因此:
SinVarDef : SinVarName { auto ast=new SinVarDef(); ast->sin_var_def=unique_ptr<BaseAST>($1); $$=ast; }|MulVarName { auto ast=new SinVarDef(); ast->sin_var_def=unique_ptr<BaseAST>($1); $$=ast; } ;
如果只聲明了變量名的話:
SinVarName : IDENT { auto ast=new SinVarName(); ast->IDENT=*unique_ptr<string>($1); $$=ast; } ;
而如果同時聲明了變量名和初值,我們知道初值一定是一個表達式,因此:
MulVarName : IDENT '=' InitVal { auto ast=new MulVarName(); ast->IDENT=*unique_ptr<string>($1); ast->init_val=unique_ptr<BaseAST>($3); $$=ast; } ; InitVal : Exp { auto ast=new InitVal(); ast->init_exp=unique_ptr<BaseAST>($1); $$=ast; } ;
最后,對於一次性聲明了多個變量的情況:
MulVarDef : SinVarDef ',' VarDef { auto ast=new MulVarDef(); ast->sin_var=unique_ptr<BaseAST>($1); ast->mul_var=unique_ptr<BaseAST>($3); $$=ast; } ;
而變量定義的Koopa怎么生成呢?上面說過一個變量是需要在內存中有自己的位置的,因此這里需要配合alloc,load,store語句使用。
在聲明一個變量時,我們要alloc一個東西:
class SinVarName:public BaseAST { public: std::string IDENT; void Dump()const override { std::cout<<"@"<<IDENT<<" = alloc i32"<<std::endl; var_type[IDENT]=1; const_val[IDENT]=0; } int Calc()const override { return 0; } };
即alloc一個32位int
而如果這個變量有初值,我們就要計算出這個初值然后存儲到這個變量里,也即:
class MulVarName:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> init_val; void Dump()const override { std::cout<<"@"<<IDENT<<" = alloc i32"<<std::endl; //const_val[IDENT]=init_val->Calc(); var_type[IDENT]=1; init_val->Dump(); std::cout<<"store %"<<nowww-1<<", @"<<IDENT<<std::endl; } int Calc()const override { return const_val[IDENT]; } };
注意這里的定義同樣維護了一個符號表:
static std::unordered_map<std::string, int> var_type;//確定某一變量的類型(常量 or 變量?)
這樣就解決了定義的問題,但是這里同樣需要支持變量的賦值。
而變量的賦值意味着左邊是一個變量名,右邊是一個表達式,於是:
Stmt : RETURN Exp ';'{ auto ast=new Stmt(); ast->exp= unique_ptr<BaseAST>($2); ast->typ=0; $$=ast; }|LeVal '=' Exp ';'{ auto ast=new Stmt(); ast->lval=unique_ptr<BaseAST>($1); ast->exp=unique_ptr<BaseAST>($3); ast->typ=1; $$=ast; } ;
這里使用的非終結符是LeVal而並不是Lval,因為我們注意到等號左邊和右邊出現的變量的行為是不一樣的,我們要把等號右邊的變量的值加載出來好參與運算,而要把運算的結果存儲到等號左邊的變量里,因此我們要對二者有所區分,其中右側需要把變量的值加載出來,需要用到load指令,也就是:
class Lval:public BaseAST { public: std::string IDENT; void Dump()const override { if(var_type[IDENT]==0) std::cout<<"%"<<nowww<<"= add "<<"0 ,"<<const_val[IDENT]<<std::endl; else std::cout<<"%"<<nowww<<"= load "<<"@"<<IDENT<<std::endl; nowww++; } int Calc() const override { return const_val[IDENT]; } };
如上述代碼所示:當一個符號出現在等號右側時,我們要將其值加載到一個臨時符號里面以便於參與后續的運算。
(這里隱含了一種代碼生成的假設:由於我們只處理所有合法的sysy程序,因此如果我需要調用Lval的Calc函數,說明此時的符號一定是一個常量,因此我可以直接在const_val中去尋找而不必擔憂出現錯誤)
class LeVal:public BaseAST { public: std::string IDENT; void Dump()const override { //assert(var_type[IDENT]!=0); std::cout<<"store %"<<nowww-1<<", @"<<IDENT<<std::endl; } int Calc()const override { return 0; } };
但是對於等號左邊的變量,我們只需要把運算結果存在其中就可以了,這樣還是用到store指令。
同時,對於一條語句,現在有了兩種可能性,於是我們應當這樣處理:
class Stmt:public BaseAST { public: std::unique_ptr<BaseAST> lval; std::unique_ptr<BaseAST> exp; int typ; void Dump() const override { if(typ==0)//return { exp->Dump(); std::cout << "ret " <<'%'<<nowww-1<<std::endl; }else //賦值 { exp->Dump(); lval->Dump(); } } int Calc() const override{ return 6; } //~Stmt(){ return 0; } };
注意到賦值時我們先輸出等號右側的表達式,再把其存到等號左邊的變量里即可。
這樣我們就基本解決了常量和變量的koopa,而在生成對應riscv時,我們需要多處理的是load,store和alloc三條指令。
那么我們要解決的其實是一個問題——對於我們聲明的每個變量,我們都需要找到一個位置用於存儲這個變量的值,我們可以把這個變量的值關聯到一個寄存器上(正如我們前三個lv所做的那樣),但是隨着我們使用的變量的增加,寄存器的數量總是不夠的。
另外的問題就是寄存器本身也區分caller saved和callee saved,也就是說我們不能胡亂破壞寄存器的值,寄存器的值在破壞之前也要保存好(除非我們肯定這個值不會被復用,但一般我們都保證不了這一點。)
於是如何進行寄存器的分配就成了相當困難的一個問題,於是文檔中給出了一個解決方案——索性不做寄存器分配了罷!我們把所有需要儲存起來留給別的指令調用的值都存儲到內存里,等到需要用的時候就從內存里隨用隨讀到寄存器里就好!
當然,這樣做是相當浪費的,但是這樣做能大幅度減小工作量,因此...我們還是選擇了這種方法。
而內存是如何布局的呢?想象內存是一個巨大的數組,我們使用的部分是一個叫做運行時棧的結構,有一個棧指針寄存器sp存儲當前棧頂所在地址,棧頂向下增長(即sp減小),每個函數能使用的部分是從上一個函數的棧頂sp向下增長出來的。
因此我們每遇到一個函數,首先要把棧頂向下增長用來給這個函數分配所需要的內存空間,但是具體分配多少空間呢?
這個分配多少空間是可以計算的,但是這個計算過程暫且留到后面,對於絕大多數內容我們開一個固定大小的棧幀就夠了,這里先選擇256
for (size_t i = 0; i < raw.funcs.len; ++i) { // 正常情況下, 列表中的元素就是函數, 我們只不過是在確認這個事實 // 當然, 你也可以基於 raw slice 的 kind, 實現一個通用的處理函數 assert(raw.funcs.kind == KOOPA_RSIK_FUNCTION); // 獲取當前函數 koopa_raw_function_t func = (koopa_raw_function_t) raw.funcs.buffer[i]; cout<<" .globl "<<func->name+1<<endl; cout<<func->name+1<<":"<<endl; cout<<" addi sp, sp, -256"<<endl; //...... cout<<" addi sp, sp, 256"<<endl; }
如上述代碼所示,把棧幀向下增長了256個字節(當然,不要忘記在處理完這個函數之后把棧幀恢復了啊!)
而對於所有的結果,我們都設法將其存放在棧上,這樣以減法指令為例:我們計算了兩個數相減的結果,然后把這個結果存儲在棧上,那么我們要記錄下這條指令的結果存放在了棧中這個位置上,因此我們還需要一個unordered_map用來維護這樣的映射(即把一條指令映射到一個內存偏移量),然后修改內存偏移量用來存儲后面的結果。
else if(exp.op==7)//減法 { slice_value(l, r, lreg, rreg); cout<<" sub t2"<<", "; print(lreg,rreg); cout<<" sw t2"<<", "<<st<<"(sp)"<<endl; M[(ull)value]=st; st+=4; }
而我們怎么獲取先前保存的結果呢?我們維護了每條指令的執行結果到內存偏移量的映射,這樣我們可以直接根據指令在內存中找到其結果對應的位置,以算術表達式為例:處理算術表達式時我們需要將兩個運算分量分別放到寄存器里,那么我們就要找到其對應的內存位置然后load出來,也就是這樣:
void slice_value(koopa_raw_value_t l,koopa_raw_value_t r,int &lreg,int &rreg) { if(l->kind.tag==KOOPA_RVT_INTEGER) { if(l->kind.data.integer.value==0) { lreg=-1; }else { cout<<" li t0"<<","<<l->kind.data.integer.value<<endl; lreg=0; } }else { cout<<" lw t0, "<<M[(ull)l]<<"(sp)"<<endl; lreg=0; } if(r->kind.tag==KOOPA_RVT_INTEGER) { if(r->kind.data.integer.value==0)rreg=-1; else { cout<<" li t1"<<","<<l->kind.data.integer.value<<endl; rreg=1; } }else { cout<<" lw t1, "<<M[(ull)r]<<"(sp)"<<endl; rreg=1; } }
類似地,對於一個alloc指令,實際上就是在申請一塊內存空間用來存放一個變量,在lv4中暫時不需要對其單獨處理,我們把它和store指令放到一起去處理。
對於store指令,我們閱讀koopa.h可以發現我們其有兩個組成部分:要store進去的value和store的目的,要store進去的value可能是一個立即數,那我們就要把這個立即數讀入寄存器里,也可能是一個運算結果,而這個運算結果根據我們剛才所說一定被存放在了棧的某個位置上,那么我們就要把它讀到一個寄存器里。而要store的目的也有兩種可能,一種可能是我們已經為其分配了一個空間,那么我們同樣可以直接從映射中讀出來,而另一種可能則是還沒有為其分配空間,那我們就先為其分配一個空間即可。最后我們把值存到分配的這個空間中去即可。
void solve_store(koopa_raw_value_t value,int &st) { koopa_raw_store_t sto=value->kind.data.store; koopa_raw_value_t sto_value=sto.value; koopa_raw_value_t sto_dest=sto.dest; if(sto_value->kind.tag==KOOPA_RVT_INTEGER) { cout<<" li t0, "<<sto_value->kind.data.integer.value<<endl; }else { cout<<" lw t0, "<<M[(ull)sto_value]<<"(sp)"<<endl; } if(M.find((ull)sto_dest)==M.end()) { M[(ull)sto_dest]=st; st+=4; } cout<<" sw t0, "<<M[(ull)sto_dest]<<"(sp)"<<endl; }
而有了上面的說法之后,處理load指令就變得容易了——load指令並不需要真正意義上處理什么,只需要記錄下這條指令的結果就存儲在被加載的對象的內存位置上即可,這樣我們如果后面需要用到load出來的值我們就直接到被加載的對象的內存位置上找總是對的。
void solve_load(koopa_raw_value_t value,int &st) { M[(ull)value]=M[(ull)(value->kind.data.load.src)]; }
這樣我們就解決了lv4的問題,然后我們就進入了lv5
lv5:
lv5要求我們支持語句塊和作用域的處理,所謂語句塊大概是這樣的東西:
int main() { int a=1; { int a=2; return a;//這里返回值應為2 } }
每個大括號會新引出一個語句塊,在這個語句塊中變量是可以與語句塊外層的變量重名的
當然了,語句塊里的變量也可以直接調用語句塊外層的變量,但是不能調用與其不相交語句塊或其內層語句塊的變量,或者說一個變量的作用域就是從其被聲明開始到這個語句塊結束為止,而使用一個變量時優先使用“最深”的那個變量。(大概說明白了?)
那么我們首先就要能夠識別這些語句塊,這樣我們發現一個stmt其實可以是return、賦值、單純的運算表達式(當然這沒什么用就是了)、單純的分號(也沒什么用)和一個語句塊!
於是我們要這樣識別:
Stmt : RETURN Exp ';'{ auto ast=new Stmt(); ast->exp= unique_ptr<BaseAST>($2); ast->typ=0; $$=ast; }|LeVal '=' Exp ';'{ auto ast=new Stmt(); ast->lval=unique_ptr<BaseAST>($1); ast->exp=unique_ptr<BaseAST>($3); ast->typ=1; $$=ast; }|Block { auto ast=new Stmt(); ast->exp=unique_ptr<BaseAST>($1); ast->typ=3; $$=ast; }|Exp';'{ auto ast=new Stmt(); ast->exp=unique_ptr<BaseAST>($1); ast->typ=2; $$=ast; }|';' { auto ast=new Stmt(); ast->typ=5; $$=ast; } ;
而同樣,一個語句塊里也可以什么都沒有,於是我們要加入這樣的識別:
Block : '{' BlockItem '}' { auto ast=new Block(); ast->block = unique_ptr<BaseAST>($2); ast->typ=0; $$=ast; }|'{' '}' { auto ast=new Block(); ast->typ=1; $$=ast; } ;
接下來我們考慮如何解決重名變量的識別問題,首先我們觀察一下這些語句塊的結構:
{ { { //... } } { //... } }
我們把語句塊抽象成這個樣子,可以看到這其實是一個嵌套括號序列的結構,比如上面的結構大概是這個樣子:((())())
而這樣的括號序列實際上又是一種樹形結構,比如如果我們對這個結構編號,大概就是這樣的一棵樹:
1有兩個子節點2和4,2有一個子節點3
據此,我們可以設法維護下這個樹結構,然后用一個全局變量記錄下我們當前位於哪個語句塊中,那么如果我們離開了當前語句塊,我們一定就走向了當前語句塊的一個父節點,而如果我們新進入了一個語句塊,那么我們就為當前節點生成一個子節點即可!
(當然,在同一時刻要被使用到的實際上只會是從根節點到某個節點的一條鏈而不需要整個樹結構)
而對於一個變量的聲明,我們為其附加一個其所屬的語句塊的編號,然后在使用這個變量的時候沿着當前所在語句塊編號向根節點在符號表中查詢直到查找到定義即可。
舉個例子:
int main() { int a=1; { return a; } }
我們在定義int a=1時,我們認為當前正位於語句塊1,這樣我們在編譯器中把變量a叫做a1
而我們試圖執行return a時,我們發現自己正處於語句塊2,那么我們首先嘗試在符號表中查找a2,發現找不到,於是我們查看當前語句塊的父節點,發現是語句塊1,那么我們嘗試在符號表中查找a1,發現查到了,於是我們使用變量a1即可
這樣我們在維護的時候就要這樣維護:
class Block:public BaseAST { public: std::unique_ptr<BaseAST> block; int typ; void Dump() override { if(typ==0) { dep++; f[dep]=nowdep; nowdep=dep; block->Dump(); nowdep=f[nowdep]; } } int Calc() override{ return 10; } //~Block(){ return 0; } };
而在定義的時候要這樣定義(以有初值的變量定義為例,其余類似):
class MulVarName:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> init_val; void Dump()override { IDENT=IDENT+std::to_string(nowdep); std::cout<<"@"<<IDENT<<" = alloc i32"<<std::endl; //const_val[IDENT]=init_val->Calc(); var_type[IDENT]=1; init_val->Dump(); std::cout<<"store %"<<nowww-1<<", @"<<IDENT<<std::endl; } int Calc()override { return const_val[IDENT]; } };
在查詢的時候只需這樣查詢:
class Lval:public BaseAST { public: std::string IDENT; void Dump()override { int tempdep=nowdep; while(var_type.find(IDENT+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep]; IDENT=IDENT+std::to_string(tempdep); if(var_type[IDENT]==0) std::cout<<"%"<<nowww<<"= add "<<"0 ,"<<const_val[IDENT]<<std::endl; else std::cout<<"%"<<nowww<<"= load "<<"@"<<IDENT<<std::endl; nowww++; } int Calc() override { int tempdep=nowdep; while(var_type.find(IDENT+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep]; IDENT=IDENT+std::to_string(tempdep); return const_val[IDENT]; } };
當然,現實情況下這樣的做法必然是有問題的,舉個例子:如果我們在語句塊1中定義了一個變量a1,而在語句塊11中定義了一個變量a,這樣就會導致重名(而實際上本沒有重名!)
為了解決這個問題,我們實際上在編譯器中對於位於語句塊x中的變量a,后面會起名字COMPILER__a_x,這樣我們能清楚地找出該變量所在的語句塊(也能滿足某種顯得“專業”的惡趣味)。
這樣我們就解決了lv5的問題
lv6:
這部分我們要處理if/else,那么首先觀察一下語法規范:
Stmt ::= ... | ... | ... | "if" "(" Exp ")" Stmt ["else" Stmt] | ...;
那這里多了兩個關鍵字if和else,因此我們首先要更改詞法分析:
"if" { return IF; } "else" { return ELSE; }
如何識別呢?和上文類似地:if語句有兩種可能:單純的if和if-else配套語句。
IfStmt : SinIfStmt { auto ast=new IfStmt(); ast->if_stm=unique_ptr<BaseAST>($1); $$=ast; }|MulIfStmt { auto ast=new IfStmt(); ast->if_stm=unique_ptr<BaseAST>($1); $$=ast; } ;
單純的if就是這樣:
SinIfStmt : IF '(' Exp ')' Stmt { auto ast=new SinIfStmt(); ast->if_exp=unique_ptr<BaseAST>($3); ast->if_stmt=unique_ptr<BaseAST>($5); $$=ast; } ;
而if-else配套使用就是這樣:
MulIfStmt : IF '(' Exp ')' Stmt ELSE Stmt { auto ast=new MulIfStmt(); ast->if_exp=unique_ptr<BaseAST>($3); ast->if_stmt=unique_ptr<BaseAST>($5); ast->el_stmt=unique_ptr<BaseAST>($7); $$=ast; } ;
當然,這樣的文法是有二義性的——比如如下的代碼if exp1 stmt1 if exp2 stmt2 else stmt3
那么按照上面的文法,我們既可以解釋成第一個if是一個單獨的if,里面有一個if-else語句,也可以解釋成第一個if與后面的else匹配,而第二個if是第一個if里面的一個語句。
但是這個問題語法分析工具會自動幫我們處理成匹配最近的if-else,因此這里就不作處理了。
而接下來,我們就需要生成if對應的koopa,這里我們需要用到跳轉指令,跳轉指令格式形如br %x,label1,label2表示按照%x是否為真,若為真跳轉到label1,否則跳轉到label2
那么怎么生成呢?我們要先對exp進行求值,假設求值的結果放在了%x里,然后我們生成分支語句,接下來生成一個標號表示為真的情況,接下來輸出為真的語句,接下來生成一個標號表示為假的情況,最后輸出為假的語句。
而對於if/else配套的語句,我們在生成if為真的語句之后,不能繼續去執行if為假的語句,因此我們要在最后生成一條跳轉指令跳轉到if結束的位置,因此我們在這里還需要生成一個if結束的標號來實現跳轉。
上面的是一個基本的思想,落實到具體實現,我們要解決一個問題:我們在生成具體的代碼之前就已經生成了標號,那么我們就要保證這個標號的正確性,因此一個最直接的方法就是我們使用一套統一的標准進行標號,然后在輸出代碼時記錄好標號即可。
接下來的一個問題就是——koopa中的一個基本塊必須只能以一個ret/jump/branch語句結束,而不能有多個,因此如果出現這樣的情況:
int main() { if(1) { return 1; }else { return 2; } return 0; }
在if為真的部分中已經出現了一個ret,此時我們不應當再生成一個跳轉語句跳轉到if結束了。
再比如一個更簡單的例子:
int main() { return 0; return 1; }
我們只應當生成一個ret語句而不應當生成多個,那么怎么保證這一點呢?
其實很簡單,我們用一個計數變量記錄當前所處的基本塊,如果當前基本塊已經生成了這樣的跳轉語句,那么就不繼續生成后面的語句了,這樣還可以節約一些資源。
而目前我們認為進入main函數會生成一個新的基本塊,進入if的時候,進入else的時候,離開if/else的時候都需要新生成基本塊,然后在基本塊中如果已經生成過ret之類的指令,那么就不生成別的跳轉指令了。
class SinIfStmt:public BaseAST { public: std::unique_ptr<BaseAST> if_exp; std::unique_ptr<BaseAST> if_stmt; void Dump()override { if(be_end_bl[nowbl])return; if_cnt++; int now_if=if_cnt; if_exp->Dump(); std::cout<<"\tbr %"<<nowww-1<<", %then"<<now_if<<", %end"<<now_if<<std::endl; std::cout<<std::endl; std::cout<<"%then"<<now_if<<":"<<std::endl; bl_dep++; nowbl=bl_dep; if_stmt->Dump(); if(!be_end_bl[nowbl])std::cout<<"\tjump %end"<<now_if<<std::endl; std::cout<<std::endl; std::cout<<"%end"<<now_if<<":"<<std::endl; bl_dep++; nowbl=bl_dep; } int Calc()override { return 20; } };
對於if/else嵌套的情況,我們使用類似的方法:
class MulIfStmt:public BaseAST { public: std::unique_ptr<BaseAST> if_exp; std::unique_ptr<BaseAST> if_stmt; std::unique_ptr<BaseAST> el_stmt; void Dump()override { if(be_end_bl[nowbl])return; if_cnt++; int now_if=if_cnt; if_exp->Dump(); std::cout<<"\tbr %"<<nowww-1<<", %then"<<now_if<<", %else"<<now_if<<std::endl; std::cout<<std::endl; std::cout<<"%then"<<now_if<<":"<<std::endl; bl_dep++; nowbl=bl_dep; if_stmt->Dump(); if(!be_end_bl[nowbl])std::cout<<"\tjump %end"<<now_if<<std::endl; bl_dep++; nowbl=bl_dep; std::cout<<std::endl; std::cout<<"%else"<<now_if<<":"<<std::endl; el_stmt->Dump(); if(!be_end_bl[nowbl])std::cout<<"\tjump %end"<<now_if<<std::endl; std::cout<<std::endl; std::cout<<"%end"<<now_if<<":"<<std::endl; bl_dep++; nowbl=bl_dep; } int Calc()override { return 21; } };
同樣在生成指令的時候,我們要先檢查這個基本塊內是否已經生成過ret之類的指令,如果生成過那么就不應該再生成后面的指令了。
class SinBlockItem:public BaseAST { public: std::unique_ptr<BaseAST> sin_block_item; void Dump()override { if(be_end_bl[nowbl])return; sin_block_item->Dump(); } int Calc() override{ return 7; } };
這樣if的問題就基本解決了,而使用處理if的方法可以實現短路求值
所謂短路求值,是指在&&和||這樣的邏輯表達式中,我們先對第一個運算分量進行求值,對於&&而言,如果第一個運算分量為假,那么我們就不對第二個分量求值直接返回假,同樣對於||而言,如果第一個運算分量為真,那么我們就不對第二個運算分量求值直接返回真。
這樣做的正確性是顯然的,而且這種要求是很有用的,比如我想對一個指針求值,我可以這樣寫:
if(p&&(*p)==1) return 1; else return 0;
如果不是短路求值,上面對指針的求值可能會帶來錯誤——即使已經判定了為空指針,也會試圖對其求值。
因此我們規定短路求值是邏輯運算的要求,而在這里我們就可以用if的方法來實現,文檔中給了說明:
短路求值 lhs || rhs
本質上做了這個操作:
int result = 1; if (lhs == 0) { result = rhs != 0; } // 表達式的結果即是 result
那么我們在求值時類比這個過程即可,比如處理邏輯與就像這樣:
class MulAndExp:public BaseAST { public: std::unique_ptr<BaseAST> and_exp; std::string op; std::unique_ptr<BaseAST> e_exp; void Dump()override { and_exp->Dump(); int now1=nowww-1; int temp=nowww; std::cout<<"\t@result_"<<temp<<" = alloc i32"<<std::endl; std::cout<<"\t%"<<nowww<<"= ne 0, %"<<now1<<std::endl; std::cout<<"\tstore %"<<now1<<", @result_"<<temp<<std::endl; nowww++; if_cnt++; int now_if=if_cnt; std::cout<<"\tbr %"<<now1<<", %then"<<now_if<<", %end"<<now_if<<std::endl; std::cout<<std::endl; std::cout<<"%then"<<now_if<<":"<<std::endl; e_exp->Dump(); int now2=nowww-1; std::cout<<"\t%"<<nowww<<"= ne 0, %"<<now2<<std::endl; nowww++; std::cout<<"\tstore "<<'%'<<nowww-1<<", @result_"<<temp<<std::endl; std::cout<<"\tjump %end"<<now_if<<std::endl; std::cout<<std::endl; std::cout<<"%end"<<now_if<<":"<<std::endl; std::cout<<"\t%"<<nowww<<"= load @result_"<<temp<<std::endl; nowww++; } int Calc() override { return (and_exp->Calc())&&(e_exp->Calc()); } };
對於邏輯或也是同理的。
接下來我們來生成RISCV:生成RISCV時我們需要處理的一些別的問題
第一,我們現在會有很多個基本塊,那么我們在循環的時候就要逐個輸出這些基本塊,因此我們需要一個循環:
for (size_t j = 0; j < func->bbs.len; ++j) { assert(func->bbs.kind == KOOPA_RSIK_BASIC_BLOCK); koopa_raw_basic_block_t bb = (koopa_raw_basic_block_t)func->bbs.buffer[j]; if(bb->name)cout<<endl<<bb->name+1<<":"<<endl; for (size_t k = 0; k < bb->insts.len; ++k) { koopa_raw_value_t value = (koopa_raw_value_t)bb->insts.buffer[k]; if(value->kind.tag == KOOPA_RVT_RETURN) { solve_return(value,st,i,typ); }else if(value->kind.tag==KOOPA_RVT_BINARY) { solve_binary(value,st); }else if(value->kind.tag==KOOPA_RVT_LOAD) { solve_load(value,st); }else if(value->kind.tag==KOOPA_RVT_STORE) { solve_store(value,st,i,typ); }else if(value->kind.tag==KOOPA_RVT_BRANCH) { solve_branch(value,st); }else if(value->kind.tag==KOOPA_RVT_JUMP) { solve_jump(value,st); }//后面的部分與lv6無關 /*else if(value->kind.tag==KOOPA_RVT_CALL) { solve_call(value,st); }else if(value->kind.tag==KOOPA_RVT_GET_ELEM_PTR) { solve_get_element_ptr(value,st); }else if(value->kind.tag==KOOPA_RVT_ALLOC) { solve_alloc(value,st); }else if(value->kind.tag==KOOPA_RVT_GET_PTR) { solve_get_ptr(value,st); }*/ } }
(通過觀察koopa.h可以找到一個基本塊的名字)
接下來我們就可以處理branch語句了,在RISCV中沒有能直接按真假進行雙目標跳轉的語句,因此我們把一個branch拆成兩部分,即為真的時候跳轉到哪和為假的時候跳轉到哪。
而koopa.h中是如何看待branch語句的呢?其由三部分組成——判斷條件(是一個之前應當已經計算出結果的表達式),為真的時候要跳轉到的目標和為假的時候要跳轉到的目標。
當然了,還有一個詭異的問題——由於某種原因,在RISCV中的分支語句的跳轉范圍是很小的,因此如果分支指令和跳轉目標離得太遠,我們就無法正常跳轉
因此一個取巧的方法是——條件跳轉很短,但是無條件跳轉很長啊!因此我們先用條件跳轉完成分支的過程,然后在分支之后用無條件跳轉跳轉到真實的語句。
那么無條件跳轉語句在RISCV中怎么處理呢?其實更加容易——無條件跳轉只需要一個跳轉目標就可以跳過去了。
void solve_branch(koopa_raw_value_t value,int &st) { koopa_raw_branch_t bran=value->kind.data.branch; cout<<" li t4, "<<M[(ull)bran.cond]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t3, (t4)"<<endl; cout<<" beqz t3, "<<bran.false_bb->name+1<<bran.false_bb->name+1<<endl; cout<<" bnez t3, "<<bran.true_bb->name+1<<bran.true_bb->name+1<<endl; cout<<bran.false_bb->name+1<<bran.false_bb->name+1<<":"<<endl; cout<<" j "<<bran.false_bb->name+1<<endl; cout<<bran.true_bb->name+1<<bran.true_bb->name+1<<":"<<endl; cout<<" j "<<bran.true_bb->name+1<<endl; cout<<endl; } void solve_jump(koopa_raw_value_t value,int &st) { koopa_raw_jump_t jum=value->kind.data.jump; cout<<" j "<<jum.target->name+1<<endl; cout<<endl; }
這樣lv6所有的問題就都解決了。
lv7:
lv7要求我們能夠處理while循環以及對應的break和continue,語法規范如下:
Stmt ::= ... | ... | ... | ... | "while" "(" Exp ")" Stmt
| "break" ";"
| "continue" ";" | ...;
首先我們在詞法分析過程中增加while,break和continue關鍵字
"while" { return WHILE; } "break" { return BREAK; } "continue" { return CONTINUE; }
接下來識別while本身是很容易的:
WhileStmt : WHILE '(' Exp ')' Stmt { auto ast=new WhileStmt(); ast->while_exp=unique_ptr<BaseAST>($3); ast->while_stmt=unique_ptr<BaseAST>($5); $$=ast; } ;
而識別break和continue也是很容易的:
ConWhile : CONTINUE ';' { auto ast=new ConWhile(); ast->str="continue"; $$=ast; }|BREAK ';' { auto ast=new ConWhile(); ast->str="break"; $$=ast; } ;
而對while的處理與if很類似,無非就是在while的判斷部分我們要生成一個標號,在循環結束時和循環開始前跳轉到這個標號,同時在循環體之前和循環結束后生成一個新的標號,這樣判斷結尾就可以生成一個分支語句決定跳轉到循環循環體還是循環末尾
同樣,與if的處理相似,為了在生成循環體之前確定好標號,我們用統一的規則進行標號。
而對於break和continue的處理,break就生成一條跳轉到循環結束的標號的語句,而continue就生成一條跳轉到循環判斷的語句即可,需要注意的是break和continue的出現意味着這個基本塊已經結束,所以我們要做好標記,防止在一個基本塊里出現多個ret/branch/jump這樣的指令
另外,由於我們需要在break和continue時獲取當前循環的標號,因此我們把循環的標號設計成全局變量,但這就意味着其會隨着循環的嵌套而改變,那么為了維護這個問題我們記錄一個改變的路徑,當結束一個循環時回退到上一個循環的標號,這樣保證在break和continue時獲取的循環標號總是正確的。
class WhileStmt:public BaseAST { public: std::unique_ptr<BaseAST> while_exp; std::unique_ptr<BaseAST> while_stmt; void Dump()override { wh_cnt++; whf[wh_cnt]=now_wh; now_wh=wh_cnt; if(!be_end_bl[nowbl])std::cout<<"jump %whilecheck"<<now_wh<<std::endl; std::cout<<std::endl; std::cout<<"%whilecheck"<<now_wh<<":"<<std::endl; while_exp->Dump(); if(!be_end_bl[nowbl])std::cout<<"\tbr %"<<nowww-1<<", %whilethen"<<now_wh<<", %endwhile"<<now_wh<<std::endl; std::cout<<std::endl; std::cout<<"%whilethen"<<now_wh<<":"<<std::endl; bl_dep++; nowbl=bl_dep; while_stmt->Dump(); if(!be_end_bl[nowbl])std::cout<<"\tjump %whilecheck"<<now_wh<<std::endl; std::cout<<std::endl; std::cout<<"%endwhile"<<now_wh<<":"<<std::endl; now_wh=whf[now_wh]; bl_dep++; nowbl=bl_dep; } int Calc()override { return 22; } }; class ConWhile:public BaseAST { public: std::string str; void Dump()override { if(str=="break") { std::cout<<"\tjump %endwhile"<<now_wh<<std::endl; be_end_bl[nowbl]=1; }else { std::cout<<"\tjump %whilecheck"<<now_wh<<std::endl; be_end_bl[nowbl]=1; } } int Calc()override { return 23; } };
而lv7並沒有使用新的指令,因此RISCV不需要重新生成,這樣lv7就結束了。
lv8:
lv8要求我們能處理函數和全局變量,首先解決一下函數:
CompUnit ::= [CompUnit] FuncDef; FuncDef ::= FuncType IDENT "(" [FuncFParams] ")" Block; FuncType ::= "void" | "int"; FuncFParams ::= FuncFParam {"," FuncFParam}; FuncFParam ::= BType IDENT; UnaryExp ::= ... | IDENT "(" [FuncRParams] ")" | ...; FuncRParams ::= Exp {"," Exp};
我們發現要處理的主要是:函數的定義,函數的參數和函數的調用。
首先函數的定義是允許返回值為void的,因此我們需要先做詞法分析:
"void" { return VOID; }
接下來對於函數的識別其實main函數已經幫我們做過了,但是我們寫的識別只能識別返回值為int、沒有參數的情況,我們對其進行拓展,就能得到:
FuncDef : INT IDENT '(' ')' Block { auto ast = new FuncDef(); ast->func_type = "int"; ast->ident = *unique_ptr<string>($2); ast->block = unique_ptr<BaseAST>($5); $$ = ast; }|INT IDENT '(' FuncParas ')' Block { auto ast = new FuncDef(); ast->func_type = "int"; ast->ident = *unique_ptr<string>($2); ast->func_para=unique_ptr<BaseAST>($4); ast->block = unique_ptr<BaseAST>($6); $$ = ast; }|VOID IDENT '(' ')' Block { auto ast = new FuncDef(); ast->func_type = "void"; ast->ident = *unique_ptr<string>($2); ast->block = unique_ptr<BaseAST>($5); $$ = ast; }|VOID IDENT '(' FuncParas ')' Block { auto ast = new FuncDef(); ast->func_type = "void"; ast->ident = *unique_ptr<string>($2); ast->func_para=unique_ptr<BaseAST>($4); ast->block = unique_ptr<BaseAST>($6); $$ = ast; } ;
(這里就使用了非常簡單粗暴的分類討論來解決了)
而對於一個有參數的函數,類似我們上面的處理,我們知道其可能有一個參數或很多個參數
FuncParas : SinFuncPara { auto ast=new FuncParas(); ast->func_para=unique_ptr<BaseAST>($1); $$=ast; }|MulFuncPara { auto ast=new FuncParas(); ast->func_para=unique_ptr<BaseAST>($1); $$=ast; }|ArrayFuncPara { auto ast=new FuncParas(); ast->func_para=unique_ptr<BaseAST>($1); $$=ast; } ;
(最后一個為數組參數,留到lv9詳細說明)
而如果至右一個參數,那么一定是一個int跟着參數名,也就是:
SinFuncPara : INT IDENT { auto ast=new SinFuncPara(); ast->IDENT=*unique_ptr<string>($2); $$=ast; } ;
而如果有很多個參數,一定是以一個參數開頭,然后后面再跟上一個或一些參數,也就是:
MulFuncPara : SinFuncPara ',' FuncParas { auto ast=new MulFuncPara(); ast->sin_func_para=unique_ptr<BaseAST>($1); ast->mul_func_para=unique_ptr<BaseAST>($3); $$=ast; }|ArrayFuncPara ',' FuncParas { auto ast=new MulFuncPara(); ast->sin_func_para=unique_ptr<BaseAST>($1); ast->mul_func_para=unique_ptr<BaseAST>($3); $$=ast; } ;
(同樣暫時忽略數組參數)
這樣我們就識別好了所有的函數參數
而關於函數調用,實際上函數調用是一種表達式,因此要在Unaryexp里加入這個表達式,也就是:
UnaryExp : PrimaryExp{ auto ast=new UnaryExp(); ast->un_exp=unique_ptr<BaseAST>($1); $$=ast; }|SinExp { auto ast=new UnaryExp(); ast->un_exp=unique_ptr<BaseAST>($1); $$=ast; }|FuncExp { auto ast=new UnaryExp(); ast->un_exp=unique_ptr<BaseAST>($1); $$=ast; } ;
而函數調用有兩種可能——有參數和沒有參數,也就是這樣:
FuncExp : IDENT '(' ')'{ auto ast=new FuncExp(); ast->IDENT=*unique_ptr<string>($1); ast->typ=0; $$=ast; }|IDENT '(' CallPara ')'{ auto ast=new FuncExp(); ast->IDENT=*unique_ptr<string>($1); ast->call_para=unique_ptr<BaseAST>($3); ast->typ=1; $$=ast; } ;
而有參數的函數調用識別參數的過程與上述函數定義的過程類似,這里不再贅述。
接下來看一下全局變量:
CompUnit ::= [CompUnit] (Decl | FuncDef);
發現這里全局變量實際上和函數定義是同層的,我們這樣識別就好:
SinCompUnit : GloDecl { auto ast=new SinCompUnit(); ast->func_def=unique_ptr<BaseAST>($1); $$=ast; }|FuncDef { auto ast=new SinCompUnit(); ast->func_def=unique_ptr<BaseAST>($1); $$=ast; } ;
而全局變量是如何定義的呢?我們這樣認為:
GloDecl : Decl { auto ast=new GloDecl(); ast->glo_decl=unique_ptr<BaseAST>($1); $$=ast; } ;
(因為全局變量從一定程度上來說和普通的局部變量是類似的)
這樣我們完成了語法分析,接下來來生成koopa:
首先對於一般的函數定義,我們與main函數的定義其實是一致的,但是值得注意的是,我們要處理好函數參數的問題,文檔建議我們把所有的參數都讀出來然后存到另一個地方去而不是直接使用,這樣做能便於目標代碼的生成,因此我們遵循文檔的指示,在進入函數之后首先把所有的參數復制一份,也就是這樣:
class SinFuncPara:public BaseAST { public: std::string IDENT; void Dump()override { std::cout<<"@"<<IDENT<<":i32"; } int Calc()override { return 25; } void Show()override { std::cout<<" @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<"= alloc i32"<<std::endl; std::cout<<" store @"<<IDENT<<", @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<std::endl; var_type["COMPILER__"+IDENT+"_"+std::to_string(nowdep)]=1; } };
(實際上大概就是干了這樣的事情——我們在生成函數體之前先定義了一堆和參數重名的變量,然后把這堆參數的值存到這些變量里,以后就直接調用這些變量就行了)
接下來,一個函數名同樣是一個符號,我們要在符號表里加入其名字,同時要記錄其返回值,最后生成函數體的內容即可。
需要特別注意的是,對於void函數本身沒有返回值,因此我可以寫一個單純的return,那么我們還要在語法分析中識別這個return,同時在函數中有些需要return的地方會省略掉return,這些地方都需要我們補充好return在里面。
class FuncDef:public BaseAST { public: std::string func_type; std::string ident; std::unique_ptr<BaseAST> func_para; std::unique_ptr<BaseAST> block; void Dump() override { std::cout << "fun "; std::cout<<"@"<<ident<<"( "; if(func_para)func_para->Dump(); std::cout<<")"; if(func_type=="int")std::cout<<": i32"; std::cout << "{ "<<std::endl; std::cout<<"%entry"<<bl_dep<<":"<<std::endl; dep++; f[dep]=nowdep; nowdep=dep; if(func_para)func_para->Show(); bl_dep++; nowbl=bl_dep; func_ret[ident]=(func_type=="int"); block->Dump(); if(!be_end_bl[nowbl]) { if(func_ret[ident])std::cout<<" ret 0"<<std::endl; else std::cout<<" ret"<<std::endl; } nowdep=f[nowdep]; std::cout << "}"<<std::endl; } int Calc() override{ return 12; } };
處理完函數的定義和參數,接下來就是函數的調用,調用函數在koopa中使用的是call指令,而傳遞的參數直接放在了括號里面,那么我們在生成koopa的時候就類似地生成即可。
class FuncExp:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> call_para; int typ; void Dump()override { if(typ) { be_func_para=1; std::vector<int> paras=call_para->Para(); if(IDENT=="getint"||IDENT=="getch"||IDENT=="getarray"||func_ret[IDENT]) { std::cout<<" %"<<nowww<<"=call @"<<IDENT<<"("; nowww++; }else std::cout<<" call @"<<IDENT<<"("; for(auto it=paras.begin();it!=paras.end();it++) { if(it!=paras.begin())std::cout<<','; std::cout<<"%"<<*it; } std::cout<<')'<<std::endl; be_func_para=0; }else { if(IDENT=="getint"||IDENT=="getch"||IDENT=="getarray"||func_ret[IDENT]) { std::cout<<" %"<<nowww<<"=call @"<<IDENT<<"()"<<std::endl; nowww++; }else std::cout<<" call @"<<IDENT<<"()"<<std::endl; } } int Calc()override { return 27; } };
如上述代碼所示,在調用一個函數的時候,首先我們需要知道其是否有參數,如果其有參數的話那么要先計算出其參數,也就是這樣:
class CallPara:public BaseAST { public: std::unique_ptr<BaseAST> call_para; void Dump()override { call_para->Dump(); } int Calc()override { return 28; } std::vector<int> Para()override { return call_para->Para(); } };
這里新定義了一個Para函數,返回值為一個vector,用來存儲所有參數所在的臨時符號,利用這個函數就可以計算出所有的參數了,具體過程大致如下:
class SinCallPara:public BaseAST { public: std::unique_ptr<BaseAST>para_exp; void Dump()override { para_exp->Dump(); } int Calc()override { return 29; } std::vector<int> Para()override { std::vector<int> ret; Dump(); ret.push_back(nowww-1); return ret; } }; class MulCallPara:public BaseAST { public: std::unique_ptr<BaseAST> sin_call_para; std::unique_ptr<BaseAST> mul_call_para; void Dump()override { sin_call_para->Dump(); mul_call_para->Dump(); } int Calc()override { return 30; } std::vector<int> Para()override { std::vector <int> ret; std::vector <int> re1=sin_call_para->Para(); std::vector <int> re2=mul_call_para->Para(); for(auto it=re1.begin();it!=re1.end();it++)ret.push_back(*it); for(auto it=re2.begin();it!=re2.end();it++)ret.push_back(*it); return ret; } };
而在koopa之中,需要特意注意的就是庫函數了,我們需要在前面加上庫函數的聲明,然后在調用的時候判斷是否是庫函數,如果是庫函數就按照對應的庫函數的要求去處理即可,這里不贅述。
接下來是koopa中生成全局變量,全局的常量其實沒什么區別,重要的是全局的變量需要用到全局的分配指令,除此之外還有初值,這里初值的計算其實和const初值的計算是一樣的——這里保證初值能計算出來,因此按照文檔的說明生成即可,由於我沒有為全局變量的聲明額外設計一些AST,因此我使用了一個全局標記用來判斷當前正在聲明的變量是局部還是全局,如果是全局就按照全局的方法生成,就像這樣:
class SinVarName:public BaseAST { public: std::string IDENT; void Dump()override { if(!glo_var) { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"\t@"<<IDENT<<" = alloc i32"<<std::endl; var_type[IDENT]=1; const_val[IDENT]=0; }else { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"global @"<<IDENT<<" = alloc i32, 0"<<std::endl; var_type[IDENT]=1; const_val[IDENT]=0; } } int Calc()override { return 0; } }; class MulVarName:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> init_val; void Dump()override { if(!glo_var) { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"\t@"<<IDENT<<" = alloc i32"<<std::endl; var_type[IDENT]=1; init_val->Dump(); std::cout<<"\tstore %"<<nowww-1<<", @"<<IDENT<<std::endl; }else { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); var_type[IDENT]=1; const_val[IDENT]=init_val->Calc(); std::cout<<"global @"<<IDENT<<" = alloc i32, "<<const_val[IDENT]<<std::endl; } } int Calc()override { return const_val[IDENT]; } };
(這里特別地,全局變量在聲明后可以在任何一個地方被訪問,因此全局變量所在的語句塊應當設為0,這個0是所有語句塊的根節點)
這樣全局變量的問題也就解決了。
而在生成RISCV時,對於函數定義並沒有新的語義,但是在函數調用時,約定使用8個寄存器存儲參數,超過8個的參數則存儲在棧上,因此我們在調用函數之前要首先把這些參數處理好,就像這樣:
void solve_call(koopa_raw_value_t value,int &st) { koopa_raw_function_t func=value->kind.data.call.callee; koopa_raw_slice_t args=value->kind.data.call.args; int nowst=0; for(int i=1;i<=args.len;i++) { if(M.find((ull)(args.buffer[i-1]))!=M.end()) { if(i<=8) { cout<<" li t4, "<<M[(ull)(args.buffer[i-1])]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw a"<<i-1<<", (t4)"<<endl; }else { cout<<" li t4, "<<M[(ull)args.buffer[i-1]]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; cout<<" li t4, "<<nowst<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; nowst+=4; } } } cout<<" call "<<func->name+1<<endl; M[(ull)value]=st; st+=4; cout<<" li t4, "<<M[(ull)value]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw a0, (t4)"<<endl; }
具體來講,這些參數是怎么存儲的呢?首先前8個參數按順序存儲在a0~a7之中,那么我們就先把這些參數存儲在的棧上的位置讀出來,然后把這個位置上的東西存到對應的寄存器里即可。
而對於超過8個的參數,我們從棧頂向上逐個存儲,也即sp所在的位置存儲第9個參數、sp+4的位置存儲第10個參數...因此我們要在棧頂保留一些位置用於存儲函數調用的參數。
而如上文所說,在函數的第一步我們要把函數參數讀到局部,而讀取函數參數的過程就是與上述存儲的過程相反,我們到存儲好參數的地方讀取即可。
void solve_store(koopa_raw_value_t value,int &st,int pos,int typ) { koopa_raw_store_t sto=value->kind.data.store; koopa_raw_value_t sto_value=sto.value; koopa_raw_value_t sto_dest=sto.dest; if(sto_value->kind.tag==KOOPA_RVT_INTEGER) { cout<<" li t0, "<<sto_value->kind.data.integer.value<<endl; }else if(sto_value->kind.tag==KOOPA_RVT_FUNC_ARG_REF) { koopa_raw_func_arg_ref_t arg=sto_value->kind.data.func_arg_ref; if(arg.index<8) cout<<" mv t0, a"<<arg.index<<endl; else { if(typ)cout<<" li t4, "<<stack_size[pos]+(arg.index-8)*4<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; } }else { if(sto_value->kind.tag==KOOPA_RVT_LOAD)solve_load(sto_value,st); cout<<" li t4, "<<M[(ull)sto_value]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; } if(sto_dest->kind.tag==KOOPA_RVT_GLOBAL_ALLOC) { cout<<" la t1, "<<sto_dest->name+1<<endl; cout<<" sw t0, 0(t1)"<<endl; }else if(sto_dest->kind.tag==KOOPA_RVT_GET_ELEM_PTR) { cout<<" li t4, "<<M[(ull)sto_dest]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, (t4)"<<endl; cout<<" sw t0, 0(t1)"<<endl; }else if(sto_dest->kind.tag==KOOPA_RVT_GET_PTR) { cout<<" li t4, "<<M[(ull)sto_dest]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, (t4)"<<endl; cout<<" sw t0, 0(t1)"<<endl; }else { cout<<" li t4, "<<M[(ull)sto_dest]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; } }
最后,函數的返回值總是保存在a0寄存器中,因此我們有責任保護好這個寄存器,通常在函數調用之前把這個寄存器的值保存在這個函數棧地址最高處,函數調用后再將其恢復即可,這一點是在函數體生成之初就做的事情。
而在函數調用返回時則要恢復這個寄存器的值,因此處理return變成了這樣
void solve_return(koopa_raw_value_t value,int &st,int pos,int typ) { koopa_raw_value_t ret_value = value->kind.data.ret.value; if(!ret_value) { cout<<" li a0, 0"<<endl; if(typ)cout<<" li t4, "<<stack_size[pos]-4<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw ra, (t4)"<<endl; if(typ)cout<<" li t4, "<<stack_size[pos]<<endl; cout<<" add sp, sp, t4"<<endl; cout<<" ret"<<endl; return; }else if(ret_value->kind.tag == KOOPA_RVT_INTEGER) { int32_t int_val = ret_value->kind.data.integer.value; cout<<" li "<<"a0 , "<<int_val<<endl; if(typ)cout<<" li t4, "<<stack_size[pos]-4<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw ra, (t4)"<<endl; if(typ)cout<<" li t4, "<<stack_size[pos]<<endl; cout<<" add sp, sp, t4"<<endl; cout<<" ret"<<endl; }else { if(ret_value->kind.tag==KOOPA_RVT_LOAD)solve_load(ret_value,st); cout<<" li t4, "<<M[(ull)ret_value]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw "<<"a0 , (t4)"<<endl; if(typ)cout<<" li t4, "<<stack_size[pos]-4<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw ra, (t4)"<<endl; if(typ)cout<<" li t4, "<<stack_size[pos]<<endl; cout<<" add sp, sp, t4"<<endl; cout<<" ret"<<endl; } }
一般地,如果沒有返回值我們就認為返回0,把0放在存儲返回值的寄存器a0中即可。
這里同時展現了一點——return意味着函數的結束,因此在遇到return時我們必須生成一些恢復指令——恢復棧幀、恢復寄存器....
最后要處理的是全局變量,RISCV中的全局變量與局部變量截然不同,全局變量並不存儲在棧上,而是存儲在.data字段中,因此我們要先生成好全局變量:
if(raw.values.len) { cout<<" .data"<<endl; for(size_t i=0;i<raw.values.len;++i) { koopa_raw_value_t data=(koopa_raw_value_t)raw.values.buffer[i]; cout<<" .globl "<<data->name+1<<endl; cout<<data->name+1<<":"<<endl; if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_INTEGER) { cout<<" .word "<<data->kind.data.global_alloc.init->kind.data.integer.value<<endl; cout<<endl; }else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_AGGREGATE) { koopa_raw_value_t val=data->kind.data.global_alloc.init; //for(int i=0;i<elems.len;i++) //{ //koopa_raw_value_t val=(koopa_raw_value_t)elems.buffer[i]; int a=1; solve_global_array(val,data,a,1); //} cout<<endl; }else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_ZERO_INIT) { koopa_raw_type_t value=data->ty->data.pointer.base; int siz=4; while(value->tag==KOOPA_RTT_ARRAY) { array_size[(ull)data].push_back(value->data.array.len); siz*=value->data.array.len; value=value->data.array.base; } cout<<" .zero "<<siz<<endl; } } }
對lv8而言,這里的全局變量只有int類型的,暫時忽略后兩種情況,我們發現int情況的處理還是並不復雜的——首先在所有全局變量之前生成一個.data標號,然后對每個變量生成一個.globl加名字表示一個全局變量,最后跟一個.word(即這個變量占了一個字長的空間)和初值,這樣就解決了所有的全局變量。
而在訪問全局變量的時候,由於全局變量並不存儲在棧上,因此在load時我們要首先load address(即加載出其地址),然后再將其地址上的值存儲到棧上便於后面調用即可。
void solve_load(koopa_raw_value_t value,int &st) { if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC) { cout<<" la t0, "<<value->kind.data.load.src->name+1<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_ELEM_PTR) { cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_PTR) { cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else { if(array_size.find((ull)(value->kind.data.load.src))!=array_size.end()) array_size[(ull)value]=array_size[(ull)(value->kind.data.load.src)]; M[(ull)value]=M[(ull)(value->kind.data.load.src)]; } }
store指令與之類似,如果想修改一個全局變量的值,我們要先讀出其地址,然后將修改后的值放在其地址上即可,代碼上面已經給出,這里不再贅述。
這樣lv8就解決了。
lv9:
lv9要求我們實現對數組的支持,這部分的工作量是相當巨大的
首先,由於一維數組與多維數組沒有特別本質的區別,因此我們直接考慮多維數組。其EBNF如下:
ConstDef ::= IDENT {"[" ConstExp "]"} "=" ConstInitVal; ConstInitVal ::= ConstExp | "{" [ConstInitVal {"," ConstInitVal}] "}"; VarDef ::= IDENT {"[" ConstExp "]"} | IDENT {"[" ConstExp "]"} "=" InitVal; InitVal ::= Exp | "{" [InitVal {"," InitVal}] "}"; LVal ::= IDENT {"[" Exp "]"};
看上去就很可怕...
我們一個一個解決,首先是數組的定義,數組的定義一定是一個int后面跟着數組名,然后是數組的維度,然后是可能有的初始值,以常量數組為例:
ConstArrayDef : IDENT ArraySize '=' ConstArrayVal { auto ast=new ConstArrayDef(); ast->IDENT=*unique_ptr<string>($1); ast->siz=unique_ptr<BaseAST>($2); ast->const_array_val=unique_ptr<BaseAST>($4); $$=ast; } ;
而數組的維度可能是一維也可能是多維,每一維都是一對中括號中間夾一個常量表達式(定義必須是常量表達式),也就是:
ArraySize : '[' ConstExp ']'{ auto ast=new ArraySize(); ast->array_size=unique_ptr<BaseAST>($2); $$=ast; }|MulArraySize { auto ast=new ArraySize(); ast->array_size=unique_ptr<BaseAST>($1); $$=ast; } ; MulArraySize : '[' ConstExp ']' ArraySize{ auto ast=new MulArraySize(); ast->sin_array_size=unique_ptr<BaseAST>($2); ast->mul_array_size=unique_ptr<BaseAST>($4); $$=ast; } ;
數組參數識別好了之后,接下來就要識別數組初值,數組初值是用大括號括起來的一些表達式或更多的大括號(常量數組初值必須是常量表達式)或者空的大括號,也就是這樣:
ConstArrayVal : '{' ConstArrVal '}'{ auto ast=new ConstArrayVal(); ast->const_array_val=unique_ptr<BaseAST>($2); $$=ast; }|'{' '}' { auto ast=new ConstArrayVal(); $$=ast; } ; ConstArrVal : ConstInitVal{ auto ast=new ConstArrVal(); ast->const_arr_val=unique_ptr<BaseAST>($1); $$=ast; }|MulConArrVal { auto ast=new ConstArrVal(); ast->const_arr_val=unique_ptr<BaseAST>($1); $$=ast; }|ConstArrayVal { auto ast=new ConstArrVal(); ast->const_arr_val=unique_ptr<BaseAST>($1); $$=ast; } ; MulConArrVal : ConstInitVal ',' ConstArrVal { auto ast=new MulConArrVal(); ast->sin_con_arr_val=unique_ptr<BaseAST>($1); ast->mul_con_arr_val=unique_ptr<BaseAST>($3); $$=ast; }|ConstArrayVal ',' ConstArrVal { auto ast=new MulConArrVal(); ast->sin_con_arr_val=unique_ptr<BaseAST>($1); ast->mul_con_arr_val=unique_ptr<BaseAST>($3); $$=ast; } ;
而變量數組與常量數組的識別類似,這里不再贅述,接下來我們考察數組的訪問,數組的訪問實際上是Lval的一種,為了與別的Lval相區別,我們這樣識別:
AllLval : Lval { auto ast=new AllLval(); ast->all_lval=unique_ptr<BaseAST>($1); $$=ast; }|ArrLval { auto ast=new AllLval(); ast->all_lval=unique_ptr<BaseAST>($1); $$=ast; } ;
即Lval可能有其他的Lval和數組的Lval
而數組的Lval長什么樣子呢?一定是數組名跟着數組維度信息,也就是這樣:
ArrLval : IDENT ArrPara { auto ast=new ArrLval(); ast->IDENT=*unique_ptr<string>($1); ast->pos_exp=unique_ptr<BaseAST>($2); $$=ast; } ;
而數組的維度信息是中括號夾着表達式,也就是這樣:
ArrPara : '[' Exp ']' { auto ast=new ArrPara(); ast->arr_para=unique_ptr<BaseAST>($2); $$=ast; }|MulArrPara { auto ast=new ArrPara(); ast->arr_para=unique_ptr<BaseAST>($1); $$=ast; } ; MulArrPara : '[' Exp ']' ArrPara { auto ast=new MulArrPara(); ast->sin_arr_para=unique_ptr<BaseAST>($2); ast->mul_arr_para=unique_ptr<BaseAST>($4); $$=ast; } ;
這樣就實現了數組訪問的識別。
其實識別本身難度並不大,但是如何處理就變得很困難了,首先我們考察數組的定義
在koopa中要定義一個數組,仍然要alloc,但alloc的不再是一個int而是一個數組,怎么定義數組呢?
先考慮一維的情況,以int a[5]為例,表示5個int構成的數組,那么alloc的時候應當是@a = alloc [i32,5]
再考慮一個數組int a[3][4],這個數組的含義是3個int[4]構成的數組,也就是說我們要這樣做:@a = alloc [[i32,4],3]
那么於是我們在定義的時候首先識別出所有的維度,然后從后向前生成即可。
接下來考慮數組的初值,其實這里是最困難的地方,我們按照文檔的進行識別,首先遇到常數就認為填充在最后一維,然后按照對齊的標准處理初始化列表。
舉個例子:如果我們給出這樣的初始化列表:
int a[2][2][2]={1,2,3,4,{{5}}}
對於1,2填充好了int a[0][0][0~1]
3,4填充好了int a[0][1][0~1]
那么最后一個初始化列表{{5}}應該對應於int a[1][0~1][0~1]
而這個初始化列表中只有一個元素{5},這也是一個初始化列表,這個初始化列表應當對應於int a[1][0][0~1]
也就是說,一個初始化列表究竟初始化的是什么數組呢?是由兩部分決定的——這個初始化列表的大括號究竟在第幾層(即大括號深度)和這個初始化列表前面的對齊情況。
因此我們使用一個返回值為vector的函數來處理所有前面已經有的返回值,根據前面填充好的數量和當前大括號深度來決定這個初始化列表初始化的是哪個數組,如果當前初始化列表中的元素不夠則需要補0對齊
大概就是這樣:
class ConstArrayVal:public BaseAST { public: std::unique_ptr<BaseAST> const_array_val; void Dump()override { const_array_val->Dump(); } int Calc()override { return 36; } std::vector<int> Para()override { brace_dep++; std::vector<int> ret; int temp=filled_sum; if(const_array_val)ret=const_array_val->Para(); int siz=1; brace_dep--; int las=1,i; for(i=array_dim.size()-1;i>=0;i--) { las*=array_dim[i]; if(temp%las)break; } for(int k=std::max(i+1,brace_dep);k<array_dim.size();k++)siz*=array_dim[k]; while(ret.size()<siz)ret.push_back(0),filled_sum++; return ret; } };
這樣的話對於多個初值的情況就很容易處理了:
class MulConArrVal:public BaseAST { public: std::unique_ptr<BaseAST> sin_con_arr_val; std::unique_ptr<BaseAST> mul_con_arr_val; void Dump()override { return; } int Calc()override { return 37; } std::vector <int> Para()override { std::vector <int> ret; std::vector <int> v1=sin_con_arr_val->Para(); std::vector <int> v2=mul_con_arr_val->Para(); for(auto it=v1.begin();it!=v1.end();it++)ret.push_back((*it)); for(auto it=v2.begin();it!=v2.end();it++)ret.push_back((*it)); return ret; } };
但是對於全局的數組,我們要生成一個初始化列表,當我們已經生成好了所有初值之后,剩下的部分就容易了
總結一下,常量數組的聲明大致有如下流程——首先計算出數組每個維度的參數放在一個vector里,然后根據這個維度以及是否是全局變量生成一個alloc聲明,然后處理出所有的初值,對於全局變量而言生成一個初始化列表(對於一個空大括號作為初值的情況,生成一個zeroinit作為初值),而作為局部變量,則計算出所有初值然后把所有的初值store進去。
而怎么store呢?這其實涉及到了數組訪問問題——以一維數組為例,對於一個int a[5],我們想要訪問a[1],那么我們要怎么做呢?我們要首先獲取一個指向a[1]的指針,因此我們要先寫:
%x=getelemptr @a,1
然后我們想要讀取出a[1]的值,我們還要:
%x+1=load %x
這樣就讀出了a[1]的值,如果想要把值存進去,我們就要這樣寫:
store %n,%x
最后我們在數組符號表中存儲好數組的信息即可。
因此整體常量數組的定義大概就這樣:
class ConstArrayDef:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> siz; std::unique_ptr<BaseAST> const_array_val; std::vector<int> size; void Dump()override { size=siz->Para(); array_dim=size; filled_sum=0; if(!glo_var) { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"\t@"<<IDENT<<" = alloc "; for(auto it=size.begin();it!=size.end();it++) { std::cout<<"["; } std::cout<<"i32"; for(int it=size.size()-1;it>=0;it--) { std::cout<<", "<<size[it]<<"]"; } std::cout<<std::endl; var_type[IDENT]=2; }else { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"global @"<<IDENT<<" = alloc "; for(auto it=size.begin();it!=size.end();it++) { std::cout<<"["; } std::cout<<"i32"; for(int it=size.size()-1;it>=0;it--) { std::cout<<", "<<size[it]<<"]"; } std::cout<<", "; var_type[IDENT]=2; } if(glo_var) { if(!const_array_val) { std::cout<<"zeroinit"<<std::endl; return; } std::vector<int> con_init_val=const_array_val->Para(); auto it=con_init_val.begin(); for(int i=0;i<size.size();i++)std::cout<<"{"; std::vector<int> v; for(int i=size.size()-1;i>=0;i--) { if(i==size.size()-1)v.push_back(size[i]); else v.push_back(size[i]*v[v.size()-1]); } int p=0; while(it!=con_init_val.end()) { std::cout<<(*it); p++; int flag=0; for(int i=0;i<v.size();i++) { if(p%v[i]==0) { flag++; std::cout<<"}"; }else break; } it++; if(it!=con_init_val.end()) { std::cout<<", "; if(flag) { for(int i=0;i<flag;i++)std::cout<<"{"; } } } std::cout<<std::endl; }else { std::vector<int> con_init_val=const_array_val->Para(); int pos=0; std::vector<int> v; int rsize=1; for(auto it=size.begin();it!=size.end();it++)rsize*=(*it); for(auto it=size.begin();it!=size.end();it++)rsize/=(*it),v.push_back(rsize); std::cout<<"\t%"<<nowww<<"= add 0, 0"<<std::endl; int reg0=nowww; nowww++; for(auto it=con_init_val.begin();it!=con_init_val.end();it++,pos++) { int temp=pos; for(auto i=v.begin();i!=v.end();i++) { if(i==v.begin()) std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", "<<temp/(*i)<<std::endl; else std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", "<<temp/(*i)<<std::endl; nowww++,temp%=(*i); } if((*it)!=0) std::cout<<"\tstore %"<<(*it)<<", %"<<nowww-1<<std::endl; else std::cout<<"\tstore %"<<reg0<<", %"<<nowww-1<<std::endl; } } array_siz[IDENT]=size.size(); } int Calc()override { return 35; } };
對於變量數組,在全局的情況是一致的,但是在局部的情況我們不能保證其初始化的值是一定可以在編譯期被計算出來的,因此我們要生成計算其值的表達式,然后把這個表達式對應的標號store進去
因此,對於在局部的變量數組,我們在計算初始值時需要存進vector里的就不再是一個個真實的初始值了,而是計算每個初始值的表達式標號了。
當然,變量數組是可以沒有初值的,因此我們同樣要區分初始化了的數組和沒有初始化的數組,這里的處理和一般的變量就類似了。
因此變量數組的整個過程如下:
class SinNameVarArrDef:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> siz; std::vector<int> size; void Dump()override { size=siz->Para(); if(!glo_var) { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"\t@"<<IDENT<<" = alloc"; for(auto it=size.begin();it!=size.end();it++) { std::cout<<"["; } std::cout<<"i32"; for(int it=size.size()-1;it>=0;it--) { std::cout<<", "<<size[it]<<"]"; } std::cout<<std::endl; var_type[IDENT]=2; }else { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"global @"<<IDENT<<" = alloc "; for(auto it=size.begin();it!=size.end();it++) { std::cout<<"["; } std::cout<<"i32"; for(int it=size.size()-1;it>=0;it--) { std::cout<<", "<<size[it]<<"]"; } std::cout<<", zeroinit"<<std::endl; var_type[IDENT]=2; } array_siz[IDENT]=size.size(); } int Calc()override { return 39; } }; class MulNameVarArrDef:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> siz; std::unique_ptr<BaseAST> init_val; std::vector<int> size; void Dump()override { filled_sum=0; size=siz->Para(); array_dim=size; if(!glo_var) { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"\t@"<<IDENT<<" = alloc "; for(auto it=size.begin();it!=size.end();it++) { std::cout<<"["; } std::cout<<"i32"; for(int it=size.size()-1;it>=0;it--) { std::cout<<", "<<size[it]<<"]"; } std::cout<<std::endl; var_type[IDENT]=2; }else { IDENT="COMPILER__"+IDENT+"_"+std::to_string(nowdep); std::cout<<"global @"<<IDENT<<" = alloc "; for(auto it=size.begin();it!=size.end();it++) { std::cout<<"["; } std::cout<<"i32"; for(int it=size.size()-1;it>=0;it--) { std::cout<<", "<<size[it]<<"]"; } std::cout<<", "; var_type[IDENT]=2; } if(glo_var) { filled_sum=0; std::vector<int> con_init_val=init_val->Para(); auto it=con_init_val.begin(); for(int i=0;i<size.size();i++)std::cout<<"{"; std::vector<int> v; for(int i=size.size()-1;i>=0;i--) { if(i==size.size()-1)v.push_back(size[i]); else v.push_back(size[i]*v[v.size()-1]); } int p=0; while(it!=con_init_val.end()) { std::cout<<(*it); p++; int flag=0; for(int i=0;i<v.size();i++) { if(p%v[i]==0) { flag++; std::cout<<"}"; }else break; } it++; if(it!=con_init_val.end()) { std::cout<<", "; if(flag) { for(int i=0;i<flag;i++)std::cout<<"{"; } } } std::cout<<std::endl; }else { std::vector<int> con_init_val=init_val->Para(); int pos=0; std::vector<int> v; int rsize=1; for(auto it=size.begin();it!=size.end();it++)rsize*=(*it); for(auto it=size.begin();it!=size.end();it++)rsize/=(*it),v.push_back(rsize); std::cout<<"\t%"<<nowww<<"= add 0, 0"<<std::endl; int reg0=nowww; nowww++; for(auto it=con_init_val.begin();it!=con_init_val.end();it++,pos++) { int temp=pos; for(auto i=v.begin();i!=v.end();i++) { if(i==v.begin()) std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", "<<temp/(*i)<<std::endl; else std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", "<<temp/(*i)<<std::endl; nowww++,temp%=(*i); } if((*it)!=0) std::cout<<"\tstore %"<<(*it)<<", %"<<nowww-1<<std::endl; else std::cout<<"\tstore %"<<reg0<<", %"<<nowww-1<<std::endl; } } array_siz[IDENT]=size.size(); } int Calc()override { return 40; } };
這樣數組定義的問題就基本解決了,同時我們也解決了數組訪問的問題,這樣我們就可以完成數組參與運算的過程:
class ArrLval:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> pos_exp; void Dump()override { int tempdep=nowdep; while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep]; IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep); std::vector<int> pos=pos_exp->Para(); for(auto it=pos.begin();it!=pos.end();it++) { if(it==pos.begin()) { if(var_type[IDENT]==3) { std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl; nowww++; std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl; }else std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl; } else std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl; nowww++; } if(!be_func_para||pos.size()==array_siz[IDENT]) { std::cout<<"\t%"<<nowww<<"= load %"<<nowww-1<<std::endl; nowww++; }else { std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", 0"<<std::endl; nowww++; } } int Calc()override { return 43; } }; class ArrLeval:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> pos_exp; void Dump()override { int tempdep=nowdep; while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep]; IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep); int now=nowww-1; std::vector<int> pos=pos_exp->Para(); for(auto it=pos.begin();it!=pos.end();it++) { if(it==pos.begin()) { if(var_type[IDENT]==3) { std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl; nowww++; std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl; }else std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl; }else std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl; nowww++; } std::cout<<"\tstore %"<<now<<", %"<<nowww-1<<std::endl; nowww++; } int Calc()override { return 44; } };
其中需要注意的就是這時如何處理數組的維度信息,其實處理方式與數組的聲明是類似的:
class ArrPara:public BaseAST { public: std::unique_ptr<BaseAST> arr_para; void Dump()override { arr_para->Dump(); } int Calc()override { return 49; } std::vector<int> Para()override { return arr_para->Para(); } }; class MulArrPara:public BaseAST { public: std::unique_ptr<BaseAST> sin_arr_para; std::unique_ptr<BaseAST> mul_arr_para; void Dump()override { sin_arr_para->Dump(); mul_arr_para->Dump(); } int Calc()override { return 50; } std::vector<int> Para()override { std::vector<int> ret; std::vector<int> v1=sin_arr_para->Para(); std::vector<int> v2=mul_arr_para->Para(); for(auto it=v1.begin();it!=v1.end();it++)ret.push_back((*it)); for(auto it=v2.begin();it!=v2.end();it++)ret.push_back((*it)); return ret; } };
這樣我們就完成了基本的數組操作。
接下來放到RISCV里,同樣,我們要解決數組如何定義、如何初始化以及如何訪問
全局數組的定義其實與全局變量是一致的,只需在.data字段生成其名字即可,而最簡單的初始化方法就是讀取我們在koopa中生成的初始化列表,然后按順序逐個生成初始元素即可,同時在這一過程中我們也可以記錄下這個數組各個維度的信息
(在處理數組時有一點值得注意——由於不同的定義和訪問順序的混用,如果我們使用類似vector這樣的結構來保存數組的維度信息,那么我們一定要搞清楚vector中究竟是以什么樣的順序存放的元素信息!)
但是這樣簡單的做法其實有一點小問題——假設我在全局開了一個const int a[100000]={}的數組,那我就要生成十萬個.word 0這樣的語句
這樣做簡直是不可接受的浪費,為了避免這樣的問題,我們觀察到全局變量的初始化方法中有一個zeroinit,正如我們上文所說,我們在生成koopa時對於這樣全零的全局數組我們直接用zeroinit初始化,然后用zeroinit對應的方法去初始化即可。
那么是怎樣初始化呢?zeroinit本質是一條指令,通過在koopa.h中研究這條指令我們可以找到這個數組的情況,然后在下面生成.zero 與數組大小即可。
因此處理全局數組的初始化大致如下:
void solve_global_array(koopa_raw_value_t value,koopa_raw_value_t ori,int &flag,int dep) { if(value->kind.tag==KOOPA_RVT_INTEGER) { cout<<" .word "<<value->kind.data.integer.value<<endl; return; }else { koopa_raw_slice_t elems=value->kind.data.aggregate.elems; if(flag==dep) { array_size[(ull)ori].push_back(elems.len); flag++; } for(int i=0;i<elems.len;i++) { koopa_raw_value_t val=(koopa_raw_value_t)elems.buffer[i]; solve_global_array(val,ori,flag,dep+1); } return; } } if(raw.values.len) { cout<<" .data"<<endl; for(size_t i=0;i<raw.values.len;++i) { koopa_raw_value_t data=(koopa_raw_value_t)raw.values.buffer[i]; cout<<" .globl "<<data->name+1<<endl; cout<<data->name+1<<":"<<endl; if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_INTEGER) { cout<<" .word "<<data->kind.data.global_alloc.init->kind.data.integer.value<<endl; cout<<endl; }else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_AGGREGATE) { koopa_raw_value_t val=data->kind.data.global_alloc.init; int a=1; solve_global_array(val,data,a,1); cout<<endl; }else if(data->kind.data.global_alloc.init->kind.tag==KOOPA_RVT_ZERO_INIT) { koopa_raw_type_t value=data->ty->data.pointer.base; int siz=4; while(value->tag==KOOPA_RTT_ARRAY) { array_size[(ull)data].push_back(value->data.array.len); siz*=value->data.array.len; value=value->data.array.base; } cout<<" .zero "<<siz<<endl; } } }
而對於局部的數組分配,就不能在像之前局部變量那樣任性地等到使用的時候再去alloc了,我們要認真地alloc出來一塊區域,同時我們要維護好數組的大小,這里使用一個從指令映射到vector的map,其中vector即為這個數組的維度信息
void solve_alloc(koopa_raw_value_t value,int &st) { if(value->ty->data.pointer.base->tag==KOOPA_RTT_INT32)M[(ull)value]=st,st+=4; else if(value->ty->data.pointer.base->tag==KOOPA_RTT_ARRAY) { koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base; array_size[(ull)value].push_back(base->data.array.len); int siz=base->data.array.len*4; while(base->data.array.base->tag!=KOOPA_RTT_INT32) { base=(koopa_raw_type_kind_t*)base->data.array.base; array_size[(ull)value].push_back(base->data.array.len); siz*=base->data.array.len; } M[(ull)value]=st; st+=siz; }else if(value->ty->data.pointer.base->tag==KOOPA_RTT_POINTER) { koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base; array_size[(ull)value].push_back(1); while(base->data.array.base->tag!=KOOPA_RTT_INT32) { base=(koopa_raw_type_kind_t*)base->data.array.base; array_size[(ull)value].push_back(base->data.array.len); } M[(ull)value]=st; st+=4; } }
這樣對於數組的定義其實已經解決了,接下來只需要解決最復雜的getelemptr即可
這個指令究竟在干什么?
其實這個指令就是在尋址,只不過是過程相對復雜的尋址。
怎么說?
比如getelemptr @a,1,我們實際上想要獲得的就是指針a+1,而這個指針是怎么生成的?
那我們首先就要找到a在哪,它如果是個全局數組,我們就要直接load address,如果是個放在棧上的局部數組,那么我們就要根據我們alloc的結果找到其在棧上的什么位置。
接下來,a+1這個指針是什么?
以int a[2][3]為例,其表示一個由兩個長度為3的int數組組成的數組,因此a+1表示的是第二個長度為3的int數組!
也就是說,我們要計算出真正的偏移量,這個偏移量需要依賴於數組的信息計算,因此當我們在對數組進行getelemptr時,我們還要在全局保存一個now_array用來記錄當前處理的是哪個數組,然后按照當前數組的信息計算出偏移量,然后把基地址(即a的地址)加上這個偏移量(即+1)即是我們getelemptr出來的結果,然后按照習慣,我們把這個結果存儲到棧上。
同樣,如果是getelemptr %x,1這樣的指令,我們還是先讀出%x的值,這個值就是基地址,然后在這個基地址的基礎上去加偏移量就好。
但是這樣就又要面對一個問題了——單看這條指令,我們是根本不知道該加多少偏移量的!
但是koopa也是我們自己生成的,在生成koopa的時候我們知道我們訪問一個數組元素的getelemptr指令是連續的,因此當我們遇到第一個getelemptr指令時我們就開始記錄當前處理了前幾維,然后我們當前處理所需的偏移量就是剩下的維度了。
void solve_get_element_ptr(koopa_raw_value_t value,int &st) { koopa_raw_value_t src=value->kind.data.get_elem_ptr.src; if(src->kind.tag==KOOPA_RVT_ALLOC) { now_array=(ull)src; deep=1; cout<<" li t4, "<<M[(ull)src]<<endl; cout<<" add t0, sp, t4"<<endl; if(value->kind.data.get_elem_ptr.index->kind.tag==KOOPA_RVT_INTEGER) cout<<" li t1, "<<value->kind.data.get_elem_ptr.index->kind.data.integer.value<<endl; else { cout<<" li t4, "<<M[(ull)value->kind.data.get_elem_ptr.index]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, "<<"(t4)"<<endl; } int p=4; for(int i=deep;i<array_size[now_array].size();i++)p*=array_size[now_array][i]; cout<<" li t2, "<<p<<endl; cout<<" mul t1, t1, t2"<<endl; cout<<" add t0, t0, t1"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, "<<"(t4)"<<endl; M[(ull)value]=st; st+=4; }else if(src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC) { now_array=(ull)src; deep=1; cout<<" la t0, "<<value->kind.data.get_elem_ptr.src->name+1<<endl; if(value->kind.data.get_elem_ptr.index->kind.tag==KOOPA_RVT_INTEGER) cout<<" li t1, "<<value->kind.data.get_elem_ptr.index->kind.data.integer.value<<endl; else { cout<<" li t4, "<<M[(ull)value->kind.data.get_elem_ptr.index]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, "<<"(t4)"<<endl; } int p=4; for(int i=deep;i<array_size[now_array].size();i++) { p*=array_size[now_array][i]; } //cout<<"??"<<array_size[now_array].size()<<endl; cout<<" li t2, "<<p<<endl; cout<<" mul t1, t1, t2"<<endl; cout<<" add t0, t0, t1"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, "<<"(t4)"<<endl; M[(ull)value]=st; st+=4; }else if(src->kind.tag==KOOPA_RVT_GET_ELEM_PTR||src->kind.tag==KOOPA_RVT_GET_PTR) { cout<<" li t4, "<<M[(ull)src]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; if(value->kind.data.get_elem_ptr.index->kind.tag==KOOPA_RVT_INTEGER) cout<<" li t1, "<<value->kind.data.get_elem_ptr.index->kind.data.integer.value<<endl; else { cout<<" li t4, "<<M[(ull)value->kind.data.get_elem_ptr.index]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, "<<"(t4)"<<endl; } deep++; int p=4; for(int i=deep;i<array_size[now_array].size();i++)p*=array_size[now_array][i]; cout<<" li t2, "<<p<<endl; cout<<" mul t1, t1, t2"<<endl; cout<<" add t0, t0, t1"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, "<<"(t4)"<<endl; M[(ull)value]=st; st+=4; } }
過程大概是這樣。
這樣我們還要修改一下load指令,在取出了我們想要的元素的地址之后,我們將其load出來的過程實際就是讀出這個地址,然后取出這個地址上的值即可。
void solve_load(koopa_raw_value_t value,int &st) { if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC) { cout<<" la t0, "<<value->kind.data.load.src->name+1<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_ELEM_PTR) { cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_PTR) { cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else { if(array_size.find((ull)(value->kind.data.load.src))!=array_size.end()) array_size[(ull)value]=array_size[(ull)(value->kind.data.load.src)]; M[(ull)value]=M[(ull)(value->kind.data.load.src)]; } }
同樣,對於store指令,由於我們已經計算出了要存儲的位置的地址,因此我們直接讀出這個地址,然后把值保存進去即可。
void solve_store(koopa_raw_value_t value,int &st,int pos,int typ) { koopa_raw_store_t sto=value->kind.data.store; koopa_raw_value_t sto_value=sto.value; koopa_raw_value_t sto_dest=sto.dest; if(sto_value->kind.tag==KOOPA_RVT_INTEGER) { cout<<" li t0, "<<sto_value->kind.data.integer.value<<endl; }else if(sto_value->kind.tag==KOOPA_RVT_FUNC_ARG_REF) { koopa_raw_func_arg_ref_t arg=sto_value->kind.data.func_arg_ref; if(arg.index<8) cout<<" mv t0, a"<<arg.index<<endl; else { if(typ)cout<<" li t4, "<<stack_size[pos]+(arg.index-8)*4<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; } }else { if(sto_value->kind.tag==KOOPA_RVT_LOAD)solve_load(sto_value,st); cout<<" li t4, "<<M[(ull)sto_value]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; } if(sto_dest->kind.tag==KOOPA_RVT_GLOBAL_ALLOC) { cout<<" la t1, "<<sto_dest->name+1<<endl; cout<<" sw t0, 0(t1)"<<endl; }else if(sto_dest->kind.tag==KOOPA_RVT_GET_ELEM_PTR) { cout<<" li t4, "<<M[(ull)sto_dest]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, (t4)"<<endl; cout<<" sw t0, 0(t1)"<<endl; }else if(sto_dest->kind.tag==KOOPA_RVT_GET_PTR) { cout<<" li t4, "<<M[(ull)sto_dest]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, (t4)"<<endl; cout<<" sw t0, 0(t1)"<<endl; }else { cout<<" li t4, "<<M[(ull)sto_dest]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; } }
這樣數組的基本操作就完成了。
接下來就是必須功能的最后一步——處理數組參數。
FuncFParam ::= BType IDENT ["[" "]" {"[" ConstExp "]"}];
那首先我們就要搞清楚數組參數長什么樣,我們發現數組參數大概是長這個樣子的:int a[][2][3]
那么這到底是個什么玩意呢?
這實際上給出的是一個指針,這個指針指向了一個int [2][3]類型的數組。
因此在處理數組參數時,我們首先要搞清楚一件事——這里的數組是可以部分解引用的。
舉個例子,我們有一個數組int a[4][2][3],那么對於上面那個數組參數,我們可以把a[0],a[1]這樣的東西扔進去,a[0],a[1]代表的是指向int [2][3]類型的一個指針
那么我們還是從語法分析開始,這個玩意怎么識別呢?
其實也很容易:
ArrayFuncPara : INT IDENT ArrayParaSize { auto ast=new ArrayFuncPara(); ast->IDENT=*unique_ptr<string>($2); ast->siz=unique_ptr<BaseAST>($3); $$=ast; } ;
即int數組名加上數組維度,而數組維度長什么樣呢?
ArrayParaSize : '[' ']' { auto ast=new ArrayParaSize(); $$=ast; }|'[' ']' ArraySize{ auto ast=new ArrayParaSize(); ast->array_para_size=unique_ptr<BaseAST>($3); $$=ast; } ;
也即可以是int a[]這樣的,也可以是int a[][2][3]這樣的(即拋去第一個空的中括號,后面與聲明數組的維度識別方法是一樣的)
那么怎么處理這個數組參數呢?首先我們和普通的數組一樣搞清楚數組的維度,然后我們說數組參數本質是一個指針,那么對於int a[]這樣的參數,我們實際上就是*i32,而對於int a[][2]這樣的參數,就是*[i32,2]
同樣,我們還是要把這個參數的值保存到局部,那么局部我們alloc一個和參數類型相同的變量用於存儲即可。
class ArrayFuncPara:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> siz; std::vector<int> size; void Dump()override { size=siz->Para(); if(size.size()==0) { std::cout<<"@"<<IDENT<<": *i32"; }else { std::cout<<"@"<<IDENT<<": *"; for(int i=0;i<size.size();i++)std::cout<<"["; std::cout<<"i32, "; for(int i=size.size()-1;i>=0;i--) { std::cout<<size[i]<<"]"; if(i!=0)std::cout<<","; } } } int Calc()override { return 51; } void Show()override { std::cout<<" @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<"= alloc "; std::cout<<"*"; for(int i=0;i<size.size();i++)std::cout<<"["; if(size.size())std::cout<<"i32, "; else std::cout<<"i32"; for(int i=size.size()-1;i>=0;i--) { std::cout<<size[i]<<"]"; if(i!=0)std::cout<<","; } std::cout<<std::endl; std::cout<<" store @"<<IDENT<<", @"<<"COMPILER__"+IDENT+"_"+std::to_string(nowdep)<<std::endl; var_type["COMPILER__"+IDENT+"_"+std::to_string(nowdep)]=3; array_siz["COMPILER__"+IDENT+"_"+std::to_string(nowdep)]=size.size()+1; } };
這里特別把數組參數的類型作出特別標記與其他數組相區別,這樣數組參數的展示就完成了,而數組參數是如何被訪問的?
如果我們的數組參數是int a[],現在想訪問a[3],我們要怎么做呢?
在這里我們詳細解釋一下getelemptr到底干了什么:getelemptr的作用是對於一個類型為*[T,N]的指針,使用后會變成*T類型的指針
這是什么意思?
首先我們要清楚一點:在koopa中alloc T語句的作用是生成一個類型為*T的指針,而load *T指令的作用則是獲得類型為T的結果
也就是說,當我們生成一個數組時,如果我們寫了@a=alloc [i32,10],我們實際上生成的東西是一個類型為*[i32,10]的指針
而getelemptr干了什么呢?執行一次這個指令,我們就獲得了一個類型為*i32的指針
這樣再load一次,我們就可以獲得i32的值,這樣做是符合邏輯的。
但是,在生成數組參數的時候,事情並不是這樣的,數組參數並不把int a[]解釋成一個數組,而是解釋成一個指針,因此我們要做的是指針運算。
也就是說,這里的a的類型是*i32,如果我們對其使用getelemptr,我們就會遇到錯誤——這是直觀的,因為getelemptr處理的類型應當是*[T,N]
因此我們應當使用的是getptr,getptr干了什么呢?它會把一個*T類型的指針變成一個*T類型的指針。
這就讓人十分舒適了,現在如果我們想訪問a[3],我們直接getptr @a,3,這樣得到的還是*i32,然后把這個值load出來就得到了a[3]
看上去很美好,只是有一個細節需要注意——我們說過alloc會生成一個類型為要分配類型的指針
也就是說,如果我們寫了這樣的東西:@a = alloc *i32,我們得到的a的類型實際是**i32!
那這其實本身也很好解決——我們只需要先load一次得到的就是*i32了,然后正常像上面一樣getptr即可
而對於多維數組也是一樣的,比如對於int a[][2],首先執行@a = alloc *[i32,2],這樣得到的a是**[i32,2]類型的,然后load一次,得到的就是*[i32,2]類型的,但是當前的指針指向的是a[0]這個數組,那么如果想訪問a[1]這個數組,我們就要執行getptr @a,1,這樣得到的仍然是一個*[i32,2]類型的指針,但是指向的東西變成了另一個數組,然后我們就可以開心地按數組進行訪問了。
那么總結一下,在使用數組參數時,我們首先應該進行一個load,然后進行一個getptr,最后視情況決定是否要進行更多的getelemptr
而在進行參數傳遞時,如果我們想把數組a傳遞給int a[]為參數的函數f,那么我們非常自然地會這樣寫:f(a)
那么我們到底傳進去了個什么?我們要知道,a是一個*[i32,N],而我們要的是*i32,因此我們需要做一次getelemptr @a ,0才能獲得要傳入的參數
這里的原理簡單解釋為一個數組的數組名可以看做指向數組第一個元素的指針,而我們傳遞給函數的必須是一個指針而非數組,因此我們要先獲取這個指針,然后再傳遞給函數,對於多維數組的處理也是類似的。
那么最后,數組參與運算(包括函數調用和函數參數)的總流程如下:
class ArrLval:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> pos_exp; void Dump()override { int tempdep=nowdep; while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep]; IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep); std::vector<int> pos=pos_exp->Para(); for(auto it=pos.begin();it!=pos.end();it++) { if(it==pos.begin()) { if(var_type[IDENT]==3) { std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl; nowww++; std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl; }else std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl; } else std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl; nowww++; } if(!be_func_para||pos.size()==array_siz[IDENT]) { std::cout<<"\t%"<<nowww<<"= load %"<<nowww-1<<std::endl; nowww++; }else { std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", 0"<<std::endl; nowww++; } } int Calc()override { return 43; } }; class ArrLeval:public BaseAST { public: std::string IDENT; std::unique_ptr<BaseAST> pos_exp; void Dump()override { int tempdep=nowdep; while(var_type.find("COMPILER__"+IDENT+"_"+std::to_string(tempdep))==var_type.end())tempdep=f[tempdep]; IDENT="COMPILER__"+IDENT+"_"+std::to_string(tempdep); int now=nowww-1; std::vector<int> pos=pos_exp->Para(); for(auto it=pos.begin();it!=pos.end();it++) { if(it==pos.begin()) { if(var_type[IDENT]==3) { std::cout<<"\t%"<<nowww<<"= load @"<<IDENT<<std::endl; nowww++; std::cout<<"\t%"<<nowww<<"= getptr %"<<nowww-1<<", %"<<(*it)<<std::endl; }else std::cout<<"\t%"<<nowww<<"= getelemptr @"<<IDENT<<", %"<<(*it)<<std::endl; }else std::cout<<"\t%"<<nowww<<"= getelemptr %"<<nowww-1<<", %"<<(*it)<<std::endl; nowww++; } std::cout<<"\tstore %"<<now<<", %"<<nowww-1<<std::endl; nowww++; } int Calc()override { return 44; } };
這件事情說清楚了以后,別的事情其實就比較好說了——由於getptr和getelemptr在匯編層面的行為基本是一致的,因此直接類似getelemptr生成代碼就好,
void solve_get_ptr(koopa_raw_value_t value,int &st) { koopa_raw_value_t src=value->kind.data.get_ptr.src; now_array=(ull)src; deep=1; cout<<" li t4, "<<M[(ull)src]<<endl; cout<<" add t4, sp, t4"<<endl; cout<<" lw t0, (t4)"<<endl; if(value->kind.data.get_ptr.index->kind.tag==KOOPA_RVT_INTEGER) cout<<" li t1, "<<value->kind.data.get_ptr.index->kind.data.integer.value<<endl; else { cout<<" li t4, "<<M[(ull)value->kind.data.get_ptr.index]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t1, "<<"(t4)"<<endl; } int p=4; for(int i=deep;i<array_size[now_array].size();i++)p*=array_size[now_array][i]; cout<<" li t2, "<<p<<endl; cout<<" mul t1, t1, t2"<<endl; cout<<" add t0, t0, t1"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, "<<"(t4)"<<endl; M[(ull)value]=st; st+=4; }
另外一些細節比如alloc的此時我們alloc的東西可能是一個指針,因此我們需要為alloc添加處理指針的情況,我們可以把一個指針看做第一維為1的數組(其實這個第一維大小主要是用來填充,但是為了計算數組大小的准確性我們用1來填充),就是這樣:
void solve_alloc(koopa_raw_value_t value,int &st) { if(value->ty->data.pointer.base->tag==KOOPA_RTT_INT32)M[(ull)value]=st,st+=4; else if(value->ty->data.pointer.base->tag==KOOPA_RTT_ARRAY) { koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base; array_size[(ull)value].push_back(base->data.array.len); int siz=base->data.array.len*4; while(base->data.array.base->tag!=KOOPA_RTT_INT32) { base=(koopa_raw_type_kind_t*)base->data.array.base; array_size[(ull)value].push_back(base->data.array.len); siz*=base->data.array.len; } M[(ull)value]=st; st+=siz; }else if(value->ty->data.pointer.base->tag==KOOPA_RTT_POINTER) { koopa_raw_type_kind_t* base=(koopa_raw_type_kind_t*)value->ty->data.pointer.base; array_size[(ull)value].push_back(1); while(base->data.array.base->tag!=KOOPA_RTT_INT32) { base=(koopa_raw_type_kind_t*)base->data.array.base; array_size[(ull)value].push_back(base->data.array.len); } M[(ull)value]=st; st+=4; } }
除此之外,在這里load **[T,N]的時候,我們還要把數組大小記錄好,因為我們后面訪問數組的指令依賴的是這個load指令load出來的數組,因此我們還要維護這一點。
void solve_load(koopa_raw_value_t value,int &st) { if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GLOBAL_ALLOC) { cout<<" la t0, "<<value->kind.data.load.src->name+1<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_ELEM_PTR) { cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else if(value->kind.data.load.src->kind.tag==KOOPA_RVT_GET_PTR) { cout<<" li t4, "<<M[(ull)value->kind.data.load.src]<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" lw t0, (t4)"<<endl; cout<<" lw t0, 0(t0)"<<endl; cout<<" li t4, "<<st<<endl; cout<<" add t4, t4, sp"<<endl; cout<<" sw t0, (t4)"<<endl; M[(ull)value]=st; st+=4; }else { if(array_size.find((ull)(value->kind.data.load.src))!=array_size.end()) array_size[(ull)value]=array_size[(ull)(value->kind.data.load.src)]; M[(ull)value]=M[(ull)(value->kind.data.load.src)]; } }
這樣,這個編譯器大體的功能就完成了。
當然,單純這樣的話其實是不夠的——我們還需要處理一些棧的問題。首先,隨着我們代碼變得復雜,棧的空間會變得越來越大,但是在表達式中能出現的立即數大小是受限的,因此我們需要先將修改棧指針的立即數加載到寄存器內,然后用這個寄存器與棧指針進行計算。
除此之外,如果對所有函數使用固定大小的棧幀,那么我們就必須使用一個非常巨大的棧幀,但是這樣的話遞歸函數遞歸幾層棧空間就不夠了,因此我們必須根據實際需要生成棧幀大小,有一種非常取巧的方法解決這個問題——我們先生成一次目標代碼,這樣我們就能確定出來每個函數所需的棧幀大小,我們把這些棧幀大小存在一個vector里,然后用計算好的棧幀大小重新生成所有的目標代碼即可。
當然,這樣做是相當麻煩的,因為相當於我們執行了兩次目標代碼生成,這一點是后期可以優化的。