Introducing ZAX
by John Hardy
ZAX is a structured assembler for the Z80 family. I built it for the stage of assembly programming where a project stops being a few routines and starts becoming a codebase. Registers, flags, memory placement, and instruction choice still matter just as much as they do in ordinary assembly. ZAX keeps those decisions explicit and adds structure around them so the source stays readable as the program grows.
The goal is practical. I want the directness of assembly without letting larger programs dissolve into labels, calling conventions in comments, and hand-maintained offset arithmetic. ZAX gives that work a place in the language itself.
Assembly remains explicit
ZAX still works in assembly terms. I choose registers. I set flags with ordinary Z80 instructions. I decide what lives in ROM and what lives in RAM. A call still lowers to a normal Z80 call sequence. A loop still depends on the machine state I established in the instructions above it.
That directness is important because the machine is the whole point. When I read a ZAX file, I still want to see how the code relates to hardware, registers, flags, and storage. ZAX keeps that view intact.
The source gains structure
What ZAX adds is structure at the level where larger projects usually become fragile. Source files become modules with imports, named sections, and exported entry points. Memory layouts can be declared with arrays, records, unions, and enums. Functions have typed arguments, locals, and a stable calling convention. Control flow can be written with if, while, repeat, and select, while still using the Z80 condition codes directly.
This gives the source a stronger shape without changing what kind of program it is. It is still assembly. It simply carries more of the design in the file itself.
Here is a small example from the kind of code ZAX is meant to support:
const MsgLen = 5
section data vars at $4000
msg: byte[5] = "HELLO"
end
extern func bios_putc(ch: byte): void at $F003
export func main(): void
var
p: addr
end
ld hl, msg
ld p, hl
ld b, MsgLen
repeat
ld hl, p
ld a, (hl)
inc hl
ld p, hl
push bc
bios_putc A
pop bc
dec b
until Z
end
The instructions are ordinary Z80 work. The file around them is the part that changes. Data has a named section. The external BIOS routine has a declaration. The program entry point is explicit. The loop reads as a loop.
Why this matters
The value in ZAX is that assembly projects keep their shape for longer. Imports show where names come from. Layout declarations keep offsets attached to the data they describe. Function boundaries give calls a stable form. Structured control flow makes the execution path easier to recover after time away from the code.
That is the concept behind the project. ZAX is assembly with enough structure to stay coherent when the program becomes real.
Why ZAX Replaces Macros with Typed Ops
By John Hardy
One of the most distinctive parts of ZAX is op, a way to define new instruction-shaped building blocks with typed operands. I wanted this because reusable patterns appear constantly in assembly work, especially on the Z80, and traditional macro systems do a poor job of carrying those patterns once a project becomes large. ZAX keeps the convenience of reusable instruction families while grounding them in the actual operands that appear at the call site.
Reuse at the instruction level
An op looks and reads like an instruction. It expands inline, so there is no call overhead, and it can be overloaded so a single name covers a family of related operand shapes. That makes it useful for the repetitive low-level work that assemblers have always needed macros for.
This is a small example:
op add16(dst: HL, src: reg16)
add hl, src
end
op add16(dst: DE, src: reg16)
ex de, hl
add hl, src
ex de, hl
end
At the call site, add16 DE, BC reads like a normal instruction. The compiler sees that the destination is DE, selects the matching form, substitutes the operands, and emits the correct inline sequence. The source stays compact and the machine behaviour stays visible.
The operands are part of the contract
The important idea is that operands are matched by kind. A declaration can require HL, any reg16, an immediate, a condition code, or another specific operand class. That gives reusable patterns clear boundaries. If I pass the wrong shape, the compiler can reject the call in terms that make sense for the original source.
This is a practical improvement over text substitution. The tool understands that DE is a register pair and Z is a condition code. It is working with the parsed program rather than a stream of pasted tokens. That keeps reusable instruction forms connected to the machine concepts they represent.
A better fit for long-lived assembler code
Typed ops matter because they let an assembler project grow without pushing more logic into fragile macro text. I can give a repeated pattern a name, define the valid operand forms once, and keep the final expansion inline and inspectable. That is exactly the kind of facility a structured assembler should offer.
ZAX uses typed ops because they are a better unit of reuse for assembly code. They keep the language close to the machine while giving repeated instruction patterns a clear and durable home.
How ZAX Makes Larger Z80 Programs Manageable
By John Hardy
ZAX matters most when a Z80 program stops being a single routine and starts becoming a codebase. At that point the problem is not instruction syntax. The problem is keeping modules, memory layouts, call boundaries, and control flow understandable as the program grows. ZAX gives those parts a clear place in the language so the source continues to read like a program instead of a pile of local assembler conventions.
Files read like modules
ZAX starts by treating the source file as a module. Imports live at module scope. Code and data live in named sections. Public entry points are marked with export. External routines at fixed addresses can be declared once and then called through a normal function-like surface. This gives a project a stable shape from the first screenful of source onward.
That structure is useful because it reduces hidden knowledge. I do not need to remember which label is intended as an entry point or which block of bytes belongs to which subsystem. The file tells me.
Memory layout lives in the source
The next improvement comes from typed layout declarations. ZAX supports arrays, records, unions, and enums so the source can describe memory the same way the program uses it. The compiler then handles the offset arithmetic that would otherwise live in comments or hard-coded constants.
That changes the way addressing reads. A form like sprites[C].x expresses the same intent that exists in my head when I write the code. The addressing model still lowers to ordinary Z80 work, but the layout calculation moves into the compiler where it belongs. The result is less drift between the program design and the addresses in the source.
Calls have a stable shape
ZAX also gives functions a real boundary. A function declaration names the arguments, return type, and locals. An external ROM or BIOS routine can be declared with the same surface, including its fixed address. Call sites then stay compact and readable while the compiler handles the push, call, and cleanup sequence consistently.
That consistency matters in assembly. A large program collects many tiny calling conventions if the tool does not provide one. ZAX gives the project a single calling model that I can rely on across modules.
Control flow stays readable
Structured control flow completes the picture. ZAX provides if, while, repeat, and select, and each one still depends on the flags that the instructions have already set. The source gains the shape of a loop or branch without losing the underlying machine logic.
This kind of loop is typical:
ld b, MsgLen
repeat
ld hl, p
ld a, (hl)
inc hl
ld p, hl
push bc
bios_putc A
pop bc
dec b
until Z
The machine story is still there. dec b sets the zero flag and until Z uses it. ZAX simply carries the branch bookkeeping so the source keeps its shape.
A better environment for real assembly work
ZAX does not try to hide the hard parts of assembly programming. It gives those hard parts a better environment. Source files gain a module structure. Layouts stay attached to the code that uses them. Calls follow a single convention. Control flow reads cleanly. That is enough to make a large difference in a long-lived Z80 project.
That is the practical promise of ZAX. It keeps assembly direct and makes bigger programs easier to hold in my head.