Python 之父的解析器系列之三:生成一個 PEG 解析器

原題 | Generating a PEG Parserpython

做者 | Guido van Rossum(Python之父)程序員

譯者 | 豌豆花下貓(「Python貓」公衆號做者)數組

聲明 | 本翻譯是出於交流學習的目的,基於 CC BY-NC-SA 4.0 受權協議。爲便於閱讀,內容略有改動。緩存

首發地址:https://mp.weixin.qq.com/s/oj...數據結構

我已經在本系列第二篇文章中簡述瞭解析器的基礎結構,並展現了一個簡單的手寫解析器,根據承諾,咱們將轉向從語法中生成解析器。我還將展現如何使用@memoize裝飾器,以實現packrat 解析。 app

【這是 PEG 系列第 3 篇。參見第1篇第2篇函數

上篇文章咱們以一個手寫的解析器結束。給語法加上一些限制的話,咱們很容易從語法中自動生成這樣的解析器。(咱們稍後會解除那些限制。)學習

咱們須要兩個東西:一個東西讀取語法,並構造一個表現語法規則的數據結構;還有一個東西則用該數據結構來生成解析器。咱們還須要無聊的膠水,我就不提啦。優化

因此咱們在這創造的是一個簡單的編譯器編譯器(compiler-compiler)。我將語法符號簡化了一些,僅保留規則與備選項;這其實對於我在本系列的前面所用的玩具語法來講,已經足夠了。動畫

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

使用完整的符號,咱們能夠爲語法文件寫出語法:

grammar: rule+ ENDMARKER
rule: NAME ':' alternative ('|' alternative)* NEWLINE
alternative: item+
item: NAME | STRING

用個花哨的叫法,這是咱們的第一個元語法(語法的語法),而咱們的解析器生成器將是一個元編譯器(編譯器是一個程序,將其它程序從一種語言轉譯爲另外一種語言;元編譯器是一種編譯器,其輸入是一套語法,而輸出是一個解析器 )。

有個簡單地表示元語法的方法,主要是使用內置的數據類型:一條規則的右側只是由一系列的條目組成的列表,且這些條目只能是字符串。(Hack:經過檢查第一個字符是否爲引號,咱們能夠區分出NAMESTRING

至於規則,我用了一個簡單的 Rule 類,因此整個語法就是一些 Rule 對象。

這就是 Rule 類,省略了 __repr____eq__

class Rule:
    def __init__(self, name, alts):
        self.name = name
        self.alts = alts

調用它的是這個GrammarParser類(關於基類Parser ,請參閱我以前的帖子):

class GrammarParser(Parser):
    def grammar(self):
        pos = self.mark()
        if rule := self.rule():
            rules = [rule]
            while rule := self.rule():
                rules.append(rule)
            if self.expect(ENDMARKER):
                return rules    # <------------- final result
        self.reset(pos)
        return None
    def rule(self):
        pos = self.mark()
        if name := self.expect(NAME):
            if self.expect(":"):
                if alt := self.alternative():
                    alts = [alt]
                    apos = self.mark()
                    while (self.expect("|")
                           and (alt := self.alternative())):
                        alts.append(alt)
                        apos = self.mark()
                    self.reset(apos)
                    if self.expect(NEWLINE):
                        return Rule(name.string, alts)
        self.reset(pos)
        return None
    def alternative(self):
        items = []
        while item := self.item():
            items.append(item)
        return items
    def item(self):
        if name := self.expect(NAME):
            return name.string
        if string := self.expect(STRING):
            return string.string
        return None

注意 ENDMARKER ,它用來確保在最後一條規則後沒有遺漏任何東西(若是語法中出現拼寫錯誤,可能會致使這種狀況)。

我放了一個簡單的箭頭,指向了 grammar() 方法的返回值位置,返回結果是一個存儲 Rule 的列表。

其他部分跟上篇文章中的 ToyParser 類很類似,因此我不做解釋。

只需留意,item() 返回一個字符串,alternative() 返回一個字符串列表,而 rule() 中的 alts 變量,則是一個由字符串列表組成的列表。

而後,rule() 方法將規則名稱(一個字符串)與 alts 結合,放入 Rule 對象。

若是把這份代碼用到包含了咱們的玩具語法的文件上,則 grammar() 方法會返回如下的由 Rule 對象組成的列表:

[
  Rule('statement', [['assignment'], ['expr'], ['if_statement']]),
  Rule('expr', [['term', "'+'", 'expr'],
                ['term', "'-'", 'term'],
                ['term']]),
  Rule('term', [['atom', "'*'", 'term'],
                ['atom', "'/'", 'atom'],
                ['atom']]),
  Rule('atom', [['NAME'], ['NUMBER'], ["'('", 'expr', "')'"]]),
  Rule('assignment', [['target', "'='", 'expr']]),
  Rule('target', [['NAME']]),
  Rule('if_statement', [["'if'", 'expr', "':'", 'statement']]),
]

既然咱們已經有了元編譯器的解析部分,那就建立代碼生成器吧。

把這些聚合起來,就造成了一個基本的元編譯器:

def generate_parser_class(rules):
    print(f"class ToyParser(Parser):")
    for rule in rules:
        print()
        print(f"    @memoize")
        print(f"    def {rule.name}(self):")
        print(f"        pos = self.mark()")
        for alt in rule.alts:
            items = []
            print(f"        if (True")
            for item in alt:
                if item[0] in ('"', "'"):
                    print(f"            and self.expect({item})")
                else:
                    var = item.lower()
                    if var in items:
                        var += str(len(items))
                    items.append(var)
                    if item.isupper():
                        print("            " +
                              f"and ({var} := self.expect({item}))")
                    else:
                        print(f"            " +
                              f"and ({var} := self.{item}())")
            print(f"        ):")
            print(f"            " +
              f"return Node({rule.name!r}, [{', '.join(items)}])")
            print(f"        self.reset(pos)")
        print(f"        return None")

這段代碼很是難看,但它管用(某種程度上),無論怎樣,我打算未來重寫它。

在"for alt in rule.alts"循環中,有些代碼細節可能須要做出解釋:對於備選項中的每一個條目,咱們有三種選擇的可能:

  • 若是該條目是字符串字面量,例如'+' ,咱們生成self.expect('+')
  • 若是該條目所有是大寫,例如NAME ,咱們生成(name := self.expect(NAME))
  • 其它狀況,例如該條目是expr,咱們生成 (expr := self.expr())

若是在單個備選項中出現多個相同名稱的條目(例如term '-' term),咱們會在第二個條目後附加一個數字。這裏還有個小小的 bug,我會在之後的內容中修復。

這只是它的一部分輸出(完整的類很是無聊)。不用擔憂那些零散的、冗長的 if (True and … ) 語句,我使用它們,以便每一個生成的條件都可以以and 開頭。Python 的字節碼編譯器會優化它。

class ToyParser(Parser):
    @memoize
    def statement(self):
        pos = self.mark()
        if (True
            and (assignment := self.assignment())
        ):
            return Node('statement', [assignment])
        self.reset(pos)
        if (True
            and (expr := self.expr())
        ):
            return Node('statement', [expr])
        self.reset(pos)
        if (True
            and (if_statement := self.if_statement())
        ):
            return Node('statement', [if_statement])
        self.reset(pos)
        return None
    ...

注意@memoize 裝飾器:我「偷運」(smuggle)它進來,以便轉向另外一個主題:使用記憶法(memoization)來加速生成的解析器。

這是實現該裝飾器的 memoize() 函數:

def memoize(func):
    def memoize_wrapper(self, *args):
        pos = self.mark()
        memo = self.memos.get(pos)
        if memo is None:
            memo = self.memos[pos] = {}
        key = (func, args)
        if key in memo:
            res, endpos = memo[key]
            self.reset(endpos)
        else:
            res = func(self, *args)
            endpos = self.mark()
            memo[key] = res, endpos
        return res
return memoize_wrapper

對於典型的裝飾器來講,它的嵌套函數(nested function)會替換(或包裝)被裝飾的函數(decorated function),例如 memoize_wrapper() 會包裝 ToyParser 類的 statement() 方法。

由於被包裝的函數(wrapped function)是一個方法,因此包裝器實際上也是一個方法:它的第一個參數是 self ,指向 ToyParser 實例,後者會調用被裝飾的函數。

包裝器會緩存每次調用解析方法後的結果——這就是爲何它會被稱爲「口袋老鼠解析」(packrat parsing)!

這緩存是一個字典,元素是存儲在 Parser 實例上的那些字典。

外部字典的 key 是輸入的位置;我將 self.memos = {} 添加到 Parser.__init__() ,以初始化它。

內部字典按需添加,它們的 key 由方法及其參數組成。(在當前的設計中沒有參數,但咱們應該記得 expect(),它剛好有一個參數,並且給它新增通用性,幾乎不須要成本。 )

一個解析方法的結果被表示成一個元組,由於它正好有兩個結果:一個顯式的返回值(對於咱們生成的解析器,它是一個 Node,表示所匹配的規則),以及咱們從 self.mark() 中得到的一個新的輸入位置。

在調用解析方法後,咱們會在內部的記憶字典中同時存儲它的返回值(res)以及新的輸入位置(endpos)。

再次調用相同的解析方法時(在相同的位置,使用相同的參數),咱們會從緩存中取出那兩個結果,並用 self.reset() 來向前移動輸入位置,最後返回那緩存中的返回值。

緩存負數的結果也很重要——實際上大多數對解析方法的調用都是負數的結果。在此狀況下,返回值爲 None,而輸入位置不會變。你能夠加一個assert 斷言來檢查它。

注意:Python 中經常使用的記憶法是在 memoize() 函數中將緩存定義成一個局部變量。但咱們不這麼作:由於我在一個最後時刻的調試會話中發現,每一個 Parser 實例都必須擁有本身的緩存。然而,你能夠用(pos, func, args) 做爲 key,以擺脫嵌套字典的設計。

下週我將統覽代碼,演示在解析示例程序時,全部這些模塊實際是如何配合工做的。

我仍然在抓頭髮中(譯註:極度發愁),如何以最佳的方式將協同工做的標記生成器緩衝、解析器和記憶緩存做出可視化。或許我會設法生成動畫的 ASCII 做品,而不只僅是跟蹤日誌的輸出。(譯註:感受他像是在開玩笑,但很難譯出這句話的原味。建議閱讀原文。)

本文及示例代碼的受權協議: CC BY-NC-SA 4.0

英文原文: https://medium.com/@gvanrossum_83706/generating-a-peg-parser-520057d642a9

做者簡介: Guido van Rossum,是 Python 的創造者,一直是「終身仁慈獨裁者」,直到2018年7月12日退位。目前,他是新的最高決策層的五位成員之一,依然活躍在社區中。

譯者簡介: 豌豆花下貓,生於廣東畢業於武大,現爲蘇漂程序員,有一些極客思惟,也有一些人文情懷,有一些溫度,還有一些態度。公衆號:「Python貓」(python_cat)。

公衆號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫做、優質英文推薦與翻譯等等,歡迎關注哦。

相關文章
相關標籤/搜索