花下貓語: Guido van Rossum 是 Python 的創造者,雖然他如今放棄了「終身仁慈獨裁者」的職位,但卻成爲了指導委員會的五位成員之一,其一舉一動依然備受矚目。近日,他開通了 Medium 帳號,並發表了第一篇文章,透露出要替換 Python 的核心部件(解析器)的想法。這篇文章分析了當前的 pgen 解析器的諸多缺陷,並介紹了 PEG 解析器的優勢,使人振奮。這項改造工做仍在進行中,Guido 說他還會寫更多相關的文章,咱們就拭目以待吧。算法
本文原創並首發於公衆號【Python貓】,未經受權,請勿轉載。express
原文地址: https://mp.weixin.qq.com/s/yq...併發
原題 | PEG Parsers函數
做者 | Guido van Rossum(Python之父)學習
譯者 | 豌豆花下貓(「Python貓」公衆號做者)測試
原文 | https://medium.com/@gvanrossum_83706/peg-parsers-7ed72462f97c優化
聲明 | 翻譯是出於交流學習的目的,歡迎轉載,但請保留本文出處,請勿用於商業或非法用途。ui
幾年前,有人問 Python 是否會轉換用 PEG 解析器(或者是 PEG 語法,我不記得確切內容、誰說的、何時說的)。我稍微看過這個主題,但沒有頭緒,就放棄了。atom
最近,我學了不少關於 PEG(Parsing Expression Grammars)的知識,現在我認爲它是個有趣的替代品,正好替換掉我在 30 年前剛開始創造 Python 時自制的(home-grown)語法分析生成器(parser generator)(那個語法分析生成器,被稱爲「pgen」,是我爲 Python 寫下的第一段代碼)。spa
我如今感興趣於 PEG,緣由是對 pgen 的侷限性感到有些惱火了。
它使用了我本身寫的 LL(1) 解析的變種——我不喜歡能夠產生空字符串的語法規則,因此我禁用了它,進而稍微地簡化了生成解析表的算法。
同時,我還發明瞭一套相似 EBNF 的語法符號(譯註:Extended Backus-Naur Form,BNF 的擴展,是一種形式化符號,用於描述給定語言中的語法),至今仍很是喜歡。
如下是 pgen 令我感到煩惱的一些問題。
LL(1) 名字中的 「1」 代表它只使用單一的前向標記符(a single token lookahead),而這限制了咱們編寫漂亮的語法規則的能力。例如,一個 Python 語句(statement)既能夠是表達式(expression),又能夠是賦值(assignment)(或者是其它東西,但那些都以 if 或 def 這類專用的關鍵字開頭)。
咱們但願使用 pgen 表示法來編寫以下的語法。(請注意,這個示例描述了一種玩具語言(toy language),它是 Python 的一個微小的子集,就像傳統中的語言設計同樣。)
statement: assignment | expr | if_statement expr: expr '+' term | expr '-' term | term term: term '*' atom | term '/' atom | atom atom: NAME | NUMBER | '(' expr ')' assignment: target '=' expr target: NAME if_statement: 'if' expr ':' statement
關於這些符號,解釋幾句:NAME
和 NUMBER
是標記符(token),預約義在語法以外。引號中的字符串如 '+' 或 'if' 也是標記符。(我之後會講講標記符。)語法規則以其名稱開頭,跟在後面的是 :
號,再後面則是一個或多個以 |
符號分隔的可選內容(alternatives)。
但問題是,若是你這樣寫語法,解析器不會起做用,pgen 將會罷工。
其中一個緣由是某些規則(如 expr
和 term
)是左遞歸的,而 pgen 還不足以聰明地解析。這一般須要經過重寫規則來解決,例如(在保持其它規則不變的狀況下):
expr: term ('+' term | '-' term)* term: atom ('*' atom | '/' atom)*
這就揭示了 pgen 的一部分 EBNF 能力:你能夠在括號內嵌套可選內容,而且能夠在括號後放 *
來建立重複,因此這裏的 expr
規則就意味着:它是一個術語(term),跟着零個或多個語句塊,語句塊內是加號跟術語,或者是減號跟術語。
這個語法兼容了第一個版本的語言,但它並無反映出語言設計者的本意——尤爲是它並無代表運算符是左綁定的,而這在你嘗試生成代碼時很是重要。
可是在這種玩具語言(以及在 Python)中,還有另外一個煩人的問題。
因爲前向的單一標記符,解析器沒法肯定它查看的是一個表達式的開頭,仍是一個賦值。在一個語句的開頭,解析器須要根據它看到的第一個標記符,來決定它要查看的 statement
的可選內容。(爲何呢?pgen 的自動解析器就是這樣工做的。)
假設咱們的程序是這樣的:
answer = 42
這句程序會被解析成三個標記符:NAME
(值是 answer
),‘=’ 和 NUMBER
(值爲 42)。在程序開始時,咱們擁有的惟一的前向標記符是 NAME
。此時,咱們試圖知足的規則是 statement
(這個語法的起始標誌)。此規則有三個可選內容:expr
、assignment
以及 if_statement
。咱們能夠排除if_statement
,由於前向標記符不是 「if」。
可是 expr
與 assignment
都能以 NAME
標記符開頭,所以就會引發歧義(ambiguous),pgen 會拒絕咱們的語法。
(這也不徹底正確,由於語法在技術上並不會致使歧義;但咱們先無論它,由於我想不到更好的詞來表達。那麼 pgen 是如何作決定的呢?它會爲每條語法規則計算出一個叫作 FIRST
組的東西,若是在給定的點上,FIRST 組出現了重疊選項,它就會抱怨)(譯註:抱怨?應該指的是解析不下去,前文譯做了罷工)。
那麼,咱們可否爲解析器提供一個更大的前向緩衝區,來解決這個煩惱呢?
對於咱們的玩具語言,第二個前向標記符就足夠了,由於在這個語法中,assignment 的第二個標記符必須是 「=」。
可是在 Python 這種更現實的語言中,你可能須要一個無限的前向緩衝,由於在 「=」 標記符左側的東西可能極其複雜,例如:
table[index + 1].name.first = 'Steven'
在 「=」 標記符以前,它已經用了 10 個標記符,若是想挑戰的話,我還能夠舉出任意長的例子。爲了在 pgen 中解決它,咱們的方法是修改語法,並增長一個額外的檢查,令它能接收一些非法的程序,但若是檢查到對左側的賦值是無效的,則會拋出一個 SyntaxError
。
對於咱們的玩具語言,這可歸結成以下寫法:
statement: assignment_or_expr | if_statement assignment_or_expr: expr ['=' expr]
(方括號表示了一個可選部分。)而後在隨後的編譯過程當中(好比,在生成字節碼時),咱們會檢查是否存在 「=」,若是存在,咱們再檢查左側是否有 target
語法。
在調用函數時,關鍵字參數也有相似的麻煩。咱們想要寫成這樣(一樣,這是 Python 的調用語法的簡化版本):
call: atom '(' arguments ')' arguments: arg (',' arg)* arg: posarg | kwarg posarg: expr kwarg: NAME '=' expr
可是前向的單一標記符沒法告訴解析器,一個參數的開頭中的 NAME
究竟是 posarg
的開頭(由於 expr
可能以 NAME
開頭)仍是 kwarg
的開頭。
一樣地,Python 當前的解析器在解決這個問題時,是經過特別聲明:
arg: expr ['=' expr]
而後在後續的編譯過程當中再解決問題。(咱們甚至出了點小錯,容許了像 foo((a)=1)
這樣的東西,給了它跟 foo(a=1)
相同的含義,直到 Python 3.8 時才修復掉。)
那麼,PEG 解析器是如何解決這些煩惱的呢?
經過使用無限的前向緩衝!PEG 解析器的經典實現中使用了一個叫做「packrat parsing」(譯註:PackRat,口袋老鼠)的東西,它不只會在解析以前將整個程序加載到內存中,並且還能容許解析器任意地回溯。
雖然 PEG 這個術語主要指的是語法符號,可是以 PEG 語法生成的解析器是能夠無限回溯的遞歸降低(recursive-descent)解析器,「packrat parsing」經過記憶每一個位置所匹配的規則,來使之生效。
這使一切變得簡單,然而固然也有成本:內存。
三十年前,我有充分的理由來使用單一前向標記符的解析技術:內存很昂貴。LL(1) 解析(以及其它技術像 LALR(1),因 YACC 而著名)使用狀態機和堆棧(一種「下推自動機」)來有效地構造解析樹。
幸運的是,運行 CPython 的計算機比 30 年前有了更多的內存,將整個文件存在內存中確實已再也不是一個負擔。例如,我能在標準庫中找到的最大的非測試文件是 _pydecimal.py
,它大約有 223 千字節(譯註:kilobytes,即 KB)。在一個 GB 級的世界裏,這基本不算什麼。
這就是令我再次研究解析技術的緣由。
可是,當前 CPython 中的解析器還有另外一個 bug 個人東西。
編譯器都是複雜的,CPython 也不例外:雖然 pgen-驅動的解析器輸出的是一個解析樹,可是這個解析樹並不直接用做代碼生成器的輸入:它首先會被轉換成抽象語法樹(AST),而後再被編譯成字節碼。(還有更多細節,但在這我不關注。)
爲何不直接從解析樹編譯呢?這其實正是它最先的工做方式,可是大約在 15 年前,咱們發現編譯器由於解析樹的結構而變得複雜了,因此咱們引入了一個單獨的 AST,還引入了一個將解析樹翻譯成 AST 的環節。隨着 Python 的發展,AST 比解析樹更穩定,這減小了編譯器出錯的可能。
AST 對於那些想要檢查(inspect)Python 代碼的第三方代碼,也更加容易,它還經過被大衆歡迎的 ast
模塊而公開。這個模塊還容許你從頭構建 AST 節點,或是修改現有的 AST 節點,而後你能夠將新的節點編譯成字節碼。
後一項能力支撐起了一整個爲 Python 語言添加擴展的家庭手工業(譯註:ast 模塊爲 Python 的三方擴展提供了便利)。(藉助 parser
模塊,解析樹一樣能面向 Python 的用戶開放,但它使用起來太麻煩了,所以相比於 ast
模塊,它就過期了。)
綜上所述,我如今的想法是看看可否爲 CPython 創造一個新的解析器,在解析時,使用 PEG 與 packrat parsing 來直接構建 AST,從而跳過中間解析樹結構,並儘量地節省內存,儘管它會使用無限的前向緩衝。
我還沒進展到這個地步,但已經有了一個原型,能夠將一個 Python 的子集編譯成一個 AST,其速度與當前 CPython 的解析器大體至關。只不過,它佔用的內存更多,因此我預計在將它擴展到整個語言時,將會下降 PEG 解析器的速度。
可是,我還沒去優化它,因此仍是挺有但願的。
轉換成 PEG 的最後一個好處是它爲語言的將來演化提供了更大的靈活性。
過去有人曾說,pgen 的 LL(1) 缺陷幫助了 Python 保持語法的簡單。這頗有道理,但咱們還有不少適當的流程,能夠防止語言不受控制地膨脹(主要是 PEP 流程,在很是嚴格的向後兼容性要求以及新的治理結構的幫助下)。因此我並不擔憂。
我還有不少內容要寫,關於 PEG 解析以及個人具體實現,可是要等我整理好代碼後,在後續的文章中再去寫了。
公衆號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫做、優質英文推薦與翻譯等等,歡迎關注哦。