Find the cause of late-stage MIPS decompilation mismatches.
Real output — decomp-workbench diagnose-dumps on the phase-shift fixture in
this repository. From verdict to edit walks
this exact screen from top to bottom.
→ Read docs/START_HERE.md. A short guided tour, with every command runnable right now against fixtures in this repository — no ROM, no compiler, no toolchain, no AI.
It answers the three questions people actually arrive with:
- Do I need to isolate the function so asm-processor (the community
preprocessor that lets hand-written MIPS assembly live inside C) stays
out of it? No.
Compare your normal full-TU build against the expected object;
--functionscopes it. Isolation changes codegen, so a harness is the wrong ground truth. - Do I need a permuter or an agent to use this? No. The verdict names the
mechanism, the
next:footer names the lever, and the field guide gives you the C. The permuter is optional, and it is a hypothesis generator rather than a solver. - Am I supposed to read
trace.lst? Not yet, and probably not at all. Traces are the last resort for one verdict class, and only if your project built an instrumented compiler.
Three pages are the entire workflow:
| Page | What it is for |
|---|---|
| Start here | One function, guided tour: diagnose → lever → repeat |
| Field guide | "The diff looks like X" → the C that moves it, with the measured effect |
| Backlog walkthrough | Thirty near matches: batch triage, and which classes to knock out first |
Already at 100%? Then your question is a different one — whether the addresses in that matched ROM are references, so code and data can be inserted, removed or resized without a hardcoded pointer quietly surviving the move. Start at the shiftability campaign: five phases, the first two of which need only a linker map and a linked image, no build. Shiftability is the reference beside it.
Everything below is reference.
Install from a checkout (Python 3.10 or newer; Python 3.10 installs one small
TOML compatibility dependency). For a standalone command, pipx keeps the
workbench isolated from system Python as recommended by the
Python Packaging User Guide:
git clone https://github.com/akratch/n64-decomp-workbench.git
cd n64-decomp-workbench
pipx install .Or, inside an activated virtual environment:
python3 -m pip install .Compare two fixture dumps whose raw words differ only in relocated fields:
decomp-workbench compare-dumps \
examples/fixtures/target.objdump \
examples/fixtures/relocated-match.objdump \
--fail-on-mismatchverdict=instruction-exact aligned_total= 0 words= 0 raw= 2 opcodes= 0 gaps= 0 reloc_syms= 0 norm= 0 ...
raw difference classes: relocation_controlled=2
next: Instruction-exact: raw differences are linker-controlled relocation fields ...
words=0 is the relocation-aware result. raw=2 shows why a literal word
comparison would have rejected the candidate.
Now diagnose a real residual—exactness and mechanism in one load, ending in a lever:
decomp-workbench diagnose-dumps \
examples/fixtures/phase-shift-target.objdump \
examples/fixtures/phase-shift-candidate.objdump \
--function animStepverdict: phase-shift structural=0 schedule=0 register=6 constant=0 hunks=1 playbook=temp-fifo-phase
signature: prefix-exact@12 state-divergence@temp:5 register-first-divergence
Six register differences, one upstream cause. Start here walks the rest of that screen.
Use it when:
- your candidate is close, but the remaining mismatch is hard to classify;
- you are compiling many source variants and need caching plus a durable ledger;
- a sweep needs row/function-scoped signals, baseline/differential controls, honest coverage, or a fresh auditable finish receipt;
- the instruction shape matches but register allocation does not;
- a function already matches and you want to remove fake-match machinery without losing exactness;
- you need to test whether uopt, ugen, or as1 (IDO's optimizer, code generator, and final assembler pass) owns a difference;
- your project is 100% matched and you need to know whether it is safe to shift, insert, or resize code and data without a hardcoded address quietly surviving the move.
You do not need a ROM or compiler to try the included fixtures. Real object comparison needs a GNU-compatible MIPS objdump. Compiler tracing and pass replay need binaries supplied by your project.
Preview a portable project config, then write it only after the inferred and explicit inputs look right:
decomp-workbench project init . \
--target target.o --candidate candidate.o \
--symbol function_name
# After reviewing the preview:
decomp-workbench project init . \
--target target.o --candidate candidate.o \
--symbol function_name --write
decomp-workbench project diagnoseDiscovery recognizes objdiff, Splat, and common build metadata but refuses to
guess which of many objects is authoritative. See project
configuration.
The same file can hold an explicit compile-one argv, sealed environment, and
frontend/backend lineage for project campaign candidates/*.c; no generic
Makefile is guessed into an executable campaign.
Compare one function out of your normal full-translation-unit build, or get exactness, mechanism, and the next lever in one invocation:
decomp-workbench compare target.o candidate.o --function function_name \
--objdump /path/to/mips64-elf-objdump --show-diff
decomp-workbench diagnose target.o candidate.o --function function_name \
--objdump /path/to/mips64-elf-objdumpdiagnose --html report.html carries the same evidence — no scripts, no
network, one file you can attach to a PR or a Discord thread.
As the work deepens, one command family per stage of a real campaign:
public-match-check— before starting a function at all, ask whether decomp.me already has a verified match. Bind on the target's address and size, not its name. See gate 0.campaign target.o candidates/*.c --compile-command ...— run hundreds of source variants through your own compile wrapper, with caching, an append-only ledger, controls, and a fresh auditable finish receipt. See candidate campaigns.check-scratch,fetch-scratch,bundle-scratch— validate, download, and package decomp.me exports without guessing at context. See checking decomp.me exports.public-match-checkandfetch-scratchare the only two commands that ever open a network connection; both are read-only and neither ever runs implicitly.object collateral— an exact function is only the first gate; compare the containing objects for.bss, GP-table, symbol, relocation, or neighboring-code collateral. See object comparison.shift audit,shift rehearse— a matched build proves the bytes at one layout, not that its addresses survive a resize. In one live gate, a hardcoded pointer that passed the project's own retail verifier was convicted byshift rehearse, by name. See shiftability and the shiftability campaign.permute sweep,permute doctor,permute classify— a bounded decomp-permuter search whose scratch reproduces the project's real per-object recipe, a preflight that says so before the search burns an hour, and a wall class read off the measurement rather than off verdict prose. See permuter sweeps.ranking stamp,ranking check— a closeness ranking measures one tree and decays the moment that tree moves. Stamp it where it is produced, check it where work is ordered by it. See the ranking decays.check-staleness, and--built-fromoncompare,compare-dumps,diagnoseanddiagnose-dumps— a comparison against a build older than its own source reads as a match, which is the expensive way to be wrong. Name the inputs and the comparison refuses. See is the thing you compared the thing you just built?.handoff audit— before publishing a proof repository, check that every local dependency actually travels. See public handoff audits.install-skill codex|claude— an optional agent skill that runs the same commands you would.
| What the comparison says | Next move |
|---|---|
structure-mismatch |
Keep working at the C/control-flow level |
constant-mismatch |
Audit the flag/enum against the assembly, then re-derive fakes |
commutative-order |
Change the expression tree (x |= y), not the allocator |
schedule-mismatch |
Regroup statements; use -g0 to locate ownership, not to prove the C |
allocation-mismatch |
Run view to name the family, then decomp-workbench guide <playbook> |
relocation-layout-mismatch |
Check relocation metadata, then the project link/ROM check |
exact=true |
Run the project’s normal collateral and full-output verification |
Whatever the verdict, --show-diff prints every differing site: no verdict
suppresses evidence. The field guide turns each of these
rows into the C that moves it, decomp-workbench guide <playbook|verdict|lever>
prints the relevant part of it in the terminal, and from verdict to
edit walks one case end to end.
The section above is one function. A backlog of them, late in a project, is a loop — and every step of it carries a guard, because the expensive mistakes at this stage are all measurements of the wrong thing:
# 1. rank — is the order still a measurement of this tree?
decomp-workbench ranking check config/ranking.json
# 2. preflight — is the scratch the object the build actually produces?
decomp-workbench permute-doctor func_80012574 --queue queue.json
# 3. sweep — bounded search, refused outright against a stale ranking
decomp-workbench permute-sweep queue.json --minutes 20 \
--ranking config/ranking.json --require-fresh
# 4. classify — what the search measured, not what the prose says
decomp-workbench permute classify out/summary.json
# 5. route — who owns what is left, and is a lever even reachable?
decomp-workbench diagnose target.o candidate.o --function func_80012574 \
--trace uopt.log --trace-proc 1
# 6. verify — against a build newer than the source it came from
decomp-workbench compare target.o candidate.o --function func_80012574 \
--built-from src/game/track.cStep 4 is the routing decision: MATCHED verifies and promotes,
P_STUCK_DESCENDING is the only class that earns trace levers or a human,
P_STUCK_FLAT is the pool from which the case for deeper instrumentation is
argued, and IMPORT_FAULT is not a result about the function at all — it
routes back to step 2.
Step 5 prints an ownership: line under the verdict —
owning_pass=... reachability=... ownership_basis=... — which says which
compiler pass owns the residual and whether any lever this evidence exposes
reaches it. --trace is what turns ownership_basis from heuristic into
trace: a declined force or a regsleft=0 contest is the one fact two
disassemblies cannot show. Scope it with --trace-proc/--trace-web; a trace
covers a whole compilation, and reading some other function's declined force as
this one's manufactures a measurement.
The two guards are not optional decoration. A ranking that stamps a different
commit orders work by a tree that no longer exists, and a comparison against a
stale object reports words=0 for a source edit that was never compiled. Both
have happened; both are one flag away from impossible.
The mechanisms behind the levers are written down as
compiler laws: L62-L70 are the most recent nine, from a
whole-ROM campaign — the float load form that decides a schedule (L62), the
temp ring's seed order (L64), the phantom pop (L65), and two measurement laws
about harnesses that lie (L69, L70). decomp-workbench guide laws ido-5.3 L64
prints one.
Run decomp-workbench commands for the annotated map of every command,
grouped by journey (object, scratch, trace, probe, sweep,
campaign, shift, ...), or decomp-workbench --help for the flat list.
Two conventions hold everywhere:
- On every command that selects one function,
--symboland--functionare the same option, and passing both with different values is refused rather than silently resolved. - Every printed label is also the JSON key for that value;
decomp-workbench --explain-keysprints the one registry of comparison, campaign, and aligned-view keys.
The name shapes — a bare verb reads objects, trace-* reads a compiler log,
probe-* reads your C source, sweep <verb> writes a family of variants —
are explained in Choose a workflow.
decomp-workbench completion bash|zsh|fish|powershell prints a completion
script.
- Start here — an almost-matched function, end to end
- Field guide — the IDO codegen levers, with the C
- Backlog walkthrough — thirty near matches, in triage order
- Documentation index — every guide, with inputs, outputs, and reading order
Comparison, ranking, campaigns, scratch bundling, trace parsing, and pass replay are adapters: bring your own object files, objdump, compiler wrapper, scratch inputs, traces, or pass binaries. These workflows support explicit IRIX 4.x frontend cells and IDO 5.3/7.1 pipelines when the project supplies the corresponding toolchain; instrumentation remains profile-specific. See the IDO support matrix.
The packaged uopt patch profiles are intentionally narrower. They accept
generated uopt.c from one pinned IDO 5.3 static-recomp revision, verify its
SHA-256 and source anchors, and reject unknown input by default. The generic
ugen instrumenter supports a broader but shallower call/free-list trace.
The repository contains no ROMs, target objects, proprietary compiler binaries, generated third-party contexts, extracted target assembly, or game assets. The CV64 campaign record retains aggregate measurements and a local regeneration recipe, while the uncleared scratch payloads remain excluded as recorded by its notice.
python3 -m pip install -e ".[dev]"
python3 -m unittest discover -s tests -v
bandit -r src -ll
codespell README.md CHANGELOG.md CONTRIBUTING.md docs examples src tests
ruff check src tests
ruff format --check src tests
mypy src testsThe test runner is unittest, not pytest. pytest is not part of the dev
extras; uv run pytest fails with collection errors unless you first run
pip install -e ".[dev]" pytest.
See CONTRIBUTING.md before adding a relocation type, instrumentation profile, or trace format.
CC0-1.0. Third-party tools and user-supplied inputs keep their own terms.
