用 Python 從零開始寫一個簡單的直譯器(4)

fzr發表於2015-10-23

在本系列的前三篇文章中,我們已經為IMP語言建立了詞法分析器解析器 和 抽象語法樹AST。我們甚至寫了自己的解析器組合庫。在這最後一篇文章中,我們將會實現直譯器的最後一個元件:求值器。

一起來了解一下通常一個程式是如何進行求值的。在任意給定的時間,有一些“控制點”,表明了程式下一步將要求值的語句。當下一個語句求值完畢,它通過推進“控制點”和改變變數值來修正程式狀態。

為了給一個IMP程式求值,我們需要三樣東西:

  1. 控制點—我們需要知道要求的值的下一條語句或表示式。
  2. 環境—我們需要一種調整“程式狀態”的方法。
  3. 求值函式—我們需要知道如何調整每個語句或表示式的狀態和控制點。

至少對IMP而言,控制點是最簡單的。我們已經將中間表示都安排在一棵樹狀結構中了。只需要為高階語句呼叫求值函式,該函式將為其中的語句和表示式遞迴呼叫求值函式。我們基本上使用Python的控制點來作為我們自己的控制點。對於具有更復雜的控制結構像函式或異常之類的語言來說,這樣做可能不那麼簡單,但對於IMP我們可以讓它簡單一些。

環境也很簡單。IMP只有全域性變數,所以我們可以用一個簡單的Python字典塑造環境。不論什麼時候,只要賦值發生了,我們就去更新字典裡的變數值。

求值函式是我們唯一真正需要考慮的事情。每一種表示式都有一個求值函式,它將根據當前的環境返回一個值。算術表示式會返回一個整數,布林表示式會返回真(true)與假(false)。表示式沒有副作用,所以不會修改環境。每種宣告語句也會有一個求值函式。宣告語句的行為是修改環境,所以沒有結果返回。

定義求值函式

我們會把求值函式定義為AST類的方法。這樣一來,每個函式都能直接訪問到它所求值的結構。這裡是算術表示式的函式集:

你會注意到,當程式設計師使用了一個尚未定義的變數(不在環境字典中)時,我們新增了一些額外的邏輯。為了儘量簡單,避免再寫一個錯誤處理系統,我們給所有未定義的變數賦值為0。

BinopAexp中我們通過丟擲一個RuntimeError來處理“未知操作符”。解析器不能使用未知操作符建立一個AST,所以在實際中我們不用擔心這個。

這裡是布林表示式的函式:

這些都比較簡單直觀。它們只是使用了Python內建的關係和邏輯運算子。

這裡是每種語句對應的求值函式:

對於AssignStatement,我們只是對右邊的算術表示式求值,然後用結果值更新環境。程式設計師可以自由地重新定義已分配的變數。

在 CompoundStatement中,我們只是挨個對每個語句求值。要記住, CompoundStatement可以用於任何可以使用語句的地方,所以比較長的語句鏈會被編碼為巢狀複合語句。

IfStatement中,我們首先求值條件布林表示式。如果結果為真,就對真分支的語句求值。如果結果為假,而且假分支的語句又存在,就對假分支的語句求值。

WhileStatement中,我們對開頭的條件求值以檢測迴圈體是否應該再執行一遍。

每次迴圈迭代結束後都應該對條件求值,以檢查它是否為真。

組合

我們已經建立好直譯器的每一個主要部分。只需要一個驅動程式來將它們整合起來:

程式只需要一個引數:要解釋的檔名。它讀入檔案並傳給詞法分析器和解析器,如果發現錯誤會報告出來。我們從解析結果中抽象出AST,然後以一個空字典作為起始環境對AST求值。由於IMP不能在終端輸出任何值,我們只能在程式完成後,列印環境中的所有值,以此確認整個過程的實際發生。

這裡是一個典型的階乘例項:

求值結果如下:

總結

在前面的四篇文章中,我們從頭開始為一門簡單語言構建了一個直譯器。儘管語言本身不是非常有用,但直譯器是可擴充套件的,而且它的主要元件(詞法分析器和解析器組合庫)都是可重用的。

我希望這能給嘗試語言設計的人們提供一個很好的起點。這裡有一些實踐建議:

  • 儘量定義區域性變數(比如,迴圈外面不應使用迴圈內的變數);
  • 新增一個for迴圈結構;
  • 為使用者輸入輸出新增scan和print宣告;
  • 新增函式。

相關文章