NBCL:N-BASIC コンパイラ 開発苦労話

制作したきっかけや、開発中の紆余曲折などを。

※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 の動作を完全に理解する必要があります。 コンパイラの仕様を考えつつ、少しずつ 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 ファイルはテキスト形式です。

この変換が結構大変で、命令ごとに文法が柔軟すぎるので(特に PRINTLINE)、手持ちのプログラムを片っ端から変換してみて、 ひたすら実装&テストを繰り返しました。 もしかするとまだ完全ではないかもしれません。

あとテキスト形式で保存するため、 実数の値が誤差で変化しないようにするのも結構面倒でした。

BCI に変換後、今度は独自仕様の VM へ変換しつつ、VM 実行部分も実装していきました。

初めにだいたいの仕様は決めたものの、実装してみると VM の処理が書きにくいことも多く、よく仕様変更していました。 スタックマシンはレジスタを単純に退避できないのが面倒ですね。

あと、文字列の扱いが非常に面倒でした。 一時的な文字列を確保することが多く、使用後は削除しなければなりません。 文字列領域が足りなくなったらガーベージコレクションも必要で、 一通り実装したのは一番最後でした。




最適化

こうして動作はするようになりましたが、 手作業コンパイルよりもだいぶ速度が劣りました。 いろいろ調べてみると、どうやら VM の実行命令数が速度に直結するようです。

そこで、複数の VM 命令を一つにまとめる最適化をしてみました。たとえば A=A+1A は整数型)なら、

; 最適化前
	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 以外は全て私が作成したものですので、 ご利用はご自由にどうぞ。


inserted by FC2 system