伝統的マクロ
VMの方は触らずに,コンパイラの方だけを修正した. マクロ展開時と定義時にマクロテーブルに追加したり,参照したりするようにしている. トップレベル変数とは名前空間が違うけど,これでいいよな… ということで以下のようにletとかbeginとかいろいろ自由に拡張できるようになった. また時間があれば他のマクロシステムも増やしたい.
(define-macro let
(lambda (binds . bodies)
(cons (append (list 'lambda (map (lambda (x) (car x)) binds))
bodies)
(map (lambda (x) (cadr x)) binds))))
(let ((a 10)) (+ a 1))
バイトコードをかっこよく
今までのバイトコードは下のような感じで,次の命令を繋げていく構造だった. schemeで扱うには適しているようなだけど,後々VMをCで書きたいのでもっと線形な感じにしたい.
(frame (halt) (constant 2 (argument (constant 1 (argument (close 0 (refer-local 1 (return 2)) (apply)))))))
こんな感じにした. これでCとのインターフェイスも簡単にかけそう.
0 frame 9 戻り番地を9にしてフレームを作る
1 constant 2 定数2をaccumulatorにロード
2 argument スタックにつむ
3 constant 1 定数1をaccumulatorにロード
4 argument スタックにつむ
5 close 0 7 8 7番地から8番地を本体にするclosureオブジェクトをaccumulatorにロード
6 apply そのclosureオブジェクトを適用
7 refer-local 1 2つめのローカル変数をロード
8 return 2 レジスタからローカル変数2つ取り除いて復元
9 halt おわり
この作業はなかなか大変だった. 問題点はclosureだった. closureをトップレベル変数に束縛しない限りは,そのclosureに対応するバイトコードが存在するから, closureオブジェクトには単純に,#(開始番地 終了番地)を入れておけばいい. しかし,トップレベル変数に束縛した場合は,呼び出し時には開始番地や終了番地が分かったところでそのバイトコードの自体がないので,呼び出せない. 色々考えてみる
- 組み込み関数(+,-,…)のclosureは命令コードをまた別の場所においておかねば
- それに,defineでclosureを定義するときにも気をつけないと
- closure生成時に本体の命令コードを全部RAMに突っ込んで,アドレスだけ書いておけば動くだろう
- いや,トップレベルに束縛される可能性のないclosureをRAMに移動させるのはコストがかかる
- よし.必要なclosureだけRAMに移動させよう
- 必要な時ってのはset!かdefineでトップレベルに束縛されるとき
(ただし,コンパイラが吐いた命令コードの領域をROM,VM自身が後から追加した命令コードの領域をRAMと 勝手に呼んでる)
まだ考えることはある
- close,test,frameは命令のアドレス(分岐先とか返り番地とか)を含んでいるので,そのアドレスをちゃんと変換せねば. - 最初油断してた - これは,再帰的に書いていけば解決できた
- 継続はどうするんや - 継続は結局中身はスタックの情報を含んだclosureオブジェクトなので大丈夫のはず - 大丈夫だった
この変換プログラムとVMコードを書き直した. 無駄にRAMを使わないようになったので,よかったよかった. VMは方は前よりも簡単になった. こんな感じになった.
schemeコード -----> マクロ展開後コード -----> 3imp VMコード -----> 線形なコード -----> VM上で実行
macro.scm compile.scm linear.scm vm.scm
マクロテーブル トップレベルテーブル