用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哦)
https://github.com/yunwei37/tryC