用c語言手搓一個500+行的類c語言解釋器: 給程式設計初學者的編譯器教程(2)- 簡介和設計
項目github位址及源碼:
https://github.com/yunwei37/tryC需要了解的一些基本概念
編譯器和解釋器的差別不同
通常我們說的 “編譯器” 是一種計算機程式,負責把一種程式設計語言編寫的源碼轉換成另外一種計算機代碼,後者往往是以二進制的形式被稱為目标代碼(object code)。這個轉換的過程通常的目的是生成可執行的程式。
而解釋器是一種計算機程式,它直接執行由程式設計語言或腳本語言編寫的代碼,它并不會把源代碼預編譯成機器碼,而是一行一行地分析源代碼并且直接執行,相對編譯器而言可能效率較為低下,但實作也相對簡單,并且容易在不同的機器上進行移植(比如x86和mips指令集的機器)。
先來看看通常的編譯器是如何實作的:
編譯器從源碼翻譯為目标代碼大緻需要這樣幾個步驟,每個步驟都依賴于上一個步驟的結果:
- 詞法分析:
編譯器對源程式進行閱讀,并将字元序列,也就是源代碼中一個個符号收集到稱作記号(token)的單元中;比如:
num = 123.4;
這樣個指派語句中,變量num算是一個token,“=”符号算是一個token,“123.4”算是一個token;每個token有自己的類别和屬性,比如“123.4”的類别是數字,屬性(值)是123.4
- 文法分析:
文法分析指将詞法分析得到的标記流(token)進行分析,組成事先定義好的有意義的語句,這與自然語言中句子的文法分析類似。通常可以用抽象文法樹表示文法分析的結果,比如指派語句:
num = 123.4 * 3;
可以用這樣一個抽象文法樹來表示:
graph TD
= --> num
= --> *
* --> 123.4
* --> 3
- 語義分析:
程式的語義就是它的“意思”,程式的語義确定程式的運作方式。語義分析階段通常包括聲明和類型檢查、計算需要的一些屬性值等等。編譯器在這個階段中通常會維護一個叫做“符号表”的東西,儲存變量的值、屬性和名稱。同樣以
num = 123.4 * 3;
為例,假如我們是第一次在這裡遇見“num”,就将num的名稱字元串“num” 和目前計算出來的初始值370.2插入符号表中,當下次再遇見num時。我們就知道它是一個數字,已經初始化完畢,并且目前值是370.2;
- 目标代碼生成:
在語義分析之後,我們就可以将文法分析和語義分析的結果(通常是抽象文法樹)轉換成可執行的目标代碼。
解釋器與編譯器僅在代碼生成階段有差別,而在前三個階段如詞法分析、文法分析、語義分析基本是一樣的。
當然,已經有許多工具可以幫助我們處理階段1和2,如 flex 用于詞法分析,bison 用于文法分析;但它們的功能都過于強大,屏蔽了許多實作上的細節,對于學習建構編譯器幫助不大,是以我們要完全手寫這些功能。
(實際上完成一個可以跑起來的解釋器并不難,而且還是一件很有成就感的事,不是嘛?)
tryC編譯器的設計:
從上面可以看出,我們的tryC解釋器需要這三個子產品:
- 詞法分析
- 文法分析
- 語義分析和解釋執行
需要這兩個資料結構(用來在階段之間儲存或傳遞值):
- token,用來在詞法分析和文法分析之間傳遞标記;
- 符号表,儲存語義分析階段遇見的變量值,使用一個數組存儲;
在了解過這些之後,我們先來大概看看代碼的基本結構:
(從上往下在代碼中依次對應,“...”表示省略的相關代碼,在後續文章中會詳細講解)
- 資料結構的聲明部分:token類型、符号表結構:
#include <stdio.h>
...
typedef struct symStruct {
int type;
char name[MAXNAMESIZE];
double value;
..........
} symbol;
symbol symtab[SYMTABSIZE]; // 符号表
int symPointer = 0;
char* src, * old_src; // 目前分析的源代碼位置指針
// tokens 的枚舉類型
enum {
Num = 128, Char, Str, Array, Func,
........
};
// token 的表示形式
int token; // current token type
union tokenValue {
symbol* ptr;
double val;
} token_val;
- 詞法分析的兩個函數:
// 擷取輸入流中的下一個記号:
void next() {
char* last_pos;
while (token = *src) {
++src;
if(token == AAA ){
.....
}else if(token == BBB ){
.....
}
}
}
// 比對一個記号,并擷取下一個token:
void match(int tk) {
if (token == tk) {
next();
}
else { // 遇到了一個錯誤
exit(-1);
}
}
- 文法分析和語義分析,以及執行階段:使用遞歸下降法實作(後面會再提到什麼是遞歸下降法啦)
// 計算表達式的值:
double expression(){}
double factor(){}
double term(){}
// 計算布爾表達式的值:
int boolOR();
int boolAND();
int boolexp();
// 執行一個語句;
double statement();
// 執行一個函數:
double function();
- main() 函數,代碼的入口,并
int main(int argc, char** argv)
{
// 往符号表裡面添加關鍵詞
int i, fd;
src = "array func else if return while print puts read";
for (i = Array; i <= Read; ++i) {
next();
symtab[symPointer -1].type = i;
}
src = old_src = (char*)malloc(POOLSIZE); // 配置設定空間
....
fd = open(*argv, 0); // 打開讀取檔案
read(fd, src, POOLSIZE - 1);
src[i] = 0;
close(fd);
next();
while (token != 0) { // 一條一條語句執行
statement();
}
return 0;
}
重要概念
- 編譯器/解釋器
- 語義分析
- token
- 符号表
可參照github源碼檢視(如果覺得寫得還行麻煩您幫我點個star哦)