Skip to content

Call 42 members by name instead of by mangled symbol - #2726

Merged
andrewboudreau merged 1 commit into
mainfrom
cpp/member-calls-2
Sep 18, 2026
Merged

andrewboudreau merged 1 commit into
mainfrom
cpp/member-calls-2

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

42 files that reached a member through an extern "C" declaration of its Itanium mangled name now call it by name. That declaration is what tiers.py counts against no_mangled_refs, which was the last criterion each of these needed for CONVERTED.

CONVERTED 2812 -> 2855. No header changes; src/ only.

Three spellings, chosen by what the header declares

header says emitted
member, receiver type already matches this->AllocateForwards(size, align)
member, receiver is a local stand-in struct ((dEnemyBase_c *)c)->UpdateYoshiEat(...)
namespace, no implicit this Sound::PlayBank3(0x62, *(const Vector3 *)&v)

The middle form is the common case and is the part worth reviewing. Most of these files declare a local stand-in struct for the class instead of including the real header, so that struct is unrelated to the one owning the member and a plain c->UpdateYoshiEat() does not resolve. A C-style cast to the declared type costs no instruction and removes the mangled name. Adopting the real types is the next layer of the migration, not this one.

Two smaller cases:

  • IRQ::DisableIRQs and Sound::PlayBank3 are namespace-scoped, so the mangled name carries no implicit this and argument 0 is a real argument -- converting them as members ate it.
  • dActor_c::DistToCPlayer and dActor_c::FarthestPlayer already called ClosestPlayer() as a member. Only a dead extern of the mangled name remained, so it is simply deleted; those two diffs are a single removed line each.

Gates

Every file was byte-verified individually with build_pin.verify under 2004/b56, and again in the full build:

[3/6] mwccarm: 8760 enrolled source file(s), -j16
      8718 reused from cache, 42 compiled
source-built functions: 11,206   reproducing: 11,206   mismatching: 0
module fidelity: 106/106 exact, 100.000000% of compared bytes
ROM-build analysis: PASS

42 compiled is the whole changed set, so this is a real recompile rather than a cache pass-through.

  • check_decl_agreement.py -- no new disagreements. 44 banked ones are now gone, because the removed externs were themselves the contradicting declarations.
  • tiers_ratchet.py --check -- PASS.
  • port_refcheck.py -- 423 references, 0 stale.
  • MSVC port smoke build -- 230/230, all five smoke executables link. Nine of the converted calls are HAL seams that port/hal/*.cpp spells as C functions, and no ROM-side gate compiles port/, so this one was run by hand.

What is deliberately not here

16 of the 58 candidates fail to compile for reasons the call spelling cannot fix, and are left calling the mangled name:

  • a local struct Vector3 / struct ModelBase colliding with the real header once it is included (structural -- needs the type adoption pass)
  • ModelBase::ApplyOpacity() declared with an arity the call site contradicts, i.e. a header defect to settle separately
  • one mwccarm internal compiler error (CClass.c line 3328)

They cost nothing to revisit later; each was reverted to its original matching state.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4FxdKHG3a6hQRYRAbx29c

Each of these files reached a member through an `extern "C"` declaration of its
Itanium mangled name -- `_ZN22ExpandingHeapAllocator16AllocateForwardsEjj(this,
size, align)` -- which is what `tiers.py` counts against `no_mangled_refs`, the
last criterion these 42 needed for CONVERTED.

Three spellings, picked by what the header actually declares:

  member, receiver type matches     this->AllocateForwards(size, align)
  member, receiver is a stand-in    ((dEnemyBase_c *)c)->UpdateYoshiEat(...)
  namespace, no implicit this       Sound::PlayBank3(0x62, *(const Vector3 *)&v)

The middle form is the common case and deserves a note. Most of these files
declare a local stand-in struct for the class rather than including the real
header, so the struct is unrelated to the one that owns the member and a plain
`c->UpdateYoshiEat()` does not resolve. A C-style cast to the declared type
costs no instruction and removes the mangled name; adopting the real types is
the next layer of the migration, not this one.

`IRQ::DisableIRQs` and `Sound::PlayBank3` are namespace-scoped, so the mangled
name carries no implicit `this` and the first argument is a real argument --
converting them as members ate it. `dActor_c::DistToCPlayer` and
`FarthestPlayer` already called `ClosestPlayer()` as a member; only a dead
`extern` of the mangled name was left, so it is simply gone.

Every file is byte-verified under 2004/b56 individually and in the full build:

  [3/6] mwccarm: 8760 enrolled source file(s), -j16
        8718 reused from cache, 42 compiled
  source-built functions: 11,206   reproducing: 11,206   mismatching: 0
  module fidelity: 106/106 exact, 100.000000% of compared bytes
  ROM-build analysis: PASS

check_decl_agreement: no new disagreements (44 banked ones are now gone, since
the removed externs were themselves the contradicting declarations).
tiers_ratchet --check: PASS, baseline 2812 -> current 2855.
port_refcheck: 423 references, 0 stale.

Nine of the touched calls are HAL seams that `port/hal/*.cpp` spells as C
functions, so the MSVC port smoke build was run too: 230/230, all five smoke
executables link.

16 of the 58 candidates are not here. They fail to compile for reasons the call
spelling cannot fix -- a local `struct Vector3` colliding with the real header,
a header declaring `ApplyOpacity()` with an arity the call site contradicts, and
one mwccarm internal compiler error. They are left calling the mangled name.

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

tangos-validator Bot commented Sep 16, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

Committed merge introduces no reconstruction or attribution regression.

Full merge validation

Check Result
Committed test merge yes
Byte-verified functions 11,177 / 11,344 (98.53%, +0)
Byte-verified code bytes 2,136,384 / 2,211,124 (96.62%, +0)
Claimed, not byte-verified 147 functions, 51,284 bytes (+0)
Perfect source moves 0 R100
Contributor credit 0 added, 0 changed, 0 lost
Relocation check 42 checked; 42 VERIFIED
Port reference check 423 checked; 0 stale
Module fidelity 106/106 exact; 100.000000% compared bytes
Code linked from verified source 11,206 functions, 2,150,424 bytes (96.08%)
Module bytes from source 2,150,424 / 3,049,600 (70.5%); 806,508 (26.4%) are data no delink entry reaches
Retail-gap contribution 894,192 module bytes (87,684 function-code; 806,508 data/non-function)
ROM data reproduced from source 717 symbol(s) exact, 231 partial, 4 differ

Byte-verified means the range carries complete in a delinks.txt, so the ROM build compiled it and compared it to the cartridge. The 147 claimed functions have a src/ file named after the symbol with no NONMATCHING banner, and nothing compiles them -- dsd fills their addresses with the ROM's own bytes. Both together are the 11,324 this project calls matched.

Per-file link-check detail

All 42 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.

File Symbol Result Slots checked
src/LoadKeyModels.cpp LoadKeyModels ✅ verified 1
src/_Z15LoadExitObjectsRN11LVL_Overlay11ObjSubTableEij.cpp _Z15LoadExitObjectsRN11LVL_Overlay11ObjSubTableEij ✅ verified 1
src/_Z25LoadTeleportSourceObjectsRN11LVL_Overlay11ObjSubTableEij.cpp _Z25LoadTeleportSourceObjectsRN11LVL_Overlay11ObjSubTableEij ✅ verified 1
src/_ZN10BrickBlock13InitResourcesEv.cpp _ZN10BrickBlock13InitResourcesEv ✅ verified 1
src/_ZN10BrickBlock8BehaviorEv.cpp _ZN10BrickBlock8BehaviorEv ✅ verified 1
src/_ZN10KoopaShell8BehaviorEv.cpp _ZN10KoopaShell8BehaviorEv ✅ verified 1
src/_ZN10SlidingIce6RenderEv.cpp _ZN10SlidingIce6RenderEv ✅ verified 1
src/_ZN11BillBlaster15OnHitByMegaCharER6Player.cpp _ZN11BillBlaster15OnHitByMegaCharER6Player ✅ verified 1
src/_ZN11BillBlaster16CleanupResourcesEv.cpp _ZN11BillBlaster16CleanupResourcesEv ✅ verified 1
src/_ZN11PowerFlower8BehaviorEv.cpp _ZN11PowerFlower8BehaviorEv ✅ verified 1
src/_ZN11dCapEnemy_c10ReleaseCapERK7Vector3.cpp _ZN11dCapEnemy_c10ReleaseCapERK7Vector3 ✅ verified 1
src/_ZN12dEnemyBase_c11UpdateDeathER10dBgCh_Actr.cpp _ZN12dEnemyBase_c11UpdateDeathER10dBgCh_Actr ✅ verified 1
src/_ZN13PrincessPeach10InitState0Ev.cpp _ZN13PrincessPeach10InitState0Ev ✅ verified 1
src/_ZN13PrincessPeach8BehaviorEv.cpp _ZN13PrincessPeach8BehaviorEv ✅ verified 1
src/_ZN13SnowmanBreath13InitResourcesEv.cpp _ZN13SnowmanBreath13InitResourcesEv ✅ verified 1
src/_ZN15BookShotSpawner13InitResourcesEv.cpp _ZN15BookShotSpawner13InitResourcesEv ✅ verified 1
src/_ZN15BookShotSpawner8BehaviorEv.cpp _ZN15BookShotSpawner8BehaviorEv ✅ verified 1
src/_ZN16dScMgSmartball_c13OnYoshiTryEatEi.cpp _ZN16dScMgSmartball_c13OnYoshiTryEatEi ✅ verified 1
src/_ZN17BowserSkyPlatform8BehaviorEv.cpp _ZN17BowserSkyPlatform8BehaviorEv ✅ verified 1
src/_ZN18NestedHeapIterator7AddLastEP13HeapAllocator.cpp _ZN18NestedHeapIterator7AddLastEP13HeapAllocator ✅ verified 1
src/_ZN18NestedHeapIterator8AddFirstEP13HeapAllocator.cpp _ZN18NestedHeapIterator8AddFirstEP13HeapAllocator ✅ verified 1
src/_ZN22ExpandingHeapAllocator8AllocateEji.cpp _ZN22ExpandingHeapAllocator8AllocateEji ✅ verified 1
src/_ZN3HUD14RenderRedCoinsEv.cpp _ZN3HUD14RenderRedCoinsEv ✅ verified 1
src/_ZN3HUD17RenderSilverStarsEv.cpp _ZN3HUD17RenderSilverStarsEv ✅ verified 1
src/_ZN3IRQ13DmaTimHandlerEj.cpp _ZN3IRQ13DmaTimHandlerEj ✅ verified 1
src/_ZN5Crate13OnTurnIntoEggER6Player.cpp _ZN5Crate13OnTurnIntoEggER6Player ✅ verified 1
src/_ZN5Stage14GraphCallback2Ev.cpp _ZN5Stage14GraphCallback2Ev ✅ verified 1
src/_ZN5Stage9VE_UpdateEv.cpp _ZN5Stage9VE_UpdateEv ✅ verified 1
src/_ZN5Stump15OnGroundPoundedER8dActor_c.cpp _ZN5Stump15OnGroundPoundedER8dActor_c ✅ verified 1
src/_ZN5Whomp6RenderEv.cpp _ZN5Whomp6RenderEv ✅ verified 1
src/_ZN6Player14IsFrontSlidingEv.cpp _ZN6Player14IsFrontSlidingEv ✅ verified 1
src/_ZN6Player17LostGrabbedObjectEv.cpp _ZN6Player17LostGrabbedObjectEv ✅ verified 1
src/_ZN8Goomboss6RenderEv.cpp _ZN8Goomboss6RenderEv ✅ verified 1
src/_ZN8IceBlock15OnHitByMegaCharER6Player.cpp _ZN8IceBlock15OnHitByMegaCharER6Player ✅ verified 1
src/_ZN8SignPost15OnGroundPoundedER8dActor_c.cpp _ZN8SignPost15OnGroundPoundedER8dActor_c ✅ verified 1
src/_ZN8dActor_c13DistToCPlayerEv.cpp _ZN8dActor_c13DistToCPlayerEv ✅ verified 1
src/_ZN8dActor_c13SpawnSoundObjEj.cpp _ZN8dActor_c13SpawnSoundObjEj ✅ verified 1
src/_ZN8dActor_c14FarthestPlayerEv.cpp _ZN8dActor_c14FarthestPlayerEv ✅ verified 1
src/_ZN9MontyMole8BehaviorEv.cpp _ZN9MontyMole8BehaviorEv ✅ verified 1
src/engine/fader/StartEntranceFaderWipe.cpp StartEntranceFaderWipe ✅ verified 1
src/engine/fader/StartExitFaderWipe.cpp StartExitFaderWipe ✅ verified 1
src/game/actors/daTrs_c/_ZN7daTrs_c16CleanupResourcesEv.cpp _ZN7daTrs_c16CleanupResourcesEv ✅ verified 1

The private worker commits a test merge, builds the stock ROM profile, compares every executable module, measures matched and source-built code, checks contributor lineage, and verifies affected relocations. The mod profile is opt-in and is not part of this merge gate.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Merge-tree check: main + #2725 + #2726 + #2727 is green.

Each of these PRs is gated alone, so green-alone does not imply green-merged. I built the three-way merge locally and ran the full set:

gate merge tree
rombuild --no-rom 106/106 exact, mismatching: 0, PASS (22 compiled)
tiers_ratchet --check PASS, CONVERTED 2812 to 2887 (+75)
check_decl_agreement no new disagreements; 76 banked ones disappear
eligible.py name set identical, 11243 / 11302
port_refcheck 423 references, 0 stale
MSVC port smoke build 230/230 from a clean configure, all five targets link

File sets are disjoint across all three pairs, so there is no textual conflict either.

One thing specifically checked, since it would have been invisible otherwise: #2727 carries a tu_manifest_resync renumber for ov023/daObjFm_Battan_c, and mwccarm uniquifier shifts compose additively. #2725 edits include/SharedFilePtr.h, which battan also includes, so the merged tree could have emitted a third number that neither branch names. It does not -- tu_manifest_resync --check reports the manifest in sync on the merge tree. Merge order does not matter here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4FxdKHG3a6hQRYRAbx29c

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Five-way merge tree gated (main + #2725 + #2726 + #2727 + #2728 + #2729)

Each of these five PRs is green on its own, but this repo has produced a red main from
two individually-green PRs before, so the set was gated as a merged tree rather than as
five branches.

Disjointness. 122 changed files across the five; all 10 pairs share zero files,
and the five branches merge onto 013370480 with no conflict.

Cold rebuild (rombuild.py -j 16 --no-rom --no-cache, all 8,760 enrolled files
compiled from scratch):

source-built functions: 11,206   reproducing: 11,206   mismatching: 0
module fidelity: 106/106 exact, 100.000000% of compared bytes
ROM-build analysis: PASS

The rebuild is deliberately cold. A warm run on this same tree reported
8759 reused from cache, 1 compiled — the objects were already built on the individual
branches and the cache is content-keyed, so the compile step exercised nothing. 0/1 compiled is this repo's classic false-green, so the numbers above come from a run with
no cache at all.

tools/tu_manifest_resync.py --check ov023/daObjFm_Battan_c reads in sync on the
merged tree — that is the manifest that shifted earlier in this batch when a shared
header gained declarations.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4FxdKHG3a6hQRYRAbx29c

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Adversarial review — seven-PR set (#2725#2731)

Re-reviewed as a critic rather than as the author. Every check below is new work, not a
restatement of the PR's own gate. Head SHAs reviewed: 2725 4f3927bd9, 2726 81040f19c,
2727 4c08d9a4d, 2728 00b15daef, 2729 fe61a984c, 2730 38aaa986b, 2731 e9b49b072;
merge tree a7f68c8b3 off 013370480.

1. Relocation destinations — the hazard the byte gate cannot see

A relocated word is a wildcard: a bl retargeted to a different but valid symbol still
byte-matches. Every verification run on these PRs so far used build_pin.verify without
the strict tuple, so none of them checked this. Re-ran all of them with
strict=(reloc_audit, name_index, config_relocs, sym_index), which resolves each relocation
and fails on WRONG-DEST:

PR files result
2725 26 26/26 OK
2726 42 42/42 OK
2727 27 + 4 promoted TUs (61 fns) 27/27 + 61/61 OK
2728 10 10/10 OK
2729 7 7/7 OK
2730 13 13/13 OK
2731 header-only n/a

0 WRONG-DEST across 125 files and 61 TU functions. This also closes the cast-receiver
question on #2726/#2728: ((X *)p)->X::M() cast to the wrong class would resolve to a
different symbol, and that is exactly what this check would have caught.

2. Merge tree, cold

mergetest/seven = all seven merged onto 013370480, 0 overlapping file pairs,
rombuild.py -j16 --no-rom --no-cache (the [3/6] line prints no cache line, so the
enrolled stage really did run cold):

reproducing: 11,206   mismatching: 0
source-owned data claims: 26  (reproducing 26, mismatching 0)
module fidelity: 106/106 exact, 100.000000% of compared bytes
ROM-build analysis: PASS

3. Change-kind census

139 files changed in the merge tree, all M — zero adds, deletes or renames. The whole
rename-hazard family (stranded port/ references, phantom file citations, stem collisions,
a stranded near-miss bank) is therefore structurally absent, not merely untriggered.

4. Local-only gates, run on the merge tree (CI runs none of these on a merge tree)

  • port_refcheck.py423/423 references resolve, 0 stale (manifests 249, cmake-symbols 21, hal-links 153).
  • check_decl_agreement.pyno new declaration disagreements; 80 banked ones are now gone.
  • langmode_audit.py --check (baseline from chaos-data) — PASS. local struct body 1676 → 1660.
  • check_decl_return_types.py — output byte-identical to origin/main, so its 3 conflicting
    symbols are pre-existing and not introduced here.

5. The merged decl baseline is not a silent clobber

config/decl-agreement-baseline.json takes edits from several of these branches and git merges
it line-wise, which can silently drop meaning. Regenerated it from scratch on the merge tree and
compared structurally against the merged copy:

merged symbols: 3891   regenerated: 3887
in MERGED but not regenerated (stale/banked, harmless):  4
in REGENERATED but not merged (a real regression the merge hid):  0

0 hidden. Every difference is in the safe direction. (Separately: ~84 stale banked entries
could be pruned — worth its own PR, not worth touching here, since a baseline edit would conflict
across all seven branches.)

6. The MSVC port build — differential, not absolute

main's port build is red and no CI job builds it, so "the port is red" proves nothing about
these PRs on its own. Made it a differential instead.

LNK2001: 106    LNK2019: 30    LNK2005: 22    LNK1120: 9      <- identical in both
distinct failing symbols: 8 vs 8, set difference in both directions: EMPTY
full LNK line sets: IDENTICAL

The other six PRs introduce zero new port link errors. What remains is the pre-existing
Model / ModelBase / ShadowModel Destructor0/Destructor1 C1/C2 collapse, which needs its
own port/hal change and is untouched by this set.

7. Per-PR findings

Verdict

No defect found, and no change requested. Both incomplete migrations are incomplete on
purpose and say so in their titles (#2725 converts 26 of ~920 references; #2727 lands the header
ahead of the bulk of the call-site conversion) — that is slicing, not a gap.

The one thing this review cannot supply is the thing the review gate actually wants: I am the
author of these PRs, so none of the above is independent. All seven still show
reviewDecision: <empty> and need a human to look at them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4FxdKHG3a6hQRYRAbx29c

@andrewboudreau
andrewboudreau merged commit a0fc674 into main Sep 18, 2026
13 of 15 checks passed
@andrewboudreau
andrewboudreau deleted the cpp/member-calls-2 branch September 18, 2026 05:16
andrewboudreau added a commit to lunavyqo/sm64ds-decomp that referenced this pull request Sep 18, 2026
Two conflicts, both resolved by hand.

src/game/actors/daTrs_c/_ZN7daTrs_c16CleanupResourcesEv.cpp (modify/delete):
this branch deletes the one-function file because the class TU
d_a_trs_cleanup_render.cpp absorbed the body; main's a0fc674 (tangosdev#2726)
had converted three mangled-symbol calls in it into real member calls.
Checked the absorbed copy before taking the delete: it already carries
that conversion, and carries it further -- dActor_c::FindWithID(),
mFoundActor->MarkForDestruction() through a typed dActor_c * member
instead of an fBase_c cast, and an unqualified UnloadCapModel()
inherited from dCapEnemy_c instead of a dCapEnemy_c cast. Nothing from
tangosdev#2726 is reverted, so the file is taken as deleted.

notes/tu-filename-reconstruction-pilot.md (content): main's 0fb9ed8
(tangosdev#2724) link-ified the pilot tables; this branch repointed the TERESA
and BOSS_TERESA rows at d_a_trs_factories.cpp. Kept main's links and
headings with this branch's targets. Main's spelling of those two rows
links at d_a_trs_teresa.cpp and d_a_trs_boss_teresa.cpp, which this
promotion deletes -- taking it wholesale fails check_dead_references.py
on the repo-rooted path inside the link text (verified by probe).

config/decl-agreement-baseline.json auto-merged without a swap: a
regenerated baseline is a strict subset of the merged one (+0 entries),
so nothing phantom was invented and nothing was laundered. The 28
entries added against main are the func_ov063_021166ac.cpp move, the
CleanupResources absorption, and the plurality flips that follow from
this branch retiring the Player::ShowMessage and dCapEnemy_c mangled
externs. The 97 now-stale banked rows are left alone: the gate is a
ratchet, not a cleanup, and reports them as "not a failure".

config/converted-baseline.json: 2813 entries, count field agrees, set
unique and sorted; the only change is OnPendingDestroy moving to the
"#" form inside the class TU.

Validation on the merged tree:
  rombuild.py -j16 --no-rom, cold cache: 8752 enrolled, 0 reused,
    8752 compiled; 11,206 source-built functions reproducing,
    0 mismatching; module fidelity 106/106 exact, 100.000000%;
    ROM-build analysis PASS
  check_decl_agreement.py: no new declaration disagreements
  check_dead_references.py: no new dead references, no broken links
  check_src_tu.py: every reference resolves
  check_tubuild_conflicts.py --list: every recorded conflict matches

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1Gdj5fTjMsW2VjRLpXxYA
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