Skip to content

Z80 assembler: missing IX/IY indexed instructions and expression operators #10

@oisee

Description

@oisee

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:

  • DD prefix turns (HL) instructions into (IX+d) variants
  • FD prefix turns them into (IY+d) variants
  • The displacement byte d is 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions