Skip to main content

upeepz80 - Universal Peephole Optimizer for Z80

A language-agnostic peephole optimization library for Z80 compilers that generate pure Z80 assembly.

Overview

upeepz80 provides high-quality optimization passes for compilers targeting the Zilog Z80 processor. Unlike upeep80, this library expects pure Z80 mnemonics in lowercase as input (ld, jp, jr, etc.) and produces optimized Z80 assembly output with lowercase mnemonics.

If your compiler generates 8080 mnemonics (MOV, MVI, LXI, etc.) that need translation to Z80, use upeep80 instead.

Features

Peephole Optimizations

  • Pattern-based optimization on Z80 assembly
  • Redundant load/store elimination
  • Jump optimization (jp to jr conversion, jump threading)
  • djnz conversion (dec b; jr nz → djnz)
  • Stack operation combining (push/pop to ld conversions)
  • Dead store elimination
  • Tail call optimization (call x; ret → jp x)
  • Register copy optimization (push hl; pop de → ld d,h; ld e,l)

Z80-Specific Features

  • Relative jump optimization (jp → jr where in range)
  • djnz loop optimization
  • Z80 block instruction awareness
  • Direct ld de,(addr) usage (Z80-only instruction)

Installation

pip install upeepz80

Or for development:

git clone https://github.com/avwohl/upeepz80.git
cd upeepz80
pip install -e ".[dev]"

Usage

Basic Usage

from upeepz80 import optimize

# Optimize Z80 assembly code
assembly = """
    ld a,0
    or b
    push hl
    pop de
    jp LABEL
LABEL:
    ret
"""

optimized = optimize(assembly)
print(optimized)
# Output:
#     xor a          ; ld a,0 → xor a (smaller): or b sets the flags xor a changes
#     or b
#     ld d,h         ; push/pop → register moves (faster)
#     ld e,l
# LABEL:
#     ret            ; jp to next instruction eliminated

Without the or b, ld a,0 stays: xor a also sets the flags, and the caller this code returns to might read them.

Using the Optimizer Class

from upeepz80 import PeepholeOptimizer

# Create optimizer
optimizer = PeepholeOptimizer()

# Optimize assembly code
optimized_asm = optimizer.optimize(assembly_text)

# Check statistics
print(f"xor a conversions: {optimizer.stats.get('zero_a_ld', 0)}")
print(f"Jump threading: {optimizer.stats.get('jump_thread', 0)}")
print(f"djnz conversions: {optimizer.stats.get('djnz', 0)}")

Optimization Phases

The optimizer runs multiple phases:

  1. Pattern Matching and Z80-Specific Optimizations - peephole patterns and inline rewrites, repeated until nothing changes (up to 10 passes)
  2. Jump Threading - Thread through intermediate jumps
  3. Pattern Matching once more, for what threading exposed
  4. Dead Store Elimination - Remove a parameter's store at procedure entry when nothing can read the byte: no load reads it, nothing gives an address from which it can be computed, such as that of the variable before it (ld hl,A / inc hl), and no jump, call or instruction before it runs it as code
  5. Relative Jumps - Convert jp to jr, and dec b; jp nz to djnz, where the target is in reach; last, because it counts bytes

Correctness

A rewrite that changes what a register or flag holds afterwards is made only where nothing reads the old value. ld a,0 → xor a changes every flag, so it is made only where the flags are overwritten before they are read; ld hl,5 / ld a,l → ld a,5 leaves HL as it was, so it is made only where HL is. The optimizer knows what each Z80 instruction reads and writes (upeepz80/z80.py), and follows every path from the rewritten code: on, into both arms of a branch, round loops, into a routine the text calls and back, from a ret to every call of the routine, and through a push to its pop. A path that leaves what the text shows counts as reading everything: a call or jump to a label defined elsewhere, call 5, jp 0, jp (hl), data, or the end of the text. So does a ret from a routine that another module may call (public, NAME::, or its address taken), and one that may not take the return address its routine was entered with. The optimizer follows where that address is: through pop, push and ex (sp),hl, and the calls of routines that take their arguments off the stack, as under PL/M-80's calling convention, which uplm80 0.4.0 uses (pop hl / ex (sp),hl). A ret goes back to the call only where the address is at the top of the stack (not after ex (sp),hl / ret), where it may not have been changed through a pointer made from SP (ld hl,0 / add hl,sp / ld (hl),e), and where the routine has not called one that goes on where the optimizer cannot follow (jp (hl), push hl / ret).

call x / ret becomes jp x only where the routine's own return address is at the top of the stack, as ret takes it. Jumped to, x finds on top of the stack what call x would have put under its return address. That matters to a callee that removes the arguments pushed for it. Nor is it made where x is a routine of the text that reaches its return address or what is above it, directly or through a routine it calls, or leaves the stack other than it found it; nor where a jump or ret the optimizer cannot follow (push bc / ld hl,HND / jp (hl), ex (sp),hl / ret) may have entered the routine with something pushed.

A program that computes an address from a label - jp BIOS+3, ld hl,L+3, jr $+3, dw START-3 - depends on the size of the code between, and may go to the code there. No rewrite changes that code, and the code at the address is taken to be entered from anywhere. So it is with the jp and jr instructions, and any nop, db or ds that pads them (jp H0 / nop / jp H1 / nop), after a label the text names other than as where a jump or call goes, or a name an equ sets to $ there (TBL equ $), a table the program may enter at an offset it computes (ld de,TBL / add hl,de / jp (hl)), and after a label the text exports, which another module may enter at an offset, as a BIOS's jump vector is. Where the program may read or write the code at the address (ld hl,(VEC+1), ld (VEC+1),hl, ld (SW),a), the instruction there may have been patched into any other: a path that goes there counts as reading everything, whatever the instruction; that instruction, and a jump of an exported vector, is taken to go where the optimizer cannot follow, or on to the line after it (a ret made nop, a jp made ld hl,nn), which may then be entered from anywhere; and no jump is threaded through it. No tail call is made to a routine that reaches it, nor, where something may be pushed there, in code that may be entered from anywhere. A dw L, where L is jp M, becomes dw M only in a table that is only jumped through, as uplm80's DO CASE tables are, and where the code at M reads neither HL, which holds L or M, nor DE, which points at the entry.

Numbers are read under the text's radix. Where it sets a .radix other than ten, only numbers that mean the same under any radix are rewritten.

Every rewrite writes only instructions the Z80 has, and a relative jump is made only where its target is known to be within reach, counting bytes.

tests/peepfuzz.py checks this: it generates random programs around every shape the optimizer rewrites, runs each before and after optimization on a Z80 interpreter (tests/z80sim.py, itself checked against a real Z80 core by tests/simcheck.py), and compares every register, flag and byte of memory.

Architecture

upeepz80 is designed to be language-agnostic:

  • Works directly on Z80 assembly text
  • No knowledge of source language required
  • Pattern-based transformation engine
  • Zero runtime dependencies

Comparison with upeep80

Feature upeep80 upeepz80
Input 8080 or Z80 mnemonics Z80 mnemonics only
Output Z80 or 8080 (configurable) Z80 only
Translation 8080 → Z80 translation None needed
Use case Compilers generating 8080 code Compilers generating Z80 code

Choose upeepz80 if your compiler already generates Z80 mnemonics. Choose upeep80 if your compiler generates 8080 mnemonics.

Used By

  • uplm80 - PL/M-80 compiler for Z80 (after migration)
  • uada80 - Ada compiler for Z80 (after migration)

Development

Running Tests

pytest

Type Checking

mypy upeepz80

Code Formatting

black upeepz80
ruff check upeepz80

Performance

Benchmarks on typical compiler workloads:

  • Peephole optimization: ~50,000 instructions/second
  • Memory usage: Minimal (pattern-based, no large data structures)

Contributing

Contributions are welcome. Open an issue or a pull request.

License

This project is licensed under the GNU General Public License v2.0 - see LICENSE for details.

History

upeepz80 is a sibling project to upeep80, designed for compilers that generate native Z80 assembly. It shares the same optimization algorithms but removes the 8080 translation layer for cleaner, more efficient code when 8080 support isn't needed.

  • 80un - Unpacker for the CP/M archive and compression formats LBR, ARC, squeeze, crunch, and CrLZH.
  • cpmdroid - Z80/CP/M emulator for Android phones and tablets. It emulates the RomWBW HBIOS interface and a VT100 terminal.
  • cpmemu - Z80/CP/M emulator for Linux and Windows, with Z80 and 8080 CPU cores. It translates the BDOS and BIOS calls of CP/M 2.2 programs to the host file system.
  • ioscpm - Z80/CP/M emulator for iOS and macOS. It emulates the RomWBW HBIOS interface and runs CP/M 2.2 and CP/M 3.
  • learn-ada-z80 - Collection of more than 90 Ada example programs for uada80, the Ada compiler for the Z80 processor and CP/M.
  • mbasic - Python interpreter for MBASIC 5.21, the Microsoft BASIC-80 for CP/M. Two compiler backends compile the programs to CP/M .COM files or to JavaScript.
  • mbasic2025 - Reconstruction of the lost source code of MBASIC 5.21, the Microsoft BASIC-80 for CP/M. The MACRO-80 source code assembles to a binary that matches mbasic.com byte for byte.
  • mbasicc - C++17 interpreter for MBASIC 5.21, the Microsoft BASIC-80 for CP/M. It runs on Linux and macOS.
  • mbasicc_web - Web browser interpreter for MBASIC 5.21, the Microsoft BASIC-80 for CP/M. Emscripten compiles the mbasicc interpreter to WebAssembly.
  • mpm2 - Z80 emulator for MP/M II, the multi-user CP/M operating system. Users connect over SSH, and SFTP clients transfer files.
  • romwbw_emu - Hardware-level Z80/CP/M emulator for Linux and macOS. It emulates the RomWBW HBIOS interface and switches banks in 512 KB of ROM and 512 KB of RAM.
  • scelbal - Floating-point BASIC interpreter for the 8080 processor and CP/M. A translator converts the original 8008 source code to 8080 source code.
  • uada80 - Ada compiler for the Z80 processor and CP/M 2.2. It compiles a subset of Ada 2012 to CP/M .COM files.
  • uc80 - C compiler for the Z80 processor and CP/M. It optimizes for small code size.
  • ucow - Cowgol compiler for the Z80 processor and CP/M. It runs on Linux in Python.
  • um80_and_friends - Linux toolchain that is compatible with Microsoft MACRO-80. It has an assembler, a linker, a librarian, and a disassembler.
  • uplm80 - PL/M-80 compiler for the Z80 processor and CP/M. It writes Intel 8080 and Zilog Z80 assembly language.
  • z80cpmw - Z80/CP/M emulator for Windows. It emulates the RomWBW HBIOS interface and boots CP/M from disk images.

See Also

Metadata

Release files for upeepz80 0.2.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for upeepz80 0.2.7
File Size Uploaded
upeepz80-0.2.7.tar.gz 91.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for upeepz80 0.2.7
File Interpreter ABI Platform
upeepz80-0.2.7-py3-none-any.whl Python 3 none any Details

Total release size: 151.8 kB

Release files / upeepz80-0.2.7.tar.gz

Download URL upeepz80-0.2.7.tar.gz
Size 91.8 kB
Tags Source
SHA-256 checksum
How to use checksums
a90b0d5c06098a2593fc1450c1457091f07ebadce03dea9ac4f922937f6ad2d5
BLAKE2b-256 checksum
How to use checksums
6ccb8d98c685e34f05aa2cdfc03e4274685e63c4356fa29de1165c6ea49b947d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release files / upeepz80-0.2.7-py3-none-any.whl

Download URL upeepz80-0.2.7-py3-none-any.whl
Size 60.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7ade3150777ebe58d11c45a2e4ffce3fae28002c455a9ac49b5613d17bee9810
BLAKE2b-256 checksum
How to use checksums
d333ca0f35ca8a531f7951f978fe75272cdc1c15cc735a2f6c9c36995798f97d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.7 This release

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release 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