天天看點

用c語言手搓一個500+行的類c語言解釋器: 給程式設計初學者的編譯器教程(2)- 簡介和設計用c語言手搓一個500+行的類c語言解釋器: 給程式設計初學者的編譯器教程(2)- 簡介和設計

用c語言手搓一個500+行的類c語言解釋器: 給程式設計初學者的編譯器教程(2)- 簡介和設計

項目github位址及源碼:

https://github.com/yunwei37/tryC

需要了解的一些基本概念

編譯器和解釋器的差別不同

通常我們說的 “編譯器” 是一種計算機程式,負責把一種程式設計語言編寫的源碼轉換成另外一種計算機代碼,後者往往是以二進制的形式被稱為目标代碼(object code)。這個轉換的過程通常的目的是生成可執行的程式。

而解釋器是一種計算機程式,它直接執行由程式設計語言或腳本語言編寫的代碼,它并不會把源代碼預編譯成機器碼,而是一行一行地分析源代碼并且直接執行,相對編譯器而言可能效率較為低下,但實作也相對簡單,并且容易在不同的機器上進行移植(比如x86和mips指令集的機器)。

先來看看通常的編譯器是如何實作的:

編譯器從源碼翻譯為目标代碼大緻需要這樣幾個步驟,每個步驟都依賴于上一個步驟的結果:

  1. 詞法分析:
    編譯器對源程式進行閱讀,并将字元序列,也就是源代碼中一個個符号收集到稱作記号(token)的單元中;比如:
               
    num = 123.4;           
這樣個指派語句中,變量num算是一個token,“=”符号算是一個token,“123.4”算是一個token;每個token有自己的類别和屬性,比如“123.4”的類别是數字,屬性(值)是123.4
           
  1. 文法分析:
    文法分析指将詞法分析得到的标記流(token)進行分析,組成事先定義好的有意義的語句,這與自然語言中句子的文法分析類似。通常可以用抽象文法樹表示文法分析的結果,比如指派語句:
               
    num = 123.4 * 3;           
可以用這樣一個抽象文法樹來表示:
           
graph TD
    = --> num
    = --> *
    * --> 123.4
    * --> 3           
  1. 語義分析:
    程式的語義就是它的“意思”,程式的語義确定程式的運作方式。語義分析階段通常包括聲明和類型檢查、計算需要的一些屬性值等等。編譯器在這個階段中通常會維護一個叫做“符号表”的東西,儲存變量的值、屬性和名稱。同樣以
               
    num = 123.4 * 3;           
    為例,假如我們是第一次在這裡遇見“num”,就将num的名稱字元串“num” 和目前計算出來的初始值370.2插入符号表中,當下次再遇見num時。我們就知道它是一個數字,已經初始化完畢,并且目前值是370.2;
               
  2. 目标代碼生成:
    在語義分析之後,我們就可以将文法分析和語義分析的結果(通常是抽象文法樹)轉換成可執行的目标代碼。
               

解釋器與編譯器僅在代碼生成階段有差別,而在前三個階段如詞法分析、文法分析、語義分析基本是一樣的。

當然,已經有許多工具可以幫助我們處理階段1和2,如 flex 用于詞法分析,bison 用于文法分析;但它們的功能都過于強大,屏蔽了許多實作上的細節,對于學習建構編譯器幫助不大,是以我們要完全手寫這些功能。

(實際上完成一個可以跑起來的解釋器并不難,而且還是一件很有成就感的事,不是嘛?)

tryC編譯器的設計:

從上面可以看出,我們的tryC解釋器需要這三個子產品:

  1. 詞法分析
  2. 文法分析
  3. 語義分析和解釋執行

需要這兩個資料結構(用來在階段之間儲存或傳遞值):

  1. token,用來在詞法分析和文法分析之間傳遞标記;
  2. 符号表,儲存語義分析階段遇見的變量值,使用一個數組存儲;

在了解過這些之後,我們先來大概看看代碼的基本結構:

(從上往下在代碼中依次對應,“...”表示省略的相關代碼,在後續文章中會詳細講解)

  • 資料結構的聲明部分: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哦)

繼續閱讀