Toy VM(7)

ようやく分かってきた。
「継続」というのは、結局、様々な本や解説で書かれているとおり、これから行う計算でしかないんだ。

GaucheVM
SECD マシン。
扱いは違うように見えるけど。

SECD マシンは、命令列もスタックもリストだった。このことが、少し比較をしにくくしている。
命令列をリストではなくベクトルで持つ SECD マシンを書こう。

まず VM を書く。わたしが書いた SECD マシンは実際は単なる関数だった。今度はオブジェクトとして書く。ただの入れ物だ。

命令列の処理のしかたはいくらでもある。ようは、命令の種類によって何かの処理が書ければいい。
scheme で書いたときは util.match によるパターンマッチがまさにぴったりのユーティリティだった。
Common Lisp で書く場合でも、パターンマッチは無くても cond や case を使えばいい。
けど、今回は dispatch という総称関数を定義してみよう。ようするに、処理を分岐できればいいんだ。
dispatch は、「総称」すると -- つまり、ひとまとめにして何か名前をつけてやるならば --
「特定の命令と VMインスタンスを受け取って処理する」だ。

(defgeneric dispatch (insn vm)
  (:documentation "dispatch vm instruction."))

Common Lisp の総称関数は eql specializer なるものを使える。実践 Common Lisp 24.7 。これを使うと、特定の命令ごとに処理が書ける。
命令を定義するときは、繰り返しが多くなりそうだから、マクロを定義しておく。
べったりと書いてもいいけど、後のことを考えて。反復練習をしたい訳じゃないんだから。

(defmacro def-insn (name (vm) &rest body)
  (let ((insn (gensym)))
  `(defmethod dispatch ((,insn (eql ,(as-keyword name))) (,vm vm))
     (declare (ignore ,insn))
     ,@body)))

こんな風に定義してやる。

(def-insn ldc (vm)
  (let ((c (fetch-operand vm)))
    (stack-push vm c)
    (incr-pc vm)
    (next vm)))

とにかく早く動くようにする。まずは LDC 命令。次に算術演算子。ここまでは簡単だ。

次が SEL だ。if を実現するための分岐。
ここまできてからまごついた。そもそもベクトルで命令を持つのだから、SECD マシンのように命令列を入れ子 - リスト - にしちゃだめだ。

;; リスト版
(LDC 1 SEL (LDC 2 JOIN) (LDC 3 JOIN))
;; リスト版
SEL     (x.s) e (SEL ct cf.c) d   ->  s e c' (c.d)
                 where c' = ct if x is T, and cf if x is F
JOIN    s e (JOIN.c) (cr.d)  ->  s e cr d

分岐 SEL, JOIN の実装もそもそも変えなきゃならない。ベクトルだから入れ子構造は取れないから、 ct, cf はそのままは使えない。

試行錯誤して、以下のようにコンパイルすればいいと考えた。(今のところ。もうすこし先に進めたら間違いがわかるかもしれないけど)

(if 1 2 3) =>
#(:LDC 1 :SEL 6 9 12 :LDC 2 :JOIN :LDC 3 :JOIN :STOP)

ここで SEL の後の数字3つはプログラムカウンタだ。6は6番目の要素(2度目の:LDC)に対応する。それぞれ ct, cf, cont に対応する。

先に VM 側を作ってやって、動作することを確認。
あとはコンパイルしてこのベクトルを作れればいい。これにはわたしの腕だと2パス必要だった。一度プログラム全体を処理して
ラベルを登録して、後で実際の値に置き換える。

さあもう少しだ。

続く。