原題 | A Meta-Grammar for PEG Parserspython
做者 | Guido van Rossum(Python之父)git
譯者 | 豌豆花下貓(「Python貓」公衆號做者)github
聲明 | 本翻譯是出於交流學習的目的,基於 CC BY-NC-SA 4.0 受權協議。爲便於閱讀,內容略有改動。本系列的譯文已在 Github 開源,項目地址:https://github.com/chinesehuazhou/guido_blog_translation編程
本週咱們使解析器生成器完成「自託管」(self-hosted),也就是讓它本身生成解析器。bootstrap
首先咱們有了一個解析器生成器,其中一部分是語法解析器。咱們能夠稱之爲元解析器(meta-parser)。該元解析器與要生成的解析器相似:GrammarParser
繼承自Parser
,它使用相同的 mark()/reset()/expect() 機制。然而,它是手寫的。可是,只能是手寫麼?數據結構
在編譯器設計中有一個傳統,即編譯器使用它要編譯的語言編寫。我深切地記得在我初學編程時,當時用的 Pascal 編譯器是用 Pascal 自己編寫的,GCC 是用 C 編寫的,Rust 編譯器固然是用 Rust 編寫的。app
這是怎麼作到的呢?有一個輔助過程(bootstrap,引導程序,一般譯做「自舉」):對於一種語言的子集或早期版本,它的編譯器是用其它的語言編寫的。(我記得最初的 Pascal 編譯器是用 FORTRAN 編寫的!)而後用編譯後的語言編寫一個新的編譯器,並用輔助的編譯器來編譯它。一旦新的編譯器運行得足夠好,輔助的編譯器就會被廢棄,而且該語言或新編譯器的每一個新版本,都會受到先前版本的編譯器的編譯能力的約束。學習
讓咱們的元解析器如法炮製。咱們將爲語法編寫一個語法(元語法),而後咱們將從中生成一個新的元解析器。幸運的是我從一開始就計劃了,因此這是一個很是簡單的練習。咱們在上一篇文章中添加的動做是必不可少的因素,由於咱們不但願被迫去更改生成器——所以咱們須要可以生成一個可兼容的數據結構。網站
這是一個不加動做的元語法的簡化版:ui
start: rules ENDMARKER rules: rule rules | rule rule: NAME ":" alts NEWLINE alts: alt "|" alts | alt alt: items items: item items | item item: NAME | STRING
我將自下而上地展現如何添加動做。參照第 3 篇,咱們有了一些帶 name 和 alts 屬性的 Rule 對象。最初,alts 只是一個包含字符串列表的列表(外層列表表明備選項,內層列表表明備選項的條目),但爲了添加動做,我更改了一些內容,備選項由具備 items 和 action 屬性的 Alt 對象來表示。條目仍然由純字符串表示。對於 item 規則,咱們有:
item: NAME { name.string } | STRING { string.string }
這須要一些解釋:當解析器處理一個標識符時,它返回一個 TokenInfo 對象,該對象具備 type、string 及其它屬性。咱們不但願生成器來處理 TokenInfo 對象,所以這裏加了動做,它會從標識符中提取出字符串。請注意,對於像 NAME 這樣的全大寫標識符,生成的解析器會使用小寫版本(此處爲 name )做爲變量名。
接下來是 items 規則,它必須返回一個字符串列表:
items: item items { [item] + items } | item { [item] }
我在這裏使用右遞歸規則,因此咱們不依賴於第 5 篇中添加的左遞歸處理。(爲何不呢?保持事情儘量簡單老是一個好主意,這個語法使用左遞歸的話,不是很清晰。)請注意,單個的 item 已被分層,但遞歸的 items 沒有,由於它已是一個列表。
alt 規則用於構建 Alt 對象:
alt: items { Alt(items) }
我就不介紹 rules 和 start 規則了,由於它們遵循相同的模式。
可是,有兩個未解決的問題。首先,生成的代碼如何知道去哪裏找到 Rule 和 Alt 類呢?爲了實現這個目的,咱們須要爲生成的代碼添加一些 import 語句。最簡單的方法是給生成器傳遞一個標誌,該標誌表示「這是元語法」,而後讓生成器在生成的程序頂部引入額外的 import 語句。可是既然咱們已經有了動做,許多其它解析器也會想要自定義它們的導入,因此爲何咱們不試試看,可否添加一個更通用的功能呢。
有不少方法能夠剝了這隻貓的皮(譯註:skin this cat,解決這個難題)。一個簡單而通用的機制是在語法的頂部添加一部分「變量定義」,並讓生成器使用這些變量,來控制生成的代碼的各個方面。我選擇使用 @ 字符來開始一個變量定義,在它以後是變量名(一個 NAME)和值(一個 STRING)。例如,咱們能夠將如下內容放在元語法的頂部:
@subheader "from grammar import Rule, Alt"
標準的導入老是會打印(例如,去導入 memoize),在那以後,解析器生成器會打印 subheader
變量的值。若是須要多個 import,能夠在變量聲明中使用三引號字符串,例如:
@subheader """ from token import OP from grammar import Rule, Alt """
這很容易添加到元語法中,咱們用這個替換 start 規則:
start: metas rules ENDMARKER | rules ENDMARKER metas: meta metas | meta meta: "@" NAME STRING NEWLINE
(我不記得爲何我會稱它們爲「metas」,但這是我在編寫代碼時選擇的名稱,我會堅持這樣叫。:-)
咱們還必須將它添加到輔助的元解析器中。既然語法不只僅是一系列的規則,那麼讓咱們添加一個 Grammar 對象,其中包含屬性 metas
和 rules
。咱們能夠放入以下的動做:
start: metas rules ENDMARKER { Grammar(rules, metas) } | rules ENDMARKER { Grammar(rules, []) } metas: meta metas { [meta] + metas } | meta { [meta] } meta: "@" NAME STRING { (name.string, eval(string.string)) }
(注意 meta 返回一個元組,並注意它使用 eval() 來處理字符串引號。)
說到動做,我漏講了 alt 規則的動做!緣由是這裏面有些混亂。但我不能再無視它了,上代碼吧:
alt: items action { Alt(items, action) } | items { Alt(items, None) } action: "{" stuffs "}" { stuffs } stuffs: stuff stuffs { stuff + " " + stuffs } | stuff { stuff } stuff: "{" stuffs "}" { "{" + stuffs + "}" } | NAME { name.string } | NUMBER { number.string } | STRING { string.string } | OP { None if op.string in ("{", "}") else op.string }
這個混亂是因爲我但願在描繪動做的花括號之間容許任意 Python 代碼,以及容許配對的大括號嵌套在其中。爲此,咱們使用了特殊標識符 OP,標記生成器用它生成可被 Python 識別的全部標點符號(返回一個類型爲 OP 標識符,用於多字符運算符,如 <= 或 ** )。在 Python 表達式中能夠合法地出現的惟一其它標識符是名稱、數字和字符串。所以,在動做的最外側花括號之間的「東西」彷佛是一組循環的 NAME | NUMBER | STRING | OP 。
嗚呼,這沒用,由於 OP 也匹配花括號,但因爲 PEG 解析器是貪婪的,它會吞掉結束括號,咱們就永遠看不到動做的結束。所以,咱們要對生成的解析器添加一些調整,容許動做經過返回 None 來使備選項失效。我不知道這是不是其它 PEG 解析器的標準配置——當我考慮如何解決右括號(甚至嵌套的符號)的識別問題時,立馬就想到了這個方法。它彷佛運做良好,我認爲這符合 PEG 解析的通常哲學。它能夠被視爲一種特殊形式的前瞻(我將在下面介紹)。
使用這個小調整,當出現花括號時,咱們可使 OP 上的匹配失效,它能夠經過 stuff 和 action 進行匹配。
有了這些東西,元語法能夠由輔助的元解析器解析,而且生成器能夠將它轉換爲新的元解析器,由此解析本身。更重要的是,新的元解析器仍然能夠解析相同的元語法。若是咱們使用新的元編譯器編譯元語法,則輸出是相同的:這證實生成的元解析器正常工做。
這是帶有動做的完整元語法。只要你把解析過程串起來,它就能夠解析本身:
@subheader """ from grammar import Grammar, Rule, Alt from token import OP """ start: metas rules ENDMARKER { Grammar(rules, metas) } | rules ENDMARKER { Grammar(rules, []) } metas: meta metas { [meta] + metas } | meta { [meta] } meta: "@" NAME STRING NEWLINE { (name.string, eval(string.string)) } rules: rule rules { [rule] + rules } | rule { [rule] } rule: NAME ":" alts NEWLINE { Rule(name.string, alts) } alts: alt "|" alts { [alt] + alts } | alt { [alt] } alt: items action { Alt(items, action) } | items { Alt(items, None) } items: item items { [item] + items } | item { [item] } item: NAME { name.string } | STRING { string.string } action: "{" stuffs "}" { stuffs } stuffs: stuff stuffs { stuff + " " + stuffs } | stuff { stuff } stuff: "{" stuffs "}" { "{" + stuffs + "}" } | NAME { name.string } | NUMBER { number.string } | STRING { string.string } | OP { None if op.string in ("{", "}") else op.string }
如今咱們已經有了一個能工做的元語法,能夠準備作一些改進了。
但首先,還有一個小麻煩要處理:空行!事實證實,標準庫的 tokenize 會生成額外的標識符來跟蹤非重要的換行符和註釋。對於前者,它生成一個 NL 標識符,對於後者,則是一個 COMMENT 標識符。以其將它們吸取進語法中(我已經嘗試過,但並不容易!),咱們能夠在 tokenizer 類中添加一段很是簡單的代碼,來過濾掉這些標識符。這是改進的 peek_token 方法:
def peek_token(self): if self.pos == len(self.tokens): while True: token = next(self.tokengen) if token.type in (NL, COMMENT): continue break self.tokens.append(token) self.report() return self.tokens[self.pos]
這樣就徹底過濾掉了 NL 和 COMMENT 標識符,所以在語法中再也不須要擔憂它們。
最後讓咱們對元語法進行改進!我想作的事情純粹是美容性的:我不喜歡被迫將全部備選項放在同一行上。我上面展現的元語法實際上並無解析本身,由於有這樣的狀況:
start: metas rules ENDMARKER { Grammar(rules, metas) } | rules ENDMARKER { Grammar(rules, []) }
這是由於標識符生成器(tokenizer)在第一行的末尾產生了一個 NEWLINE 標識符,此時元解析器會認爲這是該規則的結束。此外,NEWLINE 以後會出現一個 INDENT 標識符,由於下一行是縮進的。在下一個規則開始以前,還會有一個 DEDENT 標識符。
下面是解決辦法。爲了理解 tokenize 模塊的行爲,咱們能夠將 tokenize 模塊做爲腳本運行,併爲其提供一些文本,以此來查看對於縮進塊,會生成什麼樣的標識符序列:
$ python -m tokenize foo bar baz dah dum ^D
咱們發現它會產生如下的標識符序列(我已經簡化了上面運行的輸出):
NAME 'foo' NAME 'bar' NEWLINE INDENT NAME 'baz' NEWLINE NAME 'dah' NEWLINE DEDENT NAME 'dum' NEWLINE
這意味着一組縮進的代碼行會被 INDENT 和 DEDENT 標記符所描繪。如今,咱們能夠從新編寫元語法規則的 rule 以下:
rule: NAME ":" alts NEWLINE INDENT more_alts DEDENT { Rule(name.string, alts + more_alts) } | NAME ":" alts NEWLINE { Rule(name.string, alts) } | NAME ":" NEWLINE INDENT more_alts DEDENT { Rule(name.string, more_alts) } more_alts: "|" alts NEWLINE more_alts { alts + more_alts } | "|" alts NEWLINE { alts }
(我跨行地拆分了動做,以便它們適應 Medium 網站的窄頁——這是可行的,由於標識符生成器會忽略已配對的括號內的換行符。)
這樣作的好處是咱們甚至不須要更改生成器:這種改進的元語法生成的數據結構跟之前相同。一樣注意 rule 的第三個備選項,對此讓咱們寫:
start: | metas rules ENDMARKER { Grammar(rules, metas) } | rules ENDMARKER { Grammar(rules, []) }
有些人會以爲這比我以前展現的版本更乾淨。很容易容許兩種形式共存,因此咱們沒必要爭論風格。
在下一篇文章中,我將展現如何實現各類 PEG 功能,如可選條目、重複和前瞻。(說句公道話,我本打算把那放在這篇裏,可是這篇已寫太長了,因此我要把它分紅兩部分。)
本文內容與示例代碼的受權協議:CC BY-NC-SA 4.0
公衆號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫做、優質英文推薦與翻譯等等,歡迎關注哦。