進入語義分析的首要任務是組織和管理變量符號。這一點有多種選擇,一種是類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);
不需要考慮符號表的繼承(這難道還面向對象?),所以這些函數就這麼靜態弄,不用變成成員函數來搞。