めざそう言語処理系の沼 〜shift/resetへの旅 〜 その14 ステップ実行インタプリタ
ここまで実装してきたインタプリタは
- 式を表す文字列をパースし
- 評価して
- 結果の値を表す文字列を返していた。
もちろんそれで正しいのだが、インタプリタの中でどのような挙動になって式が値に変換されているのかがわかりにくい。
限定継続のようにかなり微妙な動きをする(そして微妙なバグが潜り込みそうな)言語機能を実装する場合、もう少し細かく内部状態がわかる「デバッグモード」がほしい。
というわけで、新しくstepwise
というexecutableが作れるようにする。
このexecutableは文字どおりステップ実行のためのインタプリタで、Execute.eval
やExecute.apply_cont
が走ってExecute.res
型の途中結果をトランポリンに返すたびにその情報をプリント出力する。
前回からの差分:
Comparing v0.13...v0.14 · zehnpaard/kontlang · GitHub
今回の変更のために以下のことをしている
- 値と継続の定義を共通のモジュールに移す
- 継続をlist listに
- 継続と変数環境を文字列化できるようにする
- ステップ実行インタプリタ
前者二つは限定継続を実装するための下準備だが、三番目の「継続と変数環境を文字列化できるようにする」のために前倒しした(継続の文字列化のロジックを後で大きく修正するのを避けたかったので)。
値と継続の定義を共通のモジュールに移す
限定継続を実装すると「継続の一部」を値として取り扱う必要が出てくる。
一番素直な実装だと、Val.t
型のバリアントの一つの内部データとしてCont.t
型(あるいはその一部)を持たせることになる。しかし継続にはすでに「ここまで評価した結果」である値が保持されているので、相互再帰的になってしまう。
OCamlだと相互再帰的なデータ型が定義できるがその場合一つのモジュールで両方の型を定義するのが一番簡単。この件に関しては以前記事に書いた:
というわけでCont.t
とVal.t
を相互再帰的にするために、新しくValcont
モジュールを作成して両者をここで定義する。さらにValcont
内で別々のVal
とCont
モジュールを定義して、Val.ml
内ではinclude Valcont.Val
、Kont.ml
ではinclude Valcont.Cont
としている。これで他のモジュールからは今までどおりVal
とCont
が使える。
本当はValcont
モジュールがVal
とCont
モジュール以外からは隠蔽したくなるのだが、調べてみたところ難しいようだ。
今になって思えばこの変更はCont.t
の文字列化にあまり影響しないので、この段階でやる必要はなかった・・・
継続をlist list
に
限定継続を実装する下準備その2。
限定継続を扱えるようにする言語機能のshift/reset
では、reset
式の中に入った部分式は継続がreset
外とは区切られていて、その中でshift
が呼ばれるとその区切りのところまでの継続が捕捉されて値に変換される。
これまで継続はフラットな一つのリストとして実装してきた。ここに例えばCont.Reset
を導入して「現在の限定継続は直近のCont.Reset
まで」と定義してもいいのだが、別解として
- 継続は
Cont.cont list list
型 reset
に入ったら継続に空リストを追加- 他の式の継続はこの先頭のリストに追加していく
shift
はこの先頭をリストをとってくるだけ
という実装を思いついたので試してみたい。
これはCont.t
の内部実装を大きく変えるところなので、Cont.to_string
を実装する前にやっておきたかった。
Cont.t
型の変更:
type t = cont list let final = []
だったものが
type t = cont list list let final = [[]]
こうなる。単なるリストに比べて追加がめんどくさくなるので専用のCont.add
関数を用意:
let add c = function | cont'::cont'' -> (c::cont')::cont'' | [] -> [[c]]
こういった変更に合わせてExecute
モジュールも変えていく。
let eval env cont = function ... | Exp.Call(e, es) -> Eval(env, Cont.Call(es, []) :: cont, e)
が
let eval env cont = function ... | Exp.Call(e, es) -> let cont' = Cont.add (Cont.Call(es, [])) cont in
になったり
let apply_cont env cont v = match cont with ... | Cont.Call(e::es, vs) :: cont' -> Eval(env, Cont.Call(es, v::vs) :: cont', e)
が
let apply_cont env cont v = match cont with ... | (Cont.Call(e::es, vs) :: cont')::cont'' -> let cont''' = Cont.add (Cont.Call(es, v::vs)) (cont'::cont'') in Eval(env, cont''', e)
になったりする。リストがネストしたぶん少し冗長になっている。
さらに
let apply_cont env cont v = match cont with ... | []::cont'' -> ApplyCont(env, cont'', v)
というケースが追加されている。これは「reset
の中の式を評価し終わったので、その限定継続から抜ける」ことを表している。
ただしまだshift/reset
が実装されていないので、継続に二つ以上のリストが含まれることはないのだが・・・
継続と変数環境を文字列化できるようにする
ステップ実行で評価途中のインタプリタの状態を表示するには、式や値だけではなく継続と変数環境も出力する必要がある。なのでCont.to_string
やEnv.to_string
を追加する。
地味なのでコードは割愛。
ステップ実行インタプリタ
これでステップ実行時に必要な情報を文字列化できるようになった。
あとはExecute
モジュールにtrampoline
にちょっと手を加えたtrampoline'
を作る。具体的にはeval
やapply_cont
が返すExecute.res
型を出力して一時停止するdisplay
関数を定義して:
let display = function | Done v -> print_endline "Done"; print_endline (Val.to_string v); ignore @@ read_line () | Eval(env, cont, e) -> print_endline @@ "Evaluating expression " ^ Exp.to_string e; print_endline @@ "Cont Head:\t" ^ Cont.to_string_hd cont; print_endline @@ "Cont Full:\t" ^ Cont.to_string cont; print_endline @@ "Environment:\t" ^ Env.to_string env; ignore @@ read_line () | ApplyCont(env, cont, v) -> print_endline @@ "Applying continuation on value " ^ Val.to_string v; print_endline @@ "Cont Head:\t" ^ Cont.to_string_hd cont; print_endline @@ "Cont Full:\t" ^ Cont.to_string cont; print_endline @@ "Environment:\t" ^ Env.to_string env; ignore @@ read_line ()
trampoline'
で次のステップに移る前にdisplay
を呼び出す:
let rec trampoline' res = match res with | Done v -> display res; v | Eval(env, cont, e) -> display res; trampoline' @@ eval env cont e | ApplyCont(env, cont, v) -> display res; trampoline' @@ apply_cont env cont v
trampoline'
を使ったrun'
やeval_string'
も定義して、最後にstepwise.ml
を作成:
open Kontlang let rec read_eval_print exp_string = let s = read_line () in if s = "" then Execute.eval_string' exp_string |> print_endline else read_eval_print (Printf.sprintf "%s\n%s" exp_string s) let () = (print_string ">>> "; read_eval_print "")
これで実装完了。
使い方
dune exec ./stepwise.exe
で実行できる。(dune
はOCamlのビルドツールで、上記のコマンドはdune
にstepwise.exe
をコンパイルして実行するよう指示している)
実行して(let [x 3] (+ (* 1 2) x))
を評価させてみる:
kontlang$ dune exec ./stepwise.exe >>> (let [x 3] (+ (* 1 2) x)) Evaluating expression (let [(x 3)] (+ (* 1 2) x)) Cont Head: Cont Full: Environment: Evaluating expression 3 Cont Head: LET x [] [] (+ (* 1 2) x) Cont Full: LET Environment: Applying continuation on value 3 Cont Head: LET x [] [] (+ (* 1 2) x) Cont Full: LET Environment: Evaluating expression (+ (* 1 2) x) Cont Head: ENV Cont Full: ENV Environment: [{x 3}] Evaluating expression + Cont Head: CALL [(* 1 2) x] [] Cont Full: CALL ENV Environment: [{x 3}] Applying continuation on value Op(+) Cont Head: CALL [(* 1 2) x] [] Cont Full: CALL ENV Environment: [{x 3}] Evaluating expression (* 1 2) Cont Head: CALL [x] [Op(+)] Cont Full: CALL ENV Environment: [{x 3}] Evaluating expression * Cont Head: CALL [1 2] [] Cont Full: CALL CALL ENV Environment: [{x 3}] Applying continuation on value Op(*) Cont Head: CALL [1 2] [] Cont Full: CALL CALL ENV Environment: [{x 3}] Evaluating expression 1 Cont Head: CALL [2] [Op(*)] Cont Full: CALL CALL ENV Environment: [{x 3}] Applying continuation on value 1 Cont Head: CALL [2] [Op(*)] Cont Full: CALL CALL ENV Environment: [{x 3}] Evaluating expression 2 Cont Head: CALL [] [1 Op(*)] Cont Full: CALL CALL ENV Environment: [{x 3}] Applying continuation on value 2 Cont Head: CALL [] [1 Op(*)] Cont Full: CALL CALL ENV Environment: [{x 3}] Applying continuation on value 2 Cont Head: CALL [x] [Op(+)] Cont Full: CALL ENV Environment: [{x 3}] Evaluating expression x Cont Head: CALL [] [2 Op(+)] Cont Full: CALL ENV Environment: [{x 3}] Applying continuation on value 3 Cont Head: CALL [] [2 Op(+)] Cont Full: CALL ENV Environment: [{x 3}] Applying continuation on value 5 Cont Head: ENV Cont Full: ENV Environment: [{x 3}] Applying continuation on value 5 Cont Head: Cont Full: Environment: Applying continuation on value 5 Cont Head: Cont Full: Environment: Done 5 5
「Cont Full」では継続すべてを(個々の部分は簡略した形で)表示し、「Cont Head」ではその一番先頭(左側)の詳細を表示している。
パッと見で直感的とは言えないが、慣れてくるとインタプリタの状態がかなりわかるようになる。例えば
Evaluating expression 1 Cont Head: CALL [2] [Op(*)] Cont Full: CALL CALL ENV Environment: [{x 3}]
であれば
- 次のステップは
1
という式を評価すること - 現在関数呼び出し二つの内側にいる
- 直近の関数呼び出しでは、すでに関数に当たる部分が評価されていてその値は
*
である - まだ未評価の引数があってその式は
2
である(整数式も整数値も同じように表示されてしまうのがややこしい・・・) - 現在の変数環境には
x
という変数に3
という値が束縛されている
ということを表している。
限定継続のような複雑な機能を実装・デバッグする上でこういう情報は非常に役に立つ。