Files
BFM-decomp/cookbook/C0143.md
T

8.8 KiB
Raw Blame History

§132 — The JR-PAIR-IN-ONE-O0-OBJECT "wall" was TWO instrument defects: a merged-double span the carve could not see, and a truncated object no rule deleted (P30 S29, func_8013B83C + func_8013BD74)

S28 ledgered a new residual class: two jr functions matched in ONE -O0 object produced undefined reference to $L105 + undefined reference to func_8013C938 from a CLEAN build, while an incremental build reported BYTE-IDENTICAL (§130). The escape recorded was §81 step 1 (isolate one into its own code subseg). Both the class and the escape are REFUTED. Neither function is on a compiler wall; both banked from a clean fleet with no isolation at all. Class RETIRED — the fourth consecutive "structural wall" to resolve to our own tooling (§124/§125/§126/§131 are the others).

Defect 1 — a pre-§8e MERGED DOUBLE is not a single-table predecessor

jtbl_carve reconstructs a touched span's table starts from the union {new fn's .s, surviving stub .s, the committed tables=, --span-tables, --like}. When a span has no JTBL_PADS line, the tool infers "SINGLE-table predecessor, so the span start IS the table start" (§8e-2, correct for the case it was written for). ov_SC01_077_o0's carve at 0xb01a4 predates the tables= persistence and is a merged double — func_8013C0F8 ($L75) and func_8013C414 ($L105), the second invisible because a matched owner's stub .s is pruned by make extract. So the tool derived 3 starts where the object emits 4 tables, wrote JTBL_PADS := 0,4,4, and jtbl_rodata_pads refused mid-stream — correctly, with the exact message ("more rodata .align directives than pad specs").

Fix (single choke point, spec_from_starts): assert the starts EXPLAIN the span, and recover what is missing from the payload — every zero word INSIDE the span is an original .align 3 pad (the same axiom the pad rule already rests on: a zero can never be an ENTRY), so the word after it STARTS a table. Recovery is a no-op wherever the structure is already known, so committed-green spans are untouched. Honest limit: only PAD-SEPARATED boundaries are recoverable; a tight (0-pad) interior boundary is indistinguishable from a continuing table in the payload — but it then makes the spec SHORT, which the filter rejects loudly at build time. Never silent in either branch.

Defect 2 — as writes a corpse and nothing deletes it

as consumes a PIPELINE. When an upstream stage dies mid-stream, as has already assembled the prefix and written a truncated .o (its only complaint is Warning: missing .end at end of assembly). make reports Error 1 correctly — and then leaves that object on disk, NEWER than its .c. The next build considers it up to date and links it. Measured here: 12 of 16 T func_ symbols, and undefined $L57/$L59/$L63/$L75/$L76. That is the entire mystery: the link error is one build DOWNSTREAM of a loud, correct, attributable compile error. Fix: .DELETE_ON_ERROR: in the Makefile (negative-control-proven: make: *** Deleting file ...). Same family as §42b/§130 — an artifact that outlives the command that failed to produce it.

The fingerprint, and the 30-second ladder that found it

An undefined $L<n> in a LINK error is never codegen. $L labels are gcc-local: the assembler resolves them within the object, so one can only be undefined if the stream that defined it was cut. Read it as "an object is truncated", not "a function is wrong".

The ladder — compile the ONE TU standalone through the real pipeline and let it name the owners:

cpp … | cc1 -O0 … | maspsx … > t.s          # no filter, no carve: one variable
grep -n '^\.section \.rodata' t.s            # how many tables does the object ACTUALLY emit?
# for each hit, the nearest preceding `.ent` is that table's OWNER; `$L<n>:` is its label

Four blocks (13/27/27/27 entries) against a 3-start derivation, and $L105 attributed to func_8013C414, in one command — before any build, carve or isolation. Then confirm against the ORIGINAL payload: table starts 0x801D8254 / 0x801D828C / 0x801D82FC / 0x801D836C, each preceded by a zero pad word, span 0xb00fc..0xb0280 = 388 B = 52+4+108+4+108+4+108 — the arithmetic closes exactly, so the C-side table count and the payload agree and the spec is 0,4,4,4.

Result: func_8013B83C (272 ins) + func_8013BD74 (198 ins) banked in ov_SC01_077 (d19c9580), R22 clean-fleet 140/140, with the 137-sibling sweep unblocked (their _o0c spans DO carry tables=+0x0,+0x70, so the recovery is a no-op there — the blindness was ov_SC01_077-only).

The transferable rule

A fail-loud guard is only as trustworthy as the artifact hygiene around it. A guard that refuses correctly but leaves a partial artifact behind converts its own honest error into a lie one build later — and the lie is more convincing than the truth, because it points at a different subsystem. When a loud refusal and a mystifying downstream failure appear in the same session, suspect they are the SAME event, one build apart.

§132a — --like is for a sibling with NO record; against one that HAS a record it over-derives (P30 S29, ov_SC07_010)

The 2×137 sweep banked 136/137 twice and failed on the SAME overlay both times (consumed 3 rodata .align(s) but 6 pad spec(s) given). Cause: jtbl_carve --like <exemplar> transfers the exemplar span's table structure and matches donor→recipient by the subseg's ROLE NAME (the name with the overlay prefix stripped). ov_SC07_010's -O0 region is named _o0 — the same role as the exemplar's, and the only other overlay so named; the other 136 are _o0c, whose role never matched, which is the only reason the sweep worked at all. The exemplar had just banked two more owners than this sibling has, so the transfer unioned its rebased 4 offsets with the sibling's real 2 plus the new table: six starts for three emitted tables.

The rule: a sibling's own committed tables= is AUTHORITATIVE; --like exists for a span with no record. Suppress the transfer when a record exists (jtbl_family_bank.like_arg, deriving the subseg via jtbl_carve.func_subseg — the same derivation the carve uses, R33). The incomplete-record case that --like used to paper over is now covered honestly by §132's payload zero-word recovery, so nothing is lost by preferring the local record.

The shape worth remembering: the transfer was wrong for 2 of 138 overlays and inert for the other 136 — so a sweep can be 99% green and still be running a defective rule. A per-sibling failure that repeats on exactly the same sibling across two independent sweeps is a property of that sibling, not noise — probe it rather than ledger it: here it cost one probe and returned 2 banks plus a real tool defect. And note the near-miss: had ov_SC07_010 been named _o0c like its 136 peers, the defect would have stayed invisible until some later family whose exemplar shares a role name with a recorded sibling — a silent wrong spec instead of a loud refusal.

§132b — When the span's already-matched owner is ITSELF multi-switch: --span-rel (P30 S1, func_8014032C)

§132's payload zero-word recovery closes the pad-separated case. Here is the case it cannot see, met head-on while sweeping the zero-crack tier's largest family (183 ins × 137).

The sibling gate-failed. The §132 ladder named it in one command: the object emits four tables because both functions in it are multi-switch (8+5 entries each), while the carve derived three starts. The missing start belongs to the already-matched func_8013FFD8, and neither oracle can reach it — make extract prunes a matched owner's stub .s, and its second table abuts its first with no pad (8 entries = 32 B ≡ 0 mod 8, so .align 3 emits nothing).

Note what is NOT wrong: the true pads [0,0,4,0] are exactly what natural alignment produces. The build breaks only because a short spec gets written and then enforced.

The lever: jtbl_family_bank --span-rel d0,d1,… — table offsets relative to the FIRST NEW table, which func_jtbls reads from the sibling's own .s. The family's layout is invariant (same code, same entry counts; only the base moves), so one measured offset list serves every sibling. Verify that invariance on ≥2 siblings before trusting it — one command, and it is the whole premise.

It is a per-family FLAG, not a default, and the failure is symmetric. The same sweep needed it ON for 127 siblings and OFF for 10: where the span holds only the new tables, forcing the extra starts over-specifies and the carve refuses ("table starts … do not fit the span"). Two passes — sweep with it, re-run the residue without — banked 137/137. A residue that fails the opposite way from the majority is a signal to flip the flag, not to add another lever.