用 Haskell 編寫 CEK 風格的直譯器
作者: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 Felleisen 和 Dan Friedman 設計的一種 λ 演算的機械模型(mechanical model),適用於設計和實現直譯器。除了高效之外,CEK 風格的直譯器還具有如下特徵:
- 易於新增複雜的語言特性,比如 continuation
- 易於中止解釋過程,並得到 stack trace
- 易於引入對執行緒的支援
- 易於在偵錯程式中單步執行
這篇文章使用 Haskell 語言來介紹 CEK 狀態機。讀者需要熟悉 λ 演算,並理解它是一個不失一般性的程式語言。否則請先閱讀下面幾篇文章,或者閱讀 Benjamin Pierce 的 Types and Programming Languages 一書。
- Implementing Java as a CESK machine, in Java
- Writing an interpreter, CESK-style
- Church encodings in JavaScript
- Compiling up to the λ-calculus
- 7 lines of code, 3 minutes: Implement a programming language
- Lambda-calculus in C++ templates
- Church encodings in Scheme
- Memoizing recursive functions in Javascript with the Y combinator
首先,我們需要如下的資料型別,來定義 λ 演算的抽象語法樹。
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
來避免命名衝突(就像 count
和 kount
),所以我猜測 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 狀態機就實現好了。
相關文章
- JavaScript 編寫的迷你 Lisp 直譯器JavaScriptLisp
- 前端與編譯原理——用 JS 寫一個 JS 直譯器前端編譯原理JS
- 前端與編譯原理——用JS寫一個JS直譯器前端編譯原理JS
- 【譯】使用 Python 編寫虛擬機器直譯器Python虛擬機
- 軟體編寫風格
- 用java寫一個lisp 直譯器JavaLisp
- 淺談彙編器、編譯器和直譯器編譯
- 如何使用Python編寫一個Lisp直譯器PythonLisp
- PEP 8 程式程式碼的編寫風格指南
- 「 giao-js 」用js寫一個js直譯器JS
- 用 JavaScript 寫一個超小型編譯器JavaScript編譯
- 優步公司的Go語言編寫風格指南Go
- 用java寫lisp 直譯器 (10 實現物件和類)JavaLisp物件
- 源語言、目標語言、翻譯器、編譯器、直譯器編譯
- 從編譯原理看一個直譯器的實現編譯原理
- 自己動手寫basic直譯器 一
- 如何使用dotnet core 編寫REST風格APIRESTAPI
- 編寫可維護的JavaScript-程式設計風格JavaScript程式設計
- javascript編寫一個簡單的編譯器JavaScript編譯
- 使用C編譯器編寫shellcode編譯
- 王垠:怎樣寫一個直譯器
- 良好的HTML編碼風格HTML
- 關於Basic程式直譯器及編譯原理的簡單化(2)---C++封裝好的Basic直譯器 (轉)C程式編譯原理C++封裝
- 寫給小白的開源編譯器編譯
- 用 Python 從零開始寫一個簡單的直譯器(3)Python
- 用 Python 從零開始寫一個簡單的直譯器(4)Python
- 用 Python 從零開始寫一個簡單的直譯器(2)Python
- 用 golang 寫一個語言(編譯器,虛擬機器)Golang編譯虛擬機
- AI在用 | 用ChatGPT、Kimi克隆自己的寫作風格AIChatGPT
- 谷歌的JavaScript編寫風格中 13點值得我們注意的!谷歌JavaScript
- HVM:Rust編寫的比Haskell GHC更好的執行時RustHaskell
- [譯]編寫可以複用的 HTML 模板HTML
- 直譯器模式模式
- 用Java寫編譯器(1)- 詞法和語法分析Java編譯語法分析
- JavaScript編碼風格指南JavaScript
- JavaScript 編碼風格指南JavaScript
- 機器學習之光:神經風格遷移的直觀指南!機器學習
- 用 Python 實現 Python 直譯器Python