2026年05月21日

Z80用picle言語コンパイラの開発


1.はじめに
 ソフトウェアを作ることが好きな人であれば誰しも一度はプログラミング言語を作ってみたいと思ったことがあるのではないでしょうか?
 「TAKE IT EASY」のコラムに書かれているように「プログラミング言語の制作は"全てを把握できる世界"の創造主になる」ことかもしれません。
 以前開発した PIC24 用の picle コンパイラの Z80 版を作ってみることにしました。
 PIC 版ではフラッシュメモリへの書込み回数を減らすため2パス方式にしていましたが、Z80 版では生成したコードを直接メモリに書き込む1パス処理にしたいと思います。
 "run" コマンド1発での「コンパイル&Go」を実現します。


2.構想
 何故、古の Z80 用のコンパイラを作ろうと思ったかというと
  • 1パス処理の実現
     コンパイル時の使用メモリを殆ど増やさずに1パス処理できそうなやり方を思いついたので実現性と効果を確認したいため
  • picle 言語の実用性の確認
     今回のコンパイラはC言語で開発していますが、完成後に picle 言語自身で記述し直したい(セルフホスティングの実現)と考えています。コンパイラを記述できればある程度は汎用的な処理を記述できると言えます。
    ※コンパクトに仕上げるには機能を絞る必要があるが低機能すぎるとセルフホスティング困難という悩ましさがある
  • 他の CPU への移植性
     特に自作の CPU 等ではセルフコンパイラの開発はアセンブラレベルでの開発になり、手間がかかります。そこで、セルフホスティングしたコンパイラがあればクロスコンパイラ化することで容易にセルフコンパイラを構築でき、ブートストラップ的な機能追加も可能になります。


3.スタックフレーム管理
 Z80はスタック関連の機能が貧弱で遅いのですが、言語として再帰呼出し機能がないとセルフホスティングの手間が膨大になってしまうので従来通り再帰呼出しに対応することにしました。
 処理/関数呼出し時の処理の概要を次のようにします。

【関数呼出し時の処理】
  1. 引数を左側から2バイト単位でスタックに PUSH
    インテルの呼び出し規約(Intel ABI)秋月Cコンパイラでは引数は右からPUSHされる
  2. CALL 命令で関数呼び出し
  3. 呼び出し関数からリターン後、POP を使ってスタック上の引数領域を解放
  4. 関数呼び出しの場合、HL レジスタにリターン値が設定される

【関数側の処理】
  1. フレームスタックレジスタ(IX)をスタックに PUSH
  2. 関数の場合はリターン値用に 0000h を PUSH
  3. 引数とオート変数アクセス用の IX に SP と同値を設定
  4. オート変数分 SP 値を削減
  5. 関数の処理を実行
  6. SP に IX の値を入れる
  7. 関数の場合は POP HL でリターン値を HL に設定
  8. POP IX でIXの値を復帰
  9. RET で呼び出し元に戻る

処理(proc)のスタックフレーム


関数(func)のスタックフレーム



4.実装状況
 それでは現状の実装状況について以降にメモしておきます。
 尚、コーディング作業自体も楽しみたい、かつコードの隅々まで自分で把握しておきたいことからAI未使用で作業しております。


4-1."hello, world" の表示
 Z80 用コンパイラのコーディングが一通り完了(ディバッグは未)し、はじめて"hello, world"が表示された時の画面のキャプチャが下図の左側になります。ソース中で名前の末尾がアンダースコアな関数は組み込み関数です。また、ニーモニックはディバッグのための表示で
  LD BC,705B
  CALL 0000
等の部分は表示後にアドレスが設定されます。1パスなのでニーモニック表示時点では未確定なためです。
 下図の右側はコンパイル直後にメモリ上にあるコードを ZSID で逆アセンブル表示したものです。参考として picle 言語のソースも貼り付けました。上記のスタックフレーム管理を踏まえて眺めると問題なくコンパイルされていることが判ります。

はじめての "Hello, world"
ZSID での逆アセンブル表示


4-2.fibonacci 数列の表示
 "for"や"if"文等が動くようになったので再帰呼出しで fibonacci 数列を表示してみました。for 文のコンパイルではコードの生成順を入れ替えることでループ内のジャンプ命令が一つになるようにしました。
 下図の左側がまだ最適化未対応の今回のコンパイラです。環境は no wait 12.5MHz の Z80(TMPZ84Compact)です。TeraTerm のマクロで時間を計測(コンパイル時間も含む)しています。
 右側は GAME 言語で再起呼出しするソースをコンパイル後、起動した結果です。上記のフレームワーク管理と同様な処理を配列を使って実現しています。このソースを見ると再帰処理により、再帰呼出しが膨大に発生する状況をより実感できますね。
 因みに GAME 言語インタープリタではサブルーチンスタックが直ぐにオーバーフローして実行できませんでした。
★追記 2026/05/22 CP/M用のGAME言語はここからダウンロードできます。

Fibonacci 数列の表示(picle言語)
Fibonacci 数列の表示(GAME言語)


4-3.固定小数点での ASCIIART
 複数あり得る "break" の箇所に前方参照のジャンプ命令を埋め込む(ループ処理が入れ子の場合も考慮)という1パス処理では悩ましい処理ですが、想定通り実装できました。"break" 機能が動作するようになったので asciiart を動かしてみました。コンパイラで扱う変数型は2バイト整数ですが2バイトの固定小数点乗算関数を用意すれば加減算処理は整数のものを利用できます。変数はアクセスが速いグローバル変数を使用してみました。

       
ASCIIART(picle言語)  
# ASCIIART in the picle compiler
#  ver 0.01 2026/05/20 skyriver
# https://piclabo.seesaa.net/article/GameAsciiart.html

var X,Y,I;
var A,B,Ca,Cb,T;
var _Hex;

# Fixed-point multiplication
func fmul( X, Y ) {
    asm_($DD,$5E,$08,$DD,$56,$09,$DD,$6E);
    asm_($06,$DD,$66,$07,$01,$00,$0F,$CB);
    asm_($7C,$28,$07,$0C,$AF,$95,$6F,$9C);
    asm_($95,$67,$EB,$29,$30,$07,$0C,$AF);
    asm_($95,$6F,$9C,$95,$67,$CB,$19,$08);
    asm_($4C,$7D,$21,$00,$00,$29,$17,$CB);
    asm_($11,$30,$03,$19,$CE,$00,$10,$F5);
    asm_($6C,$67,$08,$30,$06,$AF,$95,$6F);
    asm_($9C,$95,$67,$DD,$F9,$D1,$DD,$E1);
    asm_($C9);
}


proc main() {
    _Hex = "0123456789ABCDEF ";
    for ( Y = -12; Y < 13; Y = Y + 1 ) {
        for ( X = -39; X < 40; X = X + 1 ) {
            Ca = fmul( $0bb9, X );  # 0.0458f * X
            Cb = fmul( $1555, Y );  # 0.08333f * Y
            A = Ca;
            B = Cb;
            for ( I = 0; I < 16; I=I+1 ) {
                T = fmul( A + B, A - B ) + Ca;
                B = fmul( A, B );
                B = B + B + Cb;
                A = T;
                if ( (fmul( A, A ) + fmul( B, B )) > (4 * 256) ) {
                    break;
                }
            }
            PrnChar_( _Hex[I] );
        }
        PrnChar_( 10 );
    }
}

 下図は実行後の画面キャプチャです。TeraTerm のマクロで実行時間を計測しています。

ASCIIART 実行画面

 今回の試験は CP/M 上で行っているので当然 MSXPen でも動作しました。

MSXPen 上で実行中の ASCIIART

★追記 2026/06/27 {
 プロトタイプ宣言と外部処理定義のテスト。1パスコンパイラなのでプロトタイプ処理の実体のコードに遭遇した時にアドレスが決定し、この時前方参照してコールしている箇所のアドレスも設定している。外部処理のGoCpm()を実行することでCP/Mに戻っている。

プロトタイプ宣言と外部処理定義のテスト
}


5.おわりに
 まだ未完成ではありますが、Z80 用の picle 言語コンパイラが何とか動作するようになってきました。気が向いた時に作業を進め、冒頭に書いた構想の実現に向けて進めていきたいと思います。



[TOP] [ 前へ ] Z80用picle言語コンパイラの開発 [ 次へ ]

posted by skyriver at 20:48| Comment(0) | Z80 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
コチラをクリックしてください