Skip to content

tools: audit our class names against the cartridge's own RTTI (259 of 541 disagree, and that gates promotion) - #2069

Merged
andrewboudreau merged 1 commit into
mainfrom
tools/rtti-name-audit
Aug 31, 2026
Merged

tools: audit our class names against the cartridge's own RTTI (259 of 541 disagree, and that gates promotion)#2069
andrewboudreau merged 1 commit into
mainfrom
tools/rtti-name-audit

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Adds tools/rtti_name_audit.py and notes/rtti-name-audit.md. Tools and prose only — no source, config, manifest or gate changes.

Why

#2066 spent a full destructor migration on PoleLift before discovering the promotion it was meant to unlock could never have landed:

HOMELESS     _ZTS8PoleLift  STB_LOPROC .data size=0xa
HOMELESS     _ZTI8PoleLift  STB_LOPROC .data size=0xc
COLLIDES-GAP _ZTV8PoleLift  STB_GLOBAL .data size=0x84 already at ov045:0x02112dbc

PoleLift is daObjKm2_Ami_Bou_c in the cartridge. Promoting a TU consolidates the class's RTTI, so the compiler emits _ZTI/_ZTS under our name; the ROM holds those records under the real name, at a different length, so nothing can license them. That is a naming failure wearing a TU-boundary failure's clothes, and no amount of boundary evidence fixes it.

What it measures

Tree-wide, against origin/main's config/:

total _ZTV symbols in config    541
  RTTI name AGREES with ours    276
  RTTI name DISAGREES           259
  typeinfo word unreadable        6

The mechanism is confirmed against everything that has already landed, not just the one failure. Of the 23 manifests on main with status: promoted:

own a vtable, name agrees with the ROM 15
own a vtable, name disagrees 0
own no vtable, so RTTI never arises 8

Nothing name-mismatched has ever promoted. The zero is the load-bearing number.

What it does to the inline-destructor queue

Intersecting with the 41-TU inline-destructor census: 11 agree (all already have manifests on main), 48 disagree, 19 own no _ZTV. The destructor flip is unaffected and still lands independently — it touches no RTTI. The promotion half of that list is spent on the agreeing side. Worked top-to-bottom expecting 41 promotions, 48 of them fail at tubuild, each after a full TU reconstruction.

Disagreements concentrate: ov002 has 35, arm9 23, ov064 12, ov022 9, ov029 8.

The evidence chain

Direct and self-checking. From _ZTV address V, [V-4] is the typeinfo pointer; a __si_class_type_info record is [vptr][name ptr][base ptr], so word 1 leads to a length-prefixed _ZTS:

_ZTV8PoleLift @ 0x02112dbc            (ov045, base 0x021111a0)
  [V-4] typeinfo      = 0x02112d74
  TI[1] name ptr      = 0x02112d80 -> '18daObjKm2_Ami_Bou_c'
  TI[2] base's _ZTI   = 0x021089ec

18 is exactly len("daObjKm2_Ami_Bou_c"), so a correct read verifies itself. TI[2] also hands you the ROM-proven direct base.

Three traps, all of which produced wrong numbers here first

Documented in the docstring and the note because each one was hit during this work:

  1. Read config/ from origin/main, not a checkout. A stale working copy returned 265/543 instead of 259/541 — six renames had landed and two _ZTV symbols were gone.
  2. Resolve each address in its own overlay. ov045/ov046/ov047 all sit at 0x021111a0; an address-only or arm9-first lookup silently reads a different image. The tool takes the overlay from the path of the symbols.txt the symbol came from.
  3. Use extracted/arm9_dec.bin. extracted/dsd/arm9/arm9.bin is compressed.

And one reporting rule the tool enforces: an unreadable typeinfo word is unmeasured, not an agreement. Six are unreadable; folding them into the agreeing column would overstate what is promotable by six classes.

Using it

python tools/rtti_name_audit.py                  # summary, first 25 disagreements
python tools/rtti_name_audit.py --all            # every disagreement
python tools/rtti_name_audit.py --class PoleLift # one class, with the chain
python tools/rtti_name_audit.py --json out.json  # every row

--class is the one to run before starting a promotion.

Verification

Run from a clean worktree off origin/main, independently reproducing 541/276/259/6. --class PoleLift → DISAGREES, --class daEyBm_c → AGREES, --class NoSuchClass → exit 1.

Gates in-worktree: check_python_names PASS (261 files, 0 unresolvable), check_dead_references clean (377 prose files, 3549 references, no broken markdown links), check_header_offsets clean over 400 headers. Pre-push: port-refcheck 405/405, duplicate-sources 11158 stems none doubled, check_src_tu_compiles 92/92.

No ROM was built for this PR and none is needed — it adds no compiled source.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ

A coined class name is not just a readability debt -- it is a hard block on TU
promotion, and it does not announce itself as a naming problem. Promoting a TU
consolidates the RTTI, so the compiler emits _ZTI/_ZTS under our name while the
cartridge holds those records under the real one, at a different length. There
is no ROM symbol to license them to and tubuild refuses with HOMELESS/COLLIDES-GAP.

#2066 found this the expensive way: PoleLift is daObjKm2_Ami_Bou_c in the ROM,
so that promotion could never have landed however good its boundary evidence was.

Measured tree-wide, 541 _ZTV symbols in config: 276 agree, 259 disagree, 6
unreadable. The mechanism holds across history too -- of the 23 manifests with
status: promoted, 15 own a vtable and all 15 agree; none disagree.

Intersected with the 41-TU inline-destructor census: 11 agree, 48 disagree, 19
own no vtable. The destructor flip still lands independently, but the promotion
half of that list is gated on renames rather than on destructor mechanics.

The tool reads the chain directly -- _ZTV-4 -> typeinfo -> name ptr -> the
length-prefixed _ZTS string, which verifies itself. Three traps are documented
in the docstring because each produced wrong numbers before being caught:
read config from origin/main not a stale checkout, resolve each address in its
own overlay (ov045/046/047 share a base address), and use the decompressed
arm9_dec.bin. An unreadable typeinfo word is reported as unmeasured, never
folded into the agreeing column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
@tangos-validator

tangos-validator Bot commented Aug 31, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

noverify: no source/build-data changes in this PR

Each changed src/*.c|*.cpp is compiled and its relocated bytes compared to the binary data on a private build box. Passing requires every changed file to reproduce the ROM byte-for-byte with correct relocation targets — this catches WRONG-DEST relocations and non-reproducing near-misses that ledger-scoped linkcheck skips.

@andrewboudreau
andrewboudreau merged commit babae87 into main Aug 31, 2026
6 checks passed
@andrewboudreau
andrewboudreau deleted the tools/rtti-name-audit branch August 31, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant