1. 程式人生 > >自制指令碼語言(4) 自動生成的詞法分析器

自制指令碼語言(4) 自動生成的詞法分析器

摘要:設計並實現了詞法分析器。讀取檔案中的正則表示式及其相匹配的符號,生成由NFA到DFA轉移表,最終得到表格驅動的詞法分析器。

  詞法分析器是根據一個符號表分析文字文件。符號表中有兩種符號,一種是保留字,例如if、else這類,還有一種是正則表示式,例如整型和浮點型數字、普通的變數名等。詞法分析器的功能就是讀取原始的文字文件,將其按照符號定義分割為符號的序列。那麼對外提供一個介面getToken(),每次呼叫,返回一個已分析的符號。整個文件分析完成後,需要有個“EOF”符號。

  getToken( )的過程是讀取字元,與DFA確定型有限自動機進行匹配,當遇到空格或換行時,自動機剛好是結束狀態,則返回相應的符號。這裡面有幾個要注意的點。通常來講,這個過程是按照正則表示式,先構建NFA,然後生成DFA。但有幾個點稍微要注意。一是正則表示式無非是數字、變數名,保留字、運算子都是確定的字串。那麼,對確定的字串就沒必要生成NFA,可以直接生成DFA。二是運算子往往跟數字和變數名、保留字之間都沒有空格分開。那麼,就要自動將運算子和數字、變數名、保留字分割。

  這裡設計的幾個類,首先最基本的表示自動機的NFA_State與DFA_State。其次要有表示正則表示式的RegexPattern與普通保留字的ReservedWord。最後,有個RegexParser來分析正則表示式生成NFA。

class NFA_State:
	HashMap<Character,HashSet<NFA_State>> nfa_edges		//字元轉移的路徑
	HashSet<NFA_State> e_edges                              //e轉移的路徑
class DFA_State:
	HashMap<Character,DFA_State> dfa_edges			//字元轉移的路徑
class RegexPaser:	
	String rule;						//要解析的正則表示式String
	NFA_State parse()					//對外的解析介面
	NFA_State parsePar()					//解析()*等科林閉包形式
	NFA_State parseSqr()                                    //解析[]多選一形式
	NFA_State parseDsj()                                    //解析 | 選擇形式
	NFA_State parseSeq()                                    //解析連續的字元形式
NFA有兩種邊,一種是e轉移,一種是根據字元的轉移,對應虛擬碼裡面的Map和Set。DFA只有根據字元的轉移,對應Map。RegexParser對外的介面是parse(string rule),而內部需要根據正則表示式的()*、[ ]*、| 等符號,解析出科林閉包、選擇關係等。其實也可以歸納到BNF正規化然後用遞迴下降來寫,也就是說把正則表示式轉換成抽象語法樹。但是現在不需要支援太複雜的正則表示式,所以就用了一個簡單的“麵條式”寫法,把符號解析寫到switch...case...過程裡面。

 程式執行,讀取檔案,獲得全部RegexPattern與ReservedWord物件不提,置入兩個ArrayList表格table_pt與table_rs。然後先分析ReservedWord,生成DFA。分析每一條ReservedWord,從全域性變數dfa_start開始,對照保留字的每一個字元,查詢是否有相同字元的路徑,如果沒有,則加入新路徑;否則移動到下一狀態,繼續分析保留字的下一個字元。

generateDFA()

  再生成NFA。這裡解析正則表示式沒有生成抽象語法樹,而是直接構建了DFA。每個字元代表的NFA狀態前後都有一個e轉移,記為pre與crt,遇到( )*科林閉包,crt與pre之間新增e轉移邊。而遇到[ ]或者 | 符號選擇式,在pre與crt之間插入多條字元路徑。

generateNFA()

 有了NFA,再將NFA轉為DFA。這裡用的演算法是子集構造法。從全域性變數nfa_start開始,尋找e-closure。獲得NFA子集後,查明如果是新的子集,則生成一個新的DFA,將這個DFA標為all_start。在NFA子集中,發現全部的字元路徑,對每條路徑,給all_start加入一條新的邊,其DFA終點為原NFA集合中此字元路徑終點的全部NFA的子集對應的DFA。所以,這裡我們需要根據NFA子集查詢DFA,以及根據DFA查詢原NFA子集的兩個表。現在暫時這兩個表都用HashSet,但因為查詢子集實際上不是判斷子集的Set是否一致而是判斷Set的元素是否一致,所以Hash其實是沒有必要的。未來優化會用TreeSet,便於查詢。還有一個問題,關於終結狀態。NFA的終結態實際是所有可以e轉移到終結態的狀態,而沒有本身標記為終結態。那麼在前面的e-closure過程,子集包括了終結態的NFA子集,對應生成的DFA也應該標記為Final終結態。

NFAtoDFA()	//呼叫spreadDFA()過程,將NFA轉為DFA
spreadDFA()	//遞迴呼叫。從一個NFA開始,獲得e-closure子集,再得到所有字元路徑,生成對應的DFA加入起始DFA路徑中
getEClosure()	//由NFA子集開始,e轉移得到新的NFA子集,此子集或者可以e轉移到終結態,或者可以字元路徑轉移
getEdges()	//由NFA子集開始,分析其全體字元路徑,獲得一個Map表示不同字元轉移到不同的新DFA
combineDFA()    //合併兩個DFA
getTokenTable() //輸出轉移表
  最後要把NFA轉換的DFA與開始分析保留字的DFA合併起來。方法類似於分析保留字,邊深度遍歷DFA,邊判斷是否需要加入新的邊與新的DFA。分析合併後的DFA,最終得到一個轉移表,根據字元轉移狀態,到達某種終結態時,如果符合終結條件(例如空格或者換行,或者數字、字母與運算子operator的切換),則確認完成一個token的讀取,字元陣列作為buffer輸出,根據終結態判斷token的分類、名字、數值等屬性,送到parser處理。

  如此則完成了可處理正則表示式的詞法分析器,當然,這是沒有優化的最基礎版本,以後會找時間改進。要改進的地方包括支援更復雜的正則表示式,有可能用抽象語法樹來處理,NFA的子集的快速查詢,可能用TreeSet或者數字編號索引的辦法,還有NFA轉DFA的演算法是否可以提高,對集合的運算是否可以寫成模板方法或者自動機的模板類,還可以考慮做一個在文字中查詢匹配的正則引擎,等等等等。還有繼續完成語法分析Parser、直譯器以及編譯器、虛擬機器。

原始碼地址:https://github.com/nklofy/Compiler