A正規化されたIRをCEK抽象機械で実行
前回で定義した言語とそのA正規化されたIRをインタプリタで実行したい。
というわけでまたしてもMatt Mightのブログを参考にする:
こちらのブログはCESKマシンの話だが、今のところ破壊的変更を言語機能として実装してないので、まずはCESKのステートSを抜いてCEK抽象機械での実行を目指す。
CEK抽象機械
CEK抽象機械というのは大雑把にいうとSECD抽象機械の仲間で、抽象機械の状態とその変遷関数を厳密に定義することで「プログラムの実行」をモデル化するもの。CEKという名前はこの抽象機械の状態を構成する三つの要素の頭文字(?)からきている。Control(実行する式)、Environment(変数環境)、Kontinuation(継続)の三つである。
(control, environment, kontinuation)
という状態を変遷関数step
に渡すとcontrol
の形を元に条件分岐する。
control
が簡単な式であるなら直ちに評価する。結果が値ならkontinuation
の先頭の継続をとってきて新しいcontrol
に変換し、environment
に評価した値を追加した上で新しい状態(control', environment', kontinuation')
を返す。結果が関数適用による新しい式と環境ならそのままそれを現在の継続と合わせて新しい状態(control', environment', kontinuation)
として返す。結果が条件分岐による新しい式なら新しい状態(control', environment, kontinuation)
として返す。などなど。
control
が複雑な式なら分解して、次に「実行するべき」式を新しいcontrol
に、それ以外はkontinuation
に追加する形で退避させておくことで新しい状態(control', environment', kontinuation')
を返す。
余談だが以前作ったS式言語のインタプリタも、作った後にCEK機械の再実装であることがわかった:
ある意味自然に導出されるようなインタプリタ形態なのかもしれない。
CEK抽象機械の詳細に関しては上記のMatt MightのブログやFelleisen他のSemantics Engineering with PLT Redexに詳しく載っている:
特に後者ではラムダ計算を愚直に実行するインタプリタから効率・機能を求めてCEK、CESK抽象機械に段階的に発展していく経緯やSECD抽象機械との比較なども載っていて面白い。
CEKマシンの実装
cek.ml
モジュールでCEK抽象機械を実装する。
まずは式を評価した結果となる値と、その値に変数を束縛する変数環境の二つを型として定義:
type val_ = | Int of int | Bool of bool | Closure of Ast.t * env and env = (string * val_) list
クロージャも値の一つで、クロージャは変数環境も持つ。なので値と変数環境は相互再帰的な型になる。
次は継続の型:
type kont = | Letkont of string * env * Ast.t * kont | Halt
A正規化したIRに特化したCEK抽象機械の特徴として、継続が非常に簡単になっている。「そのまま評価できない複雑な式」というのが必ずlet v = simple_exp in ...
という形になっている、というA正規化形の性質を利用してLetkont
という継続と、終端継続であるHalt
のみを定義。
CEK抽象機械の状態は「実行中」の(control, environment, kontinuation)
のタプルか、「評価結果」の値:
type cek = | Running of Ast.t * env * kont | Done of val_
値を直接返す「アトミックな式」の評価関数:
let eval_atomic e env = match e with | Ast.Int n -> Int n | Ast.Bool b -> Bool b | Ast.Fn _ -> Closure(e,env) | Ast.Var v -> List.assoc v env | _ -> failwith "Evaluating non-atomic expression as atomic"
値を継続に渡すためのヘルパ関数:
let apply_kont k v = match k with | Halt -> Done v | Letkont(s,e,c,k) -> Running(c,(s,v)::e,k)
継続と値を受け取り、新たなCEK状態を返す。継続がHalt
の場合は抽象機械が停止し、評価結果を返す。Letkont
の場合には継続の仮引数s
、継続の変数環境e
、継続の式c
そして残りの継続k
が保持されている。受け取った値v
をs
と合わせて変数環境e
を拡張し、その変数環境で式c
を評価した結果の値を継続k
に渡す、という新しいCEK状態を返す。
step
が抽象機械の動的な振る舞いの根本である変遷関数。
ASTノードに対してパターンマッチしているので各項目を個別に見ていく。
まずはアトミックな式から:
let step c e k = match c with | Ast.Int _ | Ast.Bool _ | Ast.Fn _ | Ast.Var _ -> apply_kont k (eval_atomic c e)
整数、真偽、関数、変数は直接eval_atomic
で値に変換してからapply_kont
で継続に渡している。
整数の和・比較:
| Ast.Add(e1,e2) -> (match eval_atomic e1 e, eval_atomic e2 e with | Int n, Int m -> apply_kont k (Int(n+m)) | _ -> failwith "Adding non-integer values") | Ast.Lt(e1,e2) -> (match eval_atomic e1 e, eval_atomic e2 e with | Int n, Int m -> apply_kont k (Bool(n<m)) | _ -> failwith "Comparing non-integer values")
+と<に関してはまず引数部分をアトミックな式として評価(もしアトミックでなければここでエラーになる)、引数が整数値であることを確認して(整数でなければまたしてもエラー)、和や比較結果を値として継続に渡している。
条件分岐:
| Ast.If(cond,e1,e2) -> (match eval_atomic cond e with | Bool b -> Running((if b then e1 else e2), e, k) | _ -> failwith "Conditional on non-boolean")
条件分岐に関しては(厳密なA正規化かどうかは置いておいて)条件部分のみアトミックな式になっているはずなのでeval_atomic
して結果がBool
であることを確かめ、その値によって次のcontrol式を選択して新しいCEK状態を返す。
let
式:
| Ast.Let(s,e1,e2) -> Running(e1,e,Letkont(s,e,e2,k))
すでに述べたとおり、A正規化したIRだと継続を拡張する唯一のケースがlet
式の評価。実際には何も評価せず、新しい継続を作った上で新しいCEK状態を返している。
関数適用:
| Ast.Call(f,es) -> (match eval_atomic f e with | Closure(Ast.Fn(ss,body),e') -> let vs = List.map (fun v -> eval_atomic v e) es in Running(body,(List.combine ss vs)@e', k) | _ -> failwith "Non-function in operator position")
関数部分、実引数部分すべてがアトミックな式であるはず。なのですべてに対してeval_atomic
して、関数がクロージャ値であることを確認した上で(関数本体の式、仮引数・実引数で拡張されたクロージャの変数環境、元の継続)
という新しいCEK状態を返す。
注意すべき点としてはすべてのeval_atomic
が終わった時点で現在の変数環境は破棄されること。関数の本体部分の式はクロージャの変数環境で実行されるし、それ以降の評価ための環境は継続に保存されている。
これでstep
関数は終わり。基本的に実行時エラーが多い。本来A正規化されたIRであれば通らないはずの条件分岐の部分なわけだが、IRの型がソース言語のASTと同一なのでこういった実行時チェックが必要になる。GADTなどで型としてA正規化を表現できればここら辺はスッキリしそう。
式を受け取り値を返すeval
関数:
let eval c = let rec f = function | Running(c,e,k) -> f(step c e k) | Done v -> v in f (Running(c,[],Halt))
(実引数の式、空の環境、空の継続)
というCEK状態を作成してstep
を繰り返し適用し、状態が「停止」した時点で評価結果として値を返している。
val_
型を文字列化するヘルパ関数も定義しておく:
let string_of_val = function | Int n -> string_of_int n | Bool b -> string_of_bool b | Closure _ -> "closure"
使ってみる
main.ml
を以下のように変更して使う:
let f s = Lexing.from_string s |> Parser.f Lexer.f |> Alpha.f |> Anormal.f let () = let e = read_line() |> f in e |> L.Ast.to_string |> print_endline; e |> L.Cek.eval |> L.Cek.string_of_val |> print_endline;
これで受け取った式をまずA正規化してその式を出力、その後CEK抽象機械上でA正規化された式を実行してその結果を出力する。
試してみる:
(+ 1 2) (+ 1 2) 3
(if (< 1 2) (+ 1 (+ 3 4)) 5) (let [g0 (< 1 2)] (if g0 (let [g1 (+ 3 4)] (+ 1 g1)) 5)) 8
とりあえずこれらの入力ではうまくいっているようだ。
今回のコード
(cek.ml
とmain.ml
以外は前回の記事のものを完全に流用)