SECDV Schemeを実装する - ホストSchemeコールバックの実装
...ちょっと休憩。というわけで、ホストSchemeのコールバックを実装し、Alexpanderも繋いだので"Schemeで実装したSECDV VMとコンパイラでyuniのコードが走る"環境は作ることができた。ついでに、S式リーダはyuniのライブラリに(yuni miniread reader)として入れてあるので、ファイルを読む際はそちらを使うようにした。
テストがけっこう辛い状況で、TravisではARMビルドが時間切れ、AppVeyorでは処理系が落ちる等散々な状況なのでちょっと方針を見直す必要がありそう。。でも、常に大量のScheme処理系で走らせておくことは地味に重要で、特定の処理系でだけ失敗することで見つかったバグがそれなりにある。
- Vicareが未定義値がゼロ値なのでcaseのelse抜けを見つけた https://github.com/okuoku/yuni/commit/184b7a19fc04da19abbcbfb43a62d049d5b2d513
- Sagittariusは実際に手続きを起動しなくてもarity間違いをエラーにするので、コードの消し忘れを見つけた https://github.com/okuoku/yuni/commit/c30679bb83f479e9bcbe9b65db52a9208fc91b39
- Gaucheはコロン始まりのシンボルをキーワードとしてreadするので、自前のreaderの適用しわすれに気付いた https://github.com/okuoku/yuni/commit/7f85088fef2fe6534b6b3aba39af604f1608d950
ただ大量のScheme処理系のデバッグ手法を覚えておくのが意外と辛い。。普段はnmoshを使ってるので何もしなくてもbacktraceが出るのに慣れてるけど、デフォルトではbacktraceしない処理系も多い。
コールバックの実装
今のところ、構文以外の殆どはホストSchemeの手続きをそのまま使用している。このため、mapのような手続きを使おうとすると、ホストSchemeからSECDV VMの命令列によるクロージャを呼び出すケースが出てくる。
前回VMをフロントエンドとバックエンドに分割した( http://d.hatena.ne.jp/mjt/20170525/p1 )が、コールバックを実装するためにはこの両方に処理を実装する必要がある。
StateMachine側ではレジスタを全部保存してジャンプするだけ( https://github.com/okuoku/yuni/blob/4753c22afdbc72b7aa9a6d74bda7e311b138912a/lib/yunivm/vm/vmcore.sls#L339 )だが、シーケンサ側が地味に難しい。
;; VM core support ;; Codeflow (define %require-mit-scheme-workaround? ;; ★ http://d.hatena.ne.jp/mjt/20170515/p1 参照 (let ((x (values 10))) (procedure? x))) (define (return-to-orig-ctx cont) (let ((current-current-code current-code) (current-current-block current-block)) (lambda vals (set! current-code current-current-code) (set! current-block current-current-block) (set! jump-request #f) ;; MIT/GNU Scheme workaround (it cannot take 2 or more values ;; on continuation) (if (= 1 (length vals)) (cont (car vals)) (if %require-mit-scheme-workaround? (cont (apply values vals)) ;; ★ 4 (apply cont vals)))))) (define (%vm-callable obj) (cdr obj)) (define (%conv-datum gencb datum) (cond ;; Procedure? ((vmclosure? datum) (lambda args ;; ★ VMの手続きを変換した手続きとしてホストSchemeに渡る (call-with-current-continuation ;; ★ 1 (lambda (c) (let ((launch (gencb datum (return-to-orig-ctx c)))) ;; ★ 2 (実装はStateMachine側) (apply launch args) (unless jump-request (error "launch did not invoked jump request")) (do-cycle)))))) ;; ★ 3
まぁこの手のややこしい処理は継続を使えばどうにでもなる。つまり、
- StateMachine側には、"RET命令に出会ったらシーケンサ側をコールバックする機能"を実装する
- ★1 : 生成したクロージャがホストSchemeから呼ばれたらとりあえず継続をキャプチャする
- ★2 : RET命令が呼ばれたとき用のコールバックを生成(return-to-orig-ctx)
- ★3 : ひたすらStateMachineを回す
- ★4 : クロージャのVM命令列の終端に達しRETが呼ばれたら、VMから結果を取り出し、継続を呼んで戻る
... これ書いてから気付いたけど継続使う必要無いね! 今回継続を使って書いたのは、VM命令の1ステップ実行手続き do-cycle から脱出したかったため。do-cycleは命令列の真の終端に辿り付くまで止まらないようになっているが、停止フラグを内部に持たせる等の方法で中断可能にすれば十分。
で、MIT Schemeの継続には常に1値しか渡せないので専用のワークアラウンドを入れている。前回( http://d.hatena.ne.jp/mjt/20170515/p1 )のエントリにShiroさんもコメントされていますがイヤ本当なんでなんだ。。(call-with-current-continuation 側に手を入れるのもやってみたけど、他の手続きの動作が超クッソ激烈に遅くなったのでやめた)
次の一手 : ヒープ作り
というわけで、Scheme実行環境は揃ったのでそろそろ本題のヒープ作りへ。。
とりあえず、GCをねっとりと検証したいので"他のオブジェクトを指すことができるオブジェクト"である:
- ペア
- ベクタ
の2つを実装するのが最初の目標になる。
... car とか cdr を実装して終わりではなくて、
- applyの引数の積み方
- eqv? や equal? 等の手続き
- map等ペアを取るユーティリティ手続き
等全てを実装する必要がある。今回コールバックを実装するにあたっては、ホストScheme側の手続きを呼ぶ直前に毎回wrapするという手法を取った。しかし、ペアでこれをやってしまうとeq?性が失われる等副作用が出てしまう。逆に、stringとかbytevectorのような他のオブジェクトを指さないオブジェクトは放置で良い。
今回は諸般の事情で時間が貴重なので、とにかく綿密に計画して手戻りの無いようにしないといけない。。ここでScheme上に実装したヒープ機構をCやらBASICやらに移植して使うことになる。