Skip to main content

uc80 - ANSI C Compiler for Z80

A C compiler targeting the Z80 processor and CP/M operating system. Produces assembly compatible with the um80 assembler and linker toolchain.

Installation

pip install uc80

Or from source:

pip install -e .

Requires the um80 assembler/linker toolchain:

pip install um80

Quick Start

# Compile, assemble, and link a C program
LIB=$(uc80 --print-lib-dir)
uc80 hello.c -o hello.mac
um80 hello.mac -o hello.rel
ul80 hello.rel $LIB/libc.lib $LIB/runtime.lib -o hello.com

Finding the Libraries

Linking needs libc.lib, runtime.lib and — for separate compilation — crt0.rel. Ask uc80 where they are instead of guessing:

LIB=$(uc80 --print-lib-dir)

It prints one line and exits 0, with no input file required. From Python:

import uc80
uc80.lib_dir()             # -> Path to the library directory
uc80.lib_file("libc.lib")  # -> Path to one asset

Wheels ship the three link artifacts, so pip install uc80 is enough. In a git checkout they are build output (.gitignore covers *.lib and *.rel), so build them once:

uc80 --build-libs          # assembles libc.lib, runtime.lib and crt0.rel (~40 s)

Re-run that after editing anything in src/uc80/lib/lc/ or src/uc80/lib/rt/.

Set UC80_LIB_DIR to use libraries built somewhere else, which is what makes uc80 usable when it is installed into a read-only site-packages. The override applies per file, so a directory holding nothing but the two rebuilt .lib files works and cannot shadow crt0.mac, runtime.mac or include/ — those are version-locked to the compiler and always come from the package. Point it at a complete older library tree, though, and you get exactly the silently-stale-libc problem this flag exists to prevent.

src/uc80/lib/ is the only library directory. Nothing in uc80 looks anywhere else; do not create a top-level lib/.

Best Optimization (Whole-Program)

For smallest binaries, compile all .c files in a single invocation. This enables whole-program optimizations that are not possible when compiling files separately:

# Single-file (best optimization - all optimizations enabled by default)
uc80 main.c utils.c -o program.mac
um80 program.mac -o program.rel
ul80 program.rel $LIB/libc.lib $LIB/runtime.lib -o program.com

Default optimizations (all enabled unless disabled):

  • Whole-program mode: Dead function elimination across all files
  • Shared storage: Non-recursive functions use static allocation instead of stack frames
  • Function inlining: Small functions expanded at call sites
  • Constant propagation: Interprocedural constant folding
  • AST optimization: Expression simplification, strength reduction
  • Assembly DCE: Dead code elimination at assembly level
  • Peephole optimization: Pattern-based instruction replacement
  • Printf auto-detection: Scans format strings to link only needed handlers; rewrites printf("...\n") to puts("...") when no format specifiers are used
  • Embedded runtime: Runtime functions included as source, DCE removes unused ones

Printf Control

The compiler auto-detects which printf format specifiers your program uses and links only the needed handlers. That inference needs the whole program, so under --no-whole-program it is not used — see Separate Compilation. You can also control this explicitly:

# Command line
uc80 program.c --printf int           # %d %u %x %o %s %c %p only
uc80 program.c --printf int --printf long  # add %ld %lu %lx
uc80 program.c --printf float         # add %f

# In source code
#pragma printf int
#pragma printf long

Console Line Endings

Console output ends a line with CR LF, because that is what a real CP/M terminal needs. An ADM-3A, a Kaypro or a Televideo treats a bare LF as "cursor down" only, so output written with a bare LF stair-steps down the screen. CP/M itself translates nothing, so the program must emit both bytes. z88dk builds its CP/M library the same way, and C23 7.23.2 allows a text stream to alter characters on output to match the host convention.

libc does the translation in one place, lib/lc/lc_conout.mac, which every console writer calls. A CR is only inserted before an LF that does not already follow a CR, so a program that prints "\r\n" does not get "\r\r\n", and a "\r" progress-bar redraw still works.

To get raw LF instead:

uc80 --no-crlf program.c -o program.mac
#include <stdio.h>       /* declares __crlf_mode */
__crlf_mode = 0;         /* raw LF from here on; 1 turns it back on */

Two things to know about --no-crlf. It only takes effect on the translation unit that defines main(), because the flag is a runtime byte in libc and the compiler zeroes it at the top of main(); libc ships prebuilt, so the compiler cannot select different library source. And it links that byte's module, about 44 bytes, into a program that would otherwise do no I/O.

File streams are never translated, with or without the flag. fwrite and fputc to a FILE * write the exact bytes you hand them, in both "w" and "wb" mode.

Configurable Integer Sizes

By default int is 16 bits (natural Z80 word width). Code that assumes 32-bit int can be compiled with a CLI override — no source changes:

uc80 program.c --int=32 -o program.mac     # 32-bit int
uc80 program.c --long=64 -o program.mac    # 64-bit long

The bundled headers (<limits.h>, <stdint.h>, <stddef.h>, <inttypes.h>) derive their typedefs and limit macros from compiler-supplied __SIZEOF_*__ and __*_MAX__ macros, so the same header files work under every config. Codegen routes arithmetic, printf/scanf format dispatch, and sizeof through the selected widths automatically.

Separate Compilation

When compiling files separately for separate linking, use --no-whole-program:

uc80 --no-whole-program module.c -o module.mac

Link those with crt0.rel first — the compiler only embeds crt0 in whole-program mode:

LIB=$(uc80 --print-lib-dir)
ul80 $LIB/crt0.rel module.rel main.rel $LIB/libc.lib $LIB/runtime.lib -o prog.com

Pass the same --printf/--scanf set to every unit of the program. Each unit that calls printf emits the dispatch table, because the compiler's table has to beat the 16-bit-int default in libc; L80 keeps the first definition of a multiply-defined global and links on without complaint, so two units that disagree about the table leave the link order deciding which conversions work.

Auto-detection cannot help here — a unit only ever sees its own format strings — so with no explicit flag every handler is registered and uc80 says so:

uc80: warning: separate compilation (--no-whole-program) cannot see the format
strings in the other translation units, so every printf handler is registered;
pass an explicit --printf to select a smaller set

Passing a matching --printf to each unit silences it and shrinks the binary.

Inline Assembly

uc80 supports basic asm("..."), spelled asm or __asm__, with or without volatile. The text of the template goes into the output assembly unchanged, at the point where it is written. Write it in MACRO-80 syntax, because um80 assembles it.

int marker;

/* File scope: hand-written data and code. */
asm("\tPUBLIC\t_table\n"
    "_table:\tdw\t11,22,33\n");

void store(void) {
    /* Inside a function. */
    asm("ld hl,1234\n\tld (_marker),hl");
}

Rules for the assembly text:

  • A C object is reached through its assembler symbol. A global x is _x.
  • IX is the frame pointer. Preserve it. SP is free if you balance it. Every other register is free, because uc80 holds no value in a register across a statement.
  • An inline assembly block is a barrier. The peephole optimizer and the assembly dead-code eliminator do not change, move or delete it, and no optimization crosses it.
  • uc80 emits the block in CSEG. A block that changes the segment should change it back, because the compiler-generated code after it expects CSEG.
  • The C grammar makes asm a block item, not a statement, so an unbraced if (c) asm("nop"); is a syntax error. Write if (c) { asm("nop"); }.

Extended asm, which has an operand or clobber list, for example asm("ld hl,%0" : : "r"(x)), is not supported. asm goto is not supported. Both stop the compilation with an error message. Pass values through global variables instead. #asm/#endasm and __naked are also not supported.

Binary Size

uc80 produces the smallest known binaries for Z80/CP/M among current compilers.

Tested against z88dk (SDCC backend, -SO3 --max-allocs-per-node10000) on the Fujitsu compiler-test-suite:

Metric Result
uc80 smaller 47/47 tests (100%)
Aggregate size ratio 46% (uc80 is less than half the size)
Total uc80 170,496 bytes
Total z88dk 369,644 bytes
Minimal binary 128 bytes (vs 5,172 for z88dk)

Sample sizes (bytes):

Program uc80 z88dk Ratio
hello world (puts) 256 5,172 5%
printf %d 4,608 7,696 60%
integer math 5,248 7,948 66%
long arithmetic 5,632 7,793 72%

Test Results

Tested against multiple external test suites:

Suite Pass Rate Notes
c-testsuite 220/220 full pass
c-testsuite --int=32 219/220 00200 (long-long shift) overflows 64K TPA
c-testsuite --int=32 --long=64 218/220 same as above + marginal timeout
Fujitsu compiler-test-suite 0003 371/374
Fujitsu 0010 58/75 9 int16, 1 float, 2 timeout
Fujitsu 0011 287/335 14 int16, 5 large struct
Fujitsu 0012 4/9 4 int16/long long, 1 static DCE
SDCC regression tests 514/523 3 fail, 4 sdcc ext, 2 multi-file link

Remaining non-passing tests are environmental, not codegen bugs:

  • sdcc ext: SDCC-specific extensions (__asm, #pragma save/restore)
  • multi-file: tests that require separate compilation units
  • float precision: ACOSF/TANF near asymptotes (single-precision IEEE 754 limit)
  • malloc OOM: SDCC test asserts malloc(2000) == NULL; we have plenty of TPA
  • 00200: 67KB binary exceeds 64KB CP/M TPA

Features

  • ANSI C (C11/C23) with most standard features
  • Z80 code generation with peephole optimization
  • IEEE 754 single-precision float
  • Configurable integer sizes (--int=16|32, --long=32|64); default is 16-bit int, 32-bit long, 64-bit long long
  • Structs, unions, bitfields, enums
  • Full preprocessor (#include, #define, #if, #pragma, etc.)
  • Modular library with selective linking
  • Whole-program optimization
  • Basic inline assembly (asm("...")), emitted verbatim and never optimized
  • CP/M console line endings (CR LF by default, --no-crlf for raw LF)
  • CP/M target with embedded crt0
  • Libraries ship in the wheel and are discoverable (uc80 --print-lib-dir, uc80.lib_dir())

Related Projects

  • 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.
  • uc386 - C23 compiler for the i386 processor and MS-DOS. This sibling backend shares the uc_core frontend.
  • uc_core - Shared C23 frontend and AST optimizer for the uc80 and uc386 compilers.
  • 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.
  • upeepz80 - Peephole optimizer for Z80 compilers. It shortens jumps to jr, builds djnz loops, and removes dead stores.
  • uplm80 - PL/M-80 compiler for the Z80 processor and CP/M. It writes Intel 8080 and Zilog Z80 assembly language.
  • uplox - LR(1) and GLR parser generator. It writes the lexer and parser tables for the C23 frontend of uc_core from examples/c23.uplox.
  • z80cpmw - Z80/CP/M emulator for Windows. It emulates the RomWBW HBIOS interface and boots CP/M from disk images.

License

GPL-3.0-or-later. See LICENSE.

Release files for uc80 0.7.0

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

Source distribution (sdist)

Source distribution for uc80 0.7.0
File Size Uploaded
uc80-0.7.0.tar.gz 609.2 kB Details

Built distribution (wheel)

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

Total release size: 1.3 MB

Release files / uc80-0.7.0.tar.gz

Download URL uc80-0.7.0.tar.gz
Size 609.2 kB
Tags Source
SHA-256 checksum
How to use checksums
92b46dfcd458e87f0c86c893cecf51e8089f8d3e41f2ad4f518c5fe8d9a3a257
BLAKE2b-256 checksum
How to use checksums
e07c6dd4ba04ae39e642c274aaf3ab44033cd8b7bd070b1b63d26db4fecfdba1
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 Aug 20, 2026.

Transparency log

Release files / uc80-0.7.0-py3-none-any.whl

Download URL uc80-0.7.0-py3-none-any.whl
Size 685.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fc20c3a2124d7c2c7be408993f222de7f59c0d968fea8c2adee68dad468cb04d
BLAKE2b-256 checksum
How to use checksums
07dc473db722993332b0e28022177e29e2ab3bae228eff3521a2f0f39d49cbb9
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 Aug 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.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