そこで、方針を変えて、Virtual Machine を使う方式に変更し、ごそごそやってできたのがこれ。
https://github.com/takeisa/uschemer2/tree/v0.00
Virtual machineとコンパイラを実装した。実装中にいろいろバグが出て、コンパイラのバグなのか、Virtual machineのバグなのか、分かり難かったりしたが、四苦八苦するのも、なかなか楽しい。ただ、適当に作ってしまったのでソースが非常に汚い。orz
Three Implementation Models for Scheme
Three Implementation Models for Scheme(3imp.pdfとして有名)というScheme処理系の実装方式の解説を読んだ結果、コンパイルしたSchemeコードをScheme用に最適化したVitrual machine上で実行する方式のほうが、インタプリタ方式よりも、末尾最適化や継続の実装が簡単そうだった。
VMを使う方式では、末尾最適化と継続は次のように実装できる。
- 末尾最適化
- コンパイル時に、呼び出す関数が処理の末尾にあるか判定し、末尾の場合はコールフレームを生成せずに、関数を呼び出すコードを生成する。
- 継続
- 継続時点のスタックフレームを保存しておき、継続手続の呼び出しで、スタックフレームを復元後、値を渡す。
Virtual machine
いまのところ 3imp.pdf とほぼ同じ仕様だ。
備忘録をかねて書いておこう。
レジスタ
5つのレジスタを持つ。
| レジスタ | 機能 |
|---|---|
| a | アキュムレータ |
| x | 次に実行する式 |
| e | 現在の環境 |
| r | 引数のリスト |
| s | 現在のスタックフレーム |
命令セット
Virtual machineでサポートしている命令は以下の通り。
命令の中には、次に実行する命令を引数に取るものもあるが、以下では省略している。
| 命令 | 説明 |
|---|---|
| (halt) | Virtual machineを停止する。aレジスタの値が評価結果となる。 |
| (refer var) | 環境を参照し、変数varの値を取得し、aレジスタに設定する。 |
| (constant obj) | オブジェクトをaレジスタに設定する。 |
| (test then else) | aレジスタが非nilの場合はthenを実行し、nilの場合はelseを実行する。 |
| (assign var) | 環境を参照し、変数varの値にaレジスタの値を設定する。 |
| (conti) | 継続オブジェクトを生成する。 |
| (nuate s var) | スタックフレームsをsレジスタに設定し、変数varをaレジスタに設定する。 |
| (frame ret) | コールフレーム(戻り先retは次に実行する式として設定)を生成し、スタックフレームへ追加する。 |
| (argument) | aレジスタの値を、rレジスタに追加する。 |
| (apply) | クロージャまたはプリミティブ関数を評価する。 |
| (return) | スタックフレームのコールフレームより、フレーム生成前の状態にレジスタを戻す。 |
コンパイラ
式に応じてVirutal machineの命令を生成する。
コンパイル例
いくつかコンパイル例を示す。
| 変数aの参照 | (refer a) |
| 数値100 | (constant 100) |
| (if pred "true" "false") | (refer pred) (test (constant "true") (constant "false")) |
| (+ 1 2) | (frame) (constant 1) (argument) (constant 2) (argument) (refer +) (apply) (return) |
実行例
今はまだreplは無いので、rspec実行時のデバッグログ出力結果のみ。クロージャ
context doit {
op = compile('
(((lambda (a)
(lambda (b) (+ a b))) 10) 20)
')
res = @vm.eval(op)
res.should eq 30
}
end
(((lambda (a)
(lambda (b) (+ a b))) 10) 20)
-> (frame) ...
↓ここから下はVMが実行した命令
(frame)
(constant 20)
(argument)
(frame)
(constant 10)
(argument)
(close #<SCons:0x86e7d98> (close #<SCons:0x86e6754> (frame)))
(apply)
(close #<SCons:0x86e6754> (frame))
(return)
(apply)
(frame)
(refer a)
(argument)
(refer b)
(argument)
(refer +)
(apply)
(return)
(return)
(halt)
継続
context doit {
op = compile('(+ 1 (call/cc (lambda (k) (k 3))))')
res = @vm.eval(op)
res.should eq 4
}
end
(+ 1 (call/cc (lambda (k) (k 3)))) -> (frame) ...
↓ここから下はVMが実行した命令
(frame)
(constant 1)
(argument)
(frame)
(conti)
(argument)
(close #<SCons:0x9d1a96c> (frame))
(apply)
(frame)
(constant 3)
(argument)
(refer k)
(apply)
(nuate #<CallFrame:0x9d1a4d0> v)
(return)
(argument)
(refer +)
(apply)
(return)
(halt)
不足しているもの
- 末尾最適化
- まだ実装していない。
- define special form
- VMの命令追加が必要だ。
- REPL
- REPLが無いと不便だ。
リファクタリング
メソッド内の最大インデントは一段まで、elseは使わない等の強い制約の元で実装することで、オブジェクト指向プログラミングがうまくなるらしい。このルールに従ってリファクタリングしてみるのも、いろいろと得るものがありそうだ。
0 件のコメント:
コメントを投稿