1. 程式人生 > >手把手教你寫指令碼引擎(五)——簡單的高階語言(3,符號表)

手把手教你寫指令碼引擎(五)——簡單的高階語言(3,符號表)

手把手教你寫指令碼引擎(五)——簡單的高階語言(3,符號表)

陳梓瀚

華南理工大學軟體本科05

符號表的結構的複雜度跟語言的語義規則的複雜度有關。對於C#來說,每一個符號都附帶了一大堆資訊,譬如位置啦,所在的namespace啦,型別啦什麼的。對於JavaScript來說,符號表幾乎是不需要的,因為東西都動態了,編譯時幾乎不檢查內容。語義分析的輸出是符號表,程式碼生成的輸入是符號表和語法樹。因此語法樹除了放語法相關的內容,語義相關的內容最好放到符號表裡面(譬如說表示式的型別啦,語句的scope結果啦)。關於一個現實中的符號表組織可以看

首先我們要解決型別的表達問題。一門複雜的語言的型別有很多種。這裡的種類指的不是

intstring的區別,而是函式型別、結構型別這種區別。每一種型別還有很多附帶的屬性。在語義分析的過程中,我們經常要比較兩個型別是否一致。於是符號表的型別表達要設計成易於讀取、修改和比較。

我們通常由兩種解決方法。第一種方法是用一個繼承結構來表達。定義一個基類TypeBase,然後底下一堆繼承。乍一看很OOP,實際不然。語義分析的時候我們對每一種特殊的型別都有一些特殊的操作,我們還是舉那個判斷型別是否相等的操作來說明一下。我們知道OOP裡面的虛擬函式解決了一維的分派問題。我們拿到一個Base,對Base->Method求值,總是可以根據Base的實際型別來求值。如果我們需要對兩個型別同時進行分派呢?譬如說

Equal(Base1,Base2),這種操作當且僅當Base1Base2的實際種類相同才有比較的意義。這個時候我們改造成Base1->Equal(Base2)的話,也是免不了對Base2進行一下dynamic_cast還是什麼類似的操作的。

所以我個人比較偏向於第二種做法。我們為每一個型別建立一個唯一ID。譬如說int 0啦,int(int,int)1啦,int*2什麼的。比較兩個型別是否相等就直接拿ID去比較,ID相等則型別相等,ID不相等則型別不相等。在實際操作上怎麼做呢?我們知道語義分析的過程中會產生出一堆(理論上可以為無窮多的)新型別。每一種型別都有一些屬性。譬如說基本型別是有限的,可以用

enum來表達。而函式型別需要返回值和引數型別表。於是我們拿屬性去要一個ID的時候,符號表首先檢查這個型別是否已經存在,存在則返回對應的ID,不存在則建立一條新的記錄,然後繫結一個新的ID。譬如CMinus的型別表採用如下介面分配ID

class VL_CMinusTypeTable : public VL_Base

{

public:

VInt GetPrimitiveType(VLE_CMinusPrimitiveType Type);

VInt GetPointer(VInt Type);

VInt GetArray(VInt Type , VInt Count);

VInt GetFunction(VInt ReturnType , VL_List<VInt , true>& ParameterTypes);

VInt CreateStruct();

VL_CMinusTypeSlot* GetType(VInt Type);

};

如果我們已知一個型別的ID,求其指標型別的ID,就呼叫GetPointer(TypeID)。經過這一套函式的處理,我們總是可以不用擔心是否在什麼地方讓兩個ID指向了相同的型別,或者一個型別不小心擁有了多個ID,十分好管理。

第二個問題就是要儲存每一個表示式的型別和語句的Scope了。我不建議將這些資訊儲存在語法樹裡面。原因比較複雜,因為一份程式碼在不同的上下文中可能有不同的意思,然後我們有一天突然有需要將這些環境中的這份程式碼的語義分析結果保留下來的話,如果東西原本是存在語法樹裡面的,那就完蛋了,只能去複製語法樹了。於是我建議將語法分析得不到的資訊通通存進符號表。因為表示式和語句都是指標,我們只需要一些map就可以將表示式和語句的附加資訊存起來了。

第三個問題是scope。一個變數或引數的作用範圍是有限的,於是我們只好建立一個scope樹,其中每一個節點都看得到父節點,至於能不能看到子節點我覺得是無所謂的。於是對於一個具體的scope來說,一個scope就變成了一個連結串列,儲存了當前scope的所有符號名,然後還能知道直接或間接的父scope。下面舉個直觀的例子。假設我們有程式碼:

int A=0;

int B(int C,int D)

{

int E=0;

}

為了處理這份程式碼,我們建立了三個scope。第一個是全域性scope,記錄了AB。第二個是函式scope,記錄了CD。第三個是屬於語句的一個scope,記錄了E。於是我們用一個連結串列把他們串起來:語句scope -> 函式scope -> 全域性scope

這樣做的好處是我們查詢scope會變得很方便。譬如現在的上下文是語句scope,那麼它理應可以看見變數、引數、全域性函式和全域性變數。新增一個符號也很方便,只要當前的scope沒有這個名字,不管上面的scope有沒有我們都可以新增,新增完就把上面的scope的同名符號給覆蓋了。

一個scope其實還可以記錄其他的東西的,譬如距離最近的迴圈表示式啦(用來判斷break是否應該存在),所屬的函式啦(return後面要不要接表示式),還有其他的很多雜七雜八的東西。

第四個問題是如何建立符號表。之前的文章我們把語句和表示式都建立成了兩個大型的繼承結構。表示式新增一個函式叫GetType,返回一個ID。語句建立一個函式叫Validate,用來驗證語句是否合法。他們的引數都是符號表和當前的scope,這樣的話,表示式為了建立型別就會產生出一堆ID,語句為了讓表示式可以知道每一個變數的型別就要建立scope。這麼一遞迴下去,符號表也有了,型別也檢查完了。所以上文才會說語義分析產生符號表。

符號表就介紹到這裡了。一個高階語言所遇到的基本的問題其實都講得差不多了。接下來的文章就針對具體的問題進行講解了,譬如繼承、反射、垃圾收集等等的跟具體語言相關的問題。

posted on 2009-05-11 10:48 陳梓瀚(vczh) 閱讀(6268) 評論(1)  編輯 收藏 引用 所屬分類: 指令碼技術