Language-agnostic NASM-text optimizer for compilers targeting the Intel 386 (x86-32) processor. Three composable passes:
- peephole — pattern-based instruction rewriting to fixed point (binop_collapse, setcc_jcc_collapse, mov_zero_to_xor, …).
- asm_dce — call/reference-graph dead-code elimination,
walking reachability from
_start/_mainover the call graph of top-level functions and data labels. - libc_split — parser for a monolithic libc.asm with per- function dep tracking, so a host compiler can include only the transitive closure of routines the user code references.
Sibling to upeepz80 (Z80 peephole) and upeep80 (8080 peephole); same split pattern — once the optimizer's pattern set crystallized inside a host compiler, it became a standalone package shared across compilers for the same target.
Extracted from uc386 (the C23 → i386/DOS compiler) for shared use by ucpp386 (the C++ sibling). Both compilers emit NASM in the same dialect and benefit from the same rewrites — vtables and templates in ucpp386 make the asm-DCE cascade and the inline-load collapses more important, not less, than they are for C.
pip install upeep386
Or from source:
pip install -e .
from upeep386 import optimize, dce, parse_libc
asm = compiler.emit(...)
asm = optimize(asm) # peephole patterns to fixed point
asm = dce(asm) # drop unreachable functions / labels
# parse_libc(libc_asm) lets the host include only the libc
# routines the user actually references.
Runs to fixed point. Patterns the optimizer applies:
dead_after_terminator— drop instructions between unconditionaljmp/retand the next label/directive.jmp_to_next_label— dropjmp Ximmediately followed byX:.binop_collapse— replace 4-line stack-machine right-operand transfer with a singlemov ecx, src.store_collapse— drop push/pop pair around a store when src is a single-instruction load.leave_collapse—mov esp, ebp; pop ebp→leave.imm_store_collapse—mov eax, IMM; mov [addr], eax; mov eax, X→mov dword [addr], IMM; mov eax, X.setcc_jcc_collapse— drop thesetCC al; movzx eax, al; test eax, eax; jz/jnzboolean-then-branch sequence into a single conditional jump.push_immediate—mov eax, IMM; push eax→push IMMwhen the next instruction overwrites EAX.ecx_binop_collapse—mov ecx, src; OP eax, ecx→OP eax, src.mov_zero_to_xor—mov reg, 0→xor reg, reg(when flag-safe).store_load_collapse— drop the redundant load after a store of the same register to the same address.
See upeep386/peephole.py docstring for the complete pattern
description and bytes saved per pattern.
Input is exactly what uc386 / ucpp386 emit. The optimizer assumes:
- Section directives at column 0:
section .text/section .data/section .bss. - Top-level labels at column 0:
_name:(multi-underscore prefixes allowed for__start,___builtin_*). - Local labels inside functions:
.local:. - cdecl calling convention;
EAXis the return register;EBPis the frame pointer. - No macro expansion required at this layer — input is post- preprocessor literal NASM.
GPL-3.0-or-later. See LICENSE.