tools: let a policy license a vague-linkage duplicate against the cartridge - #1977
Merged
Conversation
…tridge `deadstrip` licenses a homeless function and is refused the moment the symbol acquires a ROM home. That is right for a discarded D2 variant, and wrong for `_ZN7Vector3D1Ev` -- a 4-byte `bx lr` that every TU holding a `Vector3` re-emits with vague linkage, while the cartridge keeps one copy at arm9:0x020072c0 that `src/_ZN7Vector3D1Ev.cpp` already owns. Four reconstructed TUs stopped at text-verified on those four bytes. Add `deadstrip-duplicate`, whose precondition is the opposite: the symbol must have a configured ROM home, and exactly one, or the row is refused rather than guessed at. rombuild sizes the home from the same symbols.txt the delink reads, reads the module image there, and hands those bytes to objisolate, which refuses unless this object's copy is STB_LOPROC and byte-identical. Discarding a duplicate cannot lose the address -- the owning source still supplies it, and if it did not, the link fails on an undefined symbol instead of leaving a hole. Also key `compiler_only_policies` on `promoted_source`. A promoted entry is enrolled under that path, so keying on `source` let the `active` filter drop the policy for all 19 entries whose src_tu and src paths differ, leaving the build to refuse the very object the policy was written for. Split out of #1878, which bundles these tools with the manifest rows that use them; the validator restores tools/ from base, so that PR can never test its own tool change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
✅ PR validation — Passednoverify: no source/build-data changes in this PR Each changed |
4 tasks
andrewboudreau
added a commit
that referenced
this pull request
Aug 30, 2026
Eight one-function sources and eight delinks entries become one .cpp and one .text span (0x02146d7c-0x02147328), the shape the original TU had. mwcc emits one .text section per function in reverse source order, so the reconstructed file is written highest-address-first; the destructor pair's D1/D0 order is the compiler's own. Merging the objects makes mwcc emit an extra vague-linkage _ZN7Vector3D1Ev into the combined object -- types.h gives Vector3 a deliberately empty inline destructor, so every TU that destroys one re-emits it. The cartridge keeps a single copy at arm9:0x020072c0 that src/_ZN7Vector3D1Ev.cpp owns, so the manifest licenses this object's copy with the deadstrip-duplicate disposition added in #1977: rombuild reads the cartridge's bytes at that address and objisolate refuses unless this copy is byte-identical STB_LOPROC output. Known coverage cost, stated plainly: the file is still a SHADOW TU. Its D0/D1 are free functions carrying @symbol markers that stamp the base vptr directly, not a real `class daObjPathLift_c`, so this object emits no _ZTV/_ZTI/_ZTS. The two per-function destructor files it replaces did emit them, and romdata_check compared 128 of 128 _ZTV15daObjPathLift_c bytes against the cartridge. After this commit those three data symbols leave the checked set entirely -- verified by diffing `rombuild --data-json` per symbol, not the summary, which counts records per object and falls on consolidation for unrelated reasons. No symbol changed verdict; three stopped being checked. 106/106 is blind to vtables, so no gate reports this. Restoring it means giving the TU a real class whose out-of-line destructor is the key function; that is the next step, not this one. Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 30, 2026
andrewboudreau
added a commit
that referenced
this pull request
Aug 30, 2026
Both come from the #1880 -> #1914 promotion chain, which bundled them with the manifest rows and sources they produce. The PR validator restores all of `tools/` from base, so no PR in that chain could ever test its own tool change -- every one of them is UNSTABLE for that reason alone. These are lifted out and land alone, off main, so the content PRs that follow are judged against tools that are already there. `tu_order_check.py` compiles a manifest TU once and reports, per licensed function, bytes and emission order. Production isolation places an object's `.text` into the spanning delink in emission order, so when a TU defines its destructor out of line and mwcc emits `D2, D0, D1` against the cartridge's `D1, D0`, the range does not link. The tool makes that visible before a promotion is attempted rather than after. `tu_promote.py` performs the mechanical half of a promotion and guesses at nothing: it collapses the per-function `delinks.txt` entries into one `complete` entry spanning the manifest's sections, `git mv`s the `src_tu/` source to its production path (R100, so credit follows), `git rm`s every `legacy_source`, rewrites the entry to `status: promoted`, and adds one `attribution.json` override per absorbed symbol. It compiles nothing -- the proof of a promotion is `rombuild.py` reporting 106/106 with `mismatching: 0` over the whole batch. The chain's `objisolate.py`/`rombuild.py`/`romdata_check.py` deltas are deliberately NOT carried: they are the predecessor of the deadstrip-data work that has since landed as #1977, #1987 and #1989, and main's versions supersede them. The remaining three files are comment-only, correcting paths that the promotion moves. Verified against main at c05617c, not against the chain's 350-commit-old base: `tu_promote --dry-run ov002/BlueCoinSwitch ov002/Exit ov045/PoleLift` resolves all three to their production paths, and refuses `ov100/daObjPathLift_c` as already promoted; `tu_order_check ov002/BlueCoinSwitch ov045/PoleLift` compiles both and reports the `D0, D1` + extra `D2` against the wanted `D1, D0`. Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Splits the tool half out of #1878 so it can actually be validated. #1878 bundles this
rombuild/objisolatechange with thedaObjPathLift_cmanifest rows that use it, andthe PR validator restores all of
tools/from the base commit — so it runs main'srombuild.py, which only knowsdeadstrip, against the new manifest and fails:That PR can never go green on its own. This one carries no manifest change, so it can.
The problem this unblocks
include/types.hdeclares~Vector3() {}inline in-class — deliberately; the ROM's__destroy_arrover arrays ofVector3proves the element type has a destructor.Consequence: every TU that so much as holds a
Vector3local emits a vague-linkage_ZN7Vector3D1Ev(4 bytes,bx lr,STB_LOPROC).It has a real ROM home at
arm9:0x020072c0, owned bysrc/_ZN7Vector3D1Ev.cpp. The oldpolicy therefore refused it —
compiler_only_outputrejects any symbol with a configuredhome (correctly: it is not compiler-only), and
externalized_outputtakes only_ZTI/_ZTSdata. Four reconstructed TUs stopped at TEXT-VERIFIED on those four bytes(
ArrowSignRight,daObjPathLift_c,arm9/Actor,ov002/LevelObjectsall say so intheir manifest notes). In production it surfaces as:
What changes
A second disposition whose precondition is the opposite of
deadstrip's:{"symbol": "_ZN7Vector3D1Ev", "disposition": "deadstrip-duplicate", "reason": "vague-linkage duplicate; arm9:0x020072c0 owns the retail copy"}deadstripstill means homeless-only — refused the moment a symbol acquires a ROMhome. Unchanged.
deadstrip-duplicaterequires a ROM home and requires it to be unambiguous (exactlyone configured home, or the row is refused rather than guessed at).
rombuildreads themodule image at that address, sizes it from the same
symbols.txtthe delink reads, andhands the cartridge's own bytes to
objisolate, which refuses unless this object's copyis
STB_LOPROCand byte-identical.Dropping a duplicate cannot lose the address: the owning source still supplies it, and if
it did not, the link fails on an undefined symbol rather than quietly leaving a hole. That
loudness is the rest of the safety argument.
deadstrip_planalready refused when asurviving section referenced the symbol; that check is untouched.
Also fixed here, because it is the same code path:
compiler_only_policieskeyed thepolicy on
source, but a promoted entry is enrolled underpromoted_source. This isforward-looking rather than a live bug -- of the 21 manifest entries carrying a
compiler_only_outputtoday, exactly one (ActorBase_SceneNode) is enrolled, and itssourceandpromoted_sourceare the same path, so the old keying happened to work. Theother 20 are unenrolled
src_tu/shadows that theactivefilter drops either way. Thefix matters the moment a TU is promoted while its manifest still names the
src_tupath-- the shape of every entry in the rest of this chain -- where the policy would otherwise
be filtered out and the build would refuse the very object it was written for.
Test plan
python -m pytest tools/test_rombuild.py tools/test_objisolate.py -q→ 52 passed,including five new cases: the cartridge-body demand, the required ROM home, the
ambiguous-home refusal, the unknown-disposition refusal, and the promoted-path keying.
python tools/rombuild.py -j 16→ 106/106 exact, 100.000000% of compared bytes; 11,082 source-built functions, 11,082 reproducing, 0 mismatching -- byte-identical to main, as expected for a change that only widens what a manifest row may declare.python tools/port_refcheck.py→ 405 checked, all references resolvepython tools/prepush_attribution.py --base origin/main --head HEAD→ 11,388 tracked, 0 changed, 0 lostNo
src/,config/orinclude/file changes, so no byte can move: this PR only widenswhat a future manifest row is allowed to declare.
Follow-up: the
daObjPathLift_cpromotion half of #1878 rebases onto this and becomes aconfig+src-only PR the validator can judge on its merits. #1880 → #1882 → #1884 → #1914
then restack behind it.