在獨自實現一個小型編譯器中你遇到過哪些困難?
比如說大學時期選修的編譯原理要求的大作業那種水平的
我還沒有上編譯原理課,憑興趣自己做了一個腳本語言,項目在 thinkermao/LL-Script。剛開始什麼都不懂,基本上邊學邊做,所以項目前前後後中斷過多次,中途換過多次代碼風格,加上不斷刪改,代碼風格略為鬼畜。在開發過程中遇到許多問題,其中部分解決了,部分還沒有解決,留坑以後填。下面列出我遇到了且花了點時間的問題和解決辦法。
一、SSA IR 構造第一個問題就是關於SSA IR構造,SSA的use-def鏈攜帶的信息使得分析優化部分更加容易。我在設計過程中曾經按照傳統的做法,從token-&>AST-&>linear IR-&>opcode。發現了SSA IR的優點後,決定將linear IR改為SSA IR(雖然最後也沒用上任何優化)。構造SSA方面,傳統是按照RON CYTRON論文中的方法,從linearIR構造。在把Engineering a Compiler, Second這本書中的方法瀏覽後就放棄了這種辦法。後來發現 Clang 中是從AST構造,且LLVM提供了Mem2Reg pass來簡化工作。不過因為看懂這部分代碼也不容易,然後選擇了另一種辦法---直接從源碼級構造SSA,連AST都省略了。方法參考論文:Simple and E?cient Construction of Static Single Assignment Form
Matthias Braun1, Sebastian Buchwald1, Sebastian Hack2, Roland Lei?a2, Christoph Mallon2, and Andreas Zwinkau1
在SSA IR構造中的另一個問題是如何設計SSA IR以保存use-def鏈。這裡參考了LLVM 2.0版本中的設計。基礎數據結構有 Use、Value、User,每一條 Instruction 既是 Value 又是 User,User 和 Value 通過 Use 關聯起來。這部分代碼可能是我寫的最爛的部分,設計得不好。
二、寄存器分配寄存器分配是我遇到的第二個大問題。寄存器分配方面,以前常用的是圖著色分配;後來隨著JIT的使用,出現了線性掃描方法;不過在最新的LLVM中使用的則是貪心演算法。權衡後我決定使用線性掃描,並找了相關論文,主要是參考:Linear Scan Register Allocation for the Java HotSpot? Client Compiler - Christian Wimmer
該演算法前面部分實現起來一帆風順,但是到了選擇區間Spill到棧上時,涉及到的具體實現細節一直存在一些問題(比如:Spill後在只能使用寄存器的情景下如何處理)。在想了一段時間無果後,放棄了這種辦法。
最後我用了一種相對簡單的辦法,將所有的變數全部spill到棧上,寄存器只用於保存運算過程中的臨時變數。寄存器數目固定,如果程序過於複雜導致某時刻無寄存器可用,則通知用戶把程序改簡單點^-^。三、Closure、Lambda與遞歸Closure的實現方式受到C++ std::bind 啟發,下面以偽代碼說明:let a = 10;
function bar(b) {
return a + b;
}
output(bar(1)); // 11
其中bar在實現過程中等價替換為:
define $lambda_bar_ = lambda(a, b) ....
define bar = $lambda_bar_(a);
這種方式實現的Closure對於普通變數是值拷貝,對於對象(指針)則是引用拷貝。這種方式實現簡單,但是引出了問題:如何處理遞歸(匿名函數如何調用自身)。
define $lambda_fib_ = lambda(fib, n) {
if (n &< 2) return 0;
return fib(n-1) + fib(n-2);
};
define fib = $lambda_fib_(?);
也就是說,如果簡單使用這種辦法替換,那麼給$lambda_fib_設置什麼參數?當然這種方式可以通過Y-combinator解決,不過我還是希望在代碼中能夠直接寫成常規方式。
後來我對遞歸訪問的函數做了特定的改進:define $lambda_fib_ = lambda(fib, n) {
fib = fib(fib);
if (n &< 2) return 0;
return fib(n-1) + fib(n-2);
}
define fib = $lambda_fib_($lambda_fib_);
fib(3); // ^_^
對於lambda參數中的fib,其值為$lambda_fib_,所以函數體內第一句代碼則是將fib改為正常fib。雖然這種辦法不是什麼好辦法,但總歸是能解決問題的。
四、GC
GC部分開始問題蠻大的,不過看了RednaxelaFX - 知乎寫的介紹Semi-space Swap演算法的文章後就沒什麼大問題了。如果將GC與Runtime結合起來,則坑就變得比較多了。Runtime中所有的對象必須保證分配自GC提供的Allocator,且代碼中如過引用會導致GC的代碼段,也要保證所有局部變數保存的值能夠和GC後同步。上面主要是關於實現部分的問題,實際上寫代碼本身也是一個問題。如前面提到,邊學邊寫,在不懂時是無法設計出一個良好的結構,不停的增改也會導致代碼逐漸失控,引出一些非常難定位的bug。一些為了省事簡單了事的代碼可能會在其他地方花費你多倍的精力。
最後,關於我的腳本具體實現可以參考LL-Script implements。缺少產品經理,想著寫小型編譯器,結果滿腦子在想著如何提高用戶體驗、改善報錯信息。
缺少架構師,好好寫一個 parser 不行,去搞一個 invertible parser,或者研究如何用 GADT 來 refine AST 的類型以及如何與 recursion scheme 配合這些毫無意義的技術內容。不要問我為什麼我知道……正在寫解釋器,困難大概是寫代碼之初不清楚各模塊之間的介面怎麼實現比較合適,於是過程中出現幾次架構推翻重寫。另外,我用的是c++,使用了一些面向對象特性。代碼的靈活性使得部分小函數頻繁被調用而不是內聯,這讓我很不舒服……
自己寫的parsergen生成的parser怎麼做都沒有Delphi的編譯器快,還需要繼續改進。
如果對已有spec的語言的編譯器,個人體會不多。但是有一些關於語言設計上的想法。關於某些設計上的取捨,與個人口味、也與個人水平和精力有關。比如,類型系統中動態與靜態的取捨。靜態檢查很強大,但是實現起來累,坑多。動態類型檢查省事,但是功能上弱了很多。最後的決定是,class、interface、generic用靜態檢查,高階函數用動態,編譯器內部偷懶用「auto」標記它們,否則要union一遍,處理各種遞歸麻煩。泛型。我選擇靜態檢查+動態生成的方式。靜態檢查的時候,把泛型形參當作臨時的類型,檢查所有的語句。而在運行時,泛型是把實參代入形參臨時生成的泛型類型。所以,避免了java擦除類型後無法寫&
Tokenizer實現中有個nfa pattern match的回溯,巨慢無比,後面改成用bitmap緩存輸入string中每個字元是否匹配的值,如果到尾部還不能匹配就backtrack,analysis3300行文件performance從10.4s到0.5s(中間手擼了一段彙編).
PS: c寫的c編譯器.Incarnation-p-lee/scil
早年的一個正式項目,需要從用戶手冊中推測語法並寫出沒有移進歸約衝突的BNF。
大三時做的編譯原理課設,寫了一篇博客,搬運過來如下。結尾附有源碼。一、設計任務
1.1程序實現要求
PL/0語言可以看成PASCAL語言的子集,它的編譯程序是一個編譯解釋執行系統。PL/0的目標程序為假想棧式計算機的彙編語言,與具體計算機無關。
PL/0的編譯程序和目標程序的解釋執行程序都是用JAVA語言書寫的,因此PL/0語言可在配備JDK的任何機器上實現。
其編譯過程採用一趟掃描方式,以語法分析程序為核心,詞法分析和代碼生成程序都作為一個獨立的過程,當語法分析需要讀單詞時就調用詞法分析程序,而當語法分析正確需要生成相應的目標代碼時,則調用代碼生成程序。
用表格管理程序建立變數、常量和過程標示符的說明與引用之間的信息聯繫。
用出錯處理程序對詞法和語法分析遇到的錯誤給出在源程序中出錯的位置和錯誤性質。
當源程序編譯正確時,PL/0編譯程序自動調用解釋執行程序,對目標代碼進行解釋執行,並按用戶程序的要求輸入數據和輸出運行結果。
1.2 PL/0語言的BNF描述(擴充的巴克斯範式表示法)
&
→ program &
& ]&
&
&
&
&
→ procedure & } &
&
|if &
|while &
|call &
|&
|read (&
|write (&
&
&
&
&
&
& → +|-
&
&
&
注釋:
&
:程序 ;& :分程序 ; &
1.3假想目標機的代碼
LIT 0 ,a 取常量a放入數據棧棧頂
OPR 0 ,a 執行運算,a表示執行某種運算
LOD L ,a 取變數(相對地址為a,層差為L)放到數據棧的棧頂
STO L ,a 將數據棧棧頂的內容存入變數(相對地址為a,層次差為L)
CAL L ,a 調用過程(轉子指令)(入口地址為a,層次差為L)
INT 0 ,a 數據棧棧頂指針增加a
JMP 0 ,a無條件轉移到地址為a的指令
JPC 0 ,a 條件轉移指令,轉移到地址為a的指令
RED L ,a 讀數據並存入變數(相對地址為a,層次差為L)
WRT 0 ,0 將棧頂內容輸出
代碼的具體形式:
其中:F段代表偽操作碼
L段代表調用層與說明層的層差值
A段代表位移量(相對地址)
進一步說明:
INT:為被調用的過程(包括主過程)在運行棧S中開闢數據區,這時A段為所需數據單元個數(包括三個連接數據);L段恆為0。
CAL:調用過程,這時A段為被調用過程的過程體(過程體之前一條指令)在目標程序區的入口地址。
LIT:將常量送到運行棧S的棧頂,這時A段為常量值。
LOD:將變數送到運行棧S的棧頂,這時A段為變數所在說明層中的相對位置。
STO:將運行棧S的棧頂內容送入某個變數單元中,A段為變數所在說明層中的相對位置。
JMP:無條件轉移,這時A段為轉向地址(目標程序)。
JPC:條件轉移,當運行棧S的棧頂的布爾值為假(0)時,則轉向A段所指目標程序地址;否則順序執行。
OPR:關係或算術運算,A段指明具體運算,例如A=2代表算術運算「+」;A=12代表關係運算「&>」等等。運算對象取自運行棧S的棧頂及次棧頂。
1.4假想機的結構
兩個存儲器:存儲器CODE,用來存放P的代碼數據存儲器STACK(棧)用來動態分配數據空間
四個寄存器:
一個指令寄存器I:存放當前要執行的代碼
一個棧頂指示器寄存器T:指向數據棧STACK的棧頂
一個基地址寄存器B:存放當前運行過程的數據區在STACK中的起始地址
一個程序地址寄存器P:存放下一條要執行的指令地址該假想機沒有供運算用的寄存器。所有運算都要在數據棧STACK的棧頂兩個單元之間進行,並用運算結果取代原來的兩個運算對象而保留在棧頂
1.5活動記錄:
RA:返回地址
SL:保存該過程直接外層的活動記錄首地址
DL:調用者的活動記錄首地址
過程返回可以看成是執行一個特殊的OPR運算
注意:層次差為調用層次與定義層次的差值
二、功能結構設計2.1綜述
本PL/0編譯器共包括詞法分析、語法分析、語義分析(包括符號表管理和目標代碼生成)、活動記錄的組織(解釋執行程序)、錯誤處理四大部分組成。
2.2詳細說明
2.2.1詞法分析
根據所給的PL/0語言的BNF描述,該語言的組成單詞包括以下元素:
關鍵字(程序保留字){program,const,var,procedure,begin,end,if,else,then,call,while,do,read,write}
運算符:{+,-,*,/,odd}
界符:{「,」,「;」,「(」,「)」}
關係運算符:{=,&<,&<=,&>,&>=,&<&>}
數字:只能為整型,且常量不可以是負數
標識符:由用戶定義,以字母開頭,由數字和字母組成
詞法分析程序讀取源文件,識別出上述關鍵字、界符、關係運算符、數字、標識符五種元素,輸出到lex.txt文件中,供後續的語法分析程序使用。其中,
(1)除標識符外,剩餘的每一種字元可以用數字表示;
(2)而標識符則統一用一個數字表示其為標識符,再將標識符本身存儲起來;
(3)數字的存儲和標識符存儲類似。
例如:
將關鍵字、界符、關係運算符共29中符號從1到29依次編號,1表示關鍵字program,2表示關鍵字const,以此類推,到29表示關係運算符&<&>,
為了和下面標識符、數字的存儲統一起來,將識別出的關鍵字以如下形式
保存在lex.txt文件中。如program則存儲為(1,-)。
用30表示是標識符,如定義了一個變數v1,則其存儲為(30,v1)。
用31表示是數字,如12,其存儲為(31,12)。
2.2.2語法分析
語法分析結合BNF產生式,利用遞歸下降的方法實現。具體實現方式是,為每一個非終結符編寫一個子程序,以&
→ program &
void prog(){
if(當前指向的字元==program){
指向下一個字元;
if(當前指向的字元==id){ //是標識符
指向下一個字元;
if(當前指向的字元==;){
block();
}else{
error;
}
}
}else{
error
}
}
說明:由於之前的詞法分析使用數字來描述的,故此處的program,id(即標識符),分號等都可以用相應的數字代替。
2.2.3語義分析
語義分析在語法分析的基礎上完成,涉及到的操作有符號表的管理和目標代碼的生成,分別對應說明語句和處理語句,下面分開來說。
(1)符號表管理
符號表中存儲以下數據:定義的變數、定義的常量、定義的過程;
定義的變數需要存儲:變數的標識符,變數定義所在層次,相對於該層次基地址的偏移量(對於基地址將在後面活動記錄中詳細說明)
定義的常量需要存儲:常量的標識符,常量的值,定義所在層次
定義的過程需要存儲:過程名,過程處理語句的開始地址(處理語句不是說明語句,說明語句中涉及到符號表的操作,而處理語句中涉及到產生目標代碼的操作),過程定義所在層次;
具體操作用偽碼描述:
a.常量說明的翻譯:
void condecl(){
if(當前指向的字元==const){
指向下一個字元;
const();
while(當前指向的字元==,){
指向下一個字元;
const();
}
}
}
void const(){
if(當前指向的字元==id){
指向下一個字元;
用name記錄下字元;
if(當前指向的字元== := ){
指向下一個字元;
if(當前指向的字元==數字){
用value記錄下值;
enter(name,value,level,addr);//將其記錄到符號表
}
}
}
}
b.變數說明的翻譯:
void vardecl(){
if(當前指向的字元==id){
用name記錄下字元;
enter(name,level,addr);
指向下一個字元;
while(當前指向的字元==,){
指向下一個字元;
if(當前指向的字元==id){
用name記錄下字元;
enter(name,level,addr);
指向下一個字元;
}
}
}
}
c.過程說明的翻譯:
proc的登錄符號表的操作與上述方法類似,不再贅述。
d.補充
但是上面我們並沒有涉及如何求得層次差和偏移量,獲得層次差和偏移量的思路大體如下,在整個語法分析函數中,定義兩個全局變數level和address,level初始化為1,其中主程序是第一層,每次遇到過程嵌套定義就將level加1,過程定義結束後再將其減1,這樣下來就實現了獲得說明語句所在層次的功能;address初始化為0,每次在符號表中登錄變數後就將address加1,其實address的作用是,結合level,可以獲得每一層有幾個變數,這樣在活動記錄的分配(也叫運行時存儲空間的劃分,運行時只為變數事先劃分存儲空間),可以根據該層變數的個數劃分相應的存儲空間。
(2)目標代碼的產生
這部分涉及到翻譯模式相關的知識,由於說明語句是不產生目標代碼的,在結合PL/0的BNF描述,會產生目標代碼的只有&
a.&
分析可以發現,主程序除去program&
l 在進入block的第一步就先產生一個無條件跳轉指令,由於中間子過程嵌套還會產生目標代碼 ,導致不知道跳轉到哪裡才是主程序的入口,這裡我們先記下這個無條件跳轉指令在目標代碼中的位置,當程序分析完變數說明、常量說明和過程說明即將進入語句處理的分析時,這時目標代碼的地址就是主程序的入口,我們根據剛才記下的無條件跳轉指令的位置將這個地址回填到跳轉指令的目標地址上,從而目標代碼第一句就是跳轉到主程序入口處的指令,對於程序中嵌套程序,用這個方法依然是可行的;
l 除此之外,在進行其他任何分析之前,還要記錄下主程序或者當前過程的數據量,即符號表中的變數數目,也即address,以便後面活動記錄的劃分可以在結束過程後返回到原來的數據棧的位置;
l 另外,如果是從過程進入到block,在產生過程調用指令需要用到這個入口地址,所以這個過程入口地址也要記錄下來,用來填寫過程調用指令的目標位置。則在進行其他任何分析之前,此時符號表中最新的一項必定是該過程的相關信息,我們可以將本過程的value值設為其入口地址,因為value值對於符號表procedure來說是無用的,即將開始地址登入到符號表,從而在做任何分析前我們也要記錄下符號表中的最新項(記錄其序號),在經過變數說明分析、常量說明分析、過程分析後即將進入語句處理分析時,便得到了該過程的入口地址,將其登錄到剛剛記錄的那個符號表最新項的value域;
可以通過判斷符號表中的最新項的序號是否為0來判斷是從過程進入&
進行完上述三步操作後即可進入語句的處理部分;
在語句的處理部分完成後,生成目標代碼的退出過程或主程序的語句,然後恢復address和符號表的序號。
b.&
此部分不直接產生目標代碼
c.&
&
l 賦值語句:此部分涉及到活動記錄的組織,先大致說一下:本目標機是哈佛架構,數據存儲器和代碼存儲器是分開的,這裡活動記錄的組織或者說運行時存儲空間的組織是指數據存儲器的組織,(代碼存儲器不需要什麼組織,目標代碼順序存放,解釋程序按目標代碼的意義進行相關控制流的操作等),對於每一個過程或主程序,都要開闢3+變數個數的存儲空間,其中3個是用來存放SL,DL,RA的,這裡我們遇到一個變數就生成STO L,A目標代碼,其中L和A據可由符號表中存儲的相關信息獲得,意思是將此時數據棧棧頂的元素,通過層差L(調用位置和定義位置之差)和偏移地址A找到這個變數在其定義的過程開闢的空間中為該變數開闢的存儲空間,然後將棧頂元素存到這個位置,具體的生成看代碼;
l if語句:用偽碼描述
if &
產生條件轉移指令「JPC,0,0」;
用記錄cx1上面那條條件轉移指令的位置;
then
&
產生無條件轉移指令「JMP,0,0」;
用cx2記錄上面那條無條件轉移指令的位置;
cx3為即將產生的目標代碼的位置;
用cx3回填cx1和cx2記錄位置的指令
else
&
cx4為即將產生的目標代碼的位置;
用cx4回填cx2記錄位置的指令
l while語句:
while&
產生田間轉移指令「JPC,0,0」
用cx1記錄下上面那條條件轉移指令的位置
&
產生無條件轉移指令「JMP,0,0」
cx2為即將產生的目標代碼的位置
用cx2回填cx1記錄位置的指令
l call語句:
產生「CAL,L,A」,意思是通過層差L和過程入口地址A,找到過程的入口地址,這裡A可以有該過程在符號表中的value域的值獲得(原因見&
l read語句:
首先從命令行獲取一個值放在棧頂,然後將棧頂值賦值給相應變數(賦值過程見前面賦值語句的翻譯)
l write語句:
直接輸出棧頂值
其他運算的翻譯較為簡單,不再贅述.
2.2.4活動記錄的組織
此部分主要是活動記錄的組織,具體如下:
為每一個過程開闢的新的存儲空間,最低層三個用來存儲SL:保存該過程直接外層的活動首地址;DL:調用者的活動記錄首地址;RA:返回地址;
具體的執行方式見代碼注釋。
2.2.5難點說明
l 變數(常數、過程)作用域的確定,分為兩步:一步是定義變數時,我們要查詢在本層有無同名的變數已經定義,若有,則報錯;二步是變數使用時,首先查詢本層有無定義該變數,若有,則使用本層定義的變數,若無,則尋找本過程直接外層有無定義同名變數,若仍無此變數,則繼續向下一個直接外層尋找,以此類推。若有,則使用最靠近本層的那個定義,若無,則報錯。由於本符號表採用的是數組管理,在實現第一步時,在變數(常數、過程)登錄符號表時記錄其所在的層次和所在層次的名字,若符號表中存在變數(常量、過程)的名字、所在層次和所在層次的名字均相同,則報錯,若無,則將定義的變數加到符號表中。在實現第二步時,結合語法的特點分析,首先在本調用過程所在的過程進行查找有無定義該變數,若有,則就使用本過程的變數,若無,則根據前面在符號表中記錄的本調用過程所在的直接外層的名字查找該直接外層有無定義此變數,以此類推,若最終有定義,則使用該變數,否則保存。
l 過程傳遞參數:我把在過程定義時的形式參數,當作在本過程定義變數進行處理,在調用過程中出傳入的實在參數當作對相應的形式參數進行賦值,具體說來就是用該過程在符號表中的size域存儲該過程中的形參個數,再用一個變數記錄過程中定義參數的個數(具體做法是定義一個變數,每次進入block時置為3,用來存儲SL、DL、RA,然後遇到變數定義就加一來記錄)當遇到過程定義時,將其說明的形參按照在本過程中定義變數的相關屬性登錄到符號表中,並且他們在活動記錄的存儲位置是緊接著SL,DL,RA;在遇到調用過程時,首先根據棧頂、棧頂-1、棧頂-2、棧頂-形參個數+1根據所存儲的位置進行賦值,然後再分配相應的活動存儲空間(如果剛開始就分配,則會導致棧頂更新,失去了原來的棧頂元素),該活動存儲空間的大小應該是參數個數+本過程定義的變數個數+3。
三、函數說明3.1類的作用的說明
l RValue+LexAnalysis+chTable完成詞法分析。
l AllPcode+PerPcode完成目標代碼生成。
l SymTable+TableRow完成符號表管理。
l Interpreter完成解釋執行程序。
l MpgAnalysis的一個函數showError完成錯誤處理程序。
l MpgAnalysis綜合上述類完成編譯功能。
3.2具體函數功能的說明
四、源代碼PL/0 Compiler
從功能上說,Function Overriding和Operator Overloading。從編譯器本身的原理上說,context sensitive reference analysis.
實現 KFFD 的時候,各種繞:macro body 的 scope,call site 的 scope,macro parameter 的 scope,等等
這種難度如果做不出來的話,應該認真考慮一下改行並成為產品經理的可能性。
後端的 SSA 的 destruction 有點坑,其餘都還好
來這裡支持下: 編譯中國
編譯到C++…你可以看成是C++的語法糖…
總之我也不懂編譯原理,不過聽松鼠姐姐講好像裡面的問題我是解決了的_(:_」∠)_https://github.com/thautwarm/SquirrelLanguage至於ReadMe和代碼注釋暫時沒時間弄額…但Squirrel的語法是真的簡單,寫demo什麼的還是很快的…https://github.com/thautwarm/My-Blog/blob/master/t1.squirrel所以說遇到的問題是什麼呢…
畢竟是編譯到C++所以很多問題都不用考慮了…大概就是如何把一個句子結構化…比如一個聲明句,有四個部分,第一個部分是變數符號說明,第二個就是聲明符 :(用來和定義符:=區別),第三個是類型說明(可推導,比如int, [int], [int,double,str],多個類型就好比一個類的定義),第四個是參數列表。聲明句沒有什麼麻煩的,主要是定義句,定義句第一個部分和第二個部分類似於聲明句,但第三個部分是參數+類型聲明,第四部分是行為說明。比如a:=[p1:int,p2:[int=&>int]=&>int]{return p1+p2(p1);};這種意思…個人感覺很優雅…定義句實現起來的話,第四部分行為說明是需要遞歸的,比如我可能返回一個函數,那麼就需要在行為說明裡可能再做很多新的定義行為或者別的什麼…感覺做完之後回憶也不難,就狀態機劃分成子狀態機,然後模式匹配以及C++11…占坑
在做一個編譯到WebAssembly的語言,等擼出了0.1版再和大家分享(^_^*)
編譯流程是從源代碼
func max(x: Number, y: Number) -&> {
if x &> y {
return x
} else {
return y
}
}
編譯到自己擼的WebAssembly IR (因為官方的S-Expression不夠直觀)
func "max(Number,Number)"(x: f64, y: f64) f64
get_local x
get_local y
call ">(Number,Number)"
if
get_local x
return
else
get_local y
return
end
end
然後再編譯到WebAssembly位元組碼,生成wasm文件或者直接包入html里
目前遇到的坑有- S-Expression坑爹
- WebAssembly文檔坑爹
- WebAssembly相關工具鏈,特別是0xd版本的太少,很多都要自己擼
- WebAssembly MVP不管怎麼和JS Object交互啊喂(╯‵□′)╯︵┻━┻
以前學8位單片機的時候試著用C#寫了一個80C51的源碼的編譯器,以及反彙編程序,不過還是有很多bug,都不好意思獻醜了
IR設計失誤,抽象性不夠。連帶的結果是SSA和寄存器分配優化寫的非常崩,約等於無。
推薦閱讀:
※ANTLR涉及編譯原理中的哪些部分?
※函數的局部變數在棧中是如何分布的?
※能否從編譯原理的角度詳細的描述一下模板編譯的過程?
※編譯程序是否有操作系統的參與?
※編譯器、解釋器和虛擬機有什麼區別和聯繫?大體原理是什麼?