-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
While developing Z80 assembly code for a ZX Spectrum book project (23 chapters, 11+ compilable .a80 examples, a wireframe torus demo), we systematically tested mza's Z80 instruction support and found several gaps. These are blocking real-world Z80 development — every example needs manual workarounds.
This issue documents all findings with minimal reproducible test cases.
1. Missing IX/IY indexed addressing modes (HIGH PRIORITY)
What works ✅
ld a, (ix+0) ; OK — load A from (IX+d)
ld (ix+0), a ; OK — store A to (IX+d)
set 0, (ix+0) ; OK — bit set
res 0, (ix+0) ; OK — bit reset
add ix, de ; OK — 16-bit add
What fails ❌
a) LD (IX+d), imm — store immediate to indexed address
ld (ix+0), 0 ; ERROR: invalid operands for LD
ld (ix+5), $FF ; ERROR: invalid operands for LD
Expected: DD 36 dd nn (3 bytes). This is one of the most common IX patterns in Z80 code.
Workaround: Two instructions instead of one:
ld a, 0
ld (ix+0), a
b) LD r, (IX+d) where r ≠ A — load register from indexed address
ld b, (ix+1) ; ERROR: invalid operands for LD
ld c, (ix+2) ; ERROR: invalid operands for LD
ld d, (ix+3) ; ERROR
ld e, (ix+4) ; ERROR
ld h, (ix+5) ; ERROR
ld l, (ix+6) ; ERROR
Expected: DD 46+r*8 dd (2 bytes). Only ld a, (ix+d) works currently.
Workaround:
ld a, (ix+1)
ld b, a
c) LD (IX+d), r where r ≠ A — store register to indexed address
ld (ix+0), b ; ERROR: invalid operands for LD
ld (ix+1), c ; ERROR
; etc. for D, E, H, L
Expected: DD 70+r dd (2 bytes). Only ld (ix+d), a works.
d) INC/DEC (IX+d) — increment/decrement indexed memory
inc (ix+1) ; ERROR: invalid operands for INC
dec (ix+1) ; ERROR: invalid operands for DEC
Expected: DD 34 dd / DD 35 dd (2 bytes).
Workaround: Three instructions:
ld a, (ix+1)
inc a
ld (ix+1), a
e) ALU operations with (IX+d)
add a, (ix+1) ; ERROR: invalid operands for ADD
sub (ix+2) ; likely fails too
and (ix+3) ; likely fails too
or (ix+4) ; likely fails too
xor (ix+5) ; likely fails too
cp (ix+6) ; likely fails too
Expected: DD 86+op*8 dd (2 bytes).
Impact
IX-indexed addressing is the standard way to access structured data (entities, sprites, records) in Z80 game and demo code. With only ld a,(ix+d) and ld (ix+d),a supported, every entity system, sprite routine, or data structure access needs 2-3x more instructions than necessary.
Our ch18 game skeleton (539 lines) required rewriting ~40 instructions to work around these limitations.
2. Missing expression operators (MEDIUM PRIORITY)
>> (right shift) not supported
sin_table: DS 256
; ...
ld h, sin_table >> 8 ; ERROR: invalid expression
This is a standard pattern for loading the high byte of a page-aligned address. Very common in Z80 code for fast table lookups (e.g., ld h, table >> 8 / ld l, index).
Workaround: Use ld hl, label instead (wastes a byte and timing, only works for page-aligned tables):
ld hl, sin_table ; 3 bytes, 10T instead of 2 bytes, 7T
<< (left shift) — untested but likely same issue
Desired: full C-style expression operators
At minimum: >>, <<, &, |, ^, ~ (shift, bitwise AND/OR/XOR/NOT).
3. Negative values in DB directives (LOW-MEDIUM)
velocity: DB -1 ; ERROR or wrong value
offset: DB -4 ; ERROR
Expected: truncate to 8-bit unsigned (-1 → $FF = 255, -4 → $FC = 252).
Workaround: Use unsigned equivalent manually:
velocity: DB 255 ; -1 as unsigned byte
offset: DB 252 ; -4 as unsigned byte
4. Parentheses in expressions parsed as memory references (LOW-MEDIUM)
ENTITY_SIZE EQU 10
ld de, (9 * ENTITY_SIZE) ; ERROR: parsed as memory load from address 90
mza interprets (expr) as indirect addressing ld de, (nn) instead of grouping.
Workaround: Remove parentheses (Z80 expressions are left-to-right anyway):
ld de, 9 * ENTITY_SIZE
This is technically correct behavior per some Z80 assembler traditions (Zilog syntax uses parentheses for indirection), but most modern assemblers (sjasmplus, pasmo, z88dk) allow (expr) as grouping in non-ambiguous contexts.
5. Case-insensitive label collision (NOTE)
STATE_TITLE EQU 0
; ...
state_title: ; ERROR: label already defined
; handler code
mza treats labels as case-insensitive, so STATE_TITLE and state_title collide.
Workaround: Use distinct prefixes:
ST_TITLE EQU 0
; ...
state_title: ; OK, different from ST_TITLE
This may be intentional design. If so, documenting it would help.
Priority ranking for fixes
| # | Issue | Impact | Frequency | Suggested priority |
|---|---|---|---|---|
| 1a | LD (IX+d), imm |
High | Very common | 🔴 Critical |
| 1b | LD r, (IX+d) for all r |
High | Very common | 🔴 Critical |
| 1c | LD (IX+d), r for all r |
High | Common | 🔴 Critical |
| 1d | INC/DEC (IX+d) |
Medium | Common | 🟡 Important |
| 1e | ALU ops with (IX+d) |
Medium | Common | 🟡 Important |
| 2 | >> / << operators |
Medium | Common in table code | 🟡 Important |
| 3 | Negative DB values | Low | Occasional | 🟢 Nice to have |
| 4 | Parens in expressions | Low | Rare | 🟢 Nice to have |
| 5 | Case-insensitive labels | Info | — | 📝 Document |
Environment
- mza version: current main branch (Feb 2026)
- Target:
--target zxspectrum - Test project: antique-toy (Z80 demoscene book)
- 11 compilable .a80 files all using workarounds for these issues
Suggested encoding references
The IX/IY prefix scheme is systematic:
DDprefix turns(HL)instructions into(IX+d)variantsFDprefix turns them into(IY+d)variants- The displacement byte
dis inserted after the opcode
So fixing LD B, (HL) (46) to also support LD B, (IX+d) (DD 46 dd) should be a pattern that applies to all similar instructions uniformly.