Decomp TU Slicing
Find which C++ classes shared original translation units in sm64ds-decomp.
Installation
- Make sure Claude is on your device and in your terminal.
Skills load from
~/.claude/skills/when Claude Code starts up β so you need it on your machine first. If you don't have it yet, install it once with the command below, then runclaudein any terminal to verify.One-time setupnpm i -g @anthropic-ai/claude-codeAlready have it? Skip ahead.
- Paste into Claude Code or into your terminal.
This copies the whole skill folder into
~/.claude/skills/decomp-tu-slicing-tangosdev/β the SKILL.md plus any scripts, reference docs, or templates the skill ships with. Safe default: works for every skill.Faster alternative (instruction-only skills)
Skips the clone and grabs only the SKILL.md file. Don't use this if the skill ships Python scripts, reference markdowns, or asset templates β they won't be downloaded and the skill will fail when it tries to load them.
Quick install (SKILL.md only)Sign up to copy - Restart Claude Code.
Quit and reopen Claude Code (or any other agent that loads from
~/.claude/skills/). New skills are picked up on startup. - Just ask Claude.
Skills auto-activate when your request matches the skill's description β no slash command needed. Trigger phrases live in the skill's own frontmatter; you can read them in the βWhat this skill doesβ section above.
Prefer to read the source first? Open on GitHub.
When Claude uses it
Find which classes shared an original .cpp translation unit in sm64ds-decomp, so a C++ conversion PR ships the right set of classes together instead of splitting a file the ROM says is one. Use before slicing a per-class migration PR, when deciding whether two classes belong in one header, when eligible.py rejects a file for extra sections or a multi-function TU, when asked which functions belong to a class in an unnamed overlay, or when reading build/tu_map.json. To then execute the merge and byte-verify it, use decomp-tu-build.
What this skill does
Translation-unit slicing
The linker erased the original .cpp boundaries; tools/tu_map.py reads them back
out. Full derivation and limits: notes/tu-boundaries.md. To execute a merge and
verify it, use decomp-tu-build β this skill is about reading the map, not acting
on it.
0. Regenerate the map, in this order
python tools/rtti_extract.py # -> build/rtti.json
python tools/rtti_vtables.py # -> build/rtti_vtables.json
python tools/tu_map.py # -> build/tu_map.json
build/ is gitignored, so a fresh worktree has none of this.
Nothing enforces that order, and skipping it fails silently. vtable_labels()
returns {} when build/rtti_vtables.json is absent, and tu_map.py then writes a
complete, self-consistent, wrong map and exits 0. Measured on main from an
empty build/:
| full chain | rtti_vtables.json absent | |
|---|---|---|
| whole-ROM TUs | 532 | 516 |
| boundaries | {low: 68, medium: 110, high: 280} | {low: 72, medium: 124, high: 246} |
| TUs with a class | 400/532 | 370/516 |
| under-segmented | ov007, main, ov075, ov084 | ov007, main, ov005, ov084 |
| ov080 | 5 TUs, 4 classed | 6 TUs, 4 classed |
--check | all gates PASS, exit 0 | all gates PASS, exit 0 |
The known-answer gate asserts ov080's classed TU count, which is 4 either way, so it
passes for the wrong reason. The under-segmented list is itself a casualty, so you
cannot use it to detect the problem either. The one visible tell is the negative-control
table, which collapses to blind 1 / blind-classed 0. There is no exit code to lean on:
check the prerequisites yourself.
--blind is not an opt-out β it is the negative control, dropping the mangled-name
signal to score what RTTI alone recovers. The whole flag set is --module, --verbose,
--check, --blind, --split-swallowers [K], --out; nothing suppresses the RTTI
labels while keeping the names. tubuild.py regenerates the map only when it is
entirely absent β a stale-but-present map is reported as a note and used as-is.
Figures move whenever the map changes, so run the command rather than quoting these.
Measured on main at 343eab070: 74 modules, 11,091 functions, 532 TUs, 305 sinits,
boundaries {low: 68, medium: 110, high: 280}, 400/532 carrying a class.
The one rule that matters
A class is not a file. Where two classes shared a .cpp, mwcc emitted their
methods interleaved in source order, so they look like many alternating blocks and are
in fact one TU. Never slice on "the class name changed."
python tools/tu_map.py --module ov080
ov080: 86 functions -> 5 TUs (4 with a class), 3 sinits / 3 ctor entries [ok]
0x2123740-0x2124a20 26 fn MontyMole, MontyMoleRock, daChoropu_c <- ONE TU
0x2124a20-0x2125404 21 fn CrazedCrate, daBttBk_c
0x2125404-0x2126fbc 29 fn Painting
0x2126fbc-0x212714c 6 fn daObjMaruta_c, daPicGate_c
0x212714c-0x212766c 4 fn (unattributed)
boundaries: {'high': 3, 'medium': 1}
MontyMole and MontyMoleRock ship together or not at all. Splitting them into two PRs invents a structure the ROM contradicts, and any later consolidation has to undo it.
Before slicing a per-class PR
python tools/tu_map.py --module <ovNNN>β find the TU holding your class.- Every class listed on that line is in scope. Names like
daChoropu_care the RTTI (EAD internal) name for a class you already know by its English name β the same class, not an extra one. Two different English names on one line means two classes genuinely co-resided. - Check
corroboratedfor the module inbuild/tu_map.json.truemeans the sinit count independently confirms the cut count β a witness that never informed the cuts.falsemeans the boundaries rest on labels alone. - Check the boundary confidence.
lowboundaries cannot distinguish two classes in one file from two files that linked adjacently; do not slice across one without further evidence.
Trust calibration
| situation | read |
|---|---|
module corroborated: true, boundary high | slice on it |
boundary medium (one side unattributed) | the classed side is sound; the run beside it is unresolved |
boundary low | contiguity only β evidence, not an answer |
module in meta.under_segmented | TU count is a lower bound: ov007, main, ov075, ov084 |
In an unnamed overlay, a TU carrying a class name is largely trustworthy β but the unattributed runs between them are unresolved, not TUs. Do not report a raw TU count from an under-segmented module as if it were the file count.
The negative control (--check, bottom table) is the calibration, and it is good but
not exact. Names stripped, RTTI alone recovers the known classed-TU count in 3 of the 4
known-answer modules β ov062 5/5, ov063 4/4, ov020 2/2 β and falls one short on ov080
(3 against 4). Its blind column is much larger than known (10, 9, 6, 4) because
without mangled names the unlabelled runs never get absorbed.
Gotchas that have already bitten
- A module can contain an object with NO
.textat all.tu_map --module ov045reports 6 TUs; ov045 has a seventh, data-only object β 2,128 bytes at the head of.data, read by three TUs'InitResources.tu_mapis built from.textruns and structurally cannot see it, so "N TUs" is a count of code-bearing objects, not of original files. (The 6 is reproducible; the 2,128 is an analysis result not recorded anywhere innotes/β treat it as unconfirmed.) It was found only because attributing.databy "whose code loads it" produced a TU ordering the linker's layout rule forbids. Distinct from the 31 whole modules that are data-only. - A size-0 function symbol is an ALIAS, not a function. All 8 in the corpus are a
second
kind:functionsymbol at an address a sized one already occupies β the ARM runtime exporting both its mwcc and AEABI name from one entry point (_u32_div_f,_s32_div_f,_dadd,_dmul,_ll_udiv,_ll_sdiv,_ull_mod) plus__cxa_vec_cleanup. They are not unrecorded sizes: no size-0 symbol is alone at its address and no address carries two sized ones, so the extent is never ambiguous. This is NOT fixed onmain.functions()reads symbols.txt straight through with no collapse by address, so these 8 still fragment real runs into 4-byte units that overlap their neighbours β 8 overlapping unit pairs in today's map, all of them inmainanditcm, which are therefore not partitions. If you inventory symbols yourself, collapse by address (one entry per address, survivor is the sized symbol) or you inherit the same defect. - RTTI spans can bridge two real TUs. A vtable slot may point at inherited or
shared code in the neighbouring object.
tu_map.pyalready suppresses this (mangled names win over RTTI spans), but if you compute spans yourself, don't re-introduce it β ov081'sdaSnowman_cand ov090'sdaMenbo_care the cases. - Inventory functions from
symbols.txt, notdelinks.txt. That is whattu_map.functions()does, and it is the right rule β delinks is a build artefact and can be re-partitioned, symbols.txt is the inventory. Its docstring justifies the choice with an ov062 hole at0x02117724-0x02117994; that hole no longer exists onmain(config/arm9/overlays/ov062/delinks.txt covers it, and a sweep of every module finds 0 symbols.txt functions uncovered by a delinks.textentry). Keep the rule, don't cite the example as current. - Key by (module, addr), never addr alone. 44 of the 93 modules
tools/modules.pylists β all of them overlays β load at base0x021111A0. - Join files to TUs by address -> symbol ->
units[].functions, never by file stem (renames) nor by TU interval β 109 functions sit outside their own TU's start/end. tu_map.pycalls arm9main. Keying onarm9silently drops 3,067 functions.
Attributing non-.text sections
If you extend a TU claim to .rodata/.data/.bss (for a TU-granular delinks.txt),
the only sound rule is: object k's contribution to every section precedes object
k+1's. Discard any rule whose per-TU intervals are not ascending and disjoint.
Do not use "TU k's code loads this address, so TU k owns it" β that direction
is what produced the impossible ordering above. Follow only pointers a run contains,
then close transitively over referrers (_ZTV -> _ZTI -> _ZTS). Expect to leave
some sections unattributed and say so: ov045's .rodata is 0/840 because its code
never references its own .rodata at all.
C++ destructors and the vtable anchor
tools/objisolate.py reduces a compiled object to the one function its delink entry
declares, so a .cpp destructor -- which mwcc emits as D0/D1/D2 plus the class
vtable and RTTI -- can link. notes/objisolate.md measures 99 of 105 .cpp destructor
files eligible (up from 30) β read that column as eligible, not enrolled β and is
required reading before touching it; in particular a _ZTV* relocation's
addend must lose 8 on rebinding, because mwcc's vtable symbol addresses the object's
start while symbols.txt's addresses the slot array. Getting that wrong links clean
and corrupts 34 modules.
mwcc anchors the vtable to the TU that DEFINES the destructor out of line β which
is why a vtable is also a .data anchor for its TU, and why moving that definition
inline removes the anchor entirely. The full source-form rules are in
decomp-cpp-class-form.
After changing tu_map.py
python tools/tu_map.py --check must stay green β but read the output, never the exit
status. On main it prints [PASS]/[FAIL] per gate and returns; the exit code is
0 either way. --check --blind fails two gates and still exits 0. --check also
does not rewrite build/tu_map.json.
The gates on main are V1 (known-answer classed-TU counts for ov062/ov063/ov080/ov020),
V1b (KoopaSmall_Spawn lands inside the Koopa TU), V2a (sinit count == .ctor entry
count), V2b (no module has more sinits than TUs) and V3 (every function in exactly one
TU), followed by the blind negative-control table. V2b earns its keep β it caught the
RTTI-bridge bug.
There is no partition gate. Nothing asserts that a module's units are disjoint, and
the JSON carries no meta.partition_defects β meta is exactly modules, total_tus,
total_functions, total_sinits, boundaries, tus_with_class, under_segmented,
caveat. Today's map has 8 overlapping unit pairs (the alias defect above) and 85 gaps,
and nothing reports either. If such a gate is ever added it must not assert that
every function lies inside its unit's range: 109 functions across 10 modules violate
that on purpose, because absorb_unlabelled attaches call-graph-proven file-local
functions without widening the cluster span. Nor should it forbid gaps β the 85 are
legitimate.
The map does not merge files. Consolidation is a separate byte-gated change:
a merged TU must emit functions in exactly the ROM's order (mwccarm emits one .text
section per function in the REVERSE of source order), notes/mwccarm-codegen.md
records that #pragma opt_* TUs cannot be merged as-is, and rombuild.py is the
verdict β not build_pin.
Related skills
PowerPoint Slide Decks
anthropics
Create, edit, read, and extract content from PowerPoint presentations.
Memory Search
davila7
Search conversation history and recall previous discussions, decisions, and context.
Slide Deck Creator
daymade
Generate professional PowerPoint presentations with structured content, charts, and speaker notes.
Competitive Ads Extractor
Prat011
Extract and analyze competitors' ads to discover messaging and creative strategies that work.