【龍書筆記】用Python實現一個簡單數學表示式從中綴到字尾語法的翻譯器(採用遞迴下降分析法)
上篇筆記介紹了語法分析相關的一些基礎概念,本篇筆記根據龍書第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變數,所以有必要知道其來歷。
1) 選擇產生式開始符號stmt作為root節點
2) 由於輸入的是for語句,故選擇產生式"for(optexpr; optexpr; optexpr) stmt"進行推導,此時lookahead符號指向終結符號"for",如上圖最上面部分的(a)所示。用for產生式中的符號構造root節點的子節點,構造後的分析樹如上圖中部的(b)所示。
3) root節點的子節點已經構造完,根據演算法流程,現在開始構造子節點的子樹,雖然"for"節點是當前分析樹最左端的尚未擴充套件的節點,但它是終結符無法擴充套件子樹,此時,若當前被考慮的終結符號與lookahead符號匹配(在本例中都指向"for",正好是匹配的),那麼圖中語法分析樹的
以此類推,最終生成的語法分析樹如下圖所示:
遞迴下降分析法是一種自頂向下的語法分析法,它使用一組遞迴過程來處理輸入串。文法產生式中的每個非終結符均有一個與之關聯的過程或函式。
預測分析法(Predictive Parsing)是一種簡單的遞迴下降分析法,在預測分析法中,每個非終結符號對應的過程或函式中的控制流可以由lookahead符號無二義性地確定,即採用預測分析法時,掃描輸入串的過程不需要回溯(backtracking)。分析輸入串時出現的過程呼叫序列隱式地定義了該輸入串的一顆語法分析樹。
本文後面給出的簡單表示式的字尾語法翻譯器就是採用預測分析法實現的。
3. 左/右遞迴(Left/Right Recursion)
當一個文法產生式左側的非終結符與右側的產生式體開始處的非終結符相同時,就可能發生左遞迴。具有下面形式的產生式是典型的左遞迴產生式:
因為該產生式中,非終結符A又出現在產生式體的最左端,在採用遞迴下降分析法推導這個產生式時,可能會導致無限迴圈呼叫,如下圖所示。
同理,下面的產生式存在右遞迴問題:
在採用遞迴下降分析法推導上面的產生式時,可能會產生右遞迴無限迴圈:
所以在使用遞迴下降分析法前,通常需要對原文法產生式做修改,以便消除左/右遞迴問題。wikipedia的Left recursion條目總結了一些典型的消除左遞迴的方法,感興趣的話,可以去探究。
我們的目標是把簡單數學表示式的中綴形式翻譯成字尾形式,假設給定的語法制導定義如下(其中,左邊為文法產生式,右邊為附加的語義規則,這些規則定義了從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 False
class 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
+
5
raw input is (infix form):
1+2-3-6+5
postfix form of the input is:
12+3-6-5+
>>>
備註:指令碼目的在於示例如何用預測分析法對輸入串做語法翻譯,所以實現比較簡單粗暴(如輸入中綴表示式時每行只能輸入一個字元,否則會報錯 -_-)。
========================= EOF ========================