1、大部分情況下都是真的需要有語法樹
2、如果要直接返回計算結果之類的事情的話,只需要寫乙個visitor執行一下語法樹就好了,除去自動生成的**以外(反正這不用人寫,不計入代價),**量基本上沒什麼區別
3、加入語法樹可以讓文法本身描述起來更簡單,如果要讓程式設計師把文法單獨放在一邊,然後自己寫完整的語義函式來讓他生成語法樹的話,會讓大部分情況(需要語法樹)變得特別複雜,而少數情況(不需要語法樹)又沒有獲得什麼好處。
儘管類似yacc這樣的東西的確是不包含語法樹的內容而要你自己寫的,但是用起來難道不是很難受嗎?
現在轉入正題。這一篇文章講的主要是構造符號表的問題。想要把符號表構造的好是一件很麻煩的問題。我曾經嘗試過很多種方法,包括強型別的符號表,弱型別的符號表,基於map的符號表等等,最後還是挑選了跟visual studio自帶的用來讀pdb檔案的dia類其中的idiasymbol(基本上一樣的結構:所有的符號都只有這麼乙個symbol類,然後包羅永珍,什麼都有。為什麼最後選擇這麼做呢?因為在做語義分析的時候,其實做的最多的事情不是構造符號表,而是查詢符號表。如果符號表是強型別的畫,譬如說型別要乙個類,變數要乙個類,函式要乙個類之類的,總是需要到處cast來cast去,也找不到什麼好方法來在完成相同事情的情況下,保留強型別而不在**裡面出現cast。為什麼語法樹就要用visitor來解決這個問題,而符號表就不行呢?因為通常我們在處理語法樹的時候都是遞迴的形式,而符號表並不是。在乙個上下文裡面,實際上我們是知道那個symbol物件究竟是什麼東西的(譬如說我們查詢了乙個變數的type,那這返回值肯定只能是type了)。這個時候我們要cast才能用,本身也只是浪費表情而已。這個時候,visitor模式就不是和面對這種情況了。如果硬要用visitor模式來寫,會導致語義分析的**分散得過於離譜導致可讀性幾乎就喪失了。這是乙個辯證的問題,大家可以好好體會體會。
說了這麼一大段,實際上就是怎麼樣呢?讓我們來看「文法規則」本身的符號表吧。既然這個新的可配置語法分析器也是通過parse乙個文字形式的文法規則來生成parser,那實際上就跟編譯器一樣要經歷那麼多階段,其中肯定有符號表:
class parsingsymbol : publicobject
;public
: ~parsingsymbol();
parsingsymbolmanager*getmanager();
symboltype gettype();
const wstring&getname();
vint getsubsymbolcount();
parsingsymbol*getsubsymbol(vint index);
parsingsymbol* getsubsymbolbyname(const wstring&name);
parsingsymbol*getdescriptorsymbol();
parsingsymbol*getparentsymbol();
bool
istype();
parsingsymbol* searchclasssubsymbol(const wstring&name);
parsingsymbol* searchcommonbaseclass(parsingsymbol*classtype);
};
因為文法規則本身的東西也不多,所以這裡的symbol只能是上面記載的9種物件。這些物件其實特別的相似,所以我們可以看出唯一的區別就是當gettype返回值不一樣的時候,getdescriptorsymbol返回的物件的意思也不一樣。而這個區別記載在了enum symboltype的注釋裡面。實際上這種做法在面對超級複雜的符號表(考慮一下c++編譯器)的時候並不太好。那好的做法究竟是什麼呢?看上面idiasymbol的鏈結,那就是答案。
不可否認,微軟設計出來的api大部分還是很有道理的,除了win32的原生gui部分。
我們還可以看到,這個parsingsymbol類的幾乎所有成員函式都是用來查詢這個symbol的內容的。這裡還有兩個特殊的函式,就是searchclasssubsymbol和searchcommonbaseclass——當且僅當symbol是classtype的時候才起作用。為什麼有了getsubsymbolbyname,還要這兩個api呢?因為我們在搜尋乙個類的成員的時候,是要搜尋他的父類的。而乙個類的父類的sub symbol並不是類自己的sub symbol,所以就有了這兩個api。所謂的sub symbol的意思現在也很明了了。enum型別裡面的值就是enum的sub symbol,成員變數是類的sub symbol,總之只要是宣告在乙個符號內部的符號都是這個符號的sub symbol。這就是所有符號都有的共性。
當然,有了parsingsymbol,還要有他的manager才可以完成整個符號表的操作:
class parsingsymbolmanager : publicobject
;
這個parsingsymbolmanager有著符號表管理器的以下兩個典型作用
1、建立符號。
2、講符號與語法樹的物件繫結起來。譬如說我們在乙個context下面推導了乙個expression的型別,那下次對於同樣的context同樣的expression就不需要再推導一次了(語義分析有很多個pass,對同乙個expression求型別的操作經常會重複很多次),把它cache下來就可以了。
3、搜尋符號。具體到這個符號表,這個功能被做進了parsingsymbol裡面。
4、儲存根節點。getglobal函式就是幹這個作用的。所有的根符號都屬於global符號的sub symbol。
因此我們可以怎麼使用他呢?首先看上面的add函式。這些函式不僅會幫你在乙個符號表裡面新增乙個sub symbol,還會替你做一些檢查,譬如說阻止你新增相同名字的sub symbol之類的。語法樹很複雜的時候,很多時候我們有很多不同的方法來給乙個符號新增子符號,譬如說c#的成員變數和成員函式。成員變數不能同名,成員函式可以,但是成員函式和成員變數卻不能同名。這個時候我們就需要把這些新增操作封裝起來,這樣才可以在處理語法樹(宣告乙個函式的方法可以有很多,所以新增函式符號的地方也可以有很多)的時候不需要重複寫驗證邏輯。
其次就是cache函式。其實cache函式這麼寫,不是用來直接呼叫的。舉個例子,在分析乙個文法的時候,我們需要把乙個「型別」語法樹轉成乙個「型別」符號(譬如說要決定乙個文法要create什麼型別的語法樹節點的時候)。因此就有了下面的函式:
parsingsymbol* findtype(ptrtype, parsingsymbolmanager* manager, parsingsymbol* scope, collections::list>&errors)return
result;
}
很明顯,這個函式做的事情就是,查詢乙個parsingdefinitiontype節點有沒有被查詢過,如果有直接用cache,沒有的話再從頭計算他然後cache起來。因此這些cache函式就是給類似findtype的函式用的,而語義分析的**則直接使用findtype,而不是cache函式,來獲取乙個型別的符號。聰明的朋友們可以看出來,這種寫法蘊含著乙個條件,就是語法樹建立完就不會改了(廢話,當然不會改!)。
語法分析器構造 詞法分析器構造實驗
借助於詞法分析程式提供的分析結果,編寫乙個算符優先語法分析程式,程式能進行語法結構分析和錯誤檢查,並產生相應的歸約資訊。同時給出出錯資訊和錯誤型別,從而加深對語法分析的理解。二 實驗內容 給定文法g和算符優先分析法,構造其算符優先分析程式。文法g 語句 賦值語句 條件語句 轉移語句 帶標號的賦值語句...
用YACC構造簡單語法分析器
課堂實驗題 說明部分 翻譯規則 用c語言編寫的輔助例程 按照yacc的源程式規則由題中所給文法寫出trans.y token digit line expr n expr expr term expr term term term term factor factor factor expr dig...
LR 語法分析器
lr語法分析器算是基本完成了,只需要乙個文法定義檔案 syntax 就可以進行對應語言的語法分析,最後形成語法樹。詞法分析是固定的,採用c 的詞法定義。以後將加入動態的詞法分析。壓縮包中檔案的描述 lrtable.exe 是用文法定義檔案 syntax檔案 生成lr動作表檔案 action檔案 使用...