253 commits, 8 frontends, 3 backends, FAT12/16 filesystem, TUI framework with compile-time metafunctions, Mini Norton Commander, @target() platform detection — in one week.
14 commits in one session: from wrong code to 100% across all 4 frontends. PBQP (global strategist) + WFC (local tactician) register allocation, with Z80-specific innovations no existing compiler has.
| Corpus | Functions (×3 machines) | Pass Rate |
|---|---|---|
| C89 | 720 | 100.0% |
| Nanz | 162 | 100.0% |
| Lizp | 57 | 100.0% |
| Lanz | 9 | 100.0% |
| Total | 948 | 100.0% |
| Innovation | What | Savings |
|---|---|---|
| IXH/IXL call-safe spill | Undocumented IX halves survive CALL | 8T vs 11T PUSH/POP |
| PBQP→WFC hint bridge | Global + local allocators cooperate | = production output |
| Tail call optimization | CALL+RET → JP | -17T per tail call |
__mul8/__mul16 runtime |
Shared multiply routines | code size vs inline |
| Save-before-overwrite | Destructive Z80 ALU handling | correctness |
→ Architecture & Innovations | Quick Reference | Journey Report
- ABAP Frontend + SQLite + Zork — 7th frontend (ABAP via abaplint), SQLite host functions in MIR2 VM, CP/M file I/O fixed (ROM protection root cause), Zork I (1983) runs in MZE.
- Nanz Z80 Showcase v2 — 12 verified examples:
abs_diff6B (optimal),swap1B (bare RET),smaller0B (EQU),popcount3-inst LUT,@smccompiled sprites, value pipes constant-folded, iterator DJNZ fusion. PluselimJrToRetpeephole:JR cc → RET→RET cc. - C89 Frontend vs SDCC — 6th frontend: C89/C99 via
modernc.org/cc/v4. Identical C source, MinZ 81B vs SDCC 179B (−55%). Pair-return byte-identical to Nanz. See table below. Progress report: 9 corpus files, 51 functions, 68 MIR2 asserts pass. - Six Frontends, Universal Assert — Nanz, Lanz, Lizp, PL/M-80, Pascal, C89 — all compile through one HIR → MIR2 → Z80 pipeline. Compile-time assert works in all 6. Pascal → CP/M hello world runs in MZE.
- Nanz Language Book v5.3 — 21 chapters + 8 appendices. New: five-frontend architecture, universal assert syntax, Pascal/Lizp imports, transpilation via
--emit. - ZX Spectrum Tetris — 853 LOC, 7 tetrominoes, SRS wall kicks, hold/next/ghost piece, T-spin scoring. Attribute-based rendering for fast frame updates.
- Nanz Language Sprint: 6 features — enums, type aliases, module imports, three string types, pipe/trans named pipelines with DJNZ fusion.
- Arena allocator + sandbox + sizeof — struct-based bump allocator with
^Arenapointer receiver,arena_splitchaining,sizeof(Type)compile-time constant. - PreallocCoalesce delivers —
mapInPlaceloop: 5 instructions → 1 DJNZ.factorial_fold: entire mul16 routine eliminated. - MOS 6502 backend alive — 35/35 tests, dual-VM oracle (MIR2 VM vs sim6502), console I/O for Apple II/C64/BBC Micro.
Identical C source compiled through both toolchains. Binary sizes (code only):
| Function | MinZ C89 | SDCC 4.2.0 | Delta | Notes |
|---|---|---|---|---|
twice(i16)→i16 |
2B | 3B | −1B | SDCC: EX DE,HL return tax |
add(i16,i16)→i16 |
2B | 3B | −1B | SDCC: EX DE,HL return tax |
max(i16,i16)→i16 |
12B | 12B | TIE | Both clever compare tricks |
abs_diff(u8,u8)→u8 |
9B | 11B | −2B | MinZ: RET Z/RET C conditional return |
sum_to(i16)→i16 |
21B | 25B | −4B | MinZ: no trampoline |
clamp8(u8,u8,u8)→u8 |
10B | 30B | −20B | MinZ: 3-reg ABI + RET Z/C |
minmax(u16,u16)→(u16,u16) |
19B | 61B | −42B | MinZ: tuple return + RET C/Z |
smaller (uses lo) |
0B | 34B | −34B | MinZ: EQU minmax (degenerate!) |
larger (uses hi) |
6B | — | — | |
| TOTAL | 81B | 179B | −55% | Full report → |
Write modern code. Run it on Z80, eZ80, 6502, and more.
Quick Start | Features | Examples | Targets | Toolchain
MinZ is a compiler toolchain for retro hardware — primarily Z80 and eZ80, with an experimental MOS 6502 backend.
The primary frontend is Nanz (.nanz) — a minimal, type-safe language that compiles through the HIR → MIR2 → Z80 pipeline with PBQP register allocation. Five additional frontends — Lanz (S-expressions), Lizp (Lisp dialect), PL/M-80, Pascal, and C89 — compile through the same backend. Cross-language imports are first-class.
Self-contained toolchain: compiler, assembler, emulator, disassembler, and remote runner. No external dependencies — pure Go.
import stdlib.cpm.bdos;
fun main() -> void {
@print("Hello from MinZ!");
let fib_a: u16 = 0;
let fib_b: u16 = 1;
for i in 0..10 {
print_u16(fib_a);
putchar(32); // space
let next = fib_a + fib_b;
fib_a = fib_b;
fib_b = next;
}
}
This compiles to Z80 assembly, assembles to a .com binary, and runs on CP/M:
$ mz fibonacci_cpm.minz -b z80 --target cpm -o fib.a80 && mza fib.a80 -o fib.com
$ mze fib.com -t cpm
Fibonacci:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
git clone https://github.com/oisee/minz.git
cd minz/minzc
make all # Build all 9 tools
make install-user # Install to ~/.local/bin/No external dependencies. Pure Go.
# Compile MinZ to Z80 assembly
./mz ../examples/hello_print.minz -o hello.a80
# Assemble to binary
./mza hello.a80 -o hello.tap
# Run in emulator
./mze hello.tapmz program.minz -b z80 --target spectrum -o prog.a80 # ZX Spectrum
mz program.minz -b z80 --target cpm -o prog.a80 # CP/M
mz program.minz -b z80 --target agon -o prog.a80 # Agon Light 2
mz program.minz -b c -o prog.c # C99 (partial — simple programs only)
mz program.minz -b crystal -o prog.cr # Crystal (stub — not functional)| Feature | Description |
|---|---|
| Types | u8, u16, i8, i16, bool, void, pointers |
| Functions | fun/fn declaration, overloading, multiple returns |
| Control flow | if/else, while, for i in 0..n, loop {} |
| Structs | Declaration, field access, UFCS method syntax |
| Arrays | Declaration, indexing |
| Globals | global counter: u8 = 0; |
| String interpolation | "Hello #{name}!" (Ruby-style) |
| Inline assembly | asm { LD A, 42 } blocks, [addr] bracket indirection |
| CTIE | Compile-Time Interface Execution (trait monomorphization) |
| True SMC | Self-modifying code optimization |
| @extern FFI | extern fun putchar(c: u8) at 0x10; with RST optimization |
| Operator overloading | v1 + v2 via impl blocks |
| Error propagation | @error(code) with CY flag ABI |
| Enums | enum State { IDLE, RUNNING } with values |
| Module system | import stdlib.cpm.bdos; |
| Lambdas | Closure syntax, zero-cost transform |
| PL/M-80 frontend | Parse + HIR lowering for all 26 Intel 80 Tools corpus files (100%); 1338 functions, 11661 statements |
| Nanz frontend | New active source language for the MIR2 backend; arithmetic, control flow, loops, function calls |
| LUTGen | u8<lo..hi> ranged type annotation → compile-time table generation; popcount loop → 3-instruction LUT at runtime |
| Flag-return ABI | Functions returning bool from a comparison pass the result via carry flag — no LD A, 0/1 materialization |
| Interprocedural CC opt | Register class chosen per call-site: params coerced to A/B/C/HL/DE based on callee contract |
| JRS pseudo-instruction | Codegen emits JRS for all branches; MZA picks JR (2B) or JP (3B) based on offset and condition |
| Feature | Status |
|---|---|
| Pattern matching | Syntax parses, codegen partial |
| Iterator chains | 9 ops on Z80 + inline lambda filters + fusion optimizer (inlines callbacks in DJNZ loops). 87+ tests, 11/11 E2E hex-verified, all pass. enumerate/reduce at MIR level (Z80 needs OpPush fix). See Status |
| MIR interpreter | Arrays/structs working, not complete |
- Register allocator has bugs with overlapping lifetimes in complex loops
- Some loop/arithmetic combinations produce incorrect code
loadToHLcan use stale values in multi-expression contexts- Loop rerolling can be too aggressive across function call boundaries
These are documented and being worked on. Simple programs (hello world, fibonacci, demos) work correctly. Complex programs with nested loops and heavy arithmetic may hit edge cases.
Nanz is the primary language for the HIR→MIR2→Z80 pipeline. Real compiled output:
fun abs_diff(a: u8, b: u8) -> u8 {
if a > b { return a - b }
return b - a
}
fun clamp(x: u8, lo: u8, hi: u8) -> u8 {
if x < lo { return lo }
if x > hi { return hi }
return x
}
Generated Z80 (actual mz output):
abs_diff:
CP C
JR Z, .abs_diff_if_join2
JR C, .abs_diff_if_join2
.abs_diff_if_then1:
SUB C
LD C, A
RET
.abs_diff_if_join2:
NEG
ADD A, C
LD C, A
RET
clamp:
CP D ; x vs lo
JR NC, .clamp_if_join2
.clamp_if_then1:
LD A, D
RET
.clamp_if_join2:
CP C ; x vs hi
JR Z, .clamp_if_join4
JR C, .clamp_if_join4
.clamp_if_then3:
LD A, C
RET
.clamp_if_join4:
RET(examples/nanz/05_four_pointers.nanz · 06_pbqp_weighted.nanz · 07_ix_load_store.nanz)
The PBQP allocator weights each virtual register's cost by its use count. A register used 10× pays 10× the slot cost, so the solver puts it in the cheapest location — even when that means displacing a low-use register.
Four simultaneously-live pointer registers → HL / DE / BC / IX (no spill):
// examples/nanz/05_four_pointers.nanz
fun four_ptrs(p0: ptr, p1: ptr, p2: ptr, p3: ptr) -> u8 {
var v0: u8 = p0[0]
var v1: u8 = p1[0]
var v2: u8 = p2[0]
var v3: u8 = p3[0] // p3 → IX under register pressure
var s01: u8 = v0 + v1
var s23: u8 = v2 + v3
return s01 + s23
}
four_ptrs:
LD C, (HL) ; p0 → HL (cost 0)
LD D, (DE) ; p1 → DE (cost 4)
LD E, (BC) ; p2 → BC (cost 6)
LD H, (IX+0) ; p3 → IX (cost 8) ← (IX+0) not $F0xx memory!
LD A, C
ADD A, D
LD C, A
LD A, E
ADD A, H
...
RETHigh-use vs low-use — PBQP always puts the hot reg in the cheap slot:
// examples/nanz/06_pbqp_weighted.nanz
fun weighted(x: u8) -> u8 {
var light: u8 = 1 // used 1× — displaced to C
var heavy: u8 = x // used 10× — stays in A (0T per use)
heavy = heavy + x // ... repeated 9 more times
...
return heavy + light
}
weighted:
LD C, 1 ; light → C (1× use, forced out of A)
ADD A, A ; heavy stays in A throughout (10× use, 0T/use)
LD D, A
ADD A, D
... ; 8 more iterations — all in A, zero memory traffic
ADD A, C ; final: heavy(A) + light(C)
RETIX store/load — undocumented HL→IX copy (16T vs 21T PUSH/POP):
// examples/nanz/07_ix_load_store.nanz
fun roundtrip_ix(hl_ptr: ptr, de_ptr: ptr, bc_ptr: ptr, val: u8) -> u8 {
bc_ptr[0] = val // bc_ptr overflows to IX under 4-reg pressure
var a: u8 = hl_ptr[0]
var b: u8 = de_ptr[0]
var back: u8 = bc_ptr[0]
return a + b + back
}
roundtrip_ix:
LD IXH, H ; undocumented DD 67 — copy HL→IX (16T, not PUSH/POP=21T)
LD IXL, L ; undocumented DD 6D
LD (IX+0), C ; store val through IX pointer
LD C, (DE)
LD D, (BC)
LD E, (HL)
...
RETAnnotate with u8<0..255> — the compiler evaluates the function for all 256 values and emits a page-aligned table:
fun popcount(x: u8<0..255>) -> u8 {
var n: u8 = 0
var v: u8 = x
while v != 0 {
n = n + (v & 1)
v = v >> 1
}
return n
}
The loop above never runs at runtime. Generated Z80:
popcount:
LD HL, popcount_lut
LD L, C ; C = input (index into table)
LD A, (HL) ; table lookup — H unchanged = page base
RET
ALIGN 256
popcount_lut:
DB 0, 1, 1, 2, 1, 2, 2, 3, ... ; 256 bytes, evaluated at compile timestruct Vec2 { x: i16, y: i16 }
impl Vec2 {
fun add(self, other: Vec2) -> Vec2 {
return Vec2 { x: self.x + other.x, y: self.y + other.y };
}
fun length_sq(self) -> i16 {
return self.x * self.x + self.y * self.y;
}
}
fun main() -> void {
let v1 = Vec2 { x: 3, y: 4 };
let v2 = Vec2 { x: 1, y: 2 };
let v3 = v1 + v2; // Zero-cost: CALL Vec2_add
let len = v3.length_sq(); // Zero-cost: CALL Vec2_length_sq
}
@ctie
fun fibonacci(n: u8) -> u8 {
if n <= 1 { return n; }
return fibonacci(n-1) + fibonacci(n-2);
}
let fib10 = fibonacci(10); // Becomes: LD A, 55 (no runtime cost)
asm fun fast_clear_screen() {
LD HL, $4000
LD DE, $4001
LD BC, 6143
LD (HL), 0
LDIR
}
import stdlib.cpm.bdos;
fun main() -> void {
@print("Hello, CP/M!");
putchar(13);
putchar(10);
let ch = getchar();
putchar(ch);
}
import stdlib.agon.mos;
import stdlib.agon.vdp;
fun main() -> void {
mos_puts("Hello from Agon Light 2!");
set_mode(3);
fill_rect(10, 10, 100, 80, 4);
}
enum FileError { None, NotFound, Permission }
fun read_file?(path: u8) -> u8 ? FileError {
if path == 0 {
@error(FileError.NotFound);
}
return path;
}
@abi("smc")
fun draw_pixel(x: u8, y: u8) -> void {
// Parameters patched directly into instruction immediates
// Single-byte opcode changes: 7-20 T-states vs 44+ for memory reads
let screen_addr = y * 32 + x;
// ...
}
MinZ aims to bring functional-style iterator chains to Z80 — with zero runtime overhead. The compiler fuses chains like .map().filter().forEach() into a single tight loop, inlining all lambdas and using DJNZ where possible.
Target syntax:
// Functional iterator chain — compiles to ONE loop, zero allocations
scores.iter()
.map(|x| x + 5)
.filter(|x| x >= 90)
.forEach(|x| print_u8(x));
// In-place mutation with ! variants
enemies.filter!(|e| e.health > 0);
particles.forEach!(|p| p.update());
// Generators (planned)
gen fibonacci() -> u16 {
let a: u16 = 0;
let b: u16 = 1;
loop {
yield a;
let tmp = a + b;
a = b;
b = tmp;
}
}
What the compiler produces — the entire chain fuses into ~25 T-states/element:
; scores.iter().map(|x| x + 5).filter(|x| x >= 90).forEach(|x| print_u8(x))
;
; No intermediate arrays. No function call overhead. Just one DJNZ loop.
LD HL, scores ; source pointer
LD B, scores_len ; counter in B for DJNZ
.loop:
LD A, (HL) ; load element (7 T)
ADD A, 5 ; .map(|x| x + 5) (4 T)
CP 90 ; .filter(|x| x >= 90) (7 T)
JR C, .skip ; skip if < 90
CALL print_u8 ; .forEach(...)
.skip:
INC HL ; next element (6 T)
DJNZ .loop ; dec B, loop (13 T)Compare: a naive indexed loop with separate map/filter passes would cost 60-150+ T-states/element and allocate intermediate arrays. The fused version uses O(1) memory and runs 3-5x faster.
Key optimizations:
- Lambda inlining — closures compile to direct
CALLor inline code, never heap-allocated - Iterator fusion — multi-stage chains merge into a single loop at compile time
- DJNZ loops — arrays ≤255 elements use Z80's dedicated loop instruction (13 T-states vs 25+ for compare-jump)
- Pointer arithmetic —
HLwalks the array withINC HL, no index multiplication
Testing (v0.19.5): 87+ tests across 7 layers — every stage of the pipeline has dedicated coverage:
| Layer | Tests | Status |
|---|---|---|
| E2E shell (hex-verified output) | 11 | all pass |
| Corpus (full compile to Z80) | 18 | all pass |
| Fusion optimizer (callback inlining) | 7 | all pass |
| MIR VM (DJNZ execution) | 8 | all pass |
| Codegen (Z80 patterns) | 7 | all pass |
| Semantic (IR generation) | 20 | all pass |
| Parser (chain conversion) | 18 | all pass |
9 operations fully working on Z80: forEach, map, filter, take, skip, peek, inspect, takeWhile, and inline lambda filters (filter(|x| x > N) compiles to CP N+1 + JR C — no function call, ~27 T-states saved per iteration). Fusion optimizer inlines small callbacks directly into DJNZ loop bodies, eliminating CALL/RET overhead and enabling bare DJNZ instruction. enumerate and reduce work at MIR level, Z80 blocked by OpPush routing. See Iterator Implementation Status for details.
Documentation:
- Iterator Implementation Status — actual compiler output, known bugs, performance reality
- Iterator Reality Check (Report #017) — grounded analysis of T-state costs
- ADR-0008: Flag-Based Boolean ABI —
CP+ flag returns for iterator predicates
| Target | Status | Binary | Notes |
|---|---|---|---|
| ZX Spectrum | Working | .tap |
Main development target, tested via mze + ZXSpeculator |
| CP/M | Working | .com |
BDOS stdlib, tested via mze with CP/M mode |
| Agon Light 2 | Working | .bin |
eZ80/ADL mode, MOS + VDP stdlib, structural testing only |
| MSX | Compiles | varies | Target config exists, limited testing |
| Backend | Status | Notes |
|---|---|---|
| Z80 | ✅ Production | Full-featured, optimized, 5500+ lines, MIR2 active target |
| QBE (native) | ✅ Working | MIR2→QBE IL→arm64/x86_64. Correctness oracle: 4/4 E2E tests. brew install qbe |
| C99 | Produced real binaries; variable redeclaration bug in scoped locals | |
| M68k | 🧪 Untested | Most complete non-Z80 (28 opcodes, real register allocator); never assembled |
| i8080 | 🧪 Untested | Structurally correct (all-memory approach); never assembled |
| 6502 | 35/35 tests, E2E via sim6502 emulator, dual-VM cross-check, console I/O (A2/C64/BBC). Report #067 | |
| LLVM | ❌ Broken | JumpIf fallthrough hardcoded, type errors; llc fails |
| WASM | ❌ Broken | Label/jump emit as comments; WAT validation fails |
| Crystal | ❌ Stub | Control flow emits comments, function args always empty |
| Game Boy | ❌ Stub | Add, Sub, LoadVar, StoreVar all emit only comments |
Only Z80 is production-quality. QBE is new (2026-03-09) — pkg/mir2qbe translates MIR2 directly to QBE IL, which compiles to native arm64/x86_64 via qbe + cc. Used as a correctness oracle: same MIR2 module → Z80 emulator vs native binary; agreement means the pipeline is correct. See Report #045.
The MOS 6502 backend compiles MIR2 IR to valid NMOS 6502 assembly, assembled and executed on an in-process emulator. 35/35 tests pass.
MIR2 IR ─┬─→ VM.Call() → reference ─┐
│ ├→ assert equal (dual-VM oracle)
└─→ M6502Codegen → asm → sim6502 → A ┘
What works (E2E verified): add, sub, neg, double, and/or/xor,
function calls (JSR/RTS), constants, console output (4 platforms).
Console I/O — four OS vectors captured simultaneously (zero conflicts):
| Address | System | Convention |
|---|---|---|
$F001 |
Bare metal | STA $F001 (I/O port) |
$FDED |
Apple II | JSR $FDED (COUT) |
$FFD2 |
C64 | JSR $FFD2 (CHROUT) |
$FFEE |
BBC Micro | JSR $FFEE (OSWRCH) |
All share the same calling convention: char in A. In MinZ:
@extern("$FFEE") fun putchar(c: u8).
Missing (roadmap): loops, 16-bit math, memory access, SMC. See Report #067 for the full feature matrix and Z80 vs 6502 comparison.
Seven source languages compile through the same HIR → MIR2 → Z80 backend:
.nanz ──→ nanz.Parse() ──┐
.lanz ──→ lanz.Compile() ──┤
.lizp ──→ lizp.Compile() ──┤
.plm ──→ plm.Compile() ──┼──→ *hir.Module ──→ MIR2 ──→ Z80/6502/QBE
.pas ──→ pascal.Compile() ──┤
.c ──→ c89.Compile() ──┤
.abap ──→ abap.Compile() ──┘ ← NEW: ABAP via abaplint!
| Frontend | Status | Purpose | Notes |
|---|---|---|---|
| Nanz | Primary | Modern systems language | Full-featured: structs, enums, iterators, lambdas, SMC, LUTGen, flag-return ABI |
| Lanz | Working | S-expression IR | 1:1 mapping to HIR. Round-trips perfectly. Used by @derive_* metafunctions |
| Lizp | Working | Lisp dialect | Macros, threading (->, ->>), defmacro/cond/when/dotimes. Desugars to Lanz |
| PL/M-80 | Working | Legacy Intel (1976) | 26/26 Intel 80 Tools corpus (100%); 1338 functions, 11661 statements |
| Pascal | Working | Turbo Pascal | WriteLn → CP/M BDOS via inline asm. mz hello.pas -t cpm -o hello.com |
| C89 | WIP | C89/C99 | modernc.org/cc/v4 parser. 9 corpus files, 68 asserts. −55% vs SDCC |
| ABAP | NEW | SAP ABAP on Z80! | abaplint parser (TS). DATA, WRITE, IF, WHILE, DO, FORM, CLASS. Examples → |
| MinZ | Frozen on MIR1 | Legacy syntax | Old MIR1 path; will be rewired through HIR→MIR2 |
Seven pipelines, one backend. .nanz, .lanz, .lizp, .plm, .pas, .c, and .abap files all go through compileViaHIR() → HIR → MIR2 → Z80. A function double(x) = x + x written in any of the seven languages produces the same Z80: ADD A, A / RET.
REPORT zfibonacci.
DATA: lv_a TYPE i VALUE 0,
lv_b TYPE i VALUE 1,
lv_temp TYPE i,
lv_i TYPE i VALUE 0.
WHILE lv_i < 10.
WRITE lv_a.
lv_temp = lv_a + lv_b.
lv_a = lv_b.
lv_b = lv_temp.
lv_i = lv_i + 1.
ENDWHILE.This compiles through: ABAP → abaplint (TypeScript parser by Lars Hvam Petersen) → JSON AST → Go lowerer → HIR → MIR2 → Z80 assembly. Your ZX Spectrum is now an enterprise-grade ABAP runtime. See 8 examples including FizzBuzz, bubble sort, OOP with interfaces, and a system info report.
Cross-language imports — Nanz can import from any frontend:
import mathlib // finds mathlib.nanz, .lanz, .lizp, .plm, or .pas
import legacy { PLM_ADD } // PL/M-80 procedure
import macrolib { lizp_double } // Lizp function
Universal compile-time assert — all 6 frontends produce the same hir.Assert, verified by dual-VM (MIR2 VM + Z80 binary):
| Frontend | Syntax |
|---|---|
| Nanz | assert double(5) == 10 |
| Lanz | (assert double 5 == 10) |
| Lizp | (assert double 5 == 10) |
| PL/M-80 | ASSERT DOUBLE(5) = 10; |
| Pascal | assert Double(5) = 10; |
| C89 | // assert double(5) == 10 |
Pascal on CP/M — hello world in one command:
mz hello.pas -t cpm -o hello.com && mze -t cpm hello.com
# Output: Hello from Pascal on Z80!PL/M-80 coverage (Intel 80 Tools corpus): algolm compiler, BASIC-E compiler/parser/synthesizer, ML80 assembler (l81/l82/l83/m81), TeX, CP/M utilities, Kermit — 1338 functions / 943 globals / 11661 statements lowered to HIR from 26 source files. Handles LITERALLY macro chains, $INCLUDE with CP/M device designators, binary literals, record field access, EXTERNAL procedures, all PL/M-80 statement forms. See ADR-0014.
Pipeline emit flags (works with all frontends):
mz program.plm --emit=nanz # Transpile PL/M → Nanz (round-trip)
mz program.pas --emit=lanz # Transpile Pascal → Lanz
mz program.lanz --emit=nanz # Transpile Lanz → Nanz
mz program.plm --emit=hir # HIR typed-tree dump
mz program.plm --emit=mir2 # MIR2 after optimisation passes
mz program.plm -o prog.com -t cpm # Assemble to CP/M binaryThe Nanz transpiler is lossless: mz prog.plm --emit=nanz | mz --stdin produces
byte-identical assembly to compiling .plm directly.
See Chapter 21 of the Nanz Language Book for the full cross-language import guide.
MinZ provides a complete, self-contained development ecosystem. Every tool you need — from source code to running program to screenshot — is a single Go binary with zero external dependencies. No fragile toolchain of third-party assemblers, separate emulators, or external debuggers. One make builds everything.
Source Code Running Program
| |
v v
[mz] compile ──> [mza] assemble ──> [mze] run (CP/M, headless)
| [mzx] run (ZX Spectrum, graphical)
| [mzrun] run (remote, DZRP)
| |
v v
[mzd] disassemble <──────────────── [mzx --screenshot] capture
| Tool | Purpose | Usage |
|---|---|---|
| mz | MinZ compiler | mz program.minz -o program.a80 |
| mza | Z80 assembler (table-driven, all Z80 ops including undocumented, [addr] bracket syntax) |
mza program.a80 -o program.com |
| mze | Z80 emulator (1335/1335 FUSE tests, profiler, console I/O, stderr port) | mze program.com -t cpm --console-io |
| mzx | ZX Spectrum emulator (T-state accurate, AY, profiler, .sna/.tap/.trd/.scl, console I/O) | mzx --snapshot game.sna |
| mzd | Z80 disassembler (IDA-like analysis, xrefs, ROM tables) | mzd program.bin --org 0x8000 |
| mzrun | Remote runner (DZRP protocol) | mzrun program.minz --reset |
| mzv | MIR VM runner (breakpoints, tracing, PNG export) | mzv program.mir |
| ❌ Broken — compilation pipeline not wired | ||
| mzlsp | LSP server (diagnostics, hover, goto-def, completion) | auto-started by VSCode extension |
T-state accurate emulation with real display output. Supports 48K and Pentagon 128K models.
# Interactive emulation
mzx --snapshot game.sna
mzx --tap game.tap
mzx --model pentagon --rom 128-0.rom --rom1 trdos.rom --trd game.trd
# Load raw binary and run (no ROM needed)
mzx --load code.bin@8000 --set PC=8000,SP=FFFF,DI
mzx --run code.bin@8000 # shortcut for --load + --set PC + SP + DI
# Bare-metal console I/O (no ROM needed)
mzx --run code.bin@8000 --frames DI:HALT --console-io
# OUT ($23),A → stdout | IN A,($23) → stdin | OUT ($25),A → stderr
# DI + HALT → exit with A register as process exit code
# Console I/O with custom port or AY serial
mzx --run code.bin@8000 --frames DI:HALT --console-to-port '$FF'
mzx --run code.bin@8000 --frames DI:HALT --console-to-port ay
# BASIC console (RST $10, needs ROM)
mzx --snapshot game.sna --console
# Headless screenshots (for CI, automated testing, book illustrations)
mzx --snapshot game.sna --screenshot shot.png --frames 100
mzx --tap game.tap --screenshot shot.png --screenshot-on-stable 3
# Execution profiling (7-channel heatmap + memory snapshot)
mzx --snapshot demo.sna --profile heatmap.json --frames 500
# Profile includes: exec, read, write, stack_push, stack_pop, io, mem_snapshot
mzx --snapshot demo.sna --trace trace.jsonl --trace-frames 100:200
# Debugging
mzx --warn-on-halt --verbose --diag --snapshot game.snaFeatures: FrameMap ULA rendering, beeper + AY-3-8912 audio (AYumi), ULA contention, .sna/.tap/.trd/.scl format support, full TR-DOS function dispatch, 7-channel execution profiler (exec/read/write/stack push/pop/IO + memory snapshot), basic-block tracer, conditional screenshots, T-state snapshots, DI+HALT exit with A as exit code, bare-metal console I/O (port $23 stdout, $25 stderr, or AY serial), 48K ROM included.
For ZX Spectrum development, mzrun compiles, assembles, and uploads to a running emulator in one command:
# Start ZXSpeculator with DZRP enabled, then:
export DZRP_HOST=localhost DZRP_PORT=11000
mzrun game.minz --reset -vmz program.minz --dump-mir # Show MIR intermediate representation
mz program.minz --dump-ast # AST in JSON format
mz program.minz --viz out.dot # MIR visualization (Graphviz)
mz program.minz -d # Verbose compilation details
mz program.minz --compile-trace # Structured log of all optimization decisionsStdlib modules are organized by domain. Quality varies — some modules are well-tested, others are experimental.
| Module | Description |
|---|---|
cpm/bdos |
CP/M BDOS calls: putchar, getchar, print_string, file I/O |
agon/mos |
Agon MOS API: mos_putchar, mos_puts, file I/O (eZ80 ADL mode) |
agon/vdp |
Agon VDP graphics: modes, shapes, sprites, buffer commands |
text/format |
Number formatting: u8_to_str, u16_to_hex |
mem/copy |
Fast memory ops: memcpy, memset (LDIR-based) |
| Module | Description |
|---|---|
math/fast |
Sin/cos/sqrt lookup tables (256 entries) |
math/random |
LFSR PRNG, noise functions |
graphics/screen |
Pixel/line/circle drawing (ZX Spectrum) |
input/keyboard |
Keyboard matrix, debouncing |
text/string |
strlen, strcmp, strcpy, strcat |
sound/beep |
Beeper SFX |
time/delay |
Frame timing, delays |
| Module | Description |
|---|---|
glsl/* |
GLSL-style shader library: fixed-point math, raymarching, SDFs |
MinZ applies optimizations at multiple levels:
- CTIE — Pure functions with constant args execute at compile time
- MIR optimizer — Constant folding, strength reduction, dead code elimination
- True SMC — Self-modifying code patches parameters into instruction immediates
- Loop rerolling — Detects repeated call sequences, collapses to loops
- Peephole optimizer — 35+ Z80-specific assembly patterns
Example: fibonacci(10) with CTIE generates LD A, 55 — zero runtime cost.
minz/
minzc/ Compiler & toolchain (Go, ~90K LOC)
cmd/ CLI tools
minzc/ mz — MinZ compiler
mza/ mza — Z80 assembler
mze/ mze — Z80 emulator (headless)
mzx/ mzx — ZX Spectrum emulator (graphical)
mzd/ mzd — Z80 disassembler
mzrun/ mzrun — DZRP remote runner
mzr/ mzr — REPL
pkg/ Core packages
parser/ Participle-based parser
semantic/ Type checking, analysis (~11K lines)
ir/ Intermediate representation
codegen/ Z80 (production), C (partial), + 8 experimental backends
optimizer/ MIR + peephole optimizers
z80asm/ Z80 assembler engine (table-driven)
spectrum/ ZX Spectrum emulation (ULA, AY, memory, ports)
emulator/ Z80 CPU emulation (remogatto/z80, FUSE-tested)
disasm/ Disassembler with IDA-like analysis
stdlib/ Standard library (.minz)
agon/ Agon Light 2 (MOS, VDP)
cpm/ CP/M (BDOS)
graphics/ Screen drawing
math/ Fast math, PRNG
text/ String, formatting
...
examples/ 270+ example programs
docs/ Technical documentation
reports/ Progress reports (date-numbered)
Active pipeline: Nanz/Lanz/Lizp/PL/M-80/Pascal/ABAP → HIR → MIR2 → Z80 (production) / QBE (native) / 6502 (experimental).
Metrics (verified 2026-03-15):
| Language frontends | 7 (Nanz, Lanz, Lizp, PL/M-80, Pascal, C89, ABAP) |
| Nanz showcase | 34/34 compile + verify |
| Compile-time asserts | 113 across all 6 frontends (45 original + 68 C89) |
| Go test packages | 26/26 pass |
| 6502 backend | 35/35 E2E tests |
| Z80 emulator | 1335/1335 FUSE tests (100%) |
| PL/M-80 corpus | 26/26 Intel 80 Tools files (100%) |
| MIR2→QBE | 4/4 E2E (correctness oracle) |
| Toolchain | 9 working binaries, pure Go, zero deps |
Known limitations: strict > codegen on Z80 (flag polarity), struct alloca encoding, as cast syntax not parsed. MinZ .minz frozen on MIR1. See Open Bugs.
See docs/GenPlan.md for the development roadmap and current priorities.
# Build all tools
cd minzc
make all
# Run all tests (emulator, assembler, spectrum, parser, etc.)
make test-all
# Test an example end-to-end
./mz ../examples/hello_print.minz -o /tmp/hello.a80
./mza /tmp/hello.a80 -o /tmp/hello.tap
./mze /tmp/hello.tap
# Screenshot an example
./mzx --rom roms/48.rom --snapshot demo.sna --screenshot shot.png --frames 50Report issues at github.com/oisee/minz/issues.
MIT. See LICENSE for details.
MinZ: Modern syntax for vintage hardware.
