metaparse - 一個非常易用的Parser

前言

如果你會Python,並接觸過parser和interpreter的知識,知道上下文無關語言(CFG),那你很可能對這個模塊感興趣(項目地址和README)。項目結構目前還不成熟T.T,有興趣的童鞋可以下載metaparse.py單獨使用(觀眾:...)。歡迎幫忙測試並提出意見。

之前@陳天 老師寫過一篇文章文章推廣parser的概念,頗受歡迎。文中有一個有趣的問題,人們為何不像使用正則一般地使用CFG parser?是否因為工具不足?

有了Python這種靈活簡潔的語言,事情可以很簡單優雅地解決,不遜於instaparse。話不多說,我們馬上寫一個計算器,支持加、乘、乘方表達式和變數賦值。

示例:簡易計算器

第一步,設計文法:

stmt → ID = exprstmt → exprexpr → NUMexpr → ( expr )expr → expr? + expr?expr → expr? * expr?expr → expr? ** expr?

第二步,用Python方法簽名形式模擬該文法:

def stmt(ID, EQ, expr): ...def stmt(expr): ...def expr(ID): ...def expr(NUM): ...def expr(L, expr, R): ...def expr(expr_1, ADD, expr_2): ...def expr(expr_1, MUL, expr_2): ...def expr(expr_1, POW, expr_2): ...

會不會給人一種天然統的感覺…?

第三步,基於metaparse做三個微小的工作:詞法聲明,句法聲明及實現文法語義:

# 導入cfg元類和LALR解析器from metaparse import cfg, LALR# 全局變數table = {}class G_Calc(metaclass=cfg): # 詞法(基於正則,順序匹配) IGNORED = rs+ L = r( R = r) EQ = r= NUM = rd+ ID = rw+ POW = r**, 3 # 指配Token的優先順序,幫助LALR消歧 MUL = r* , 2 ADD = r+ , 1 # 句法和語義(句法制導翻譯,即Yacc的SDT風格) def stmt(ID, EQ, expr): table[ID] = expr return expr def stmt(expr): return expr def expr(ID): if ID in table: return table[ID] else: raise ValueError(ID yet bound: {}.format(ID)) def expr(NUM): return int(NUM) def expr(L, expr, R): return expr def expr(expr_1, ADD, expr_2): # TeX風格下標區分重名的形參 return expr_1 + expr_2 def expr(expr, MUL, expr_1): # 下標足以區分即可 return expr * expr_1 def expr(expr, POW, expr_1): return expr ** expr_1

第四步,用這個Grammar構建Parser:

>>> type(G_Calc)<class metaparse.Grammar>>>> calc = LALR(G_Calc)

然後變數計算器就完工了,在REPL中亦可賽艇:

>>> Calc.interpret( (3) )3>>> Calc.interpret( x = 3 )3>>> Calc.interpret( y = 4 * x ** (2 + 1) * 2)216>>> table{x: 3, y: 216}

如果我們需要語法樹,用parse方法替換interpret方法即可:

>>> tr = Calc.parse(" w = 1 + 2 * 3 ** 4 + 5 ")>>> tr(assign, [(ID: w)@[1:2], (EQ: =)@[4:5], (expr, [(expr, [(expr, [(NUM: 1)@[6:7]]), (ADD: +)@[8:9], (expr, [(expr, [(NUM: 2)@[10:11]]), (MUL: *)@[12:13], (expr, [(expr, [(NUM: 3)@[14:15]]), (POW: **)@[16:18], (expr, [(NUM: 4)@[19:20]])])])]), (ADD: +)@[21:22], (expr, [(NUM: 5)@[23:24]])])])

然後用tr.translate()方法可以得到語法樹的遍歷執行結果。由於LALR的特性,interpret過程中是不顯式生成語法樹的。

內核:元編程

由於對元類metaclass的支持問題,以上寫法只在Python 3裡面有效。元類的目的很簡單:

  • 在聲明類的過程中,按順序記錄一個類的屬性;
  • 允許記錄重名屬性實例。

用來記錄這些的結構可以是一個key-value-pair list(即assoc-list),支持__setitem__和__getitem__即可。詳情可見官方文檔中__prepare__和__new__的說明,以及David Beazley的教程。

成功獲取屬性列表之後,藉助inspect.getargspec(func)得到其中函數對象的簽名,利用它們構造一個兼具形式和語義的Rule對象,基於它們來構造Grammar對象。

深度元編程和ast模塊

若不依賴metaclass(比如在Python 2中),我們放棄class結構,轉而使用def結構。metaparse提供了一個decorator,如下使用可以得到和以上相同的結果:

@cfg.v2def Calc(): IGNORED = rs+ ... <same-stuff> ...

簡而言之cfg.v2做了幾件事:

  • 藉助inspect.getsource得到源碼字元串;
  • 用ast.parse解析得到語法樹;
  • 遍歷語法樹,從子結構中搜集對象來構造Grammar。

關於ast的文檔比較少,但是它已經被廣泛用於構造DSL和模板引擎之類的工具。篇幅所限,這裡不再展開。以後可以寫一篇文章和大家探討。

關於Parser後端

上面提到的這些元編程的tricks只提供了一種便捷地方式來聲明CFG,而後端parsing演算法可以有多種實現。目前模塊除了實現LALR,還有簡單孱弱的Weak-LL(1)(之所以weak因為沒有用FOLLOW)以及可以處理歧義文法的GLR和Earley(暫不支持包含loop的文法——會導致死循環)。

對於某種LL(1)文法,我們可以使用WLL1,比如萌萌的LISP:

class LISP(metaclass=cfg): IGNORED = rs+ LAMBDA = r(s*lambda LEFT = r( RIGHT = r) SYMBOL = r[^()s]+ def sexp(var): return var def sexp(abst): return abst def sexp(appl): return appl def var(SYMBOL): return SYMBOL def abst(LAMBDA, LEFT, parlist, RIGHT_1, sexp, RIGHT_2): return (LAMBDA, parlist, sexp) def appl(LEFT, sexp, sexps, RIGHT): return [sexp, sexps] def parlist(SYMBOL, parlist): return [SYMBOL] + parlist def parlist(): return [] def sexps(sexp, sexps): return [sexp] + sexps def sexps(): return []p_lisp = WLL1(LISP)>>> p_lisp.interpret((lambda (x y) (+ x y)))(LAMBDA, [x, y], [+, [x, y]])

或者對於歧義文法用GLR,比如Dangling-Else:

class G_IfThenElse(metaclass=cfg): IF = rif THEN = rthen ELSE = relse EXPR = rd+ SINGLE = rw+ def stmt(IF, EXPR, THEN, stmt): return (i-t, stmt) def stmt(IF, EXPR, THEN, stmt_1, ELSE, stmt_2): return (i-t-e, stmt_1, stmt_2) def stmt(SINGLE): return SINGLEp_ite = GLR(G_IfThenElse)>>> p_ite.interpret_many(if 1 then if 2 then if 3 then a else b else c)[(i-t, (i-t-e, (i-t-e, a, b), c)), (i-t-e, (i-t, (i-t-e, a, b)), c), (i-t-e, (i-t-e, (i-t, a), b), c)]

上面計算器的文法也是歧義文法,只不過藉助了Token的優先順序指配消除了歧義,這裡也可以通過給ELSE高於THEN的優先順序來消除歧義,具體情形不再贅述。

後話

這個模塊目前還不太成熟,但我個人認為前端很好用,而後端的演算法也可以在未來不斷優化。希望有興趣的童鞋可以幫助測試這個模塊並給予反饋。大家可以去開ISSUE,互相交流學習。

更多文檔和例子參見項目地址,歡迎fork。


推薦閱讀:

自動變化的C++ 03 stateful 方案
R語言進階 | 非標準計算tidyeval
R語言進階 | 非標準計算base
Go 實現泛型展開以及展開時計算
玩模板元編程走火入魔是一種怎樣的體驗?

TAG:Parser | 元编程 | Python |