Skip to main content

z80-python

A readable, pure-Python Z80 instruction-core reference implementation.

z80-python is a SingleStep-complete and ZEX-certified processor core built to be read, learned from, embedded in real machines, and inspected by humans and AI tools. It implements the Z80 instruction set and deterministic processor lifecycle at instruction boundaries while leaving memory maps, devices, and machine scheduling to the host.

The project is deliberately:

  • readable — instruction families are organized as ordinary Python rather than generated tables, native extensions, or opaque optimizations;
  • pure Python — the core has no runtime dependencies and supports CPython and PyPy;
  • independently validated — correctness claims come from external test oracles, not from code-generation confidence;
  • embeddable — a host supplies four memory and I/O methods and controls when the processor advances; and
  • inspectable — processor state, disassembly, bounded debugging, and structured traces make execution explainable without contaminating the hot core path.

Validation

The instruction core has passed:

  • all 1,604 opcode/prefix files in the pinned SingleStepTests/z80 corpus—1,604,000 state transitions covering registers, flags, memory, I/O ordering, T-states, alternate registers, R, WZ/MEMPTR, Q, and undocumented behavior; and
  • ZEXDOC and ZEXALL long-sequence CRC exercisers under both CPython and PyPy.

The current processor implementation was recertified after the lifecycle and inspection work. Exact source revisions, hashes, commands, timings, and scope limits are recorded in the validation evidence.

This is an instruction-level semantic and lifecycle claim. It is not a claim of cycle-accurate bus-pin behavior or of a complete computer.

Vibe coded, oracle validated

This project was developed with extensive AI assistance—“vibe coded” in the colloquial sense. That history is intentional and worth stating plainly: the implementation demonstrates what AI-assisted engineering can produce when the feedback loop is stronger than the model's confidence.

Generated emulator code can be plausible and wrong, especially around prefixes, undocumented flags, WZ/MEMPTR, Q, and refresh behavior. The project therefore does not treat AI output or code review as proof. Independent SingleStep vectors, focused regressions, ZEX CRCs, and real emulator integrations are the authority. See AI-assisted development and validation.

Version status

The current release is 0.3.0.

Source Status Contents
PyPI 0.3.0 Published release Complete validated core and inspection toolkit
GitHub v0.2.0 Published milestone RESET, CPU state, disassembly, and debugger foundations
GitHub v0.3.0 Current release API hardening and advanced trace diagnostics

Install from PyPI

python -m pip install z80-python

Install the source tree

git clone https://github.com/alewman/z80-python.git
cd z80-python
python -m pip install -e .

For tests, linting, and development tools:

python -m pip install -e ".[dev]"

The distribution is named z80-python; its public import is z80_python to avoid ambiguity with the unrelated z80 distribution on PyPI.

Minimal host

A machine subclasses Z80CPU and supplies its 16-bit memory and I/O spaces:

from z80_python import Z80CPU


class Machine(Z80CPU):
    def __init__(self) -> None:
        super().__init__()
        self.memory = bytearray(0x10000)
        self.ports = bytearray(0x10000)

    def read_byte(self, addr: int) -> int:
        return self.memory[addr & 0xFFFF]

    def write_byte(self, addr: int, value: int) -> None:
        self.memory[addr & 0xFFFF] = value & 0xFF

    def read_port(self, addr: int) -> int:
        return self.ports[addr & 0xFFFF]

    def write_port(self, addr: int, value: int) -> None:
        self.ports[addr & 0xFFFF] = value & 0xFF


cpu = Machine()
cpu.memory[:3] = bytes((0x3E, 0x2A, 0x3C))  # LD A,2Ah; INC A
assert cpu.step() == 7
assert cpu.step() == 4
assert cpu.a == 0x2B

step() advances one instruction or accepted lifecycle boundary and returns its documented T-state total. Registers and modeled processor state are directly readable and writable. decode_and_execute() remains the historical instruction-only entry point; new hosts should normally use step().

Processor lifecycle

The core models external processor inputs as deterministic transitions between instructions:

  • RESET is host-controlled and level-sensitive. While asserted, each step() applies the documented processor-state effects in 3 T-states without fetching an instruction or accessing the stack.
  • NMI is accepted at the next available boundary, wakes HALT, preserves the prior IFF1 in IFF2, pushes PC, and enters 0x0066 in 11 T-states.
  • Maskable interrupts honor IFF1, the one-instruction EI delay, HALT, and interrupt modes 0, 1, and 2. IM 0 is intentionally bounded to device-supplied RST opcodes.

RESET has priority over NMI; NMI has priority over an acceptable maskable interrupt. Hosts schedule devices from returned T-state totals and request interrupts through the public lifecycle API rather than mutating PC, SP, or interrupt flip-flops to synthesize entry.

See the interrupt lifecycle contract for exact state transitions and exclusions.

Reference-core boundary

z80-python owns:

  • Z80 instruction semantics and documented instruction T-state totals;
  • registers, flags, alternate registers, and modeled internal processor state;
  • RESET, NMI, maskable interrupts, EI delay, RETN, and HALT behavior; and
  • processor-level observation and debugging values.

A host machine owns:

  • ROM, RAM, memory maps, ports, mappers, and open-bus behavior;
  • video, audio, input, and other devices;
  • frame, scanline, clock, and interrupt scheduling;
  • device events and side-effect-free memory peeking; and
  • complete machine save states, deterministic replay, and rewind.

Accordingly, the project does not claim:

  • cycle-accurate bus cycles, contention, WAIT, BUSREQ, or pin timing;
  • interrupt-acknowledge bus callbacks, daisy chains, or arbitrary IM 0 injected instructions;
  • a complete CP/M system, arcade board, console, or computer; or
  • a universal machine scheduler or save-state format.

These are scope boundaries, not unfinished promises. They keep the core readable, portable, and useful across different machines.

Learning and inspection

The implementation is organized by instruction family behind a small public Z80CPU facade. Conventional Python control flow keeps opcode behavior easy to trace from dispatch to implementation, including documented comments around undocumented Z80 behavior.

The development tree also provides immutable CPUState capture and restoration for processor-owned state and a complete structured disassembler for every opcode form supported by the core. Disassembly requires an explicit side-effect-free byte reader: debugging must not accidentally acknowledge or mutate a mapped device.

These APIs make the project useful as:

  • a reference while learning Z80 instructions and flags;
  • a tested foundation for emulators and machine experiments;
  • a source of deterministic examples and regression witnesses; and
  • an executable environment where an AI agent can inspect real state instead of guessing from source code alone.

See CPU state, disassembly, and undocumented behavior.

Advanced diagnostics and tooling

The following development-tree features support emulator diagnosis but are secondary to the instruction core itself:

  • DebugSession wraps an existing host with bounded execution, execute breakpoints, lifecycle-aware records, T-state totals, and bounded history. It adds no callback or history overhead when unused.
  • CommandDebugger is a dependency-free text-stream frontend for registers, stepping, bounded runs, breakpoints, disassembly, memory display, and history.
  • Trace comparison finds the first differing instruction, lifecycle boundary, timing result, or individual CPU-state field across two executions.
  • Versioned JSON Lines persistence allows large traces to be written, reloaded, and compared incrementally without buffering complete runs.
  • Live lockstep comparison can advance two machines under a finite budget and stop at the causal processor-visible difference while preserving bounded pre-divergence context.

A Galaxian integration proved the intended diagnostic behavior: two otherwise identical boards differed only in their vblank NMI-enable latch. Comparison stopped at the exact boundary where one scheduler first asserted pending NMI—one step before the later control-flow divergence into 0x0066.

These are deterministic tools, not an embedded LLM or AI provider. Any human UI, agent, MCP adapter, or model can consume the same structured contracts. See debug sessions, trace comparison, and the tooling roadmap.

Development

Run the ordinary quality gate:

python -m pytest -q
python -m ruff check .
python examples/minimal_z80_host.py

The vector corpus and ZEX binaries are external artifacts and are intentionally not bundled. Fetch the pinned vector corpus with:

python scripts/fetch_test_vectors.py
python -m pytest -q

ZEX recertification is reserved for release candidates and semantic-core changes; reproduction instructions and finite execution budgets are documented in the validation evidence.

Project records

License

MIT. External validation artifacts retain their own licenses and are not bundled in this distribution.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

z80_python-0.3.0.tar.gz (134.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

z80_python-0.3.0-py3-none-any.whl (39.9 kB view details)

Uploaded Python 3

File details

Details for the file z80_python-0.3.0.tar.gz.

File metadata

  • Download URL: z80_python-0.3.0.tar.gz
  • Upload date:
  • Size: 134.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.10

File hashes

Hashes for z80_python-0.3.0.tar.gz
Algorithm Hash digest
SHA256 b5b0bfe912623f2a33517922e2c4e3ebf5ba407c4e251f4181c7b982653dcb0a
MD5 6b2f4441ac6937976a178289c9ccb51d
BLAKE2b-256 1a24cb6556b1cba636ad27867e1468b2b46b950437c2e97eae949efb785efa72

See more details on using hashes here.

File details

Details for the file z80_python-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: z80_python-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 39.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.10

File hashes

Hashes for z80_python-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 da2107ff71ce62330060a7df2ccf5215af795c14ccbc2b4026172833dc2523d1
MD5 b1a715182e56c7fdb81b28494b6bc77d
BLAKE2b-256 981624ef198c60da74d7f33d37f21fe621998c71b745fbef09899c1cb61cd320

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page