用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;
- 在表達式中,當遇見一個變量時,我們需要獲取它的值;
- 在定義語句中,對變量進行定義和在符號表中插入相關信息;
詞法分析階段
當我們在詞法分析的時候,對變量的處理需要以下幾個步驟:
- 獲取完整的變量名:
- 在符號表中查找變量,從上往下查找,這樣返回的一定是最近作用域的那個變量:
- 如果在符號表中找到了變量,根據變量不同的類型,返回不同的token值;
- 如果沒有找到,在符號表中間插入新的變量,返回的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;
}
...
在表達式中對變量的處理:
在表達式中遇到的標識符可能是三種形式:
- 普通變量:Char或Num,token_val傳遞數值類型;
- 函數變量:進行調用函數操作;
- 數組變量:獲取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的函數實現完整代碼:這個函數做了以下幾件事:
- 對變量的作用域進行控制;
- 將函數參數中的變量直接插入作用域;
- 保存當前詞法分析的源代碼位置和token,並跳轉到函數定義時的源代碼位置和token;
- 語法分析和執行定義時的函數體,如果碰到返回語句,就將返回值存入return_val;
- 恢復保存的當前源代碼位置和token;
- 返回值從全局變量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