標籤:compiler predictive parsing
上篇筆記介紹了文法分析相關的一些基礎概念,本篇筆記根據龍書第2.5節的內容實現一個針對簡單運算式的尾碼式文法翻譯器Demo。
備忘:原書中的demo是java執行個體,我給出的將是邏輯一致的Python版本的實現。
在簡單尾碼翻譯器代碼實現之前,還需要介紹幾個基本概念。
1. 自頂向下分析法(top-down parsing)
顧名思義,top-down分析法的思路是推導產生式時,以產生式開始符號作為root節點,從上至下依次構建其子節點,最終構造出文法分析樹。在具體實現時,它會把輸入字串從左至右依次掃描一遍,在掃描過程中構建分析樹。
假設現有下面的一組文法產生式:
再假設現在要推導的輸入字串為:
for ( ; expr; expr; ) other
則top-down分析法的推導過程如所示:
採用的演算法步驟為:
1) 以產生式開始符號(即非終結符號"stmt")作為root節點
2) 在標號為非終結符A的節點N上,選擇A的一個產生式(在上述樣本中,輸入的是for語句,故選擇for語句對應的產生式),並為該產生式體中的各個符號構造出N的子節點
3) 尋找下一個節點來構造子樹,通常選的是文法分析樹最左邊的尚未擴充的非終結符
4) 重複第2-3步,直至輸入串掃描完畢
在演算法實現時,有一個重要的約定術語叫做lookahead symbol,它是指輸入串中當前正在被掃描的終結符,我們經常會在文法掃描的實現代碼中看到lookahead變數,所以有必要知道其來曆。
根據上述演算法流程對前面推導for語句的做簡單說明:
1) 選擇產生式開始符號stmt作為root節點
2) 由於輸入的是for語句,故選擇產生式"for(optexpr; optexpr; optexpr) stmt"進行推導,此時lookahead符號指向終結符號"for",如最上面部分的(a)所示。用for產生式中的符號構造root節點的子節點,構造後的分析樹如中部的(b)所示。
3) root節點的子節點已經構造完,根據演算法流程,現在開始構造子節點的子樹,雖然"for"節點是當前分析樹最左端的尚未擴充的節點,但它是終結符無法擴充子樹,此時,若當前被考慮的終結符號與lookahead符號匹配(在本例中都指向"for",正好是匹配的),那麼圖中文法分析樹的箭頭和輸入串的箭頭都要向前推進一步,即文法分析樹的節點指向"("節點,lookahead符號指向輸入串中的"("。由於"("又是個終結符且與lookahead符號匹配,故再次向前推進一步,此時,文法分析樹箭頭指向標號為optexpr的非終結符節點,而lookahead符號指向了輸入串的終結符";",由於lookahead符號指向的";"與以optexpr開始的產生式無法匹配,故使用空產生式擴充optexpr節點的子節點。
以此類推,最終產生的文法分析樹如所示:
2. 遞迴下降分析法(Recursion-Descent Parsing)
遞迴下降分析法是一種自頂向下的文法分析法,它使用一組遞迴過程來處理輸入串。文法產生式中的
每個非終結符均有一個與之關聯的過程或函數。
預測性分析法(Predictive Parsing)是一種簡單的遞迴下降分析法,在預測性分析法中,每個非終結符號對應的過程或函數中的控制流程可以由lookahead符號無二義性地確定,即採用預測性分析法時,掃描輸入串的過程不需要回溯(backtracking)。分析輸入串時出現的程序呼叫序列隱式地定義了該輸入串的一顆文法分析樹。
本文後面給出的簡單運算式的尾碼文法翻譯器就是採用預測性分析法實現的。
3. 左/右遞迴(Left/Right Recursion)
當一個文法產生式左側的非終結符與右側的產生式體開始處的非終結符相同時,就可能發生左遞迴。具有下面形式的產生式是典型的左遞迴產生式:
因為該產生式中,非終結符A又出現在產生式體的最左端,在採用遞迴下降分析法推導這個產生式時,可能會導致無限迴圈調用,如所示。
同理,下面的產生式存在右遞迴問題:
在採用遞迴下降分析法推導上面的產生式時,可能會產生右遞迴無限迴圈:
所以在使用遞迴下降分析法前,通常需要對原文法產生式做修改,以便消除左/右遞迴問題。wikipedia的Left recursion條目總結了一些典型的消除左遞迴的方法,感興趣的話,可以去探究。
上面介紹的是略顯枯燥的基礎概念,下面開始本篇筆記的正題—用Python實現一個簡單數學運算式的尾碼文法翻譯器。
我們的目標是把簡單數學運算式的中綴形式翻譯成尾碼形式,假設給定的文法制導定義如下(其中,左邊為文法產生式,右邊為附加的語義規則,這些規則定義了從infix到postfix轉換的語義規則):
上述文法制導定義對應的文法制導翻譯計劃如下:
由於上面翻譯計劃的產生式存在左遞迴問題(由非終結符
expr引起),所以需要做調整以便消除左遞迴,調整後的文法制導翻譯計劃如下:
針對上述翻譯計劃,採用預測性分析法,根據龍書第2.5節描述的演算法流程,實現了Python版本的簡單數學運算式從中綴到尾碼文法的翻譯器,完整的代碼如下。
#!/bin/env python'''This demo is inspired by Section 2.5 of the 'Dragon Book': <Compilers: Principles, Techniques, and Tools>It implements a syntax-directed translator for simple expressions like '1+2-3'It translate infix expression into postfix form'''class Parser(object): lookahead = '' def __init__(self): print 'Please input an expression with infix form (One Character Per Line):' Parser.lookahead = raw_input() self.infix_list = [Parser.lookahead] self.postfix_list = [] def expr(self): self.term() while True: if Parser.lookahead == '+': self.match('+') self.term() self.postfix_list.append('+') elif Parser.lookahead == '-': self.match('-') self.term() self.postfix_list.append('-') else: print 'raw input is (infix form):' print ''.join(self.infix_list) print 'postfix form of the input is:' print ''.join(self.postfix_list) return def term(self): if self._isdigits(Parser.lookahead): self.postfix_list.append(Parser.lookahead) self.match(Parser.lookahead) else: print 'term: syntax error' def match(self, t): if Parser.lookahead == t: Parser.lookahead = raw_input() self.infix_list.append(Parser.lookahead) else: print 'match: syntax error' def _isdigits(self, s): try: int(s) return True except Exception, e: return Falseclass Postfix(object): def main(self): parser = Parser() parser.expr() print '\n'if '__main__' == __name__: postfix = Postfix() postfix.main()
運行上述指令碼,輸入合法的簡單數學運算(
由產生式可知,目前只支援0-9內的數學加減法)中綴運算式,指令碼會將其翻譯為尾碼文法。互動樣本如下:
>>> Please input an expression with infix form (One Character Per Line):1+2-3-6+5raw input is (infix form):1+2-3-6+5postfix form of the input is:12+3-6-5+>>>
備忘:指令碼目的在於樣本如何用預測性分析法對輸入串做文法翻譯,所以實現比較簡單粗暴(如輸入中綴運算式時每行只能輸入一個字元,否則會報錯 -_-)。
【參考資料】
1. 龍書第2.3-2.5節
2. wikipedia: Recursive descent parser
3. wikipedia: Left Recursion
========================= EOF ========================
【龍書筆記】用Python實現一個簡單數學運算式從中綴到尾碼文法的翻譯器(採用遞迴下降分析法)