用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(6)- 語義分析:符號表和變量、函數

用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(6)- 語義分析:符號表和變量、函數

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

項目github地址及源碼:
https://github.com/yunwei37/tryC

這一部分,我們再回過頭來看看變量、函數是怎樣存儲和處理的、以及符號表是怎樣構建的。

符號表

我們先來回顧一下符號表的定義:

符號表是一種用於語言翻譯器(例如編譯器和解釋器)中的數據結構。在符號表中,程序源代碼中的每個標識符都和它的聲明或使用信息綁定在一起,比如其數據類型、作用域以及內存地址。

簡單來說就是,我們在符號表中存儲對應的變量的各種信息,在定義的時候對符號表進行插入,以便下次碰見它的時候可以知道這個變量的具體信息。

我們可以在符號表中保存五種變量:Num(數值), Char(字符), Str(字符串), Array(數組), Func(函數)

tryC符號表的完整定義如下:

/* this structure represent a symbol store in a symbol table */
typedef struct symStruct {  
    int type;                  // 符號的類型:  Num, Char, Str, Array, Func
    char name[MAXNAMESIZE];    // 符號名稱
    double value;              // 如果是數值變量,記錄它的值; 如果是數組或者字符串,記錄它的長度
    union {
        char* funcp;            // 指向函數定義在源代碼中位置的字符指針
        struct symStruct* list; // 指向數組列表
    } pointer;
    int levelNum;               // 作用域層
} symbol;
symbol symtab[SYMTABSIZE];      // 用數組定義符號表
int symPointer = 0;             // 符號表數組當前使用的最大下標的指針+1(棧頂 + 1)
int currentlevel = 0;           // 當前作用域層

作用域

作用域就是程序中定義的變量所存在的區域,超過該區域變量就不能被訪問。

(這裏就不具體舉例介紹了)

作用域可以相互嵌套;當內層作用域和外層作用域存在同名變量時,在內層的程序訪問的應當是內層的變量,在外層的程序訪問的應當是外層的變量;在函數中的變量,只有在所在函數被調用時才動態地爲變量分配存儲單元,並在調用結束時回收。

作用域可以是塊作用域、函數作用域等,tryC中只實現了函數作用域。

我們可以用currentlevel這個變量記錄當前的嵌套深度;

int currentlevel = 0; 

對於函數作用域我們可以這樣處理:在函數調用時加深作用域層,並把需要傳入的參數插入符號表;並在函數退出的時候,刪除該作用域層的所有變量,並減少作用域層,對應代碼如下:

double function() {
    currentlevel++;
    return_val = 0;

    .....

    while (symtab[symPointer - 1].levelNum == currentlevel) {
        symPointer--;
    }
    currentlevel--;
    return return_val;
}

由於插入的變量肯定在符號表數組的最上面,因此只要減少符號表數組最大值的指針就可以表示刪除啦。

變量

對變量的處理主要分爲幾個部分:

  • 詞法分析階段,當我們遇見一個標識符名稱時,需要返回對應的token;
  • 在表達式中,當遇見一個變量時,我們需要獲取它的值;
  • 在定義語句中,對變量進行定義和在符號表中插入相關信息;

詞法分析階段

當我們在詞法分析的時候,對變量的處理需要以下幾個步驟:

  1. 獲取完整的變量名:
  2. 在符號表中查找變量,從上往下查找,這樣返回的一定是最近作用域的那個變量:
  3. 如果在符號表中找到了變量,根據變量不同的類型,返回不同的token值;
  4. 如果沒有找到,在符號表中間插入新的變量,返回的token值爲void;這時應該對應賦值語句
...
        else if ((token >= 'a' && token <= 'z') || (token >= 'A' && token <= 'Z') || (token == '_')) {
            last_pos = src - 1;             // process symbols
            char nameBuffer[MAXNAMESIZE];
            nameBuffer[0] = token;
            while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9') || (*src == '_')) {
                nameBuffer[src - last_pos] = *src;
                src++;
            }
            nameBuffer[src - last_pos] = 0;                 // get symbol name
            int i;
            for (i = symPointer-1; i >= 0; --i) {           // search symbol in symbol table 
                if (strcmp(nameBuffer, symtab[i].name) == 0) {      // if find symbol: return the token according to symbol type
                    if (symtab[i].type == Num || symtab[i].type == Char) {
                        token_val.ptr = &symtab[i];
                        token = Sym;
                    }
                    else if (symtab[i].type == FuncSym) {
                        token_val.ptr = &symtab[i];
                        token = symtab[i].type;
                    }
                    else if (symtab[i].type == ArraySym) {
                        token_val.ptr = &symtab[i];
                        token = symtab[i].type;
                    }
                    else {
                        if (symtab[i].type == Void) {
                            token = Sym;
                            token_val.ptr = &symtab[i];
                        }
                        else token = symtab[i].type;
                    }
                    return;
                }
            }
            strcpy(symtab[symPointer].name, nameBuffer);        // if symbol not found, create a new one 
            symtab[symPointer].levelNum = currentlevel;
            symtab[symPointer].type = Void;
            token_val.ptr = &symtab[symPointer];
            symPointer++;
            token = Sym;
            return;
        }
...

在表達式中對變量的處理:

在表達式中遇到的標識符可能是三種形式:

  1. 普通變量:Char或Num,token_val傳遞數值類型;
  2. 函數變量:進行調用函數操作;
  3. 數組變量:獲取token_val傳遞的數組指針,獲取下標,進行邊界檢查,獲取元素;
double factor() {
    double temp = 0;
    .....
    else if (token == Sym) {                // 普通變量
        temp = token_val.ptr->value;
        match(Sym);
    }
    else if (token == FuncSym) {            // 函數變量
        return function();
    }
    else if (token == ArraySym) {           // 數組變量
        symbol* ptr = token_val.ptr;
        match(ArraySym);
        match('[');
        int index = (int)expression();
        if (index >= 0 && index < ptr->value) {
            temp = ptr->pointer.list[index].value;
        }
        match(']');
    }
    return temp;
}

在變量定義語句中對變量的處理

由於是動態類型語言,我們對變量的定義語句也是變量的賦值語句;根據賦值的類型確定變量的類型。進入賦值語句時,傳遞過來的token_val包含的是一個指向當前變量結構體的指針,賦值就是對其進行操作:

賦值語句的左邊可以是數組中間的一個單元,也可以是一個變量,右邊是字符串或表達式、字符。

數組需要先定義才能進行賦值。

...
    else if (token == Sym || token == ArraySym) {
        symbol* s = token_val.ptr;
        int tktype = token;
        int index;
        match(tktype);
        if (tktype == ArraySym) {                   // 對數組進行特殊判斷:獲取要賦值的數組單元;
            match('[');
            index = expression();
            match(']');
            match('=');
            if (index >= 0 && index < s->value) {
                s->pointer.list[index].value = expression();
            }
        }
        else {
            match('=');
            if (token == Str) {                     // 根據賦值類型進行不同的操作
                s->pointer.funcp = (char*)token_val.ptr;
                s->type = Str;
                match(Str);
            }
            else if (token == Char) {
                s->value = token_val.val;
                s->type = Char;
                match(Char);
            }
            else {
                s->value = expression();
                s->type = Num;
            }
        }
        match(';');
    }
...

函數

tryC的函數實現完整代碼:這個函數做了以下幾件事:

  1. 對變量的作用域進行控制;
  2. 將函數參數中的變量直接插入作用域;
  3. 保存當前詞法分析的源代碼位置和token,並跳轉到函數定義時的源代碼位置和token;
  4. 語法分析和執行定義時的函數體,如果碰到返回語句,就將返回值存入return_val;
  5. 恢復保存的當前源代碼位置和token;
  6. 返回值從全局變量return_val中獲取;

由於function()函數本身是遞歸的,且變量作用域等可以得到控制,因此可以實現函數的遞歸調用。

double function() {
    currentlevel++;
    return_val = 0;             // 對變量的作用域進行控制;

    symbol* s = token_val.ptr;  // 將函數參數中的變量直接插入作用域;
    match(FuncSym);
    match('(');
    while (token != ')') {
        symtab[symPointer] = *token_val.ptr;
        strcpy(symtab[symPointer].name, token_val.ptr->name);
        symtab[symPointer].levelNum = currentlevel;
        symPointer++;
        match(Sym);
        if (token == ',')
            match(',');
    }
    match(')');
    char* startPos = src;                   // 保存當前詞法分析的源代碼位置和token
    char* startOldPos = old_src;
    int startToken = token;
    old_src = src = s->pointer.funcp;       // 跳轉到函數定義時的源代碼位置和token;
    token = (int)s->value;
    statement();                            // 語法分析和執行定義時的函數體
    src = startPos;
    old_src = startOldPos;
    token = startToken;                     // 恢復保存的當前源代碼位置和token;

    while (symtab[symPointer - 1].levelNum == currentlevel) {
        symPointer--;
    }
    currentlevel--;
    return return_val;
}

可對照源碼查看(如果覺得寫得還行麻煩您幫我點個star哦)
https://github.com/yunwei37/tryC

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