ここでは v1.03 時点の仕様について解説します。 後のバージョンでは違いがあるかもしれません。
なお、ここでは各種 Byte 数を「アドレス.2b」のように表します。
NBCL が扱うファイルは以下の通りです。
.cmt
(入力):N-BASIC
で保存されたカセットテープイメージ$0E
,
行番号.2b)がアドレス情報(中間言語 $0D
,
アドレス.2b)に置き換えられたままセーブされたファイルがあったため、
念のため配置アドレスを推測したりしています。.bci
(入出力):独自中間言語ファイル.z80s
(入出力):Z80 アセンブリファイル.cmt
/.t88
/.bin
(出力):
マシン語実行ファイル.t88
フォーマットにも対応していますが、ほとんど検証はしていません。.bin
はヘッダなどの無い、生のバイナリデータです。最初に .cmt
から BCI へ変換します。
なるべく汎用性を重視したフォーマットにしましたが、N-BASIC
は文法が命令によってバラバラなので、変換はかなり面倒です。
アーカイブ内の nbasic/tobci.txt
は変換規則をなんとなく書いてみたファイルですが、
今のところ使用していません。おそらくいろいろと問題が出ると思います。
.bci
ファイルは行単位の記述になっており、
最初の行はバージョン、その後は、
'*'
で開始)'#'
で開始)'.'
で開始)のいずれかとなっています。このうち代入と命令は、
'{'
~'}'
は 1 パラメータ扱い':'
で行終了といった規則になっています。 詳しい説明は省略しますが、変換して N-BASIC と比較すればだいたいわかると思います。
なお、-i
オプションで .bci
ファイルを出力した場合、
自動的に再び読み込んで内部構造が変化しないかチェックされます。
元々はデバッグ用ですが、念のため残してあります。
BCI を直接 Z80 コードには変換せず、一種の VM(Virtual Machine:仮想マシン)命令に変換します。 実行速度は犠牲になりますが、コードサイズが比較的小さくて済みます。
たとえば「X=A+B
」(全て整数変数とする)という
N-BASIC プログラムを変換するとします。
; Z80 : 16Bytes LD HL,(VARI_A) LD DE,(VARI_B) OR A ADC HL,DE ; ADD HL,DE では P/V フラグが変化しない JP V,_ERROR_OVERFLOW LD (VARI_X),HL ; VM : 10Bytes DB VM_VARI ; A の値をスタックに積む DW VARI_A DB VM_VARI ; B の値をスタックに積む DW VARI_B DB VM_ADD ; スタックから値を 2 つ取り出し、加算してスタックに積む DB VM_LETI ; スタックから値を取り出し、X に格納 DW VARI_X ; VM 最適化 : 7Bytes DB VM_ADDI_VVV ; A の値と B の値を加算して X に格納 DW VARI_X DW VARI_A DW VARI_B
N-BASIC では 6Bytes(式と ':'
もしくは行終端)なので、16Bytes と
10Bytes(最適化すると 7Bytes)の差は結構大きいわけです。
ただし実行速度差も大きいので、-O
3
オプションで一部の命令を直接 Z80 コードに変換します。
今のところ、コードサイズがかなり大きくなる割に効果は限定的です。
上記の例でなんとなくわかると思いますが、VM
命令の大部分はスタックを利用します。たとえば WIDTH
命令なら、
; WIDTH 80,25 DB VM_IMMU8 ; 値 80 をスタックに積む DB 80 DB VM_IMMU8 ; 値 25 をスタックに積む DB 25 DB VMI_WIDTH ; スタックから値を 2 つ取り出し、WIDTH 命令を実行
このようにパラメータをスタックに積んでおいて実行します。
ところで、この例では VM 命令を 3 つも使用していますが、仮に、
; WIDTH 80,25 DB VMI_WIDTH_IMM,80,25
といった VM 命令を追加すれば 1 命令で済み、VM コードサイズも小さくなります。VM 命令数は速度にも直結するので、命令をまとめるのは重要です。
ただ、パラメータの種類ごとに VM 命令を用意しなければならないので、 ライブラリが大きくなることも考慮しなければなりません。
WIDTH
は初めに 1 回実行する程度なので割に合わない感じがしますが、
頻繁に実行する命令では効果的です。現在は代入や計算式の他に
POKE
や CHR$
も最適化しています。
BCI を VM コードに変換した後、VM
を実行する Z80 コードが別途必要になります。それが nbasic
フォルダにあるライブラリ群です。
ライブラリの簡単な解説は _libmanual.txt
にありますが、
base.z80s
:基本ライブラリequ.z80s
:定数定義macro.z80s
:マクロ定義が基本部分、
inst.z80s
:命令ライブラリfunc.z80s
:関数ライブラリop.z80s
:演算子ライブラリなどが VM 実行部分となっています(他にもありますが省略)。 細かくモジュールに分け、必要なモジュールのみリンクしています。
ライブラリを作るにあたって、少々速度が犠牲になっても、 コードサイズがあまり大きくならないように意識しています。
ただ、現時点では正常動作することが最優先で、 まだ突き詰めてはいません。少しずつ改良していこうと思います。
開始~メインループは base.z80s
にあります。
主要ラベルは以下の通りです。
_COLD_START
:開始_HOT_START
:変数をクリアせずに開始(未テスト)_VMLOOP
:メインループ_VMLOOP2
:DE
レジスタ退避時のメインループ(命令用)_VMLOOP3
:DE
レジスタ退避時のメインループ(関数/演算子用)_RETURN_BASIC
:N-BASIC もしくはマシン語モニタに戻るメインループでは DE レジスタが VM 命令アドレスです。
この DE レジスタを退避する際、スタックの都合で
PUSH DE
しにくい場合に、
LD (_VMLOOP2+1),DE
(VM処理)
JPR _VMLOOP2
などとしています。
なるべくコードサイズを小さくするため、N-BASIC ROM 内ルーチンやワークを利用しています。 特に下記ワーク(と関連する ROM 内ルーチン)を頻繁に利用しています。
_ACC=$F0A8
:値(具体的には下記参照、倍精度は注意)_ACCT=$EF45
:値の型(_ACCT)=$02
:整数型、値は
$F0A8
.2b(_ACCT)=$04
:単精度実数型、値は
$F0A8
.4b(_ACCT)=$08
:倍精度実数型、値は
$F0A4
.8b(_ACCT)=$03
:文字列型、ディスクリプタアドレスは
$F0A8
.2b値の型は、数値に関してはそれぞれのサイズになっています。
文字列型のディスクリプタは「長さ.1b, 文字列本体アドレス.2b」の計 3Bytesです(長さが 0 の場合の文字列本体アドレスは不定です)。文字列本体は 0 終端ではないので気を付けてください。
変数はコンパイラがワークを静的確保します。 ただし配列変数の本体と文字列の本体は動的確保します。
変数ラベル名の命名規則とサイズは以下の通りです(*
は変数名)。
VARI_*
:整数型.2bVARF_*
:単精度型.4bVARD_*
:倍精度型.8bVARS_*
:文字列型.3bARYI_*
:整数型配列.2bARYF_*
:単精度型配列.2bARYD_*
:倍精度型配列.2bARYS_*
:文字列型配列.2b配列変数は本体開始アドレスを表します(まだ DIM
で確保されていなければ 0)。
本体は以下のフォーマットになっています。
スタックのフォーマットは以下の通りです(下位アドレスから)。
$02
, 値.2b$04
, 値.4b$08
, 値.8b$03
, ディスクリプタ.3b$00
GOSUB
:$FE
, リターンアドレス.2bFOR
$F8
, 変数アドレス.2b, STEP.2b, 終値.2b,
ループ開始アドレス.2b$F4
, 変数アドレス.2b, STEP.4b, 終値.4b,
ループ開始アドレス.2b$FF
NBCL のコンパイルオプションについて補足しておきます。
-d n
:デバッグレベル#define DEBUG
」を定義します。
現在は一時文字列の解放忘れチェックに利用しています。log.txt
、-l
オプションで変更可)を出力します。レベル 1 でフェーズ進行状況、レベル
2 で行単位で進行状況も出力します(大量のログになります)。