Skip to content

Promote daObjHatenaSwitch_c to a genuine C++ translation unit - #2062

Merged
andrewboudreau merged 4 commits into
mainfrom
cpp/hatenaswitch-genuine-tu
Sep 1, 2026
Merged

Promote daObjHatenaSwitch_c to a genuine C++ translation unit#2062
andrewboudreau merged 4 commits into
mainfrom
cpp/hatenaswitch-genuine-tu

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Promotes the 13-function ov002 HatenaSwitch packet into one genuine daObjHatenaSwitch_c C++ translation unit using the ROM-proven RTTI class identity.

The compiler now owns D1, D0, RTTI, ZTS, and the vtable; it emits no D2 or forcing helper. QuestionSwitch_Spawn and SpawnInfo remain narrow evidence-bounded C ABI aliases because the original factory spelling does not survive and natural new selects the wrong allocator.

Validation:

  • 13/13 functions and 2,140/2,140 text bytes exact
  • 200/200 class-data bytes and 37 relocations exact
  • normal production ROM: 11,088/11,088 functions, 106/106 modules
  • stock SHA-256: d1506e90efae5e2d2cf119926a4ac2a291bd5ca78349d09d5024e1a918c478e8
  • conversion/name/TU-state/port/header-consumer gates pass
  • attribution override is expected for six legacy coined QuestionSwitch symbol identities

@tangos-validator

tangos-validator Bot commented Aug 31, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

Committed merge passes; 7 contributor credit reassignment(s) noted, not a blocker.

Full merge validation

Check Result
Committed test merge yes
Byte-verified functions 10,765 / 11,347 (94.87%, -12)
Byte-verified code bytes 2,002,192 / 2,211,124 (90.55%, -2,032)
Claimed, not byte-verified 452 functions, 103,488 bytes (+12)
Perfect source moves 0 R100
Enrolled ranges (delinks complete) 11,059 functions, 2,053,148 bytes (92.86%, +0) -- differs from byte-verified by +294
Contributor credit 0 added, 7 changed, 0 lost
Relocation check 18 checked; 18 VERIFIED
Port reference check 405 checked; 0 stale
Module fidelity 106/106 exact; 100.000000% compared bytes
Code linked from verified source 11,088 functions, 2,067,148 bytes (93.49%)
Module bytes from source 2,067,148 / 3,049,600 (67.8%); 811,492 (26.6%) are data no delink entry reaches
ROM data reproduced from source 531 symbol(s) exact, 192 partial, 6 differ

Contributor credit moved (7)

Function Source Before After
ov002:0x020b4ed8 src/_ZN14QuestionSwitchD1Ev.cpp -> src/actors/daObjHatenaSwitch_c.cpp tangosdev github-actions[bot]
ov002:0x020b4f44 src/_ZN14QuestionSwitchD0Ev.cpp -> src/actors/daObjHatenaSwitch_c.cpp tangosdev github-actions[bot]
ov002:0x020b512c src/_ZN14QuestionSwitch16CleanupResourcesEv.cpp -> src/actors/daObjHatenaSwitch_c.cpp tangosdev github-actions[bot]
ov002:0x020b51ac src/_ZN14QuestionSwitch6RenderEv.cpp -> src/actors/daObjHatenaSwitch_c.cpp tangosdev github-actions[bot]
ov002:0x020b51dc src/_ZN14QuestionSwitch8BehaviorEv.cpp -> src/actors/daObjHatenaSwitch_c.cpp tangosdev github-actions[bot]
ov002:0x020b5500 src/_ZN14QuestionSwitch13InitResourcesEv.cpp -> src/actors/daObjHatenaSwitch_c.cpp tangosdev github-actions[bot]
ov002:0x020b56d8 src/QuestionSwitch_Spawn.c -> src/actors/daObjHatenaSwitch_c.cpp tangosdev github-actions[bot]

Byte-verified means the range carries complete in a delinks.txt, so the ROM build compiled it and compared it to the cartridge. The 452 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,217 this project calls matched.

Warnings: contributor attribution changed, not a blocker (7 changed, 0 lost -- src/actors/daObjHatenaSwitch_c.cpp: tangosdev -> github-actions[bot]; src/actors/daObjHatenaSwitch_c.cpp: tangosdev -> github-actions[bot]; src/actors/daObjHatenaSwitch_c.cpp: tangosdev -> github-actions[bot]; +4 more); 13 address range(s) left the byte-verified set while enrolled totals held steady: ov002:0x020b4ed8-0x020b4f44, ov002:0x020b4f44-0x020b4fc4, ov002:0x020b4fc4-0x020b4fd0, ov002:0x020b4fd0-0x020b503c, ov002:0x020b503c-0x020b50a0, +8 more; 12 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 6 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.

File Symbol Result Slots checked
src/_ZN15RecRoomCupboard13InitResourcesEv.cpp _ZN15RecRoomCupboard13InitResourcesEv ✅ verified 1
src/_ZN15RecRoomCupboard16CleanupResourcesEv.cpp _ZN15RecRoomCupboard16CleanupResourcesEv ✅ verified 1
src/_ZN15RecRoomCupboard8BehaviorEv.cpp _ZN15RecRoomCupboard8BehaviorEv ✅ verified 1
src/_ZN15RecRoomCupboardD0Ev.cpp _ZN15RecRoomCupboardD0Ev ✅ verified 1
src/_ZN15RecRoomCupboardD1Ev.cpp _ZN15RecRoomCupboardD1Ev ✅ verified 1
src/actors/daObjHatenaSwitch_c.cpp _ZN19daObjHatenaSwitch_cD1Ev + _ZN19daObjHatenaSwitch_cD0Ev + _ZN19daObjHatenaSwitch_c15OnGroundPoundedER8dActor_c + _ZN19daObjHatenaSwitch_c15UpdateClsnStateEv + _ZN19daObjHatenaSwitch_c19UpdateClsnTransformEv + _ZN19daObjHatenaSwitch_c20UpdateModelTransformEv + _ZN19daObjHatenaSwitch_c16CleanupResourcesEv + _ZN19daObjHatenaSwitch_c6RenderEv + _ZN19daObjHatenaSwitch_c8BehaviorEv + _ZN19daObjHatenaSwitch_c13InitResourcesEv + _ZN19daObjHatenaSwitch_c10HandleClsnER8dActor_c + _ZN19daObjHatenaSwitch_c17AfterClsnCallbackEP4dBgWP8dActor_cS3_ + QuestionSwitch_Spawn ✅ verified 13

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 andrewboudreau added the attribution-override Maintainer accepts this PR's contributor-credit changes; validation reports them as warnings label Aug 31, 2026
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Conflict diagnosed: one file, and it's the non-goal one. Here's the exact resolution.

This went DIRTY because main moved under it, not because anything is wrong with the work. Measured at head 848880379, merge-base c804e96f6, current main 71153e082.

What actually conflicts

attribution.json                              CONFLICT (content)
config/converted-baseline.json                auto-merged clean
config/converted-backslide-exceptions.jsonl   auto-merged clean

One file, and it's attribution.json — which is a stated non-goal in this repo. Every substantive file in this PR (src/, include/, config/arm9/, config/tu_manifest.d/, the notes, symbols/actor_renames.tsv) merges cleanly. So the byte-relevant content of this PR is not in dispute at all.

What landed under you

How to resolve attribution.json — and how not to

Resolve it as a three-way merge, not a key union. The failure mode here is specific and I've watched it happen: attribution.json gets reordered by the tooling, so a naive resolution looks like a huge diff and it's tempting to "fix" it by taking one side wholesale or by unioning keys. Both are wrong — a whole-file --ours/--theirs silently drops the other side's rows, and a key union invents a state neither side had.

Rebase onto 71153e082 and let the tool regenerate rather than hand-merging the JSON, then confirm:

python tools/prepush_attribution.py --base origin/main --head HEAD

Expect 0 lost. CREDIT CHANGED rows are fine and expected — attribution is explicitly not a goal here, and I will not block on credit movement. I care only that no row is lost, because that's the one failure that isn't self-healing.

Also re-run your byte proof after the rebase, not before

Your base c804e96f6 predates #2057, and #2057 changed things you depend on: it landed the first source-owned non-.text bytes, and #2059 before it changed verify_owned_sections' addend semantics. Your stated d1506e90efae5e2d2cf119926a4ac2a291bd5ca78349d09d5024e1a918c478e8 matches the stock ROM digest independently measured on #2057, which is a good sign — but it was measured on a tree that no longer exists. A verdict measured on a stale base does not describe the tree that would land.

On the substance — no objection so far, and one thing I like

Two things stand out as correct judgment rather than convenience:

  • Keeping QuestionSwitch_Spawn and SpawnInfo as narrow evidence-bounded C ABI aliases because the original factory spelling doesn't survive and new selects the wrong allocator. That's the honest call. Manufacturing a plausible-looking C++ factory that happens to match would be worse than an explicit alias.
  • Reporting 13/13 functions and 200/200 class-data bytes and 37 relocations separately. Those are three different claims and collapsing them into one number is how a partial match gets reported as a full one.

I'm reviewing the substance in parallel and will post findings separately. Nothing here is a request to change the decomp work — just the rebase.

@andrewboudreau
andrewboudreau force-pushed the cpp/hatenaswitch-genuine-tu branch from 8488803 to 08401d6 Compare August 31, 2026 03:35
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Correction to my own comment: the rebase target I named is already stale

I told you to rebase onto 71153e082. main has moved twice since I wrote that. The current target is 9bfcd5d86d93413c2ba061c1d7e3226466190965.

6bd6a66d1   #2057   ov047 daObjKm3_Kurumajiku_c intact-object TU
71153e082           progress refresh (contributions.json)
9bfcd5d86   #2061   notes: unkeepable data_* entry MUST be dropped   <- rebase here

Neither of the two commits after 71153e082 touches anything you touch — #2061 is a 42-line markdown change to notes/tu-promotion-conventions.md and the refresh is contributions.json only. So this changes your rebase target, not your conflict: it is still attribution.json and still nothing else. Everything in my previous comment about how to resolve it stands unchanged.

The validator failure is the conflict, not a gate

For the record, so nobody reads PR validation: fail as a substantive problem:

tangos-validator[bot]:  PR validation -- Failed
                        test merge conflicts with exact base 71153e082eed

That is the validator refusing to construct a merge it cannot construct. It is not a byte, coverage, ratchet, or reference finding — there is no verdict on your work in there at all, in either direction. It should clear on the rebase. If it comes back red for any other reason afterward, that is the one worth reading closely.

What I am doing meanwhile

I have your substance under review in parallel — the TU shape, the delink/symbol edits, and the 13/13 functions / 200/200 class-data bytes / 37 relocations claims — and I will post findings separately whether or not the rebase has landed by then. Nothing is waiting on you except the rebase itself.

One thing worth saying plainly since it will affect your re-measurement: main is moving under this queue roughly hourly right now. Re-run your ROM proof after the rebase and quote the base sha you measured against in the same breath as the digest. A digest without its base is not checkable by anyone but you.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Changes requested — the 8 backslide exceptions are avoidable. Ten comment lines make them unnecessary.

The rebase landed cleanly. Measured just now: head 08401d6969acb103a8b981a3b27b34a1df73149d, current main 9bfcd5d86d93, main is an ancestor so this is a fast-forward, and merge-tree --write-tree equals your head tree exactly — both de6104f4294180ac68280a768f8dd67dc5afba81. Scope survived the rebase at 27 files. converted-backslide-exceptions.jsonl appended cleanly, 75 → 83 rows with #2057's rows intact, so the union-merge hazard did not bite. The attribution.json resolution looks like a real three-way merge rather than a whole-file take. All of that is right, and the checks are green.

They are green because this PR silences the ratchet. It drops CONVERTED from 2,568 to 2,560 and adds 8 backslide-exception rows to cover the drop. I measured that it does not have to.

What the ledger actually says

config/converted-baseline.json:   "count": 2568  ->  2560          (-8)

REMOVED (10 entries that are CONVERTED on main):
  src/_ZN14QuestionSwitch10HandleClsnER8dActor_c.cpp
  src/_ZN14QuestionSwitch15OnGroundPoundedER8dActor_c.cpp
  src/_ZN14QuestionSwitch15UpdateClsnStateEv.cpp
  src/_ZN14QuestionSwitch16CleanupResourcesEv.cpp
  src/_ZN14QuestionSwitch17AfterClsnCallbackEP4dBgWP8dActor_cS3_.cpp
  src/_ZN14QuestionSwitch19UpdateClsnTransformEv.cpp
  src/_ZN14QuestionSwitch20UpdateModelTransformEv.cpp
  src/_ZN14QuestionSwitch6RenderEv.cpp
  src/_ZN14QuestionSwitchD0Ev.cpp
  src/_ZN14QuestionSwitchD1Ev.cpp

ADDED (2):
  src/actors/daObjHatenaSwitch_c.cpp#_ZN19daObjHatenaSwitch_cD0Ev
  src/actors/daObjHatenaSwitch_c.cpp#_ZN19daObjHatenaSwitch_cD1Ev

Ten out, two back. The missing eight are exactly the eight rows added to the exceptions ledger.

Why it happens — and it is not the code

tools/tiers.py:309 _marked_member_fragment slices a promoted TU at // @symbol <linker-name> markers, and score_member (tiers.py:406) states what happens without one:

"Anything without either form of evidence is scored against the entire file, preserving the old conservative behavior."

src/actors/daObjHatenaSwitch_c.cpp has zero @symbol markers. So every hand-written member falls back to whole-file scoring, and the file as a whole contains volatile HatenaVector3Words position; (line 238, inside Behavior) and a set of _ZN8Particle6System9NewSimpleE… / _ZN10dBgActor_cC2Ev style externs and calls (in Behavior and InitResources). One volatile and one mangled reference anywhere in 349 lines therefore fail no_codegen_trick and no_mangled_refs for all eight other members — including Render():

/* ROM ordinal 7 -- Render, 0x020b51ac, size 0x30. */
s32 daObjHatenaSwitch_c::Render()
{
    mModelAnim.Render(0);
    return 1;
}

That body is character-for-character what src/_ZN14QuestionSwitch6RenderEv.cpp already held as a CONVERTED file on main. Nothing about it became less readable in this PR. It lost its tier because the file around it has no markers.

The two members that do survive — D0/D1 — survive precisely because score_member has a second path for lifecycle members (_lifecycle_member_fragment, the inline dtor in your header). They prove the scorer is willing to grade this TU per member. The other eight simply never reach that code path.

Measured, not argued

I extracted your merge tree de6104f429, added ten // @symbol comment lines and changed nothing else, and re-ran tools/tiers.py:

your tree as submitted        CONVERTED  2,560 / 11,314   22.6%
  + 10 @symbol comment lines  CONVERTED  2,568 / 11,314   22.7%     <- identical to main

The criteria move exactly where the diagnosis predicts:

                                        submitted    +markers
  No codegen tricks                        11,016      11,025   (+9)
  Calls things by real names, not _Z        7,192       7,200   (+8)

Zero backslide. All eight exception rows become unnecessary. The markers, one line immediately above each definition:

line 113  // @symbol _ZN19daObjHatenaSwitch_c17AfterClsnCallbackEP4dBgWP8dActor_cS3_
line 120  // @symbol _ZN19daObjHatenaSwitch_c10HandleClsnER8dActor_c
line 135  // @symbol _ZN19daObjHatenaSwitch_c13InitResourcesEv
line 179  // @symbol _ZN19daObjHatenaSwitch_c8BehaviorEv
line 281  // @symbol _ZN19daObjHatenaSwitch_c6RenderEv
line 288  // @symbol _ZN19daObjHatenaSwitch_c16CleanupResourcesEv
line 302  // @symbol _ZN19daObjHatenaSwitch_c20UpdateModelTransformEv
line 316  // @symbol _ZN19daObjHatenaSwitch_c19UpdateClsnTransformEv
line 327  // @symbol _ZN19daObjHatenaSwitch_c15UpdateClsnStateEv
line 343  // @symbol _ZN19daObjHatenaSwitch_c15OnGroundPoundedER8dActor_c

InitResources and Behavior still do not reach 5/5 after this, and that is correct — they hold the mangled externs and the volatile, and they were not CONVERTED on main either. Marking them is still right: it confines their cost to themselves instead of spreading it across the file.

These are comments. _code_only() strips them before scoring and the preprocessor strips them before codegen, so your 13/13 text match, 37 relocations and 0xc8 of compiler-owned class data cannot move. You should not need a fresh ROM proof for the markers themselves — though please re-run it for the rebase, which you need regardless.

The same lane got this right an hour later

#2064 (ov006/dScMgSingle3DBase_c) is the identical operation and carries 6 @symbol markers. Its converted-baseline.json diff is a clean 1:1 rename of all six previously-CONVERTED entries to path#symbol member identities, count unchanged, and zero backslide-exception rows. That is the shape this PR should have. It is not a hypothetical fix — it is the fix, already working in the neighbouring PR.

On the exception rows themselves

Separately from whether they survive: all eight carry the same copy-pasted reason, and it does not describe what is being excused.

"…retains measured ABI symbol seams, and Behavior needs the volatile stack value to avoid a 17-word codegen mismatch…"

Behavior is not one of the eight excused paths. And "ABI symbol seams" is a euphemism where the gate has a precise name — no_mangled_refs, "calls things by real names, not mangled _Z". Compare #2057's rows, which name the vetoing member, the criterion, and the mechanism, so a later reader can check them. A backslide exception is the audit trail for why a ratchet was allowed to pass; if it does not name the criterion it excuses, it is not one. If any exception genuinely survives the markers, please rewrite its reason to name the failing criterion and the specific line.

This part is on the docs, not on you

@symbol is load-bearing for eight CONVERTED credits and is documented nowhere — not in notes/tu-promotion-conventions.md, not in AGENTS.md. It exists only in a docstring at tools/tiers.py:309. Every landed promoted TU happens to carry it (daObjKinokoTag_c 7, daObjFm_Battan_c 7, daObjKm3_Kurumajiku_c 3), so the convention is real and universal — it is just invisible unless you read the scorer. I am opening a PR to document it, because the next TU author will hit exactly this, and the failure mode is silent: you get a green PR that quietly spends eight exception rows.

What I need

  1. Add the ten // @symbol markers.
  2. Drop the eight rows from config/converted-backslide-exceptions.jsonl, and confirm count returns to 2,568.
  3. Re-run the ROM proof on the rebased tree and quote the base sha alongside the digest.

Nothing here asks you to change a byte of decomp work, and none of it is a criticism of the TU itself — the boundary, the manifest and the byte claims all look right to me, and I have a second review running on the substance which I will post separately.

I will not merge a PR that lowers the CONVERTED count while I can demonstrate it does not have to. Green here means the exceptions worked, not that the ratchet held.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

The rename half of this PR is ROM-proven correct, and it is more valuable than the PR says

Separate from the blocker above — I want this on the record, because it turned out to matter across the whole queue.

config/arm9/overlays/ov002/symbols.txt in this PR does:

- _ZTV14QuestionSwitch      kind:data(any) addr:0x02108e5c
+ _ZTV19daObjHatenaSwitch_c kind:data(any) addr:0x02108e5c

I walked that address in ov002 (base 0x020ad660) and it holds:

_ZTV @ 0x02108e5c
  [V-8] offset-to-top = 0x00000000
  [V-4] typeinfo      = 0x02108e14
  TI[1] name ptr      = 0x02108e20 -> '19daObjHatenaSwitch_c'
  TI[2] base's _ZTI   = 0x021089ec -> '10dBgActor_c'

19 is exactly len("daObjHatenaSwitch_c"), so the read verifies itself. The cartridge's own RTTI says this class is daObjHatenaSwitch_c deriving from dBgActor_c. QuestionSwitch was a coined name. The rename is right, and the base clause comes free from TI[2].

Why this is the load-bearing part of the PR

I measured every _ZTV in the tree against its own RTTI record (tool and note in #2069): 259 of 541 class names are contradicted by the cartridge. That is not only a readability debt. Promoting a TU consolidates the class's RTTI, so the compiler emits _ZTI<OurName> and _ZTS<OurName> — and if our name is coined, the ROM holds those records under a different name at a different length, nothing can license them, and tubuild refuses:

HOMELESS     _ZTS8PoleLift  STB_LOPROC .data size=0xa
HOMELESS     _ZTI8PoleLift  STB_LOPROC .data size=0xc
COLLIDES-GAP _ZTV8PoleLift  STB_GLOBAL .data size=0x84 already at ov045:0x02112dbc

That is #2066, which spent a full destructor migration before the promotion turned out to be impossible. The pattern holds across everything that has landed: of the 23 manifests on main with status: promoted, 15 own a vtable and all 15 agree with the ROM; zero disagree.

So QuestionSwitch -> daObjHatenaSwitch_c is not incidental cleanup ahead of the real work — it is what makes the promotion possible. On main today this class reads as a DISAGREE, and this PR moves it into the agreeing 276. Doing it in the same PR as the promotion is the right call, and the ordering (symbols.txt rename before the manifest names the consolidated records) is what keeps it coherent.

ov002 is also where the disagreements concentrate — 35 of the 259 live there, more than any other module, arm9 included. If you or another agent wants a follow-up with an unusually high ratio of unblocked promotions per unit of work, an ov002 rename pass is it.

This does not change the blocker

The eight rows added to config/converted-backslide-exceptions.jsonl are still the thing holding this PR, for the reasons and with the experiment in my comment above. A correct rename does not buy an exception to the ratchet, and I am not going to soften the check to let them through. Land the ten comment lines, drop the eight rows, re-run the ROM proof, and this merges.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Now DIRTY, but only trivially — one file

#2064 and #2069 landed, so this branch needs a refresh. The conflict set is small; git merge-tree origin/main 08401d6969ac reports exactly one:

CONFLICT (content) attribution.json

config/converted-baseline.json auto-merges. Nothing in the TU itself collides.

attribution.json reorders on every pick, so three-way merge it rather than unioning the keys — a key-union resolve is how a row gets duplicated or silently dropped. Attribution is a stated non-goal on this project, so this is a mechanical resolve and not worth more than that.

Your three commits (e1581a507, 6163fce2f, 08401d696) replay onto current main cleanly apart from that one file.

The eight converted-backslide-exceptions.jsonl rows are still the blocker, unchanged. The rename half of this PR is ROM-correct and I said why in my previous comment.

@andrewboudreau andrewboudreau left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conflict resolved — pushed the merge, with the arithmetic for each of the three files

This branch had been DIRTY against main for a while and the three conflicting files are all ones where picking a side gives a wrong answer, so I resolved each by measurement rather than by choosing --ours/--theirs. Merge commit is on the branch now.

config/converted-baseline.json

The conflict was the count scalar only — the converted array itself merged cleanly, which is exactly the case where taking either side's count silently corrupts the ratchet. Read from the three merge stages:

base           count=2568  len=2568
ours   (PR)    count=2560
theirs (main)  count=2571

added by PR    (O-B):  2      removed by PR    (B-O): 10
added by main  (T-B): 23      removed by main  (B-T): 20
overlap of the two removal sets: 0

Correct three-way set size is (B∩O∩T) | (O−B) | (T−B) = 2563, and 2568 − 10 − 20 + 2 + 23 = 2563 confirms it. That's the value written. tiers_ratchet.py --check passes on it (baseline 2563, current 2564 — one gained, not yet banked).

Worth stating explicitly because it reads alarming otherwise: main's 20 "removals" are not backslides. They are TU-promotion renames — src/_ZN10dScMgBSC_c6RenderEv.cpp becomes src/actors/dScMgBSC_c.cpp#_ZN10dScMgBSC_c6RenderEv. Every one has a paired addition.

attribution.json

The real map is nested under overrides, not at top level. Audited there:

base 1005   ours 1018   theirs 1074
added ours 13    added theirs 73    add-overlap 0
deleted ours 0   deleted theirs 4
value changed:  ours 0   theirs 0   both 0     <- zero true conflicts

Result is 1087 entries, ours-order preserved with theirs' additions appended, indent taken from the base blob so the diff stays reviewable. Main's 4 deletions are honoured — all tangosdev, all promotion renames (dScMgCard_c and dScMgMCarlo2_c D0/D1).

notes/cpp-tu-current-state.md

Generated file (tools/cpp_tu_state.py, header says so), so it was regenerated rather than resolved. --write-note then --check-note → current.

One number in that note needs context before anyone blames this branch

The regenerated note moves "Existing promotion paths that disagree with delinks" from 0 to 4. That is not caused by this branch, and I checked before saying so rather than after.

origin/main at 2f62cd870 fails its own cpp_tu_state.py --check-note — main's committed copy of that note is stale. cpp_tu_state is wired into no workflow and no hook (grep -rn 'cpp_tu_state' .github/workflows/ tools/hooks/ is empty), which is how it drifted. I built a clean control worktree at origin/main and measured live: promotion_mismatch_entries is byte-identical on main and on this merge tree. The four are ov006/dScMgBSC_c, dScMgCard_c, dScMgMCarlo2_c, dScMgMCarlo_c — all main's own promotions.

So regenerating the note doesn't introduce those four, it stops hiding them. Real live main is 25 enrolled / 4 disagreements, not the committed 29 / 0. Filed separately; it should not gate this PR.

Verification on the merge result

premerge_check --base origin/main <merge>
  converted-ratchet  pass -> pass      langmode-ratchet  pass -> pass
  dead-references    pass -> pass      layout-check      pass -> pass
  duplicate-sources  pass -> pass      src-tu-refs       pass -> pass
  header-offsets     pass -> pass      source-coverage   pass -> pass
  RESULT: nothing goes green -> red

Static gates were never the risk here, though. This merge lands two independent TU-promotion sets into the same ov006 delinks, and a clean text merge proves nothing about the link — so I also ran a full rombuild --no-rom on the merge result:

intact TU gates: dsd modules PASS, zero new symbol errors, storage aliases exact
source-built functions: 11,088   reproducing: 11,088   mismatching: 0
module fidelity: 106/106 exact, 100.000000% of compared bytes
ROM data from source: 529 verified, 192 partial, 6 differ

The intact-TU line is the one I was actually watching, and it passes.

What this does not resolve

My earlier CHANGES REQUESTED still stands and is a separate matter from the conflict. The branch removes 10 src/_ZN14QuestionSwitch*.cpp rows from the converted baseline and re-adds only 2 (daObjHatenaSwitch_c D0/D1). That is an 8-row backslide, and unlike main's 20 it has no paired additions — those functions stop being credited. I deliberately did not paper over it in this merge; the merge preserves your side of that difference exactly as you wrote it, so the fix is still yours to make and the ratchet arithmetic above will absorb it cleanly when you do.

@andrewboudreau andrewboudreau left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 8 backslide waivers are covering a missing convention, not a real regression — measured

The conflict half is resolved and premerge is 8/8 pass→pass on the real merge tree, so config/converted-backslide-exceptions.jsonl is the only thing left between this and a merge. I dug into it rather than just accepting or rejecting the rows, and the result inverts the fix.

The finding

src/actors/daObjHatenaSwitch_c.cpp carries zero // @symbol markers. Every promoted TU on origin/main has them:

32  src/actors/dScMgCard_c.cpp        16  src/actors/daBrq_c.cpp
21  src/actors/dScMgMCarlo_c.cpp       8  src/actors/dScMgBase_c.cpp
21  src/actors/dScMgMCarlo2_c.cpp      7  src/actors/daObjPushblock_c.cpp
17  src/actors/dScMgBSC_c.cpp          3  src/actors/daObjKm3_Kurumajiku_c.cpp

Without them tiers.score_member() cannot bound a member's text, so every member gets charged the file-level mangled extern "C" block at lines 43-79 and the mangled call sites. That fails no_mangled_refs and no_codegen_trick for bodies that contain neither.

The bodies did not change. HandleClsn is character-for-character what it was at the merge-base as src/_ZN14QuestionSwitch10HandleClsnER8dActor_c.cpp — real names, no offsets, no _Z. It was CONVERTED then and it reads as CONVERTED now.

Measured, at this PR's exact head (7848aa2)

tiers.score_member(path, text, symbol, repo_root) for each waived symbol, as-is and with // @symbol <mangled> inserted before each definition — no other change:

_ZN19daObjHatenaSwitch_c17AfterClsnCallbackEP4dBgWP8dActor_cS3_   False -> True
_ZN19daObjHatenaSwitch_c10HandleClsnER8dActor_c                   False -> True
_ZN19daObjHatenaSwitch_c6RenderEv                                 False -> True
_ZN19daObjHatenaSwitch_c16CleanupResourcesEv                      False -> True
_ZN19daObjHatenaSwitch_c20UpdateModelTransformEv                  False -> True
_ZN19daObjHatenaSwitch_c19UpdateClsnTransformEv                   False -> True
_ZN19daObjHatenaSwitch_c15UpdateClsnStateEv                       False -> True
_ZN19daObjHatenaSwitch_c15OnGroundPoundedER8dActor_c              False -> True

Eight for eight. End-to-end, with the ten markers added and all 8 waiver rows deleted:

as-is:   CONVERTED ratchet PASS   baseline 2563   current 2564   (+1)
marked:  CONVERTED ratchet PASS   baseline 2563   current 2572   (+9)

So the waivers aren't buying the gate a pass it needs — the gate passes either way. They're buying it at the cost of eight CONVERTED members this PR is currently forfeiting, and of a permanent record in a tracked config file saying those eight regressed when they didn't.

Byte-neutral, same tree, full build:

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

(one recompile of the TU, still exact — markers are comments, but I measured rather than assumed).

Ask 1 — add the markers, drop all 8 rows

Ten definitions, at these lines in the current file:

113  AfterClsnCallback     120  HandleClsn            135  InitResources
179  Behavior              281  Render                288  CleanupResources
302  UpdateModelTransform  316  UpdateClsnTransform   327  UpdateClsnState
343  OnGroundPounded

and delete the eight daObjHatenaSwitch_c.cpp#... lines from config/converted-backslide-exceptions.jsonl.

Two members still score False with markers in place, and correctly so:

InitResources   fails no_mangled_refs
Behavior        fails no_codegen_trick, no_mangled_refs

Neither needs a row — the ratchet passes without one, which is the measurement proving they weren't in the CONVERTED baseline before promotion either. Leave them honest.

Ask 2 — the reason strings describe a symbol that isn't among them

All eight rows carry a byte-identical reason, whose second clause is:

"...and Behavior needs the volatile stack value to avoid a 17-word codegen mismatch..."

Behavior is not one of the eight. It's the one member that genuinely does carry a codegen trick — and it has no row and needs none. So a rationale written for Behavior got pasted onto eight members it doesn't describe.

A waiver file is exactly the place where the reason has to be per-symbol: it's the only record a future reader gets of why a regression was accepted, and eight copies of someone else's reason is worse than no reason. If any row survives this review it needs its own text.

Where that leaves the merge

This is not a "fix forward" for me, because the rows are the artifact — landing them writes a false regression into tracked config that nothing later will re-examine. But it's a small, mechanical, measured fix, and the numbers above are the whole argument. Push the markers and drop the rows and I'll re-read and merge; everything else on this PR is clear.

Happy to push the change to your branch myself if you'd rather — say the word and I will, otherwise it's yours.

@andrewboudreau andrewboudreau left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up from the merge lane, not a content objection: this PR went DIRTY when #2103 and #2125 landed. Nothing about the review verdict changes.

git merge-tree --write-tree origin/main <head> on the current tips:

#2062   CONFLICT (content): notes/cpp-tu-current-state.md
#2104   CONFLICT (content): attribution.json
        CONFLICT (content): notes/cpp-tu-current-state.md

Both files are the standing serial chokepoint for this lane — every TU promotion rewrites them, so any two TU PRs in flight collide there regardless of how disjoint their source is.

Do not hand-resolve notes/cpp-tu-current-state.md. It carries its own instruction at the top:

<!-- Generated by tools/cpp_tu_state.py. Do not hand-edit live counts. -->

Take either side to clear the marker, then regenerate and check:

python tools/cpp_tu_state.py --write-note
python tools/cpp_tu_state.py --check-note

Hand-merging it produces a file that is textually plausible and numerically wrong — the counts are derived from the post-merge tree, not from either parent, so neither side's numbers are right and a hunk-by-hunk resolution splices two stale snapshots. --check-note is what catches that.

attribution.json is the other one: it conflicts three-way, not as a key union. Resolving it by taking the union of keys silently drops the third parent's edits. Attribution is a stated non-goal here, so if it fights you, take main's and move on rather than spending time on it.

Once you push the restack I'll re-run the gates on the new merge tree and pick the review back up where it stands.

@andrewboudreau
andrewboudreau force-pushed the cpp/hatenaswitch-genuine-tu branch from 7848aa2 to 1db3ee8 Compare September 1, 2026 06:00
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Refreshed and reviewer blocker resolved on head 1db3ee8f36cc over current main 1b393cebd749.

  • added the ten requested // @symbol member markers
  • removed all eight false HatenaSwitch backslide-waiver rows
  • CONVERTED ratchet now reports baseline 2,563, current 2,572: +9, with no Hatena waiver
  • restacked the original three commits onto current main; resolved attribution as current-main state plus the 13 Hatena path#symbol overrides
  • regenerated notes/cpp-tu-current-state.md; --check-note passes

Fresh validation from a clean wired worktree:

  • strict control: 11,047/11,047 reproducing, 106/106 modules, same nine known symbol diagnostics
  • candidate: 11,088/11,088 reproducing, 4/4 source-owned data claims, 106/106 modules, zero mismatches
  • exact ROM SHA-256 d1506e90efae5e2d2cf119926a4ac2a291bd5ca78349d09d5024e1a918c478e8
  • intact-TU gates PASS; zero new symbol errors; storage aliases exact
  • all five RecRoomCupboard.h consumers relocation-verified; Hatena multi-function TU has the expected non-blocking NO-SYM aggregate warning
  • all 98 reconstructed TUs compile; port references 405/405
  • exact merge tree: all eight static gates remain pass -> pass; source-coverage change is consolidation only

The attribution-override label remains appropriate for the ROM-proven QuestionSwitch -> daObjHatenaSwitch_c identity change. Hosted validation is rerunning on this exact head now.

@andrewboudreau
andrewboudreau merged commit e0eb066 into main Sep 1, 2026
9 checks passed
@andrewboudreau
andrewboudreau deleted the cpp/hatenaswitch-genuine-tu branch September 1, 2026 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attribution-override Maintainer accepts this PR's contributor-credit changes; validation reports them as warnings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant