romdata_check: stop charging a vtable's preamble to the symbol before it (465 -> 527 verified, 0 regressions) - #2113
Merged
Conversation
`symbols.txt` points `_ZTV<C>` at the slot array, so the vtable object's offset-to-top and typeinfo words sit at `addr - 8` under no symbol at all. `rom_data_index()` sized every data symbol as distance-to-the-next-symbol and therefore handed those eight bytes to whatever came before, which then scored PARTIAL by exactly eight bytes with nothing it could ever emit to close the gap. `check_symbol` already knows about the preamble -- it applies `OI.VTABLE_PREAMBLE` on the emitted side and corrects reloc addends by the same 8 -- so this was the one place that didn't. 522 of this ROM's 540 `_ZTV` symbols are unowned by any source, so the defect is not rare: measured on this tree, `verified` goes 465 -> 527 and `partial` 253 -> 191, with **zero** records moving the other way and `differ` unchanged at 6. Formulated as a boundary set rather than a subtraction, because config already names some preamble words: a blanket "subtract 8 from whatever precedes a vtable" drove 23 extents to <= 0, one of them to -4. Where a real symbol sits nearer than V-8 it simply wins and gets the shorter extent. Gated on the cartridge rather than assumed. 9 of the 414 vtable addresses config names have something other than a preamble below them -- `_ZTV8dActor_c` reads the tail of a string, `_ZTV8dCcPos_c` two code pointers -- and inserting a boundary there would strip eight owned bytes off a neighbour and manufacture a false VERIFIED, which is this same defect with the sign flipped. So the boundary goes in only where offset-to-top is zero and the typeinfo word is null or an address `symbols.txt` gives to some `_ZTI`. Where the evidence is absent the old sizing stands, which can only leave an extent too long (a PARTIAL that could have been VERIFIED) and never too short. The boundary arithmetic is split into a pure `_module_extents(entries, has_preamble)` so CI can test it with a stub predicate and no cartridge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
✅ PR validation — Passednoverify: no source/build-data changes in this PR Each changed |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
rom_data_index()sizes every data symbol as distance-to-the-next-symbol. When the next symbol is a_ZTV, that hands the previous symbol eight bytes it does not own — and there are 522 unowned vtables in this ROM for it to do that with.The defect
symbols.txtuses the convention that_ZTV<C>is the slot array, already past mwcc's two-word object header. So the offset-to-top and typeinfo words live ataddr - 8under no symbol at all. Distance-to-the-next-symbol sizing charges them to whatever came before, and that symbol then scoresPARTIALby exactly eight bytes with nothing it could ever emit to close the gap.check_symbolalready knows about this — it appliespreamble = OI.VTABLE_PREAMBLE if name.startswith("_ZTV") else 0on the emitted side, and corrects reloc addends by the same 8.rom_data_index()was the one place that didn't. The file's own docstring describes the convention under "THE VTABLE PREAMBLE, AND WHY THE COMPARE IS OFFSET"; this makes the index agree with it.Measured, on this tree
Full
rombuild.py -j16 --no-rom --data-json, stock tool then patched, same worktree, same commit:Per-record, deduped on
(module, symbol, addr)across all 1,246 named records:differdoes not move.106/106 exact, 100.000000%and11,088/11,088reproducing on both runs — this touches no bytes, only how the index describes them.validate_merge's romData ratchet is one-directional (fails only whenverifiedfalls), so this direction is safe by construction, and the 62 flips ratchet the floor up.Why a boundary set and not a subtraction
The obvious formulation — "if the next symbol is a
_ZTV, subtract 8" — is wrong, and I only found that out by simulating it. Config already names some preamble words:data_ov012_02112404sits four bytes below_ZTV13BasementWater,data_0209a73csits eight below_ZTVSt9type_info. A blanket subtraction drove 23 extents to ≤ 0, one of them to −4.So instead V−8 joins the sorted boundary list alongside the real symbol addresses, and each symbol's extent runs to the next boundary of either kind. Where a named symbol sits nearer than V−8 it simply wins and gets the shorter extent. Zero non-positive extents result, asserted in the tests.
Why it is gated on the cartridge
Not every
_ZTVhere has a preamble below it. Sweeping all 414 vtable addresses config names, 9 do not:Inserting a boundary at those would strip eight owned bytes off the neighbour and turn a legitimate PARTIAL into a false VERIFIED — this same defect with the sign flipped, and the more dangerous sign, since it is the one the ratchet cannot catch.
So the boundary goes in only where the cartridge shows a preamble: offset-to-top zero, and the typeinfo word either null (a class with no RTTI still gets the two words) or an address
symbols.txtgives to some_ZTI, in this module or inarm9. Where the evidence is absent, today's behaviour stands — an extent too long, which can only cost a VERIFIED that was available, never invent one that wasn't.Tests
Twelve new cases, all cartridge-free. The boundary arithmetic is split out of
rom_data_index()into a pure_module_extents(entries, has_preamble)so it can be driven by a stub predicate, and_vtable_preamble_atis exercised against a fakeRVfor each gate arm — including the "Play"/" Roo" bytes as a literal regression case.tools/test_romdata_check.pyis already enumerated in.github/workflows/tool-tests.yml, so these run in CI without a workflow change.What this does not fix
The other sign of the same family:
_ZTV14dScMgD3DBase_cat0x0213c62chas extent 80 against a real 144, becausedata_ov006_0213c67csits inside the table. It scores VERIFIED anyway, because the verdict test islen(linked) >= extent— sixteen of its slots are never compared. That fix lowersverifiedand therefore trips the ratchet, so it needs its own PR and its own baseline conversation. Deliberately not bundled here.Context: raised on #2111 and #2112.
_ZTV12dScMgSlot3_c's extent goes 152 → 144 here; it stays PARTIAL onmainbecausemain's source doesn't yet emit all 36 slots, and flips to VERIFIED on top of the slot stack, which gives this change a free second positive control once #2112 lands.🤖 Generated with Claude Code
https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ