天天看點

[符号表]靜态位址管理符号表接口及使用原則

    進入語義分析的首要任務是組織和管理變量符号。這一點有多種選擇,一種是類C/C++語言那樣的靜态處理,即将變量映射為位址,運作時記憶體中根據位址取得變量值,這樣運作效率無論是從記憶體使用量和通路速度上都會較高;而另一種就是可反射的符号表,即在運作時根據變量名在符号表中取得變量值,這樣做的典型是Python語言,雖然這樣效率較低,不過好處是友善ORM等反射機制的實作。不過既然現在Jerry并沒有意向弄得那麼複雜,是以就采用前一種。

    首先,對于這樣的符号表,基本的操作包括:

    - 聲明變量,将變量存入符号表

    - 取得變量類型

    - 取得變量首位址

    - 對于數組,取得變量某一維的大小

第一個是必須的毫無疑問。不過有一個問題,那就是調用該函數,傳入的參數是什麼。是一個DeclarationNode嗎?當然這是可以的,但是這樣就違反了資料封裝的原則,即在設計該函數時必須對DeclarationNode的内部結構有了解,這加大了難度。是以可以考慮其參數全部為基本資料類型。包括其它函數也是這樣,在取得變量資訊時全部使用基本資料類型,而不用VariableNode之類的東西。

void regVar(char* ident, AcceptType type, int nrDim, int* dims);
AcceptType getVarType(char* ident);
int getVarAddr(char* ident);
int getVarNrDim(char* ident);
int getVarDimSize(char* ident, int index);      

其中函數的作用分别是

    regVar        在符号表中注冊一個變量,給出辨別符、類型及次元資訊

    getVarType    根據辨別符擷取變量類型

    getVarAddr    根據辨別符擷取變量首位址

    getVarNrDim   根據辨別符擷取變量的次元數

    getVarDimSize 根據辨別符和某一維的大小

當然,第一個函數就沒什麼好說的了。第二個函數擷取類型是為了做靜态上下文類型判斷與轉型,如 i 是一個整型變量而 j 是一個實型變量,如有一條語句為

    i = j;

那麼在運作時會将先将 j 的值放在棧頂,然後轉換成整型,再賦給 i 所在的位址。getVarAddr則是用來擷取變量在記憶體中的首位址,對于數組變量也是這樣,數組變址則要靠最後一個函數。getVarNrDim函數用以擷取變量的次元,在通路變量時,首先要對其次元數量進行比對(因為Jerry沒有考慮支援數組,是以引用時的變量次元數量必須跟聲明時一樣)。現在來看最後一個函數。當對一個數組變量進行合法引用時,比如

    i[1][2][x]

這時為了擷取這個變量的位址,該怎麼辦呢?注意到最後一維的下标是個變量,這也就意味着無法通過靜态計算來獲得,必須動态綁定。過程是這樣的,首先查詢 i 在聲明時各次元大小,假設是d0,d1,d2, i 的首位址是a0,那麼首先計算 i[1][2] 的位址:

    a0 + d1 * d2 * 1 + d2 * 2

在運作時再将這個位址與 x 的值相加,就得到了該變量的值,至于取得d0,d1,d2這些資訊,就得靠函數getVarDimSize了。(注意:以上運算并不正确,因為位址偏移并未考慮到每個該類型變量占用的位元組數,必須在運算時乘算該大小因子。)為了避免每次在擷取數組高次元的偏移時都要重複計算(如剛才例子中最高次元的偏移是d1 * d2),可以在調用regVar時,讓符号表中存放的數組次元資訊就是偏移,而不是每個次元的大小。

當然符号表管理并不是這樣就好了,以上這些接口是相當理想化的接口,它無法處理兩件事:

    - 出錯

    - 内層變量覆寫外層變量

為了對付出錯,那麼就得讓函數有所“表示”,最基本的方法就是傳回錯誤值,那麼原來的傳回值呢?答案是,将其位址作為參數傳入,這是一種基本的政策,C語言庫函數中的fgets也是這麼幹的,我們可以效仿之。至于花括号内外層這檔子事兒,就必須采用使用多個符号表,并附加一個符号表棧來實作。一開始該棧裡面有一個符号表,當到達基本塊開頭(也就是遇到正花括号)時,壓入一個新的符号表進棧,直到基本塊結束才将其彈出棧,當要擷取變量資訊時,依次從棧頂向下逐個對符号表進行查詢,直到找到該變量,或者到達棧底的樓下(這時報錯)。不過關于符号表棧這東西,并不由符号表本身來考慮,應該放在語義分析的控制部分或者中間插入一個變量管理層進行。對于符号表,現在比較合理的接口形式是(struct SymbolTable表示符号表結構體)

/* 符号表操作過程中可能的錯誤 */
/* const.h */
typedef enum {
    SymTab_OK = 0, // 無錯誤
    SymTab_MultiDef, // 重複定義(在同一基本塊内, 内外層則會覆寫)
    SymTab_NotDef, // 找不到定義
    SymTab_SizeExceeded, // 大小超限, 由于聲明的變量太多了或數組太大了
    SymTab_ArrDimOutOfRange // 數組次元通路越界, 請求次元大于次元數量
} SymbolTableError;

/* 建立一個符号表 */
struct SymbolTable* newSymbolTable(int base);
/* 讓符号表退休 */
void finalizeSymbolTable(struct SymbolTable* table);

/* 參數名為 ret 的都是傳回值,現在它們由位址傳入,函數内部将傳回值寫入位址 */
SymbolTableError regVar(struct SymbolTable* table, char* ident,
                        AcceptType type, int nrDim, int* dims);
SymbolTableError getVarType(struct SymbolTable* table, char* ident,
                            AcceptType* ret);
SymbolTableError getVarAddr(struct SymbolTable* table, char* ident,
                            int* ret);
SymbolTableError getVarNrDim(struct SymbolTable* table, char* ident,
                             int* ret);
SymbolTableError getVarDimSize(struct SymbolTable* table, char* ident,
                               int* ret, int index);      

不需要考慮符号表的繼承(這難道還面向對象?),是以這些函數就這麼靜态弄,不用變成成員函數來搞。