Skip to content

TU: inline the destructors, then promote twenty more TUs - #1882

Closed
andrewboudreau wants to merge 3 commits into
tu/promote-batch-1from
tu/inline-dtor-order
Closed

TU: inline the destructors, then promote twenty more TUs#1882
andrewboudreau wants to merge 3 commits into
tu/promote-batch-1from
tu/inline-dtor-order

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Stacked on #1880. Review that one first.

The blocker was source form

#1880 said the remaining ready entries were blocked on one thing and it was
not tooling: their TUs define the destructor out of line, so mwcc emits
D2, D0, D1 while the ROM has D1 then D0 and no D2. Production
isolation places an object's .text sections into the spanning delink in
emission order, so that disagreement decides whether the range links.

Moving the empty body into the class body is the whole fix:

-    virtual ~Seaweed();
+    virtual ~Seaweed() {}
-Seaweed::~Seaweed()
-{
-}

That is the form the cartridge's own header must have had. It emits D1, D0,
in that order, first — and emits no D2, so ten hand-written
compiler-only_output dead-strip rows for homeless _ZN<C>D2Ev symbols come
out of the manifest at the same time.

Twenty TUs took it, and tools/tu_order_check.py (new) says so for each:
one compile, and it answers both questions together — do the licensed
functions still reproduce their ROM bytes, and are they emitted in ROM order.

What lands

  • 145 hand-placed structor files deleted. Twenty overlays each carried one
    file per mangled symbol under a // @symbol marker. The compiler places
    them now.

  • Five deadstrip-duplicate rows for _ZN7Vector3D1Ev, written by
    tu_order_check --write-policy. It writes only the two shapes with a
    mechanical justification — a homeless D2, and a vague-linkage duplicate with
    exactly one configured home — and reports anything else for a human, because
    the reason column is the point.

  • ChillBully+daIDonketu_c.cppChillBully_daIDonketu_c.cpp. src_tu/
    spells a multi-class TU with +; nothing ever links those, so the convention
    was safe right up to promotion. dsd writes bare unquoted object names into
    the LCF and mwldarm reads + as punctuation:

    mwldarm.exe: File not found: ChillBully
    mwldarm.exe: Expecting: (
    

    which fails the whole ROM, not one overlay. tu_promote now refuses a
    promoted_source outside [A-Za-z0-9_./-], so the next one costs a second.

Gates

module fidelity: 106/106 exact, 100.000000% of compared bytes
source-built functions: 11,065   reproducing: 11,065   mismatching: 0

ROM data went up, measured against this branch's base:

base here
symbols verified 376 378
symbols differing 56 54
gap bytes verified 29,816 30,064

layout_check clean · port_refcheck 400/400 · check_dead_references no new
· check_src_tu_compiles 88/88 · prepush_attribution 154 consolidated with
credit intact, 0 changed, 0 lost
· pytest tools/test_{objisolate,rombuild,romdata_check,tubuild}.py 96 passed.

Two things a reviewer should know

The middle commit does not build on its own. Inlining the destructor makes
the header's inline body collide with the legacy out-of-line _ZN<C>D1Ev.cpp
still sitting in src/, and the promotion commit is what deletes those. The
two are one change split for rename detection (the twenty git mvs record
R100 that way); the branch head is what is gated. Do not bisect through
c18849e8a expecting a build.

tools/test_tubuild.py was pinned end to end to ov045/PoleLift, which
this branch promotes out of src_tu/ entirely. Repointed at
ov002/daObjAbuku_c — same seven-function shape, still text-verified, still
7/7 contribution-equivalent, and blocked from promotion for an unrelated
reason (it owns delinked data), so it stays a valid subject. Its unlicensed
inventory count has now been three different numbers (15 when written, 12 once
measured, 6 for the new subject); the comment says to re-measure rather than
tune it.

Not in this batch

ov013/ClockPaintingPendulum took the inline change and then emitted no
destructor variants at all
— nothing in the TU odr-uses them. That is the
documented second condition (the TU must actually instantiate the class) and
is separate source work, so it is left out.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VregK5ZWRa2NbUcneaprG6

andrewboudreau and others added 3 commits August 27, 2026 14:27
Twenty text-verified TUs were blocked on one thing, and it was source form,
not tooling. Each defined its destructor OUT OF LINE:

    Seaweed::~Seaweed()
    {
    }

mwcc answers that form with `D2, D0, D1`. The cartridge has `D1` then `D0`
and no `D2` at all. Production isolation places an object's `.text` sections
into the spanning delink in emission order, so that disagreement is not
cosmetic -- it is the whole reason these entries could not be promoted.

Moving the empty body into the class body (`virtual ~Seaweed() {}`) is the
form the cartridge's own header must have had: it emits `D1, D0`, in that
order, first, and emits no `D2`. The homeless `_ZN<C>D2Ev` each of these was
carrying -- a symbol with no `symbols.txt` entry, dead-stripped only by an
explicit compiler-only policy row -- simply stops existing, so ten of those
policy rows come out here.

`tools/tu_order_check.py` compiles each TU once and reports both questions
together: does every licensed function still reproduce its ROM bytes, and are
they emitted in ROM order. All twenty answer ALL MATCH, ROM order.

Five of them re-emit `_ZN7Vector3D1Ev`, the deliberately empty inline
destructor `include/types.h` gives `Vector3`. That is vague linkage doing
exactly what it is supposed to, and the `deadstrip-duplicate` disposition
already models it: rombuild proves this object's copy against the cartridge's
bytes at `arm9:0x020072c0` before discarding it. `--write-policy` writes those
rows, and only the two shapes with a mechanical justification -- a homeless
D2, and a duplicate with exactly one configured home. Anything else it
reports and leaves for a human, because the reason column is the point.

ov013/ClockPaintingPendulum is left out: inlining its destructor made the TU
emit no destructor variants at all, because nothing in it odr-uses them. That
is the documented second condition -- the TU must actually instantiate the
class -- and it is a separate piece of source work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VregK5ZWRa2NbUcneaprG6
The compiler places these constructors and destructors now. Each of these
twenty overlays used to carry one file per mangled symbol -- 145 of them --
each holding a single function under a `// @symbol` marker, with production
isolation keeping exactly one variant out of whatever mwcc emitted alongside.
That whole apparatus exists because the reconstructed TU could not be linked
as a TU. Now it can be: the previous commit gave each of these the source form
that emits `D1, D0` in the cartridge's order, so the spanning delink entry
lines up and the per-function files have nothing left to do.

    src/actors/Seaweed.cpp:
        complete
        .text start:0x0208d4c8 end:0x0208d7bc

replaces eight entries, and the eight files behind them are gone.

`ChillBully+daIDonketu_c.cpp` became `ChillBully_daIDonketu_c.cpp` on the way
in. src_tu/ spells a multi-class TU with `+`, and nothing ever links those,
so the convention was safe right up to promotion -- dsd writes bare, unquoted
object names into the linker command file and mwldarm's parser reads `+` as
punctuation:

    mwldarm.exe: File not found: ChillBully
    mwldarm.exe: Expecting: (

which fails the entire ROM, not one overlay. tu_promote now refuses a
promoted_source outside `[A-Za-z0-9_./-]` so the next one costs a second
rather than a link.

    module fidelity: 106/106 exact, 100.000000% of compared bytes
    source-built functions: 11,065  reproducing: 11,065  mismatching: 0

145 attribution overrides carry each absorbed symbol's authors onto the TU
that now holds it, so the consolidation reads as consolidated, not lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VregK5ZWRa2NbUcneaprG6
145 files stopped existing, so everything naming one had to move with them.

`port/slice_gate9.txt` listed ArrowSignRight's five per-function objects plus
its spawn; they are one file now, so the slice names that file once. The gate
still describes the same slice -- a real actor spawning through its factory
and dispatching through its vtable -- with fewer moving parts.

tools/test_tubuild.py was pinned end to end to ov045/PoleLift, which this
branch promoted out of src_tu/ entirely, so four of its cases were asserting
against a file that no longer exists. Repointed at ov002/daObjAbuku_c: the
same seven-function shape, still text-verified, still 7/7 contribution-
equivalent, and blocked from promotion for an unrelated reason (it owns
delinked data), so it will stay a valid subject for a while.

Two of its numbers were pinned to whatever PoleLift happened to emit. The
unlicensed inventory read 15 when it was written, 12 once someone measured
it, and 6 for the new subject -- three different answers to a question about
one TU's incidental output. The comment now says so and says to re-measure
rather than tune.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VregK5ZWRa2NbUcneaprG6
@tangos-validator

tangos-validator Bot commented Aug 27, 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 10,874 / 11,347 (95.83%, -125)
Byte-verified code bytes 2,029,376 / 2,211,124 (91.78%, -12,344)
Claimed, not byte-verified 325 functions, 72,004 bytes (+125)
Perfect source moves 0 R100
Enrolled ranges (delinks complete) 11,036 functions, 2,047,728 bytes (92.61%, +0) -- differs from byte-verified by +162
Contributor credit 0 added, 0 changed, 0 lost
Relocation check 145 checked; 145 VERIFIED
Port reference check 400 checked; 0 stale
Module fidelity 106/106 exact; 100.000000% compared bytes
Code linked from verified source 11,065 functions, 2,061,728 bytes (93.24%)
Module bytes from source 2,061,728 / 3,049,600 (67.6%); 811,492 (26.6%) are data no delink entry reaches
ROM data reproduced from source 378 symbol(s) exact, 206 partial, 54 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 325 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,199 this project calls matched.

Warnings: 145 address range(s) left the byte-verified set while enrolled totals held steady: ov002:0x020b05d0-0x020b0600, ov002:0x020b0600-0x020b0644, ov002:0x020b0644-0x020b064c, ov002:0x020b064c-0x020b0650, ov002:0x020b0650-0x020b0658, +140 more; 125 more function(s) now claim a match that nothing compiles; enroll them in a delinks.txt to have the ROM build check them.

Per-file link-check detail

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

File Symbol Result Slots checked
src/actors/AmbientSoundEffects.cpp _ZN19AmbientSoundEffectsD1Ev + _ZN19AmbientSoundEffectsD0Ev + _ZN19AmbientSoundEffects16CleanupResourcesEv + _ZN19AmbientSoundEffects16OnPendingDestroyEv + _ZN19AmbientSoundEffects6RenderEv + _ZN19AmbientSoundEffects8BehaviorEv + _ZN19AmbientSoundEffects13InitResourcesEv + AmbientSoundEffects_Spawn ✅ verified 8
src/actors/ArrowSignRight.cpp _ZN14ArrowSignRightD1Ev + _ZN14ArrowSignRightD0Ev + func_ov098_02137c8c + _ZN14ArrowSignRight4KillEv + _ZN14ArrowSignRight11OnAttacked1ER8dActor_c + _ZN14ArrowSignRight15OnHitByMegaCharER6Player + _ZN14ArrowSignRight16CleanupResourcesEv + _ZN14ArrowSignRight6RenderEv + _ZN14ArrowSignRight8BehaviorEv + _ZN14ArrowSignRight13InitResourcesEv + ArrowSignRight_Spawn ✅ verified 11
src/actors/BasementWater.cpp _ZN13BasementWaterD1Ev + _ZN13BasementWaterD0Ev + _ZN13BasementWater16CleanupResourcesEv + _ZN13BasementWater6RenderEv + _ZN13BasementWater8BehaviorEv + _ZN13BasementWater13InitResourcesEv + BasementWater_Spawn ✅ verified 7
src/actors/BlueCoinSwitch.cpp _ZN14BlueCoinSwitchD1Ev + _ZN14BlueCoinSwitchD0Ev + _ZN14BlueCoinSwitch16CleanupResourcesEv + _ZN14BlueCoinSwitch6RenderEv + _ZN14BlueCoinSwitch8BehaviorEv + _ZN14BlueCoinSwitch13InitResourcesEv + func_ov002_020f1578 + func_ov002_020f15b8 + BlueCoinSwitch_Spawn ✅ verified 9
src/actors/CameraTag.cpp _ZN9CameraTagD1Ev + _ZN9CameraTagD0Ev + _ZN9CameraTag16CleanupResourcesEv + _ZN9CameraTag16OnPendingDestroyEv + _ZN9CameraTag6RenderEv + _ZN9CameraTag8BehaviorEv + _ZN9CameraTag13InitResourcesEv + CameraTag_Spawn ✅ verified 8
src/actors/ChillBully_daIDonketu_c.cpp _ZN10ChillBullyD1Ev + _ZN10ChillBullyD0Ev + func_ov027_02111680 + func_ov027_021116f0 + _ZN10ChillBully8BehaviorEv + _ZN10ChillBully13InitResourcesEv + ChillBully_Spawn ✅ verified 7
src/actors/Exit.cpp _ZN4ExitD1Ev + _ZN4ExitD0Ev + func_ov002_020b0a0c + _ZN4Exit16CleanupResourcesEv + _ZN4Exit16OnPendingDestroyEv + _ZN4Exit6RenderEv + _ZN4Exit8BehaviorEv + _ZN4Exit13InitResourcesEv + Exit_Spawn ✅ verified 9
src/actors/InvisiblePole.cpp _ZN13InvisiblePoleD1Ev + _ZN13InvisiblePoleD0Ev + _ZN13InvisiblePole16CleanupResourcesEv + _ZN13InvisiblePole16OnPendingDestroyEv + _ZN13InvisiblePole6RenderEv + _ZN13InvisiblePole8BehaviorEv + _ZN13InvisiblePole13InitResourcesEv + InvisiblePole_Spawn ✅ verified 8
src/actors/OneUpLogo.cpp _ZN9OneUpLogoD1Ev + _ZN9OneUpLogoD0Ev + func_ov002_020f0e54 + _ZN9OneUpLogo16CleanupResourcesEv + _ZN9OneUpLogo6RenderEv + _ZN9OneUpLogo8BehaviorEv + _ZN9OneUpLogo13InitResourcesEv + OneUpLogo_Spawn ✅ verified 8
src/actors/PoleLift.cpp _ZN8PoleLiftD1Ev + _ZN8PoleLiftD0Ev + _ZN8PoleLift16CleanupResourcesEv + _ZN8PoleLift6RenderEv + _ZN8PoleLift8BehaviorEv + _ZN8PoleLift13InitResourcesEv + PoleLift_Spawn ✅ verified 7
src/actors/PowerStarCreate.cpp _ZN15PowerStarCreateD1Ev + _ZN15PowerStarCreateD0Ev + _ZN15PowerStarCreate8BehaviorEv + PowerStarCreate_Spawn ✅ verified 4
src/actors/Seaweed.cpp _ZN7SeaweedD1Ev + _ZN7SeaweedD0Ev + func_ov002_020bc488 + _ZN7Seaweed16CleanupResourcesEv + _ZN7Seaweed6RenderEv + _ZN7Seaweed8BehaviorEv + _ZN7Seaweed13InitResourcesEv + Seaweed_Spawn ✅ verified 8
src/actors/ShipWater.cpp _ZN9ShipWaterD1Ev + _ZN9ShipWaterD0Ev + _ZN9ShipWater16CleanupResourcesEv + _ZN9ShipWater6RenderEv + _ZN9ShipWater8BehaviorEv + _ZN9ShipWater13InitResourcesEv + ShipWater_Spawn ✅ verified 7
src/actors/SoundObject.cpp _ZN11SoundObjectD1Ev + _ZN11SoundObjectD0Ev + func_ov002_020f93a8 + func_ov002_020f9468 + _ZN11SoundObject8BehaviorEv + _ZN11SoundObject13InitResourcesEv + SoundObject_Spawn ✅ verified 7
src/actors/SwitchPillar.cpp _ZN12SwitchPillarD1Ev + _ZN12SwitchPillarD0Ev + _ZN12SwitchPillar15OnGroundPoundedER8dActor_c + _ZN12SwitchPillar16CleanupResourcesEv + _ZN12SwitchPillar6RenderEv + _ZN12SwitchPillar8BehaviorEv + _ZN12SwitchPillar13InitResourcesEv + SwitchPillar_Spawn ✅ verified 8
src/actors/VirtualDoor.cpp _ZN11VirtualDoorD1Ev + _ZN11VirtualDoorD0Ev + _ZN11VirtualDoor16CleanupResourcesEv + _ZN11VirtualDoor16OnPendingDestroyEv + _ZN11VirtualDoor6RenderEv + _ZN11VirtualDoor8BehaviorEv + _ZN11VirtualDoor13InitResourcesEv + VirtualDoor_Spawn ✅ verified 8
src/actors/WaterfallMist.cpp _ZN13WaterfallMistD1Ev + _ZN13WaterfallMistD0Ev + _ZN13WaterfallMist8BehaviorEv + _ZN13WaterfallMist13InitResourcesEv + WaterfallMist_Spawn ✅ verified 5
src/actors/daObjCannonShutter_c.cpp _ZN20daObjCannonShutter_cD1Ev + _ZN20daObjCannonShutter_cD0Ev + func_ov002_020bc990 + _ZN20daObjCannonShutter_c16CleanupResourcesEv + _ZN20daObjCannonShutter_c6RenderEv + _ZN20daObjCannonShutter_c8BehaviorEv + _ZN20daObjCannonShutter_c13InitResourcesEv + daObjCannonShutter_c_Spawn ✅ verified 8
src/actors/daObjFl_Fall_Block_c.cpp _ZN20daObjFl_Fall_Block_cD1Ev + _ZN20daObjFl_Fall_Block_cD0Ev + _ZN20daObjFl_Fall_Block_c16CleanupResourcesEv + _ZN20daObjFl_Fall_Block_c13InitResourcesEv ✅ verified 4
src/actors/daObjKm3_Kuruma_c.cpp _ZN17daObjKm3_Kuruma_cD1Ev + _ZN17daObjKm3_Kuruma_cD0Ev + _ZN17daObjKm3_Kuruma_c16CleanupResourcesEv + _ZN17daObjKm3_Kuruma_c13InitResourcesEv ✅ verified 4

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 added a commit that referenced this pull request Aug 28, 2026
…he class

Twenty-five reconstructed translation units become canonical here, absorbing
346 legacy one-function sources. `tools/rombuild.py -j16 --no-rom` reports
106/106 exact, 11,065 functions reproducing, 0 mismatching.

WHY THESE TWENTY-FIVE NEEDED AN EDIT FIRST

`tools/tu_order_check.py` across the 51 text-verified entries said "text-
verified" is not "promotable": 14 were clean, 34 emitted `D2, D0, D1` where
the ROM has `D1` then `D0` and no `D2` at all, 2 emitted extra symbols and 1
emitted nothing. The 34 all shared one cause -- an OUT-OF-LINE empty
destructor body, which makes mwcc emit the D2 base variant. Moving the empty
body into the class declaration (`virtual ~C() {}`) is the form #1882
established as period-accurate, and it emits `D1, D0` in ROM order with no
D2. All 34 were fixed by that one edit; zero still emit the wrong order.

Nine did not survive the rest of the gate and are NOT in this commit:

  * Eight hit #1882's documented second condition -- nothing in the TU
    odr-uses the destructor, so with the body inline the TU emits none at
    all: Flag, LightBeam, Trap, ClockPaintingPendulum, IceSlideManager,
    RacingPenguin, RotatingPlatformWdw, KoopaFlag. Reverted.
  * ov014/ChainChomp is refused for a different, genuine reason: a surviving
    .text section references the compiler-only `_ZN7Vector3D1Ev` at 0xa0, so
    that duplicate cannot be dropped. Left text-verified.

Three obsolete `_ZN<C>D2Ev` policy rows come out (HealingHeart, Number,
WingFeather): isolation refuses a policy for a symbol the object no longer
defines. WingFeather's `_ZN7Vector3D1Ev` row claimed `deadstrip`, which was
a latent false claim -- the symbol HAS a ROM home at arm9:0x020072c0, so it
is `deadstrip-duplicate` with that home named.

FOUR TUs CARRIED STALE SPELLINGS AND WOULD NOT LINK

The shadow sources predate renames that landed on main, so promoting them
broke the link on four undefined symbols. Each is repointed at the name the
tree uses now, which is also the more readable form:

  * daObjRc_Dorifu_c, daObjKm1_Dorifu_c, RickshawPlatformBs -- the two
    `func_ov002_020b4b6c` / `func_ov002_020b4d58` helpers became
    `daObjDorifu_c::CleanupResources` / `::InitResources` in #1832. The
    hand-rolled `ResourceDescriptor` shim and its `extern "C"` block are
    gone; each file now declares `extern daObjDorifuResources data_...[5]`
    and calls the base method, matching the per-function sources it absorbs.
  * Chuckya -- `func_ov002_020ada40` became
    `_ZN12dEnemyBase_c20KillByInvincibleCharERK10Vector3_16R6Player5Fix12IiE`
    in #1839 and `data_02082128` is `IDENTITY_MATRIX4X3`. Its local
    declaration of the first is dropped: `include/decl_Enemy.h`, already
    included, declares it, and two extern "C" declarations with different
    parameter types is `illegal function overloading`.

THREE PATH-KEYED GATES, INCLUDING TWO THIS BRANCH WAS ALREADY RED ON

An agent review of the previous commit found two CI gates that a promotion
turns red and that commit did not mention. Both are addressed here for the
whole branch, not just this batch:

  * dead references: promotion deletes the paths prose names. 36 references
    across 11 files are repointed at the owning TU (the `src/actors/*.cpp`
    that now contains the code); `port/docs/mmio-inventory.md`'s Squasher
    line number is corrected to its new line. Two docstrings in
    `tools/check_src_tu*.py` are DELIBERATELY historical -- they describe
    what was true when a past PR landed -- so they are baselined instead of
    rewritten, which is what `--update` is for.
  * converted ratchet: 278 banked paths are removed and 372 added
    (1958 -> 2052). Only ~80 of the removals are this branch's; ~99 were
    already red at the branch base and 4 on main. Every removed path lives
    on inside its owning TU, which 106/106 with mismatching 0 proves.
  * langmode: `cv_launder` rises 3 -> 4, `cv_launder_sites` 6 -> 7. The
    audit enumerates `src/` only, so promoting a TU moves its codegen levers
    from unscanned into scanned territory. The single new site is
    `src/actors/ActorBase.cpp:283`, `volatile int *p = data_02099f24;`,
    which arrived by `git mv` in the previous commit and is load-bearing for
    that function's bytes. The root `langmode-baseline.json` override is
    refreshed; every other count in it FALLS (launder_or_forced 213 -> 212,
    extern_vtable 347 -> 169, pad_layout 614 -> 523), so this is a net
    tightening.

The seven remaining "Manually curated shadow translation unit." banners are
corrected -- five of them from the previous commit, which this pass missed
because its regexes did not cover that wording.

WHAT THE NUMBERS DID

`port_refcheck` stays at 385, all resolving. The ROM-data headline moves
373/202/53 -> 376/202/50, and a per-symbol `--data-json` diff against the
previous commit shows ZERO symbols worse, ZERO better, ZERO gained and ZERO
lost: the record count falls 7,054 -> 6,795 purely because consolidation
removes duplicate per-function records for the same symbol. This is the
metric that has misled this project before, so it was diffed rather than
read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HiJPTehCABiu3NQUCz7uDq
andrewboudreau added a commit that referenced this pull request Aug 30, 2026
…ns as deletions (#2007)

Two defects in the CONVERTED-tier gate, both of the same species: the gate
counted something other than what it says it counts.

1. tools/tiers.py scored hardware registers as match hacks

`no_codegen_trick` ORed LAUNDER, VOLATILE and ASM, and VOLATILE was a bare
`\bvolatile\b`. On a Nintendo DS the only way to reach VRAM, the geometry
engine or the IPC/DMA/divider registers is a volatile-qualified pointer, so
the criterion failed the code that had no alternative:

  src/_ZN8dScene_c22ResetHardwareRegistersEv.cpp   74 hits, all 0x0400xxxx
  src/_ZN2GX13SetBankForTexEt.cpp                  25 hits, all VRAM banks
  src/_ZN3G2x12SetBGyAffineEPVtP9Matrix2x2iiii.cpp the block is a PARAMETER

A reconstructed TU absorbing any of those inherits the failure whole, so it
compounds with the TU work rather than sitting still.

The shapes are separable by WHAT is volatile-qualified. MMIO qualifies the
pointed-to type, so a `*` follows it. A match hack qualifies an OBJECT --
`volatile int li;`, `volatile Vector3 v;`, the `(s32)(volatile s32)rsc`
round-trip, `Node *volatile arr[4]` where the pointer not the pointee is
volatile.

  VOLATILE = re.compile(r"\bvolatile\b(?![\s\w:]*\*)")

Files scoring a codegen trick: 655 -> 254. All 401 released were checked to be
MMIO-only, and the 254 kept still contain every match-hack form -- the negative
direction was measured, not assumed. CONVERTED 2,511 -> 2,568 functions
(22.20% -> 22.71%); no_codegen_trick 10,623 -> 11,020. Additions only.

2. tools/tiers_ratchet.py reported a TU promotion as a vanished file

A promotion consolidates N per-symbol src/_ZN....cpp files into the one
src/actors/X.cpp they always were; git records N deletions plus one addition,
and every one read as `GONE -- not a tracked source file any more`. Measured on
PR #1882 (tu/inline-dtor-order, 9c6396c): 90 of 90 backslid paths were TU
`legacy_source` entries whose TU is "status": "promoted" and whose
`promoted_source` exists on the branch. Zero were real deletions.

A GONE path is now resolved through the manifest (via tools/tu_manifest.py,
never the files) and reported as a MOVE naming the absorbing file and what that
file does with the five criteria.

A promotion is NOT free. The criteria are file-wide, so a clean function merged
into a file with one bad line loses its status, and that still exits 1. Only a
move into a file that itself passes all five is silent. In practice a promotion
lands in the failing case by construction -- a reconstructed TU must spell
_ZN7fBase_cnwEj, _ZN8dActor_cC2Ev and _ZN8dActor_cD2Ev directly or its range
will not link -- so no_mangled_refs can never pass for one. That is structural;
the answer is --update --reason, not exempting mangled refs.

Re-banked config/converted-baseline.json in the same commit: 1,957 -> 2,567,
+610 / -0. Set-diffed against HEAD to confirm zero removals.

tools/test_tiers.py pins both readings in both directions (29 tests) and
converted-ratchet.yml now runs it, plus watches tu_manifest.py and
config/tu_manifest.d/**.


Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Triage: leaving this open, but it is the weakest case in the chain.

This PR promotes 20 TUs. #1993 (tu/restack-inline-dtors) covers 19 of them:

AmbientSoundEffects, ArrowSignRight, BasementWater, BlueCoinSwitch, CameraTag, ChillBully (this branch spells the TU ChillBully+daIDonketu_c; main renamed it to ChillBully.cpp — same TU), Exit, InvisiblePole, OneUpLogo, PowerStarCreate, Seaweed, ShipWater, SoundObject, SwitchPillar, VirtualDoor, WaterfallMist, daObjCannonShutter_c, daObjFl_Fall_Block_c, daObjKm3_Kuruma_c.

The one it does not cover is src_tu/actors/PoleLift.cpp -> src/actors/PoleLift.cpp (config/tu_manifest.d/ov045/PoleLift.json), and that is a deliberate exclusion, not an oversight. #1993 commit d3cb31f4c removed it:

PoleLift is tools/test_tubuild.py's permanent pilot fixture -- four of its tests assert against src_tu/actors/PoleLift.cpp by path, and one asserts the literal git mv src_tu/actors/PoleLift.cpp the promote command prints. Promoting it consumes the fixture, and the PR validator restores all of tools/ from base, so no PR can repair those tests in the same change that breaks them.

Note the second half of that: the same validator constraint that blocked #1993 also blocks this PR from promoting PoleLift. This branch cannot land its one unique promotion either.

Also note include/PoleLift.h: the inline ~PoleLift() and the promotion are one atomic edit here. Un-promoting without reverting the header leaves src/_ZN8PoleLiftD1Ev.cpp failing to compile with object 'PoleLift::~PoleLift()' redefined.

Left open only because it is technically the sole carrier of the PoleLift promotion. This is a valid candidate to close once #1993 landsPoleLift should be promoted in its own PR, after or together with a tools/test_tubuild.py change that repoints the four path assertions at a different fixture. Do not rebase this branch; regenerate.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Gatekeeper review — keep open, and the blocker on your one uncovered promotion has just been lifted.

Status: this PR is not superseded by #1993. I checked the coverage rather than assuming it. The #1880#1882#1884 chain promotes 66 TUs against #1993's 26, and five promotions are uncovered by anything else in the queue: ov029/ArrowLift and arm9/Scene (#1880), ov045/PoleLift (#1882), ov002/Platform and ov002/Warp (#1884).

The PoleLift finding. #1993 deliberately dropped PoleLift in d3cb31f4c, on this reasoning: it is tools/test_tubuild.py's permanent pilot fixture — four tests assert against src_tu/actors/PoleLift.cpp by path, and one asserts the literal git mv src_tu/actors/PoleLift.cpp that the promote command prints — and because the validator restored all of tools/ from base, no PR could both consume the fixture and repair the tests. That made PoleLift structurally unpromotable.

That is no longer true as of today. fa5aa1f65 ("Validation now runs the pull request's own tools/", #1994) reversed it. The workflow comment now states verbatim that "the worker DOES run this branch's own tools/, deliberately, so that a pull request can exercise its own tool change", and grepping .github/workflows/ for any checkout/restore of tools returns nothing — the base-restore step is gone from the repo entirely.

So a single PR can now promote PoleLift and update the four fixture assertions in the same change. Please verify that empirically before betting the branch on it — #1994's own diff is only the comment, so the executable half lives in the self-hosted worker outside this repo, and the title's "now" is a claim I could not confirm from the repo alone.

What I need before this can merge, in order:

  1. Rebase/merge current origin/main (bfd908b32). Two things moved under you: Spell the TU-promotion metavariable as <Class>, unbreaking main's references gate #2010 fixed a dead-reference red, and Re-bank the two daObjAbuku_c paths #1996 absorbed, and check the ratchet on main #2011 re-banked the two daObjAbuku_c paths that TU: promote daObjAbuku_c as genuine C++ #1996 absorbed.
  2. Resolve config/converted-baseline.json by regeneration, not key union. And note a correction to guidance I issued earlier: tiers_ratchet.py is a set ratchet — --check computes missing = banked - current and the printed count is informational. There is no count floor to hold, and a promotion batch legitimately lowers the count. The real gate is (a) every removed path classified MOVED -- absorbed into <file> by TU promotion (<tu_id>), and (b) every named absorbing file exists and is tracked.
  3. Confirm no delinks range is orphaned: every promoted manifest entry needs an existing tracked promoted_source and exactly one complete .text entry. This is the one failure mode the pipeline cannot self-detect — a range that stops being named complete gets refilled by dsd with retail cartridge bytes while the ROM still byte-matches and every gate stays green.

Do not run rombuild.py or eligible.pybuild/ is shared across worktrees and concurrent runs produce fake failures.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Status note, so nobody spends a validator slot chasing this. The red ratchet on this PR is a stale run, not a live regression:

FAILURE   converted ratchet   ratchet   2026-08-27T20:00

That is three days old. It predates #2007's re-bank, #2011, and today's merges (#1991, #1978, #2000, #1985). gh pr checks shows two checks both named ratchetlangmode ratchet (passing) and converted ratchet (this one) — so the bare name is ambiguous; read the workflow column.

This is almost certainly the base-desync shape rather than a real backslide: a PR cut before a re-bank merges against a baseline that predates it and inherits the red. The fix for that is to merge main in, never to lower the pin. I am not doing that yet, deliberately — see below.

Two corrections to what I told people earlier, both mine and both wrong:

  • The converted ratchet is a SET ratchet, not a count ratchet. --check computes missing = banked - current and fails only when a banked path leaves the set. The printed count is informational and there is no floor number to defend — a TU-promotion batch is expected to lower it. I circulated "the floor is 2565, never go below it" to several agents; it was wrong in both the number and the concept. Correct two stale claims in tiers_ratchet.py's docstring #2014 fixes the docstring that said otherwise.
  • The validator no longer restores tools/ from baseValidation now runs the pull request's own tools/ #1994 reversed that this morning. A PR now exercises its own tool change.

Why this is parked rather than refreshed

#1993 is the restack of this chain, and it is the queue chokepoint: it regenerates config/converted-baseline.json wholesale, so it must be the last thing to touch that file. Refreshing this PR now would (a) consume a slot on the serial validator that #1993 and the ov070 stack need, and (b) produce a green that #1993 immediately invalidates.

Order I am walking: #2002#2005#1995#1993, then re-assess what of this chain is still unlanded. Current signal is that the remainder is smaller than the diffstat suggests — a parallel review of #1914 found all 37 of its uniquely-promoted sources already present on origin/main under src_tu/, with the real harvest being a few dozen header inline-destructor flips. I want #1993 landed and measured before deciding whether this is a re-cut or a merge.

Nothing here is closed and nothing is being discarded. Coverage on main is holding at 2,066,772 B built from source, 0 bytes handed back to the cartridge, and I will re-check it after every merge.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Closing — see #1880 for the full reasoning. This PR sits on #1880's rejected plan_many
mechanism (RTTI sections dropped uncompared), so it cannot land without it.

Two things specific to this PR:

1. It reintroduces coined names main has already recovered. Main superseded this TU in
#1971 (744ddb5b2 Recover typed factories and Bully virtual hooks):

main ov027/ChillBully.json this branch ov027/ChillBully+daIDonketu_c.json
id ov027/ChillBully ov027/ChillBully+daIDonketu_c
funcs …14UpdateRunStateEv, …16UpdateDeathStateEv func_ov027_02111680, func_ov027_021116f0

Merging this resurrects two func_ov027_* names, puts a + back in a manifest id (which the
LCF cannot carry in a filename), and leaves two manifest files for one TU. That is a naming
regression, not a promotion.

2. The converted ratchet failure is genuine and gets worse on refresh. CI reports
baseline 1958 current 2260 (+392 gained, -90 lost). All 90 backslides are GONE — banked
one-function src/_ZN…cpp files consolidated into the promoted TUs — with zero criterion
failures. Control: origin/main alone has 0 GONE against its own baseline, and simulating
the refresh via git merge-tree gives 101, because main's larger baseline banks additional
one-function files this branch also deletes. The remedy exists on main and postdates this PR:
tiers_ratchet.py --update plus a line in config/converted-backslide-exceptions.jsonl (which
has grown 13 → 29 entries, the newest being exactly this case for #1996).

The destructor-inlining insight is sound and worth redoing from current main — the 11 removed
_ZN…D2Ev plain-deadstrip rows are benign for exactly the reason you give (c18849e8a
inlines those destructors so the D2 is no longer emitted).

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