1. 程式人生 > >自制指令碼語言(8) 從LR(1) 到 GLR parser generator

自制指令碼語言(8) 從LR(1) 到 GLR parser generator

摘要:升級前文的LR(1) parser generator為GLR,為自制指令碼語言加入面向物件、泛型模板和函數語言程式設計等語法做好準備。

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

  LR(1) parser為前文的自制指令碼語言立下了汗馬功勞。但是指令碼語言不能只滿足於C的子集,至少應該OOP型別繼承、泛型要加入吧,甚至lambda表示式和函數語言程式設計也要加入吧。那樣LR(1)就不夠用了。一個比較快捷的方法是,LR(1)升級到GLR。parser generator只需要少量修改,parser需要將原來的符號棧改為棧圖。目前,parser generator和parser已經完成。

  GLR parser,是可以容許shift-reduce衝突、reduce-reduce衝突的LR解析方法。它的基本思想是:每次讀入一個token作為前瞻符號,執行reduce和shift,如果遇到shift-reduce衝突或reduce-reduce衝突,就暫時保留所有可能的parsing路線,當遇到reduce或shift都不能執行,則去掉該parsing線路。當最終讀取eof並規約到goal時,parsing結束。比起LR(1),因為有大量的可能路線同時存在,全部都要執行shift和reduce,其實可能只有一條正確的路線,這樣就導致大量時間浪費,效率差。

  在Elkhound的論文<A fast, parctical GLR parser generator>中,提出了幾種優化GLR的辦法。其中最重要的是GLR和LR的切換。在某些情況下,只有單線parsing向前推進,這種情況下,reduce可以採用LR的規約方式,即直接向前查詢。當遇到分岔路時,必須採用GLR的規約方式。另外,Elkhound提到了一些其他技巧,包括公共子樹重用,合併二義性,物件管理等。我設計的parser generator,主要就是應用了GLR/LR自動切換,另外有一個HashMap,把活動的棧頂狀態按照狀態編號放入,能夠及時識別和合並相同狀態。因為Java語言有GC,所以就不用像Elkhound的論文中那麼特意關注物件管理的問題。下面詳細介紹我這裡設計的parser generator.

  首先是與LR變化不大的action表與goto表。LR的shift和reduce都是確定性的,而GLR是非確定性的。在前面的LR generator中,根據語法定義,很可能出現shift-reduce衝突或reduce-reduce衝突,GLR的對策是將衝突全部記錄下來。比如原來s18或r19,現在可能是s18r19,或者r19r31,甚至s18r13r17r19. 相應的,parser中讀取action表時,需要用兩個表記錄reduce。第一個是list<Integer>, 第二個是list<list<Integer>>,第一個list是在無reduce-reduce衝突時使用,用-1表示無reduce項,-2表示出現reduce-reduce衝突而需要使用第二個表格。全部的reduce規則都連續記錄到第二個表格的第二個list中。

ArrayList<ArrayList<Integer>> shift_table
ArrayList<ArrayList<Integer>> goto_table
ArrayList<ArrayList<Integer>> reduce_table
ArrayList<ArrayList<ArrayList<Integer>>> reduces_table
HashMap<Integer,ParseState> states_active
class Symbol{    //解析的每個狀態,生成symbol來儲存已解析的資訊,尤其是reduce的路徑,這裡保留了路徑的頭、尾、長度
	AST ast
	int line
	ParseState path_start    //起點
	ParseState path_end    //終點
	int path_count    //規約路徑的長度
	String type
	String name
	String value
}

class ParseState{    //解析棧圖的狀態結點
	boolean fixed=true    //解析是否確定
	ParseState pre_state    //在解析棧圖的前結點
	LinkedList<ParseState> pre_states //在解析棧圖的多個前結點
	Symbol symbol	//規約的symbol
	LinkedList<Symbol> symbols    //多個可能規約的symbol
	int state_sn    
	int det_depth;    //直接LR的深度,用於GLR/LR切換
	boolean addLink(ParseState pre_state,Symbol symbol)    //建立前後結點的連結
}

  這裡要說棧圖的概念。LR是一個符號棧。shift時入棧,reduce時按語法產生式的符號數出棧,然後按照棧頂狀態和goto表產生新狀態併入棧。開始parsing時,初始狀態為0。結束時,終結狀態也是0. 而GLR把棧擴充套件為棧圖,意即多個可能的狀態,都用圖的方式表示而不是棧。shift時,新狀態作為新結點,連結到前面的狀態後。reduce時,不是出棧而是根據語法產生式的符號數向前回溯,同樣查詢goto表產生新狀態,把新狀態連結到回溯的位置處。棧圖維持一個HashMap,表示活動的棧頂狀態。reduce和shift執行後,新狀態加入HashMap。HashMap的作用還有一條就是及時合併不同路徑中的相同狀態。

  Parser在不斷迴圈的過程中,每次讀取一個token。首先執行reduce。查表得到存在或不存在reduce,或者有reduce-reduce衝突。儘可能的把所有可規約的語法都用上,得到新的狀態。新狀態加入棧圖後,嘗試shift操作。若可以shift(即action table中查shift不等於-1),則執行shift,並把新狀態放入active HashMap。下一輪讀取新token,執行新reduce和shift,是從active HashMap中取出各個元素來執行的。

  這裡面有幾個關鍵點。

  1,空轉移。比如static public class A。static和public可以省略,在語法中就可以為空,記作ε(程式中寫為e)。在我的parser裡面,執行reduce之前首先執行e-shift,就是儘量嘗試可行的e轉移。按照shift table中e符號下的轉移狀態轉移後,再執行reduce,以及reduce後的新狀態再嘗試e-shift。這裡需要一個list,儲存待執行reduce的狀態。e-shift後的狀態都加入這個list後面。

  2,可能有shift-reduce衝突或reduce-reduce衝突。導致了棧圖的分叉與合攏。這個就要靠前面提到的reduce list了。reduce後狀態為s_r, shift後狀態為s_s,s_s加入active map,但是s_r要放入reduce list,因為s_r可能除了shift以外也能繼續reduce。對reduce-reduce衝突的情況,因為reduce table記錄了所有reduce的語法,所以對多條語法產生式只需要依次進行reduce操作,並加入到reduce list即可。而活動棧頂的HashMap,保證了相同狀態能夠及時合併。在我的parser裡面,合攏動作只在shift時進行。因為只要保證每一輪reduce-shift能及時合攏就足夠了。並不需要reduce時合一次,shift時再合一次。合攏動作需要HashMap,兩個Map在我看來是沒有必要的。

  3,如何判斷GLR和LR的切換。棧圖中,每個狀態結點都有個屬性depth,表示從該結點可以無歧義的reduce的最大深度。例如,當兩個狀態shift或reduce後,進行到同一個狀態,則這個終點的depth設為0. 之後如果是單線前進,則depth每一步加1. 遇到reduce時,只要當前depth大於等於語法產生式的符號數量,則可知回溯必定為一條單線。則執行普通的LR reduce。但是如果depth小於語法產生式的符號數量,則首先單線回溯depth個狀態,然後進入GLR reduce過程。GLR reduce過程是,一步一步向前,每一步把可能的狀態放入一個HashSet。可能的狀態是指狀態儲存的符號名與語法產生式的符號名一致。HashSet每步清空一次,然後尋找,並把所有可能的狀態加入。直到回溯完成語法產生式的符號數量。

  下面是GLR棧圖的示意圖:


從狀態0出發,shift時加入新結點和邊,reduce時向前回溯,並按照goto表,得到新狀態和加入新邊。shift和goto的新結點是圓形,shift和goto過程用實線箭頭表示。reduce的結點是方形,reduce過程用虛線表示。活動棧頂儲存在active states裡面,用紅色虛線指向活動的棧頂結點。注意到結點7出現一個shift-reduce衝突。