用 Haskell 編寫 CEK 風格的直譯器

Xiao Jia發表於2013-01-16

作者:Matt Might
譯者:Xiao Jia
本文連結:http://blog.xiao-jia.com/2013/01/15/cek-machines/

這篇文章基本翻譯自 Matt Might 的 Writing CEK-style interpreters (or semantics) in Haskell,略去了我認為無關緊要的部分,在不容易理解的地方加入了新的內容。

CEK 狀態機(CEK machine)是由 Matthias FelleisenDan Friedman 設計的一種 λ 演算的機械模型(mechanical model),適用於設計和實現直譯器。除了高效之外,CEK 風格的直譯器還具有如下特徵:

  • 易於新增複雜的語言特性,比如 continuation
  • 易於中止解釋過程,並得到 stack trace
  • 易於引入對執行緒的支援
  • 易於在偵錯程式中單步執行

這篇文章使用 Haskell 語言來介紹 CEK 狀態機。讀者需要熟悉 λ 演算,並理解它是一個不失一般性的程式語言。否則請先閱讀下面幾篇文章,或者閱讀 Benjamin Pierce 的 Types and Programming Languages 一書。

首先,我們需要如下的資料型別,來定義 λ 演算的抽象語法樹。

type Var = String
data Lambda = Var :=> Exp
data Exp = Ref Var
         | Lam Lambda
         | Exp :@ Exp

下表給出了上述資料型別和 λ 演算的對應關係:

λ 演算   Exp 型別
v        Ref "v"
λv.e     Lam ("v" :=> e)
f(e)     f :@ e

注意,這裡使用了以冒號開頭的型別建構函式(:=>:@),詳情可以參考這裡這裡。此外,本文使用 Haskell 語言的風格,和一般慣用的方式略有不同,本文更傾向於使用貼近數學表示的形式。


在實現 CEK 狀態機之前,我們先來回顧一下狀態機是什麼,以及如何用狀態機來描述一個直譯器。

從數學上說,一個狀態機包括一個狀態空間和一個轉移關係,這個轉移關係確定了一個狀態的後續狀態。在程式碼中,狀態空間表示為一個型別,而這個型別的值就是一個狀態。我們使用 Σ 作為狀態空間的型別名。轉移關係在最簡單的情況下是確定性的,也就是說,一個狀態只有唯一的後續狀態。在這一條件下,我們可以用一個全函式來描述這個轉移關係。我們稱這樣一個函式為 step

step :: Σ -> Σ

為了執行一個確定性狀態機,我們需要從一個狀態轉移到另一個狀態,直至達到終止狀態。函式 terminal 描述了這一過程:

terminal :: (Σ -> Σ) -> (Σ -> Bool) -> Σ -> Σ
terminal step isFinal ς0 | isFinal ς0 = ς0
                         | otherwise  = terminal step isFinal (step(ς0))

為了使用該函式,我們還需要提供一個 isFinal 函式,來判斷當前狀態是否有後續狀態。其型別如下:

isFinal :: Σ -> Bool

一個狀態機的結構就是這樣了。為了使用狀態機來描述一個直譯器,我們需要一個 inject 函式,把一個程式對映到一個狀態:

inject :: Program -> Σ

對於 λ 演算來說,type Program = Exp

有了這些函式之後,就很容易定義直譯器的求值函式了:

evaluate :: Program -> Σ
evaluate pr = terminal step isFinal (inject(pr))

求值函式將一個程式對映到它的終止狀態;如果這個程式不終止,這個函式的計算過程也不終止。


下面來說 CEK 狀態機。CEK 狀態機由三部分組成:控制部分(control)、環境部分(environment)、continuation 部分。一般在程式中命名 continuation 的時候,都會使用 Kontinuation 來避免命名衝突(就像 countkount),所以我猜測 CEK 這個名字就是這樣來的(Control, Environment, Kontinuation)。

控制部分就是當前正在求值的表示式。環境部分是一個從變數到值的對映。環境是當前表示式的區域性求值上下文。continuation 部分是當前的棧空間,儲存了之前的呼叫幀(活動記錄)。continuation 是當前表示式的動態求值上下文。每一幀都表示了一個等待求值的上下文。

在 Haskell 中,狀態空間 Σ 表示為一個三元組:

type Σ    = (Exp,Env,Kont)
data D    = Clo (Lambda, Env)
type Env  = Var -> D
data Kont = Mt
          | Ar (Exp,Env,Kont)
          | Fn (Lambda,Env,Kont)

型別 D 包括了所有的值。對於基本的 λ 演算來說,只有一種值,那就是閉包(closure)。一個閉包包括一個 λ 項和一個環境,該環境定義了該 λ 項中自由變元的值。如果我們要在語言中增加其它型別比如整數或字串,就應該擴充型別 D。(D 這個名字來源於程式語言的歷史,它同時表示“domain of values”和“denotable values”。)

有三種型別的 continuation:空的 continuation(Mt),正在等待求值的參數列達式(Ar),以及正在對引數求值的函式(Fn)。

為了使用這個 CEK 狀態機,首先需要將表示式對映到狀態:

inject :: Exp -> Σ
inject (e) = (e, ρ0, Mt)
 where ρ0 :: Env
       ρ0 = \ x -> error $ "no binding for " ++ x

step 函式向前執行一步操作:

step :: Σ -> Σ

step (Ref x, ρ, κ)                    
   = (Lam lam,ρ',κ) where Clo (lam, ρ') = ρ(x)

step (f :@ e, ρ, κ)                  
   = (f, ρ,  Ar(e, ρ, κ))

step (Lam lam, ρ, Ar(e, ρ', κ))      
   = (e, ρ', Fn(lam, ρ, κ))

step (Lam lam, ρ, Fn(x :=> e, ρ', κ)) 
   = (e, ρ' // [x ==> Clo (lam, ρ)], κ)

下面是剛才用到的兩個輔助函式:

(==>) :: a -> b -> (a,b)
(==>) x y = (x,y)

(//) :: Eq a => (a -> b) -> [(a,b)] -> (a -> b)
(//) f [(x,y)] = \ x' ->
                 if (x == x') 
                 then y
                 else f(x')

非形式化地,step 函式一共有四種情況:

  • 如果正在求值一個變元的引用,直接在環境中查詢即可
  • 如果正在求值一個函式應用,首先求值函式本身
  • 如果函式已經求值完畢,繼續對引數求值
  • 如果引數已經求值完畢,將函式應用到引數進行求值

終止狀態的判斷很簡單,只要檢查是不是一個空的 continuation 就行了:

isFinal :: Σ -> Bool
isFinal (Lam _, ρ, Mt) = True
isFinal _              = False

至此,一個 CEK 狀態機就實現好了。

相關文章