練習 34:分析器
譯者:飛龍
自豪地采用谷歌翻譯
你現在有了一個解析器,它應該生成一個語法產生式對象樹。我會將其稱為“解析樹”,這意味著你可以從“解析樹的頂部開始,然后“遍歷”它,直到你訪問每個節點來分析整個程序。當你了解BSTree
和TSTree
數據結構時,你已經做了這樣的事情。你從頂部開始訪問了每個節點,并且你訪問的順序(深度優先,廣度優先,順序遍歷等)確定了節點的處理方式。你的解析樹具有相同的功能,編寫微型 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
類,然后創建IntExpr
和AddExpr
類。每個都僅僅擁有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 來說,只需假設有一個巨大的表(
TSTree
或dict
),所有變量都在這里。這意味著hello(x, y)
函數的x
和y
參數實際上是全局變量。 - 跟蹤函數的位置,以便以后運行它們。我們的微型 Python 只有簡單的函數,但是當
Interpreter
運行時,它需要“跳轉”到并運行它們。最好的辦法保留它們,便于之后使用。 - 檢查你可以想到的任何錯誤,例如使用中缺少的變量。這是棘手的,因為 Python 這樣的語言,在解釋器階段中進行更多的錯誤檢查。你應該決定在分析過程中,可能出現哪些錯誤并實現它們。例如,如果我嘗試使用未定義的變量,會發生什么?
- 如果你正確地實現了 Python
INDENT
語法,那么你的FuncCall
產生式應該有額外的代碼。解釋器將需要它來運行它,所以確保有一個實現它的方式。
研究性學習
- 這個練習已經很難了,但是如何創建一個更好的方式,來存儲變量,至少實現一個額外的作用域層級?記得“作用域”的概念是,
hello(x, y)
中的x, y
不影響hello
函數之外的你定義x
和y
。 - 在
Scanner
,Parser
和Analyzer
中實現賦值。這意味著我應該可以執行x = 10 + 14
,你可以處理它。
深入學習
研究“基于表達式”和“基于語句”的編程語言之間的區別。較短版本是一些只有表達式的語言,所以任何東西都有與之相關的某種(返回)值。其他語言的表達式擁有值,語句沒有,因此把它們賦給變量會失敗。Python 是哪種語言?