はじめに
Z80 アセンブラなのに「現代的」とはこれ如何に...
私が開発したゲーム機「VGS-Zero」では CPU に 16MHz の Z80 が搭載されていて、C 言語でゲームを開発できますが、Z80 アセンブリ言語でゲームを開発することもできます。
ですが、私が Z80 アセンブリ言語でゲームを開発した実績は、今の所ゲームギア用のゲーム 1 本のみで、まだ本格的なゲームを VGS-Zero + Z80 では完成させていません。
何度か開発のキックオフを試みているのですが、断念したり中断している状況です...
これは良くない兆候です。
ゲームギアソフトの開発については、C言語で開発したVGS-Zero用ゲーム(既存ゲーム)の「移植」だったので完成させることができたのかも...と疑っているところです。
アセンブリ言語でのゲーム開発は中々大変です。
それでもアセンブリ言語でのゲーム開発を進めようとしている理由は、上記のゲームギアソフトの開発で、Z80 アセンブリ言語でゲーム開発することで 魂を吹き込みやすい という何とも非合理的なメリットを見出すことに成功 (?) したためです。
私見ですが、プログラム開発というのは言わば「合理の追求行為」であるのに対して、コンテンツ創作とは逆に「非合理の追求行為」という相反関係にあるから話がややこしくなるのかもしれません。
私は職業プログラマなので、プログラムを組むことについてはそこそこ上手かもしれませんが、コンテンツ創作についてはプロではないので、私がゲーム(コンテンツ)を開発をするのであれば Z80 アセンブリ言語の方が良い塩梅になる可能性があるのではないかと。
ただ、既製のアセンブラには伝統的なアセンブリ言語の機能しかない(または弱い)ものが多く、プログラミングをするには若干の野暮ったさが拭い去れません。もちろん、私のアセンブリ言語習熟度の低さも一因ではありますが、Z80 を採用したゲーム機のプラットフォーマーの立場として、「現代のプログラム言語に慣れたプログラマにとっての参入障壁が高い状態」 は解消したいところです。
そこで、「魂の吹き込みやすさを失わない程度に野暮ったさを排除した 現代的 な Z80 アセンブラ」を自作してみることにしました。
要するに、VGS-Zero でのフルアセのゲーム開発が芳しくない状況にあるのは「筆の問題」ではないかなと。「弘法筆を選ばず」かもしれませんが、私は弘法大師ではないので筆から疑ってみることにしました。
本書では、この自作アセンブラについての特徴や設計思想について、掻い摘んで解説します。
なお、この自作アセンブラは本書執筆時点(2024.10.24)では未だベータ版です。今年中には Version 1.0.0 が完成する見込みですが、Qiitaで情報発信しておけば Z80 の猛者からアドバイスを頂ける可能性が高いと思われたので、完成前時点(主要な仕様設計はだいたい完了していて概ね動くぐらいのステータス)で記事を書いてみることにしました。
構造体対応
vgsasm
は Z80 アセンブリ言語で利用するのに適した 構造体 をサポートしています。
構造体 のようなもの は z88dk (z80asm) では defvars
で定義することができますが、これは「構造体」としての利用を想定したものではなく、恐らく(名前の通り)「変数の定義」を想定しているものだと考えられます。
AILZ80 では enum
でだいたい同じようなことが出来ると考えられますが、これも構造体ではなく飽くまでも「列挙体」だと考えられます。(※列挙体は列挙体として欲しいので vgsasm では構造体とは別に 列挙体 もサポート)
私は、ゲームを開発する上で構造体が必須 だと考えているので、vgsasm では構造体の仕様を一番最初に設計しました。
struct name $C000 {
var1 ds.b 1 ; name.var1 = $C000 ... offset(name.var1) = 0
var2 ds.w 1 ; name.var2 = $C001 ... offset(name.var2) = 1
var3 ds.b 4 ; name.var3 = $C003 ... offset(name.var3) = 3
var4 ds.w 4 ; name.var4 = $C007 ... offset(name.var4) = 7
} ; sizeof(name) = 15
vgsasm の構造体は 名前
と 開始アドレス
を指定して、スコープ内で メンバ変数群
を定義できる仕様です。
コードで 構造体名.メンバ変数名
を指定すると、それが整数のリテラルとして解釈されます。
LD HL, name.var2 ; HL = $C001
また、sizeof(構造体名)
で構造体のメンバ変数サイズの合計値を求めることができます。
LD A, sizeof(name) ; A = 15
更に、offset(構造体名.メンバ変数名)
で構造体のメンバ変数のオフセット位置を求めることができます。
LD A, offset(name.var4) ; A = 7
struct
, sizeof
, offset
がワンセットで必要です。
vgsasm の構造体は、Z80 のインデックスレジスタ(IX, IY)を用いてアクセスすることを想定しています。
Z80 では、インデックスレジスタを使ったロード・ストアの時に +d (-d) で差分オフセットを指定する仕様なので、「インデックスレジスタに構造体の先頭アドレス」、「オフセットに offset
演算子」を指定することで 簡潔なデータアクセス が記述できます。
LD IX, name ; IX = 0xC000
LD A, (IX + offset(name.var3)) ; A = (IX + 3)
そして、構造体は配列として扱うケースが多くあるので、vgsasm の構造体は、(特別な宣言をしなくても)配列としてアクセスできるようになっています。
LD HL, name[0] ; LD HL, $C000 + sizeof(name) * 0
LD HL, name[1] ; LD HL, $C000 + sizeof(name) * 1
LD HL, name[2] ; LD HL, $C000 + sizeof(name) * 2
LD HL, name[2].var3 ; LD HL, $C000 + sizeof(name) * 2 + offset(name.var3)
「特定要素への配列アクセス」であれば上記のコードで可読性の高い表記ができますが、構造体は「複数配列要素への一括アクセス」をしたいケースが多く有ります。
複数配列要素への一括アクセスをする場合、最初に配列の先頭アドレスを IX or IY にセットしておき、ループ終端で sizeof
演算子を用いて IX or IY に配列サイズを加算すれば次要素へアクセスできる形になります。
以下に、offset
と sizeof
を駆使した構造体配列アクセスの具体的なコード例を示します。
;------------------------------------------------------------
; オブジェクトをアニメーションしながら移動
;------------------------------------------------------------
.move_obj
ld b, SPRITE_NUM
ld iy, OAM[0]
ld ix, OBJ[0]
@Loop
; X 座標を計算して OAM を更新
ld hl, (ix + offset(OBJ.x)) ; X 座標(固定少数点数)を HL へ
ld de, (ix + offset(OBJ.vx)) ; X 座標の移動速度(固定少数点数)を DE へ
add hl, de ; X += VX
ld (ix + offset(OBJ.x)), hl ; X 座標を更新
ld (iy + offset(OAM.x)), h ; X 座標の整数部を OAM の X 座標へ設定
; Y 座標を計算して OAM を更新
ld hl, (ix + offset(OBJ.y)) ; Y 座標(固定少数点数)を HL へ
ld de, (ix + offset(OBJ.vy)) ; Y 座標の移動速度(固定少数点数)を DE へ
ld hl, de ; Y += VY
ld (ix + offset(OBJ.y)), hl ; Y 座標を更新
ld (iy + offset(OAM.y)), h ; Y 座標の整数部を OAM の Y 座標へ設定
; アニメーション
ld a, (ix + offset(OBJ.an)) ; アニメーション変数を取得
inc a ; アニメーション変数をインクリメント
ld (ix + offset(OBJ.an)), a ; インクリメント結果のアニメーション変数を保持
and %00001100 ; アニメーション変数からパターン番号を計算 (1/3)
sr a, 2 ; アニメーション変数からパターン番号を計算 (2/3)
inc a ; アニメーション変数からパターン番号を計算 (3/3)
ld (iy + offset(OAM.ptn)), a ; OAM にアニメーション変数から求めたパターン番号を設定
; 次要素へインデックスを移動
add ix, sizeof(OBJ) ; 次のオブジェクトへインデックスを移動
add iy, sizeof(OAM) ; 次のOAMへインデックスを移動
djnz @Loop ; SPRITE_NUM 回ループ
ret
上記コードの全体像は コチラ
上記のコードでは 座標と移動速度の実数(16bit 固定少数点数)とアニメーション変数が格納されている OBJ 構造体(要素数: SPRITE_NUM)のポインタを IX、OAM 構造体(要素数: 256)のポインタを IY に設定して、フィールドアクセス時の +d に offset
、ポインタ加算に sizeof
を使っています。
これが、私が想定する struct
, offset
, sizeof
の典型的なユースケースです。
既存のZ80アセンブラだと、このようなシンプルに記述できるものが(私が調べた範囲では)存在しませんでした。
Z80 の IX, IY は構造体アクセスの為に設計されたインデックスレジスタだと考えられますが、処理速度的な事情により現役当時(1980年代)は活用しきれていなかった印象があり、そのこともあって既存の Z80 アセンブラも IX, IY を活かしきれていない言語仕様のもの が多かったのかもしれません。
しかし、VGS-Zero なら高速な Z80 が搭載されているため、IX, IY をガンガン使っても問題ありません。
VGS-Zero は最適化が弱い SDCC でも実用的な速度のゲームを開発できたので、IX, IY をガンガン使ってもフルアセンブリ言語なら大丈夫だろう...という想定です。
vgsasm の構造体は IX, IY を扱いやすくすること に着眼して設計しましたが、その他にも色々と便利な使い方ができます。
例えば、VGS-Zero の VRAM マップ を struct
で以下のように宣言することもできます。
truct VRAM $8000 {
bg_name ds.b 32 * 32 ; BG name table
bg_attr ds.b 32 * 32 ; BG attribute table
fg_name ds.b 32 * 32 ; FG name table
fg_attr ds.b 32 * 32 ; FG attribute table
oam OAM 256 ; Sprites (Object Attribute Memory)
palette ds.w 16 * 16 ; Palettes
oam16 OAM16 256 ; 16bit position for sprites
reserved ds.b 0x100 ; reserved area
vcnt ds.b 1 ; R0: Scanline Vertical Counter
hcnt ds.b 1 ; R1: Scanline Horizontal Counter
bgSX ds.b 1 ; R2: BG Scroll X
bgSY ds.b 1 ; R3: BG Scroll Y
fgSX ds.b 1 ; R4: FG Scroll X
fgSY ds.b 1 ; R5: FG Scroll Y
irq ds.b 1 ; R6: IRQ scanline position
status ds.b 1 ; R7: Status
bgDPM ds.b 1 ; R8: BG Direct Pattern Maaping
fgDPM ds.b 1 ; R9: FG Direct Pattern Maaping
spDPM ds.b 1 ; R10: Sprite Direct Pattern Maaping
mode1024 ds.b 1 ; R11: 1024 pattern mode
}
struct
を用いることで「構造」を有するデータ全般の可読性が高くなります。
私は「ビデオゲーム」とは 構造化されたデータに対する処理の集合 だと考えているため、ゲームのプログラミングをする上で構造体の重要度は 必須レベル だと考えています。
オブジェクト指向が出来れば尚良いですが、アセンブリ言語でのオブジェクト指向プログラミング対応についての良いアイディアは思い浮かんでいません...ただ、オブジェクト指向は「有ればベター」で構造化プログラミングは「必須」というのが私の基本的な考え方なので、そもそもオブジェクト指向をサポートするモチベーションはあまり高くありません。(つまり、私のゲーム開発生産性にはあまり寄与しないかなと判断して優先度を落としました)
自動展開命令への対応
前章のコードを見て「add ix, nn なんて命令無いぞ!」と気づかれた方は流石です。この他にも IX+d からレジスタペアにロード・ストアする命令も Z80 には存在しません。
vgsasm ではこういった「かゆいところに手が届かない命令不足」を 自動展開命令 という機能で補完しています。
以下は、Version 0.3 (beta 3) の時点でサポートしている自動展開命令の一覧です。
Instruction | Auto-expand |
---|---|
LD RP1, RP2 *RP = {BC|DE|HL|IX|IY}
|
LD r1H,r2H , LD r1L,r2L or PUSH RP2 , POP RP1
|
LD BC,nn |
LD B,n(high) , LD C,n(low)
|
LD DE,nn |
LD D,n(high) , LD E,n(low)
|
LD HL,nn |
LD H,n(high) , LD L,n(low)
|
LD IX,nn |
LD IXH,n(high) , LD IXL,n(low)
|
LD IY,nn |
LD IXH,n(high) , LD IXL,n(low)
|
LD {IXH|IXL|IYH|IYL},(HL) |
PUSH AF , LD A,(HL) , LD {IXH|IXL|IYH|IYL},A , POP AF
|
LD (HL),{IXH|IXL|IYH|IYL} |
PUSH AF , LD A,{IXH|IXL|IYH|IYL} , LD (HL),A , POP AF
|
LD {BC|DE},(HL) |
LD rL,(HL) , INC HL , LD rH,(HL) , DEC HL
|
LD {IX|IY},(HL) |
PUSH AF , LD A,(HL) , LD I{X|Y}L,A , INC HL ,LD A,(HL) , LD I{X|Y}H,A , DEC HL , POP AF
|
LD (HL),{BC|DE} |
LD (HL),rL , INC HL , LD (HL),rH , DEC HL
|
LD (HL),{IX|IY} |
PUSH AF , LD A,I{X|Y}L , LD (HL),A , INC HL ,LD A,I{X|Y}H , LD (HL),A , DEC HL , POP AF
|
LD {BC|DE|HL},(IX+d) |
LD rL,(IX+d) , LD rH,(IX+d+1)
|
LD {BC|DE|HL},(IY+d) |
LD rL,(IY+d) , LD rH,(IY+d+1)
|
LD (IX+d),{BC|DE|HL} |
LD (IX+d),rL , LD (IX+d+1),rH
|
LD (IY+d),{BC|DE|HL} |
LD (IY+d),rL , LD (IY+d+1),rH
|
LD (BC), n |
PUSH HL , LD H,B , LD L,C , LD (HL),n POP HL
|
LD (DE), n |
PUSH HL , LD H,D , LD L,E , LD (HL),n POP HL
|
LD (nn), n |
PUSH AF , LD A, n , LD (nn), A , POP AF
|
ADD HL,nn |
PUSH DE , LD DE,nn , ADD HL,DE , POP DE
|
ADD IX,nn |
PUSH DE , LD DE,nn , ADD IX,DE , POP DE
|
ADD IY,nn |
PUSH DE , LD DE,nn , ADD IY,DE , POP DE
|
ADD BC,A |
ADD C , LD C,A , JR NC, +1 , INC B
|
ADD DE,A |
ADD E , LD E,A , JR NC, +1 , INC D
|
ADD HL,A |
ADD L , LD L,A , JR NC, +1 , INC H
|
ADD HL,(IX+d) |
PUSH DE , LD E,(IX+d) , LD D,(IX+d+1) , ADD HL,DE , POP DE
|
ADD HL,(IY+d) |
PUSH DE , LD E,(IY+d) , LD D,(IY+d+1) , ADD HL,DE , POP DE
|
ADD (nn) |
PUSH HL , LD L,nL , LD H,nH , ADD (HL) , POP HL
|
ADC (nn) |
PUSH HL , LD L,nL , LD H,nH , ADC (HL) , POP HL
|
SUB (nn) |
PUSH HL , LD L,nL , LD H,nH , SUB (HL) , POP HL
|
SBC (nn) |
PUSH HL , LD L,nL , LD H,nH , SBC (HL) , POP HL
|
INC (nn) |
PUSH HL , LD HL,nn , INC (HL) POP HL
|
DEC (nn) |
PUSH HL , LD HL,nn , DEC (HL) POP HL
|
SHIFT r, n |
SHIFT r x n times (n bits) |
SHIFT (HL), n |
SHIFT (HL) x n times (n bits) |
SHIFT (IX+d), n |
SHIFT (IX+d) x n times (n bits) |
SHIFT (IY+d), n |
SHIFT (IY+d) x n times (n bits) |
JP (BC) |
PUSH BC , RET
|
JP (DE) |
PUSH DE , RET
|
似たような機能は z88dk にもあるのですが、残念ながら私が欲しいパターンは網羅されていません。vgsasmの自動展開は、私が実際に VGS-Zero の examples を vgsasm で Z80 化してみて「これがやりたい」と思ったものを随時全部入れながら開発しています。
自動展開命令を使うデメリットとしては性能面の問題があります。(これは IX,IY レジスタを使用することについても言えます)
しかし、VGS-Zero の Z80 はものすごく高速(16MHz)なので、性能面での問題に直面する可能性は低いと考え、利便性を優先して自動展開命令をごりごり増やしています。
性能面の問題に直面した時、「正しい Z80 の知識」があれば性能を簡単にオプティマイズできるので、正しい知識を身につけることも有用かもしれません。
なお、RaspberryPi Zero 2W だと 16MHz が限界ですが、ターゲットを PC (Steam) のみに絞れば 100MHz 以上でも問題なく動かすことができるということを補足しておきます。(VGS-Zeroのココをイジれば OK!)
実際のところ、性能面の問題よりもプログラムサイズ面の問題の方が悩ましいかもしれません。(バンク切り替えで何とかなりますが...)
AILZ80 なら 64KB オーバーのコードも出力できるようですが、石が Z80 である以上プログラムで扱えるアドレス幅は 64KB に限られます。64KB オーバー(VGS-Zeroの場合は 32KB オーバー)のコードに対応するにはバンク切り替え込で上手くやらなければならないという課題があります。
VGS-Zero 専用命令対応(掛け算や三角関数等)
VGS-Zero の HAGe の一部を「命令」に昇格させました。
- MUL - Multiplication ... 掛け算
- DIV - Division ... 割り算
- MOD - Modulo ... 剰余残
- ATN2 - atan2 ... Fortranのatan2関数相当(8bit特化)
- SIN ... 8bit 三角関数(正弦)
- COS ... 8bit 三角関数(余弦)
これにより Z80 なのに掛け算や割り算の命令を記述できます。
私の記憶では掛け算や割り算が命令として追加されたのは 16bit CPU 以降で、16bit CPU の時代の掛け算命令はあまり効率が良くなく使い物にならなかったような気がしますが、VGS-Zero (HAGe) の掛け算命令では実用的な性能が期待できます。
なお、これらの命令は厳密には「命令」ではなく、前章の自動展開命令と同じ仕組みで実現しています。つまり、仮に Z80 を実機 IC にしても(HAGe IC が搭載されていれば)使うことができます。
残念ながら半世紀近く続いていた実機 Z80 IC の供給は今年(2024)ついに EOL になってしまいましたが、Z80 のトランジスタ数は8200個ぐらいらしいのでギリギリ自作できそうですね...しないけど。実用的には FPGA なりソフトウェアでエミュレータを動かすことができるので EOL でも何ら問題はありません(余談ですが FPGA を Not emulation と呼ぶのには違和感しかありません)
関数対応
構造体をサポートした勢いで「よっしゃ関数にも対応しとくか」程度の軽いノリで関数にも対応しておくことにしました。
しかし、関数対応は 結構面倒くさい です。
主に呼び出し規約対応が面倒です。
呼び出し規約とは、関数に渡す 引数 や 戻り値 を「どのように受け渡すか」を定義した規約です。
Z80 では CALL
と RET
でサブルーチンを記述することができますが、サブルーチンは戻りアドレスに関する規約(CALL 時スタックにCALLの次命令位置が PUSH され、RET 時にスタックから POP したアドレスへジャンプすること)のみが規定されていて、引数と戻り値についての規定はありません。
つまり、サブルーチンで引数を使いたい場合、呼び出し元と呼び出し先で「パラメタを HL に設定して呼び出す」などの独自の規約を定義して使う必要があります。
この規約をいちいち覚えるのが非常に面倒くさい...
サブルーチンのラベル名を ほにゃらら_with_HL
などとする手もありますが、少し気持ち悪いです。(こういう「運用でカバー」的なものは新規開発の言語仕様としては避けるべきです)
そこで、引数呼び出し規約を簡単に定義できる #macro
というプリプロセッサを作りました。
.main
hoge(1, 2, 3)
HALT
#macro hoge(arg1, arg2, arg3) {
PUSH BC
PUSH DE
PUSH HL
LD BC, arg1 ; 引数1 (arg1) を BC にセット
LD DE, arg2 ; 引数2 (arg2) を DE にセット
LD HL, arg3 ; 引数3 (arg3) を HL にセット
call _hoge ; hoge のメイン処理(サブルーチン)を呼び出す
POP HL
POP DE
POP BC
}
_hoge:
; hoge の処理を記述(BC, DE, HL を使ってゴニョゴニョする)
; 戻り値の規約化はできないので A or F あたりでやって頂ければ...
ret
マクロは、呼び出し元で与えられた数値(文字列リテラルやラベルでも可)をマクロ内コードに展開したコードを呼び出し元で展開するプリプロセッサです。
これなら比較的簡単に関数対応ができます。
文字列リテラル
アセンブリ言語で沢山文字を表示するプログラムを書く時、DB
で文字列リテラルを定義(ラベル付与)して、ラベルのアドレス参照をするステップでプログラムを書く必要があり、「ものすごく野暮ったい」です。
.main
LD HL, HelloText
CALL print_text
HALT
HelloText: DB "Hello, World!", 0
そこで、vgsasm では 文字列リテラル の機能をサポートしました。
以下の簡潔なコードで上述のコードと等価の処理を記述できます。
.main
LD HL, "Hello, World!"
CALL print_text
HALT
文字列リテラルは、内部的に次のようなコード展開を自動的に行います。
LD BE, "HELLO,WORLD!"
LD DE, "HOGE"
LD HL, "HELLO,WORLD!"
:
LD BE, $0
LD DE, $1
LD HL, $0
:
$0: DB "HELLO,WORLD!", 0
$1: DB "HOGE", 0
(補足)
- 自動的に 0x00 終端の文字列リテラルをコード末尾に追記
- 完全に同じ文字列リテラルについては自動集約
-
$
で始まるラベルはシステムラベル(ユーザープログラムでは定義不可)
ラベル
vgsasm では次の3種類の ラベル をサポートしました。
- 通常ラベル (
LABEL:
or.LABEL
) - インナーラベル(
@LABEL
) - 匿名ラベル(
@+n
)
インナーラベルは通常ラベルの配下に属するラベルです。
.Hoge
:
@Foo ; Foo@Hoge
:
.Hige
:
@Foo ; Foo@Hige
:
JR @Foo ; Foo@Hige へジャンプ
匿名ラベルは行番号を基準にした匿名のラベルです。
AND A ; @-3
LD A, ($FFFF) ; @-2
XOR A ; @-1
JR @+4 ; @+0 <--- use anonymous label
LD A, B ; @+1
LD A, ($1234) ; @+2
LD (IX+4), A ; @+3
MUL HL, C ; @+4 <--- Jump here
HALT ; @+5
なお、匿名ラベルには別のラベル(別の匿名ラベルを含む)を跨げない制約があります。
匿名ラベルは「命令が何バイトなのかを計算しなくても良いショートジャンプ用のラベル」です。Z80 では割とショートジャンプしたいケースへのエンカウントが多くあるので、その時にいちいちラベルを書くのはものすごく面倒くさいのです。
インクリメント演算子
例えば「HL に格納されたアドレスを A に取り出す処理の繰り返し」は割と多くあるので、vgsasm では C言語 と同様のインクリメント演算子(ポストインクリメント、プリインクリメント、ポストデクリメント、プリデクリメント)に対応しています。
例えば以下のコードは、
LD A, (HL++)
次のように展開されます。
LD A, (HL)
INC HL
もちろん、HL に限らず全てのレジスタで使うことができます。
A レジスタを 1 bit 左シフトした後で 1 を加算したい時は次のように記述できます。
SLA A++
また、HL が指すアドレスに 1 加算した A を代入した後で A を元に戻したいという複雑な処理も 1 行で記述できます。
LD (HL), ++A--
代入演算子
Z80 に慣れてくると、何の疑問もなく LD A, B
のような記述をしています。
Z80 に慣れていないと「LD
って何やねん!」となる筈です。
ですが A = B
なら「A に B を代入する」と プログラマなら 誰でも簡単に理解できます。(A と B は違うだろ!というツッコミはさておき)
という訳で vgsasm では 代入演算子 に対応しました。
A = B ; expand to -> LD A, B
(HL) = A ; expand to -> LD (HL), A
$C000 = A ; expand to -> LD ($C000), A *Can omit addressing (bracketing) at load time
($C000) = A ; expand to -> LD ($C000), A *same as `$C000 = A`
A = (HL) ; expand to -> LD A, (HL)
A = $FF ; expand to -> LD A, $FF
A = $C000 ; expand to -> LD A, $C000 *Error (out of range)
A = ($C000) ; expand to -> LD A, ($C000) *Addressing at store requires bracketing
BC = $C000 ; expand to -> LD BC, $C000
BC = ($C000) ; expand to -> LD BC, ($C000)
A += B ; expand to -> ADD A, B
HL += DE ; expand to -> ADD HL, DE
A -= B ; expand to -> SUB A, B
A &= B ; expand to -> AND A, B
A |= B ; expand to -> OR A, B
A ^= B ; expand to -> XOR A, B
A <<= 3 ; expand to -> SLA A, 3
A >>= 3 ; expand to -> SRL A, 3
もちろん、伝統的なニーモニックの記述も可能ですが、基本的に代入演算子を使用することを推奨します。
理由は、代入演算子を使った方が入力の手間を 1 命令あたり 1〜2 バイト程度削減することができるためです。
1000 ステップの代入コードなら、冗長な情報量が 1k 以上ある状態だと思われるので、代入演算子を使うことでプログラムの可読性がかなり高くなります。
Kotlin では Java から「行末の ; を省略できる」という微細な仕様変更がされていますが、たったそれだけの変化が割と大きな違いがあったという実体験から、現代のプログラム言語ではこういう「冗長な情報を削る対応」が必要なものだと判断しました。
Kotlinがあればもう言語としてのJavaは要らない(JREだけあれば良い)と思われることもあり、もう長らく本家Javaを触っていないこともあり自信ありませんが、後になってから本家Javaでも ; は排除されたかも?
ツールチェインとして組み込みやすいこと
これは言語自体の仕様とは異なりますが、私は Z80 アセンブラを VGS-Zero のツールチェインとして組み込みたい と考えています。
ですが、z88dk の z80asm は依存する submodule が複雑なので(一応実験的に組み込みできましたが)ゲーム開発者が VGS-Zero SDK を準備する手間が増えてしまい良くないと考えました。
z88dk 以外の選択肢も調査しましたが、同様に dotnet で作られていたり、Boost を使っていたりと、依存関係を含めたコードサイズが大きなものしかありませんでした。依存関係を含めたコードサイズは通常考慮する必要が無いので当然のことかもしれません...
そこで、vgsasm は build-essentials
のみでコンパイル可能な状態にしました。
sudo apt install build-essential
git clone https://github.com/suzukiplan/vgsasm
cd vgsasm
make
エンドユーザ向けのツールのようにバイナリコードで配布するのも手かもしれませんが、Linux の ABI は割とコロコロ変わるのでバイナリ配布はなるべく避けたいと思っています。
更に、コンパイル対象のソースコードがシングルコードなので、クリーンビルドに掛かる時間もわずか 2〜3 秒です。
$ make clean all
rm -f vgsasm
g++ -std=c++17 -g -o vgsasm src/vgsasm.cpp
$
vgsasm は「Specialized for VGS-Zero」を標榜していますが、例えばゲームギアに特化した ggasm を vgsasm をコードベースにして作ってみるのも面白いかもしれませんね。GPLv3 の許諾範囲内でご自由にどうぞ。