詞法分析: 詞法分析——TEST編譯器(1)
語法分析: 語法分析——TEST編譯器(2)
語義分析: 語義分析——TEST編譯器(3)
1 虛擬機
1.1 功能
讀取中間代碼並執行,此處的虛擬機相當於模擬了一個在機器上運行編譯好的TEST代碼的過程
1.2 特點
- 讀取時採用結構體數組code儲存指令及其操作數
- 執行時採用操作數棧stack進行操作
- 棧頂寄存器top爲棧頂的下一個單元的下標,基址寄存器base爲當前函數的數據區在stack中的起始地址
- 到此處一般就沒有什麼錯誤了,因爲錯誤都在前面幾個過程報錯了,能到達虛擬機的都是沒有問題的了
1.3 設計思路
-
由於虛擬機的執行過程就是不斷讀取中間代碼的指令,所以關鍵就是把每一個指令對應的功能實現,並且確定下一個指令是哪一條。所以只需要一條條讀取中間代碼,每個指令作爲一個分支,根據指令名字執行對應的功能即可
-
如何確定下一條指令是哪一個:如果學過計算機組成原理的話,就知道在運行程序的時候會有一個程序計數器ip,用來確定下一條指令是第幾個,此處虛擬機也要使用這個,正常情況都是按順序讀取中間代碼的指令,每讀取一條ip就加一,但如果遇到BR、BRF等需要跳轉位置的指令的話,ip就會直接賦值成對應的操作數,然後再繼續按順序執行。
-
操作數棧原理:操作數棧就是記錄在程序運行過程中每個變量的值怎麼變化,判斷條件是什麼等等。既然說是棧,那麼就說明所有的操作都是在棧頂完成的,例如STO就是把棧頂元素的值賦值給操作數對應的變量。操作數棧還會涉及到函數開闢空間的問題,每進入一個函數就要開闢一個空間,用來存放返回的位置,基地址和該函數的局部變量,有了這些之後就知道該函數調用完後要回到哪裏,該函數從棧的第幾個位置開始。這些東西都是在語義分析的時候就確定了具體要開闢多少空間。
-
由於指令有很多種,在判斷每一個指令時可以選擇使用許多的if或者switch進行判斷,但是選哪一個更好呢,我之前看過一篇關於if和switch進行比較的博客,裏面提到switch的平均執行完成時間比 if 的平均執行完成時間快了約 2.33 倍。原因是在 switch 中只取出了一次變量和條件進行比較,而 if 中每次都會取出變量和條件進行比較,因此 if 的效率就會比 switch 慢很多。而且分支越多switch性能高的特性體現就越明顯,所以在我們的虛擬機程序中選擇各種指令時,使用了switch進行分支選擇。同時,爲了使代碼更容易理解、閱讀,可以用枚舉類型表示每一個case。
-
使用switch進行分支選擇,但是如何表示每一個case呢?如果是用數字1、2、3等等代表每一個執行指令則會非常不方便,使代碼的閱讀、理解難度增大,所以是不適合的,如果換成每個指令的字符串形式,也有點麻煩但是閱讀起來就很容易理解很多了。所以我們選擇結合這兩種類型,使用枚舉類型表示每一個指令,這樣一來,判斷相等、閱讀代碼等很多方面就會便捷很多。在我理解中,枚舉就有點像#define指令,給一個東西一個重命名,可以方便我們讀代碼的時候好理解含義。思考之後認爲使用枚舉的好處是:如果有大量需要重命名的內容,如果全部用#define指令會把代碼弄的很長、很麻煩。而且在判斷枚舉類型數據時不需要像字符串一樣使用strcmp,直接使用==就可以判斷,就很方便。
-
最開始打虛擬機代碼的時候,就一直在糾結虛擬機部分的代碼需不需語義分析產生的符號表,以爲沒有符號表就找不到具體的標識符了。但是在重複學習了分配內存的原理之後,明白了實際上是不需要再傳入符號表的。在中間代碼中也只有需要操作數的時候纔會查符號表,那麼在語義分析的時候比如STO的操作數就已經得到變量的相對地址了,那麼對我有用的就是通過這個相對地址區分變量就好了,從而也沒必要再次知道這個變量長什麼樣子。所以在虛擬機部分只需要傳入中間代碼即可,不再需要符號表了。
2 完整代碼
#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<string>
#include<map>
using namespace std ;
struct Code{ //中間代碼
char opt[10];
int operand;
};
Code code[1000];//用於保存中間代碼
map<string, int> choseOpt ; //用map集合方便switch中選擇不同case,判斷字符串方便
enum opts {LOAD, LOADI, STO, ADD, SUB, MULT, DIV, BR, BRF, EQ, NOTEQ, GT, LES, GE, LE, AND,
OR, NOT, IN, OUT, CAL, ENTER, RETURN}; //用具體的名稱代替1234使代碼更易閱讀
//初始化map,方便之後switch中選擇case
void mapInit() {
choseOpt["LOAD"] = LOAD ; choseOpt["LOADI"] = LOADI ; choseOpt["STO"] = STO ;
choseOpt["ADD"] = ADD ; choseOpt["SUB"] = SUB ; choseOpt["MULT"] = MULT ;
choseOpt["DIV"] = DIV ; choseOpt["BR"] = BR ; choseOpt["BRF"] = BRF ;
choseOpt["EQ"] = EQ ; choseOpt["NOTEQ"] = NOTEQ ; choseOpt["GT"] = GT ;
choseOpt["LES"] = LES ; choseOpt["GE"] = GE ; choseOpt["LE"] = LE ;
choseOpt["AND"] = AND ; choseOpt["OR"] = OR ; choseOpt["NOT"] = NOT ;
choseOpt["IN"] = IN ; choseOpt["OUT"] = OUT ; choseOpt["CAL"] = CAL ;
choseOpt["ENTER"] = ENTER ; choseOpt["RETURN"] = RETURN ;
}
//虛擬機
void TESTmachine(){
FILE *in;
char codein[100]; //輸入文件名
int codenum=0; //指令條數
int top=0, base=0 ; //棧頂和棧底
int ip=0; //當前指令位置
int stack[1000]; //操作數棧
printf("請輸入目標文件名(包括路徑):");
scanf("%s",codein);
if((in=fopen(codein, "r"))==NULL){//打開輸入文件
printf("\n打開%s錯誤!\n",codein);
exit(-1) ; //出錯就運行結束
}
while(!feof(in)){ //讀取中間代碼
fscanf(in,"%s %d",&code[codenum].opt,&code[codenum].operand);
codenum++;
}
codenum-- ; //最後一次讀取會多加1
fclose(in);
// for(int i=0;i<codenum;i++)
// printf("%s %d\n",code[i].opt,code[i].operand);
stack[0]=0;
stack[1]=0;
mapInit() ; //將map初始化
memset(stack, 0, sizeof(stack)) ;
while(ip < codenum){ //執行指令直到執行到最後一條指令爲止,最後一條指令代表主函數結束
Code temp = code[ip] ; //用一個臨時變量來執行操作
ip++ ; //每執行一個指令地址就往後移動一位
switch(choseOpt[temp.opt]) { //根據操作碼進行選擇要執行的指令
case LOAD : { //LOAD D 將D中的內容加載到操作數棧
stack[top] = stack[temp.operand+base] ; //找到棧中存放變量的位置
top++;
break ;
}
case LOADI : { //LOADI a 將常量a壓入操作數棧
stack[top]=temp.operand;
top++;
break ;
}
case STO : { //STO D將操作數棧頂單元內容存入D
top--; //先將棧頂元素減一
stack[temp.operand+base] = stack[top] ;
break ;
}
case ADD : { //ADD 將棧頂單元與次棧頂單元出棧並相加,和置於棧頂
stack[top-2] += stack[top-1] ;
top--;
break ;
}
case SUB : { //將次棧頂單元減去棧頂單元並出棧,差置於棧頂。
stack[top-2] = stack[top-2]-stack[top-1];
top--;
break ;
}
case MULT : { //將次棧頂與棧頂單元出棧並相乘,積置於棧頂。
stack[top-2] = stack[top-1]*stack[top-2];
top--;
break ;
}
case DIV : { //將次棧頂與棧頂單元出棧並相除,商置於棧頂
stack[top-2] = stack[top-2]/stack[top-1];
top--;
break ;
}
case BR : { //BR lab 無條件轉移到lab
ip = temp.operand; //操作數記錄的就是要跳轉的位置
break ;
}
case BRF : { //BRF 若棧頂單元邏輯值,假(0)則轉移到lab
if(stack[top-1]==0)
ip = temp.operand; //操作數記錄的就是要跳轉的位置
top-- ;
break ;
}
case EQ : { //將棧頂兩單元做相等比較,並將結果真或假(1或0)置於棧頂
stack[top-2] = (stack[top-2] == stack[top-1]) ;
top--;
break ;
}
case NOTEQ : { //棧頂兩單元做不等於比較,並將結果 (1或0)置於棧頂
stack[top-2] = (stack[top-2] != stack[top-1]) ;
top--;
break ;
}
case GT : { //次棧頂大於棧頂操作數,則棧頂置1,否則置0
stack[top-2] = (stack[top-2] > stack[top-1]) ;
top--;
break ;
}
case LES : { //次棧頂小於棧頂操作數,則棧頂置1,否則置0
stack[top-2] = (stack[top-2] < stack[top-1]) ;
top--;
break ;
}
case GE : { //次棧頂大於等於棧頂操作數,則棧頂置1,否則置0
stack[top-2] = (stack[top-2] >= stack[top-1]) ;
top--;
break ;
}
case LE : { //次棧頂小於等於棧頂操作數,則棧頂置1,否則置0
stack[top-2] = (stack[top-2] <= stack[top-1]) ;
top--;
break ;
}
case AND : { //將棧頂兩單元做邏輯與運算,並將結果 (1或0)置於棧頂
stack[top-2] = (stack[top-2] && stack[top-1]) ;
top--;
break ;
}
case OR : { //將棧頂兩單元做邏輯或運算,並將結果 (1或0)置於棧頂
stack[top-2] = (stack[top-2] || stack[top-1]) ;
top--;
break ;
}
case NOT : { //將棧頂的邏輯值取反
stack[top-1] = !stack[top-1];
break ;
}
case IN : { //從標準輸入設備(鍵盤)讀入一個整型數據,併入操作數棧
printf("輸入數據:\n");
scanf("%d", &stack[top]) ;
top++;
break ;
}
case OUT : { //將棧頂單元內容出棧,並輸出到標準輸出設備上(顯示器)
printf("輸出:%d\n",stack[top-1]);
top--;
break ;
}
case CAL : { //調用函數
stack[top] = base ; //記錄主函數的基地址
stack[top+1] = ip ; //記錄函數執行完要返回主函數的位置
ip = temp.operand ; //執行指令位置跳轉到函數開始的位置
base = top ; //進入函數後對於當前函數的基地址
break ;
}
case ENTER : { //進入函數體
top += temp.operand ; //爲函數開闢空間
break ;
}
case RETURN : { //函數返回
top = base ; //釋放函數開闢的空間
ip = stack[top+1] ; //第二個位置存放的是返回到主函數的位置
base = stack[top] ; //回到棧底就是主函數的基地址,重新賦值
break ;
}
}
}
}
int main() {
TESTmachine();
return 0 ;
}
3 總結
- 這就是TEST編譯器實驗的全部內容了,說實話上理論課的時候,講的都是啥根本聽不懂,只有自己打過這個代碼之後才恍然大悟。還有學過計算機組成原理之後再來看編譯原理就感覺輕鬆很多。
- 過段時間還要參加一個編譯器的比賽,希望到時候能做出比這個好很多倍的代碼,說實話做這個代碼的時候老師太噁心人,實在沒有心情好好做,就隨便做了一下應付,真正的還要做好多優化,並且我這個代碼還存在許多不合理但是能混過檢查的地方。