PoleLift: define the destructor in the class body (and why promotion stops at the coined name) - #2066
Conversation
An out-of-line ~PoleLift() makes mwccarm 2004/b56 emit three variants -- D2, D0, D1 -- and the cartridge holds D1 and D0 with no D2 at all. Defining the destructor in the class body emits exactly the ROM's two, in the ROM's order. tools/tu_order_check.py ov045/PoleLift now reports "ALL MATCH, ROM order, nothing unlicensed"; before this it reported a homeless D2 and an out-of-order ordinal pair. The two per-symbol sources are rewritten as forcing helpers rather than deleted, so no commit in between is unbuildable: an explicit p->~PoleLift() forces the out-of-line D1 copy and a delete-expression forces D0, and objisolate keeps the symbol each file is bound to. Byte-neutral, measured: 106/106 modules exact, 11,088 reproducing / 0 mismatching, ROM-build analysis PASS, and the built ROM is sha256-identical to stock. ROM-data object records fall 7,636 -> 7,625 with no symbol and no verdict lost -- 1,245 distinct symbols and 462 VERIFIED / 774 PARTIAL / 9 DIFFERS before and after. That is de-duplication: the key function moves from ~PoleLift(), which both destructor files defined, to Behavior(), which exactly one file defines. _ZTV8PoleLift stays VERIFIED. This does NOT promote the TU. The manifest records why, re-measured: the D2 blocker is discharged, and what remains is that the consolidated object emits _ZTI8PoleLift and _ZTS8PoleLift, for which no compiler_only_output disposition is admissible while the class carries a coined name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
✅ PR validation — PassedCommitted merge introduces no reconstruction or attribution regression. Full merge validation
Byte-verified means the range carries Per-file link-check detailAll 6 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.
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. |
Approving. The negative result is the valuable part, and I reproduced it from the cartridge.You stopped on a measurement instead of forcing a promotion through, and then shipped the half that stands on its own. That is exactly the right call, and the reasoning survives in the diff where the next person will find it. I re-derived the blocker independentlyI did not take the RTTI reading on trust — a name is the one thing bytes are easiest to over-claim about, and this repo has been burned by a "byte-verified" name before. Read straight out of
The exposure is not PoleLift-shaped, it is tree-shapedYour blocker is that promotion consolidates the RTTI and the Just under half the vtable-bearing classes in the tree are exposed to the wall you hit. The six unreadable are Then I checked the mechanism against what has already landed rather than only against your one failure. Of the 23 manifests on Zero promoted TUs in the tree carry a name the ROM contradicts. Not one has ever slipped through, which is what I would expect if the constraint is real rather than incidental to PoleLift, and it is a much better argument for your position than the single failure is. And the wave itself re-scores. Intersecting the 41-TU inline-destructor census against this: 11 of its classes have ROM-agreeing names, 48 disagree, 19 own no vtable. All 11 agreeing ones already have manifests on On the merge itselfDocs and comments aside, the substance is: an out-of-line destructor becomes an inline one, and the two Two small things I checked rather than assumed:
I am not vouching for the byte figures. No ROM was built by me, your BEFORE numbers were measured on Merging. Re-gated against current main ( |
A coined class name is not just a readability debt -- it is a hard block on TU promotion, and it does not announce itself as a naming problem. Promoting a TU consolidates the RTTI, so the compiler emits _ZTI/_ZTS under our name while the cartridge holds those records under the real one, at a different length. There is no ROM symbol to license them to and tubuild refuses with HOMELESS/COLLIDES-GAP. #2066 found this the expensive way: PoleLift is daObjKm2_Ami_Bou_c in the ROM, so that promotion could never have landed however good its boundary evidence was. Measured tree-wide, 541 _ZTV symbols in config: 276 agree, 259 disagree, 6 unreadable. The mechanism holds across history too -- of the 23 manifests with status: promoted, 15 own a vtable and all 15 agree; none disagree. Intersected with the 41-TU inline-destructor census: 11 agree, 48 disagree, 19 own no vtable. The destructor flip still lands independently, but the promotion half of that list is gated on renames rather than on destructor mechanics. The tool reads the chain directly -- _ZTV-4 -> typeinfo -> name ptr -> the length-prefixed _ZTS string, which verifies itself. Three traps are documented in the docstring because each produced wrong numbers before being caught: read config from origin/main not a stale checkout, resolve each address in its own overlay (ov045/046/047 share a base address), and use the decompressed arm9_dec.bin. An unreadable typeinfo word is reported as unmeasured, never folded into the agreeing column. Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#2075) Two assertions in tools/test_tubuild.py went stale at #2066 (467bde0), which moved ~PoleLift() into the class body. One of them was not merely stale -- it asserted the anomaly #2066 fixed: assert "1 ordinal pair(s) NOT in ROM order: [(0, 1)]" in out An out-of-line destructor makes mwccarm emit D2/D0/D1 with D0 before D1, opposite the ROM; the pilot report called that non-fixable. Defining the destructor inline emits exactly the ROM's two variants in ROM order. So the only way to make this assertion pass by "fixing the code" would be to revert #2066. Measured across 467bde0^ -> 467bde0, swapping the shadow source and the class header together (the pre-#2066 source does not compile against the post-#2066 header, which is why this surfaced as a failing assert rather than a quietly wrong number): before 37 sections, 8 .text, D2 present, "1 ordinal pair(s) NOT in ROM order: [(0, 1)]", 12 unlicensed after 35 sections, 7 .text, no D2, "ALL MATCH, ROM order", 11 unlicensed Section arithmetic: each vague-linkage symbol that stops being emitted costs two sections, itself plus its own .rela. That is 43->37 at #1555 (3 symbols) and 37->35 here (1). Rather than bump the numbers, the stale expectations become regression guards stated as absences, so reintroducing an out-of-line definition fails loudly here: assert "NOT in ROM order" not in out assert "_ZN8PoleLiftD2Ev" not in out assert "UNLICENSED function symbols" not in out The last is an absence because tubuild prints no function block at all when the count is zero -- asserting "(0)" would never match. Negative control: reverting both files to 467bde0^ fails the new assertion at tools/test_tubuild.py:166. The guards have teeth. No path naming PoleLift appears in the new prose. PoleLift is a coined name the cartridge contradicts (ROM RTTI: 18daObjKm2_Ami_Bou_c) and the rename is a prerequisite for promoting this very TU, so such a path is a dead reference with a scheduled fuse -- see #2074. Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Defines
~PoleLift()in the class body. This is the prerequisite flip for promotingov045/PoleLiftto a genuine TU. It does not promote the TU — I attempted thepromotion, measured a hard refusal that only a class rename can clear, and backed it
out. That measurement is the second half of this PR, recorded in the manifest.
Base:
a00c09854(#2063). No ledger file is touched — noconfig/converted-baseline.json,no
attribution.json, nodelinks.txt, nosymbols.txt— so this PR is not in theledger queue and does not contend with a promotion PR.
The flip
An out-of-line
~PoleLift()makes mwccarm 2004/b56 emit D2, D0, D1. The cartridgeholds D1, D0 and no D2. Defined in the class body, the compiler emits exactly the
ROM's two variants in the ROM's order:
The two per-symbol sources are rewritten as forcing helpers, not deleted, so there
is no unbuildable intermediate state: an explicit
p->~PoleLift()forces theout-of-line D1 copy, a delete-expression forces D0, and
objisolatekeeps the symboleach file is bound to. This mirrors the already-landed
ov029/ArrowLiftshape.Byte proof — before and after, both on this base
d1506e90...c478e8d1506e90...c478e8(stock)eligible.pyport_refcheck.pylangmode_audit --checktiers_ratchetThe -11 records, per symbol
Not a coverage loss. Per-record
--data-jsondiff on both trees: 1,245 distinctsymbols before and after, and an identical verdict tally of 462 VERIFIED / 774
PARTIAL / 9 DIFFERS. No symbol is lost and no verdict changes.
It is de-duplication. Before the flip,
~PoleLift()was the key function and bothdestructor files defined it, so each emitted the whole vtable/RTTI group. Inline, the
key function becomes
Behavior()— the first declared non-inline virtual — whichexactly one file defines.
_ZTV8PoleLiftstays VERIFIED, re-attributed fromsrc/_ZN8PoleLiftD1Ev.cpptosrc/_ZN8PoleLift8BehaviorEv.cpp. Every dropped record isa second copy of one that survives.
Applying #2061's discriminator: no
data_*entry is dropped here at all, keepable orotherwise, so neither the gate-silencing nor the unkeepable case applies.
Why the promotion is not in this PR
I ran
tu_promote.py ov045/PoleLiftand built it. TheD2blocker is discharged —that part of the census prediction is now compiled rather than inferred. What replaces
it is the class's own RTTI.
A consolidated object takes
objisolate.derive_deadstrip, which admits no unlicensednon-
.textcontent. Defining the key function, the TU emits eleven such records. Nineresolve to a configured ROM home and take a
deadstrip-datarow; those are accepted.Two do not, and all four available routes refuse:
isolate: unlicensed content in text-only multi-symbol object: section[10] .data size 0xa defines ['_ZTS8PoleLift']; section[17] .data size 0xc defines ['_ZTI8PoleLift']deadstrip-data...is declared compiler-only data but has no configured ROM home; a homeless object is a plain deadstripdeadstrip...is an RTTI/vtable record banked as a plain deadstrip, which is never compared against the cartridgeproduction_mode: intact-objectThe cause is a coined class name, and the cartridge says so directly. Read out of
build/build/arm9_ov045.bin:0x02112db8reads0x02112d74;0x02112d74is_ZTI18daObjKm2_Ami_Bou_c, whose name pointer is0x02112d80andwhose base pointer is
0x021089ec=_ZTI10dBgActor_c— the base this header alreadydeclares, so the hierarchy is right;
0x02112d80holds the literal ASCII18daObjKm2_Ami_Bou_c.An
_ZTSrecord is a length-prefixed mangled string, so the compiler's8PoleLift(10 bytes) can never match the cartridge's
18daObjKm2_Ami_Bou_c(21 bytes) — under anyproduction mode. The gate's own remedy is the right one: rename this class to
daObjKm2_Ami_Bou_c, which the ROM evidences at0x02112d74/0x02112d80. That is aseparate PR with its own rename procedure, and deliberately not done here.
The manifest entry now records all of this:
unlicensed_output_observedis re-measuredpost-flip (
.textlist now empty; eleven data records with their homes, or theirabsence, spelled out), the
verification.criteriarows that named D2 are corrected, andthe
notesbullet that said promotion needed "a licensed home for_ZN8PoleLiftD2Ev"is replaced with the blocker that actually remains.
Conventions checklist
Items 1-4 and 6 concern a promotion's diff. This diff promotes nothing, renames nothing,
deletes no file and touches no ledger, so they are vacuous rather than passed — with two
exceptions worth checking:
that
PoleLiftis itself a coined class name; this PR does not act on that, itdocuments it in the manifest with the ROM addresses that settle it.
extern int _ZTV<C>[];— not applicable while unpromoted, and flagged asoutstanding work:
src_tu/actors/PoleLift.cppcurrently spells the storep[0] = (int)(_ZTV8PoleLift + 2);, where the convention asks for*(int *)p = (int)&_ZTV8PoleLift[2];. Same arithmetic, different spelling; leftalone here to keep this diff byte-neutral and in scope.
_ZTVaddress unchanged /completespan —_ZTV8PoleLiftremains0x02112dbcinconfig/arm9/overlays/ov045/symbols.txt; this PR does not touch thatfile. No
completespan is added or widened —delinks.txtis untouched, so ov045keeps its seven per-function entries. No
data_*or bss entry is dropped.func_<module>_<addr>declarations — nothing renamed, so nothing toremove;
include/decl_common.his untouched.my change invalidated are corrected:
src_tu/actors/PoleLift.cpp's note about theD2 section, and
include/PoleLift.h's claim that the flat-C twin exists because "theD0 file is a C translation unit that reads these fields". Every includer of that
header is C++ today, and D0 now names no field; the twin is left in place, only its
justification is corrected.
the queue. TU: first compiler-built vtable — promote ov047/daObjKm3_Kurumajiku_c to intact-object production #2057 merged before it was opened.
Gates run
rombuild -j16(full ROM),eligible.py,port_refcheck.py,langmode_audit --check,tiers_ratchet.py,tu_order_check.py,check_src_tu.py,check_src_tu_compiles.py(92/92),
check_header_offsets.py include/PoleLift.h(2 commented fields, 0 mismatched),check_dead_references.py,check_references.py,check_duplicate_sources.py,check_data_definitions.py. All pass.twin_align.py --checkFAILs — identically, on the same two headers, on unmodifiedorigin/main.PoleLift.his not among them. Pre-existing and untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ