制作したきっかけや、開発中の紆余曲折などを。
※v1.03 公開時点で書いてます。
以前に 3D ダンジョンを作ってみて、 「BASIC は遅い!」 「マシン語はすぐ暴走する!」という、 数十年前の悪夢(笑)を改めて体験しました。
当時もいろんなコンパイラが存在していましたが、 別の言語だとか、整数しか扱えないとか、なにかと不便なものでした。 市販のものに関しては、買う余裕が無かったので知りません(涙)。
でも今は PC 上でクロスコンパイラを作れば、大掛かりなものを作れるし、 エミュレータでテストも楽にできます。
今までコンパイラを作ったことも無かったので、 プログラミング修行を兼ねて N-BASIC コンパイラを作ってみることにしました。
「BASIC プログラムをそのままコンパイルできる」 が前提条件なのは、初めから決定していました。 ここを妥協すると、ネタとして面白くありません(そこかよ)。
初めは BASIC から直接 Z80 コードに変換することを考えていました。 それではコードサイズが大きくなりそうなので(詳しくはこちらを参照)、どう縮めるかを検討していました。
たとえば Z80 の RST
命令(1Byte
コール命令)で開始して、計算式をコマンド形式で並べるとか。たとえば
X=A+B
は、
RST $20 DB _CMD_PUSH_INT DW VARI_A DB _CMD_PUSH_INT DW VARI_B DB _CMD_ADD DB _CMD_LET_INT DW VARI_X DB _CMD_END
こんな感じに。などと考えてるうちに、「あれ?それなら全部 VM でいいんじゃね?」ということで、現在の形になりました。
N-BASIC プログラムをそのままコンパイルするには、N-BASIC の動作を完全に理解する必要があります。 コンパイラの仕様を考えつつ、少しずつ N-BASIC の解析をしていきました。
外部サイトの資料だけでは全然足りないので苦労しましたが、 よくわからないときはエミュレータで動作確認するなどして、 いくつかのデバイス(コンパイラで対応する予定が無いもの) に関するもの以外は、ほぼ全部解析しました。
正直なところ、 逆アセンブルリストを斜め読みしかしていない部分もありますが(笑)。
解析していると、ときどき「んっ?」と思うことがあったので、こちらにまとめてあります。 解析は大変な作業ですが、こういうのを見つけるとちょっと楽しくなります。
コンパイラをいきなり全部作ろうとすると動くまでが大変なので、よく BASIC のベンチマークテストに使われる「ASCIIART(マンデルブロ集合)」を手作業コンパイルしてみて、 どれくらいの速度になるかを実験しました。
このプログラムは単精度実数の計算が主なので、 劇的に速くはならないのはわかっていましたが、それでも簡単に 2 倍速以上になることは確認できました。
ちなみに N-BASIC で 8:36(516 秒)、NBCL v1.02 で 2:36(156 秒)と、約 3.3 倍速になっています。
次に、N-BASIC カセットテープイメージ(.cmt
)から、
まず独自中間言語(BCI と命名)に変換することを考えました。
BASIC は 1 行に多くの命令を詰め込むことができて、
エラーが出ても具体的にどの命令なのかわかりにくいため、1 命令を 1
行で表すことにしました。そのため .bci
ファイルはテキスト形式です。
この変換が結構大変で、命令ごとに文法が柔軟すぎるので(特に
PRINT
や
LINE
)、手持ちのプログラムを片っ端から変換してみて、
ひたすら実装&テストを繰り返しました。
もしかするとまだ完全ではないかもしれません。
あとテキスト形式で保存するため、 実数の値が誤差で変化しないようにするのも結構面倒でした。
BCI に変換後、今度は独自仕様の VM へ変換しつつ、VM 実行部分も実装していきました。
初めにだいたいの仕様は決めたものの、実装してみると VM の処理が書きにくいことも多く、よく仕様変更していました。 スタックマシンはレジスタを単純に退避できないのが面倒ですね。
あと、文字列の扱いが非常に面倒でした。 一時的な文字列を確保することが多く、使用後は削除しなければなりません。 文字列領域が足りなくなったらガーベージコレクションも必要で、 一通り実装したのは一番最後でした。
こうして動作はするようになりましたが、 手作業コンパイルよりもだいぶ速度が劣りました。 いろいろ調べてみると、どうやら VM の実行命令数が速度に直結するようです。
そこで、複数の VM 命令を一つにまとめる最適化をしてみました。たとえば
A=A+1
(A
は整数型)なら、
; 最適化前 DB VM_VARI ; A の値をスタックに積む DW VARI_A DB VM_IMMU8 ; 1 をスタックに積む DB 1 DB VM_ADD ; スタックから値を 2 つ取り出し、加算してスタックに積む DB VM_VARADDRI ; A のアドレスをスタックに積む DW VARI_A DB VM_LET ; スタックからアドレスと値を取り出して代入 ; 最適化後 DB VM_ADDI_VI ; A に 1 を足す DW VARI_A DW 1
といった具合です。よく出てくるパターンを考えて、 コツコツ実装しています。その分ライブラリが増えますが、VM コードが減るのでトータルでは小さくなる傾向のようです。
この作業は速度に直結するのでわりと楽しいですが、 実装ミスで暴走することも多くて心臓に悪いです(笑)。
さらなる高速化として、一時的に VM から Z80
に切り替える最適化も実装しました(-O 3
オプションで有効)が、
コードサイズがかなり大きくなる割には効果は限定的です。
なかなか難しいですね。
一通り動いたところで v1.00 として公開したところ、何人かの方々がいろいろなプログラムをコンパイルして、 不具合報告をしてくれました。協力してくださった方々に感謝します。
私もテスト用に作ったプログラムだけでなく、 手持ちのプログラムでもある程度はテストしていましたが、v1.00 と v1.01 で計 10 個ほどのバグが見つかりました。
修正&要望対応して、v1.02 でかなりの完成度になったかと思います。
なお、「著作権について」にも記述してありますが、同梱サンプルの
mandel.cmt
以外は全て私が作成したものですので、
ご利用はご自由にどうぞ。