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

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

用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(1)- 目標和前言
用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(2)- 簡介和設計
用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(3)- 詞法分析
用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(4)- 語法分析1:EBNF和遞歸下降文法
用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(5)- 語法分析2: tryC的語法分析實現

項目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

  2. 語法分析:

    語法分析指將詞法分析得到的標記流(token)進行分析,組成事先定義好的有意義的語句,這與自然語言中句子的語法分析類似。通常可以用抽象語法樹表示語法分析的結果,比如賦值語句:

        num = 123.4 * 3;
    

    可以用這樣一個抽象語法樹來表示:

    3
    =
    num
    *
    123.4
  3. 語義分析:
    程序的語義就是它的“意思”,程序的語義確定程序的運行方式。語義分析階段通常包括聲明和類型檢查、計算需要的一些屬性值等等。編譯器在這個階段中通常會維護一個叫做“符號表”的東西,保存變量的值、屬性和名稱。同樣以

        num = 123.4 * 3;
    

    爲例,假如我們是第一次在這裏遇見“num”,就將num的名稱字符串“num” 和當前計算出來的初始值370.2插入符號表中,當下次再遇見num時。我們就知道它是一個數字,已經初始化完畢,並且當前值是370.2;

  4. 目標代碼生成:
    在語義分析之後,我們就可以將語法分析和語義分析的結果(通常是抽象語法樹)轉換成可執行的目標代碼。

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

當然,已經有許多工具可以幫助我們處理階段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
  • 符號表

可參照源碼查看
https://github.com/yunwei37/tryC

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章