笨辦法學 Python · 續 練習 34:分析器

練習 34:分析器

原文:Exercise 34: Analyzers

譯者:飛龍

協議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

你現在有了一個解析器,它應該生成一個語法產生式對象樹。我會將其稱為“解析樹”,這意味著你可以從“解析樹的頂部開始,然后“遍歷”它,直到你訪問每個節點來分析整個程序。當你了解BSTreeTSTree數據結構時,你已經做了這樣的事情。你從頂部開始訪問了每個節點,并且你訪問的順序(深度優先,廣度優先,順序遍歷等)確定了節點的處理方式。你的解析樹具有相同的功能,編寫微型 Python 解釋器的下一步是遍歷樹并分析它。

分析器的任務是,在你的語法中找到語義錯誤,并修復或添加下一階段需要的信息。語義錯誤是錯誤,雖然語法正確,但并不適合 Python 程序。這可以是一個尚未定義的遍歷,也可以是不符合邏輯的代碼,它根本沒有意義。一些語言語法是如此松散,分析器必須做更多的工作來修復解析樹。其他語言很容易解析和處理,甚至不需要分析器的步驟。

為了編寫分析器,你需要一種方法來訪問解析樹中的每個節點,分析錯誤,并修復任何缺少的信息。有三種通用方法可以用于實現它:

  • 你創建一個分析器,它知道如何更新每個語法產生式。它將以和解析器相似的方式遍歷解析樹,對每種生產式類型都擁有一個函數,但他的任務是更改,更新和檢查產生式。
  • 你改變你的語法產生式,讓他們知道如何分析自己的狀態。那么你的分析器就僅僅是一個引擎,它遍歷解析樹,調用每個產生式的analyze()方法。使用這種風格,你將需要一些狀態,它們會傳遞給每個語法產生式類,這個類應該是第三個類。
  • 你創建一組單獨的類來實現最終分析后的樹,你可以將其傳遞給解釋器。通過許多方式,你將使用一組新的類來映射語法分析器的語法產生式,這些新的類接受全局狀態,語法產生式,并配置其__init__,使其為分析后的結果。

我建議你現在使用 #2 或 #3 來完成挑戰練習。

訪客模式

“訪問者模式”是面向對象語言中非常常見的技術,其中你可以創建一些類,它們知道被“訪問”時應該做什么。這可以讓你將處理某個類的代碼集成到這個類。這樣做的優點是,你不需要大型的if語句來檢查類上的類型,來了解該做什么。相反,你只需創建一個類似于這個的類:

class Foo(object):
    def visit(self, data):
        # do stuff to self for foo

一旦你擁有了這個類(visit可以叫任何東西),你可以遍歷列表來調用它。

for action in list_of_actions:
    action.visit(data)

你將使用這種模式用于 #2 或 #3 風格的分析器;唯一的區別是:

  • 如果你決定,你的語法產生式也將是分析結果,那么你的analyze()函數(也就是我們的visit())只會將該數據存儲在產生式類,或者在提供給它的狀態中。
  • 如果你決定,你的語法產生式將為解釋器生成另一組類(請參閱練習 35),那么每次analyze的調用都將返回一個新對象,該對象將放入列表中以供以后使用,或將其作為子樹附加到當前對象。

我將介紹第一種情況,其中你的語法產生式也是你的分析器結果。這適用于我們簡單的微型 Python 腳本,你應該遵循這種風格。如果你想嘗試其他的設計,那么你可以之后嘗試。

簡短的微型 Python 分析器

警告

如果你想自己嘗試,為你的語法產生式嘗試實現訪客模式,那么你應該停在這里。我將給出一個相當完整但簡單的例子,它充滿了障礙。

訪客模式背后的概念似乎是奇怪的,但它是完全有意義的。每個語法產生式都知道在不同階段應該做什么,所以你可以把這個階段代碼放在需要的數據附近。為了演示這個,我寫了一個小型的偽造的PunyPyAnalyzer,它僅僅使用訪客模式打印出解析。我只完成一個語法產生式的樣例,所以你可以理解這是如何完成的。我不想給你太多的線索。

我做的第一件事是,定義一個Production類,我的所有語法產生式都將繼承它。

class Production(object):
    def analyze(self, world):
        """Implement your analyzer here."""

它擁有我的初始的analyze()方法,并接受我們隨后使用的PunyPyWorld。第一個語法產生式的示例使FuncCall產生式:

class FuncCall(Production):

    def __init__(self, name, params):
        self.name = name
        self.params = params

    def analyze(self, world):
        print("> FuncCall: ", self.name)
        self.params.analyze(world)

函數調用有名稱和params,它是一個Parameters產生式類,用于函數調用的參數。看看analyze()方法,你會看到第一個訪客函數。當你訪問PunyPyAnalyzer時,你將看到如何運行,但是請注意,此函數之后在每個函數的參數上調用param.analyze(world)

class Parameters(Production):

    def __init__(self, expressions):
        self.expressions = expressions

    def analyze(self, world):
        print(">> Parameters: ")
        for expr in self.expressions:
            expr.analyze(world)

這就產生了Parameters類,它包含每個表達式,它們組成函數的參數。Parameters.analyze僅僅遍歷它的表達式列表,其中我們擁有兩個:

class Expr(Production): pass

class IntExpr(Expr):
    def __init__(self, integer):
        self.integer = integer

    def analyze(self, world):
        print(">>>> IntExpr: ", self.integer)

class AddExpr(Expr):
    def __init__(self, left, right):
        self.left = left
        self.right = right

    def analyze(self, world):
        print(">>> AddExpr: ")
        self.left.analyze(world)
        self.right.analyze(world)

在這個例子中,我只添加了兩個數字,但是我創建一個基本的Expr類,然后創建IntExprAddExpr類。每個都僅僅擁有analyze()方法,打印出其內容。

因此,我們有用于分析樹的類,我們可以做一些分析。我們需要的第一件事是一個世界,它可以跟蹤變量定義、函數、以及我們的Production.analyze()方法所需的其他東西。

class PunyPyWorld(object):

    def __init__(self, variables):
        self.variables = variables
        self.functions = {}

當調用任何Production.analyze()方法時,PunyPyWorld對象被傳遞給它,因此analyze()方法知道世界的狀態。它可以更新變量,尋找函數,并在世界中執行任何所需的事情。

然后我們需要一個PunyPyAnalyzer,它可以接受解析樹和世界,并使所有的語法產生式運行:

class PunyPyAnalyzer(object):
    def __init__(self, parse_tree, world):
        self.parse_tree = parse_tree
        self.world = world

    def analyze(self):
        for node in self.parse_tree:
            node.analyze(self.world)

函數的簡單調用hello(10 + 20)的配置相當簡單。

variables = {}
world = PunyPyWorld(variables)
# simulate hello(10 + 20)
script = [FuncCall("hello",
            Parameters(
                [AddExpr(IntExpr(10), IntExpr(20))])
            )]
analyzer = PunyPyAnalyzer(script, world)
analyzer.analyze()

要確保你理解了我構造script的方式。注意到第一個參數是一個列表了嘛?

解析器與分析器

在這個例子中,我假設PunyPyParser已將NUMBER記號轉換為整數。在其他語言中,你可能只擁有記號,并讓PunyPyAnalyzer進行轉換。這一切都取決于,你想讓錯誤發生在哪里,以及哪里可以做最有用的分析。如果你將工作放在解析器中,那么你可以馬上給出格式化方面的早期錯誤。如果將其放在分析器中,那么你可以給出錯誤,使用整個解析文件來有所幫助。

挑戰練習

所有這些analyze()方法的要點不僅僅是打印出來,而是改變每個Production子類的內部狀態,以便解釋器可以像腳本一樣運行它。你在這個練習中的任務是,接受你的語法產生式類(可能與我的不同)并進行分析。

隨意借鑒我的出發點。如果需要,可以使用我的分析器和我的世界,但是你應該嘗試首先編寫自己的分析器。你還應該將練習 33 中的產生式類與我的比較。你的更好嗎?它們能支持這種設計嗎?如果他們不能則改變它們。

你的分析器需要做一些事情才能使解釋器正常工作:

  • 跟蹤變量定義。在一個實際的語言中,這將需要一些非常復雜的嵌套表,但是對于微型 Python 來說,只需假設有一個巨大的表(TSTreedict),所有變量都在這里。這意味著hello(x, y)函數的xy參數實際上是全局變量。
  • 跟蹤函數的位置,以便以后運行它們。我們的微型 Python 只有簡單的函數,但是當Interpreter運行時,它需要“跳轉”到并運行它們。最好的辦法保留它們,便于之后使用。
  • 檢查你可以想到的任何錯誤,例如使用中缺少的變量。這是棘手的,因為 Python 這樣的語言,在解釋器階段中進行更多的錯誤檢查。你應該決定在分析過程中,可能出現哪些錯誤并實現它們。例如,如果我嘗試使用未定義的變量,會發生什么?
  • 如果你正確地實現了 Python INDENT語法,那么你的FuncCall產生式應該有額外的代碼。解釋器將需要它來運行它,所以確保有一個實現它的方式。

研究性學習

  • 這個練習已經很難了,但是如何創建一個更好的方式,來存儲變量,至少實現一個額外的作用域層級?記得“作用域”的概念是,hello(x, y)中的x, y不影響hello函數之外的你定義xy
  • ScannerParserAnalyzer中實現賦值。這意味著我應該可以執行x = 10 + 14,你可以處理它。

深入學習

研究“基于表達式”和“基于語句”的編程語言之間的區別。較短版本是一些只有表達式的語言,所以任何東西都有與之相關的某種(返回)值。其他語言的表達式擁有值,語句沒有,因此把它們賦給變量會失敗。Python 是哪種語言?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容