LL(1)分析法和遞歸下降分析法同屬于自頂向下分析法。相對于遞歸下降而言,LL通過顯示
地維護一個棧來進行語法分析,遞歸下降則是利用了函數調用棧。
LL分析法主要由分析棧、分析表和一個驅動算法組成。其實LL的分析算法還是很容易懂的,
主要就是一個匹配替換的過程。而要構造這里的分析表,則還涉及計算first集和follow集
的算法。
個人覺得龍書在解釋這些算法和概念時都非常清楚細致,雖然也有人說它很晦澀。
first集和follow集的計算,拋開書上給的嚴密算法,用人的思維去理解(對于compiler
compiler則需要用程序去構造這些集合,這是讓計算機去理解),其實很簡單:
1、對于某個非終結符A的first集(first(A)),簡單地說就是由A推導得到的串的首符號的
集合:A->aB,那么這里的a就屬于first(A),很形象。
2、follow(A),則是緊隨A的終結符號集合,例如B->Aa,這里的a就屬于follow(A),也很形
象。
當然,因為文法符號中有epsilon,所以在計算上面兩個集合時則會涉及到一種傳遞性。例
如,A->Bc, B->epsilon,B可以推導出epsilon,也就是基本等同于沒有,那么first(A)中
就會包含c符號。
在了解了first集和follow集的計算方法后,則可以通過另一些規則構造出LL需要的分析表。
編譯原理里總有很多很多的理論和算法。但正是這些理論和算法,使得編譯器的實現變得簡
單,代碼易維護。
在某個特定的編程語言中,因為其文法一定,所以對于其LL(1)實現中的分析表就是確定的
。我們也不需要在程序里動態構造first和follow集合。
那么,要實現一個LL(1)分析法,大致步驟就集中于:設計文法->建立該文法的分析表->編
碼。
LL分析法是不能處理左遞歸文法的,例如:expr->expr + term,因為左遞歸文法會讓對應
的分析表里某一項存在多個候選式。這里,又會涉及到消除左遞歸的方法。這個方法也很簡
單,只需要把文法推導式代入如下的公式即可:
A -> AB | C 等價于:A -> CX, X -> BX | epsilon
最后一個問題是,如何在LL分析過程中建立抽象語法樹呢?雖然這里的LL分析法可以檢查文
法對應的語言是否合法有效,但是似乎還不能做任何有意義的事情。這個問題歸結于語法制
導翻譯,一般在編譯原理教程中語法分析后的章節里。
LL分析法最大的悲劇在于將一棵在人看來清晰直白的語法樹分割了。在遞歸下降分析法中,
一個樹節點所需要的屬性(例如算術運算符所需要的操作數)可以直接由其子節點得到。但
是,在為了消除左遞歸而改變了的文法式子中,一個節點所需要的屬性可能跑到其兄弟節點
或者父節點中去了。貌似這里可以參考“繼承屬性”概念。
不過,綜合而言,我們有很多業余的手段來處理這種問題,例如建立屬性堆棧。具體來說,
例如對于例子代碼中計算算術表達式,就可以把表達式中的數放到一個棧里。
例子中,通過在文法表達式中插入動作符號來標識一個操作。例如對于文法:
expr2->addop term expr2,則可以改為:expr2->addop term # expr2。當發現分析棧的棧
頂元素是'#'時,則在屬性堆棧里取出操作數做計算。例子中還將操作符壓入了堆棧。
下載例子,例子代碼最好對照arith_expr.txt中寫的文法和分析表來看。
PS,最近在云風博客中看到他給的一句評論,我覺得很有道理,并且延伸開來可以說明我們
周圍的很多現象:
”很多東西,意識不到問題比找不到解決方法要嚴重很多。比如one-pass 這個,覺得實現
麻煩不去實現,和覺得實現沒有意義不去實現就是不同的。“
對于以下現象,這句話都可以指明問題:
1、認為造輪子沒有意義,從不考慮自己是否能造出;
2、常告訴別人某個技術復雜晦澀不利于團隊使用,卻并不懂這個技術;
3、籠統來說,【覺得】太多東西沒有意義,雖然并不真正懂這個東西。