Skip to content

Fix: main cannot build the ROM -- accept a vptr store already in the ROM's vtable convention - #1989

Merged
andrewboudreau merged 1 commit into
mainfrom
fix/vptr-addend-zero
Aug 30, 2026
Merged

Fix: main cannot build the ROM -- accept a vptr store already in the ROM's vtable convention#1989
andrewboudreau merged 1 commit into
mainfrom
fix/vptr-addend-zero

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

main cannot build the ROM

python tools/rombuild.py -j 16 on baa3d97ee stops at step 3:

[3/6] mwccarm: 11077 enrolled source file(s), -j16
!! mwccarm failed (exit 1)
src/actors/daObjPathLift_c.cpp: isolate: compiler-only deadstrip refused:
  _ZTV10dBgActor_c: unexpected reloc type=2 addend=0

Reproduced on a clean detached checkout of origin/main. #1987 and #1979 are each
fine on their own; the break is in the pair. #1987 added a shape survey for the RTTI
relocations a promoted TU's surviving code makes, and #1979's promoted
ov100/daObjPathLift_c is the first source on main to exercise it.

The cause

There are two legitimate spellings of a vptr store, and they differ by exactly one
vtable preamble. The survey accepted only one of them.

mwcc's _ZTV<C> addresses the vtable object, so a store against a definition in
the same object carries the 8-byte preamble skip, and has to lose it once that
definition is externalised — that is the case #1987 was written for, and it is right.

But a source may equally declare the ROM symbol itself. daObjPathLift_c.cpp:79:

extern int _ZTV10dBgActor_c[];
...
*(void***)p = (void**)_ZTV10dBgActor_c;

That is symbols.txt's convention, where the symbol is the slot array. The store
relocates with addend 0, there is nothing to correct, and _apply already leaves it
alone — its rewrite is keyed on addend >= VTABLE_PREAMBLE. The survey refused it
anyway, so the first promoted TU to inherit a base's vtable rather than own one took
the whole build down.

This is not specific to daObjPathLift_c. Every promoted TU whose class inherits its
vptr — the common case — spells it this way.

The fix

Accept both spellings. Anything strictly between them is still refused rather than
guessed at, which is the point of having a survey at all.

shape = ((addend == 0 or addend >= VTABLE_PREAMBLE)
         if target.name.startswith("_ZTV") else addend == 0)

Two tests: one builds a class whose destructor stores an extern _ZTVBase, asserts the
reference relocates at 0 and comes back out untouched; the other retargets a known-good
addend to 4 and asserts the refusal still fires.

Verification

  • python tools/rombuild.py -j 16106/106 exact, 100.000000% of compared bytes,
    PASS
    ; 446 verified / 239 partial / 15 differ from 7,798 object records.
  • pytest tools/test_objisolate.py tools/test_rombuild.py tools/test_tubuild.py
    99 passed.

🤖 Generated with Claude Code

main cannot build the ROM. `python tools/rombuild.py` on baa3d97 stops at
step 3 with

    src/actors/daObjPathLift_c.cpp: isolate: compiler-only deadstrip refused:
      _ZTV10dBgActor_c: unexpected reloc type=2 addend=0

#1987 added a shape survey for the RTTI relocations a promoted TU's surviving
code makes, and it accepted only ONE of the two legitimate spellings of a vptr
store. mwcc's `_ZTV<C>` addresses the vtable OBJECT, so a store against a
definition in the same object carries the 8-byte preamble skip and has to lose
it once that definition is externalised. But a source may equally declare the
ROM symbol itself -- `daObjPathLift_c.cpp:79` has

    extern int _ZTV10dBgActor_c[];

-- which is symbols.txt's convention, where the symbol IS the slot array. That
store relocates with addend 0, there is nothing to correct, and `_apply` already
leaves it alone because its rewrite is keyed on `addend >= VTABLE_PREAMBLE`.
The survey refused it anyway, so the first promoted TU on main to inherit a
base's vtable rather than own one took the whole build down with it.

Both spellings are now accepted. Anything strictly between them is still
refused rather than guessed at, which is the point of the survey.

Two tests: one builds a class whose destructor stores an extern `_ZTVBase`,
asserts the reference relocates at 0 and comes back out untouched; the other
retargets a known-good addend to 4 and asserts the refusal still fires.

python tools/rombuild.py -j 16 on this branch: 106/106 exact, 100.000000% of
compared bytes, PASS. 99 tests pass across objisolate/rombuild/tubuild.

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

tangos-validator Bot commented Aug 30, 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 5a8efec into main Aug 30, 2026
5 checks passed
@andrewboudreau
andrewboudreau deleted the fix/vptr-addend-zero branch August 30, 2026 04:41
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>
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