Files
BFM-decomp/cookbook/C0044.md
T

18 KiB
Raw Blame History

§41 — The DEF-SIDE canonical-sig wall: mechanically banking a drafted giant past conflicting types (Phase 25 T5b batch-2, 2026-07-09; tools/canon_sig_reconcile.py, byte-proven on func_8013B274)

Addendum (P31 S64 t5o-t5r, func_8017BEBC @ ov_SC03_007, 246 ins) — A FINDABILITY KEY, not a new law. §41 step 3 ("Cast each type-changed param AT ITS USES — NEVER via an intermediate local") also owns a purely REGALLOC symptom, and this section's declaration-wall title hides it: a drafter hunting a register residual greps past §41 entirely. The tell: a closeness plateau that will NOT move under clamp / loop-shape / integer-width levers, with the instruction COUNT already exact, on a function whose C declares T *out = (T *)paramN; and stores through out[i]. The named alias is a fresh pseudo — gcc gives it its own register, which frees the incoming arg register for a competing tail temp and shifts the whole prologue schedule. Casting in place at each store (((T *)paramN)[i] = …) adds no pseudo and is byte-free. Byte evidence: n7 (s16 *out = (s16 *)a1; out[i] = …) = near, closeness 17, nins 246; n9 (same body, alias deleted, ((s16 *)a1)[i] = …) = MATCH, closeness 0, nins 246; n11 re-confirms. ⚠ The decl-order control is INERT (§67): n10 swapped the two pointer locals' declaration order, kept the alias, and stayed at 17 — so an unchanged closeness under reordering does NOT exonerate the alias. This is the FOURTH confirmation of §41 step 3 / §67's two-pseudo law, and the transcript printed only aggregate status closeness nins lines, so it sharpens findability, not the mechanism.

The wall (dominant for GIANTS — ~universal, vs ~35% clean-bank for small fns): a drafter writes an isolation-MATCH giant body (match_one c=0) with Ghidra-derived TYPED params — void func(u32 *a0, s16 *a2). Placed in the real overlay TU it fails the whole-binary gate on conflicting types for func_X (a declaration conflict, NOT a byte diff). The gate's sig_unify/cast_call_sites can't fix it and it banks 0/16. Two sources:

  1. The TU's callers reference the fn through the CANONICAL signature declared in src/shared/engine_core.h (extern void func_8013B274(s32 a0, s32 a1, void *a2);) — but that decl lives inside a DEFINE_func_* macro, so sig_unify (which rewrites file-scope externs) never sees it. The draft's typed sig conflicts with it.
  2. Absent an engine_core.h decl, a caller above the definition gives gcc-2.7.2 an implicit K&R int func_X(); the draft's void/typed-param def conflicts with that. (This is why the overlay's "Phase-17 canonical-sig layer" at each _a.c top uses s32 func(s32,…) — it's K&R-int-compatible AND byte-neutral.)

The crack — reconcile the DEF to the canonical sig, BYTE-NEUTRALLY (3 steps, all proven on func_8013B274 → banked d19c9580 byte-identical):

  1. Strip the draft's redefinitions of ambient symbols. A typedef … P_TAG; identical to engine_types.h's is a redefinition error in gcc-2.7.2/C89 (not "compatible" like C11). Strip identical-def typedefs; strip extern decls (func / data D_* / memcpy) the TU or engine headers already declare — the draft's Ghidra-typed re-declaration is a conflict source. (memcpy always: a mismatched prototype trips conflicting types for built-in memcpy; the TU macros / builtin provide it.)
  2. Rewrite the def signature to the canonical (engine_core.h decl if present, else the implicit-int-compatible s32 func(s32,…) at the draft's arity; arity-grow adds unused params so an N-arg implicit caller still matches).
  3. Cast each type-changed param AT ITS USES — NEVER via an intermediate local. THE load-bearing insight: u32 *a0 = (u32*)arg0; at the top introduces a fresh pseudo → gcc allocates it a different reg → regalloc shifts → byte diff (measured: cast-locals gave 70ff4748, wrong). Casting the param in place — ((s16*)a2)[i], ((s16*)param + 1) (preserves stride!), *(T*)p — adds no pseudo, is free, and preserves the isolation-match codegen. tools/canon_sig_reconcile.py blanket-wraps every use of a changed param in ((origtype)name) (correct for index/deref/arith/member/already-cast alike). Give it --tu <split.c> so it treats that TU's already-declared symbols as ambient (strips their redundant draft externs too).

Result: 5/16 batch-2 giants banked purely mechanically (func_8013B274 80130D48 80167DBC 8016DC20 8018514C); 3 then family_sweep'd ×134. This is the phase's #1 def-side lever, now partly automated — reusable across the whole giant tier AND the batch-1 backlog of "match_one-MATCH but gate-rejected" near-misses (the dominant gate-failure).

The residual walls (the other 11 — genuine per-fn T7, NOT this mechanical pass; backlogged with cause): (a) non-identical ambient types — draft's SVEC/ApplyMatrixSV differ in layout from engine_types.h's (can't strip: not identical; can't keep: conflicts) → needs a rename or a real layout reconcile. (b) data symbols declared inside DEFINE_ macros (D_80078EB0) — macro-local, not file-scope, so stripping the draft's extern leaves the body referencing an undeclared symbol, and keeping it conflicts → reconcile_decls.py/§33 byte-neutral-access-cast territory. (c) primitive-typedef redefs the reconcile missed; (d) genuine byte-diff (reconcile compiles but codegen differs — back to permuter/hand). Sweep fragility: a reconciled body carries ov_SC01_077-specific canonical sigs/casts, so family_sweep to sibling overlays (with their OWN engine_core.h decls) byte-matches only some siblings (func_8016DC20 = 133 siblings failed → exemplar-only). A robust sweep of reconciled giants must re-reconcile per sibling TU (T7 follow-up).

§41a — v3.1: the def-side wall was ~71% TOOL-shaped — the five measured defects + the laws that dissolve them (Phase 25 T6, Fable5, 2026-07-09)

The T6 curriculum session R14-re-verified ALL 95 draftable-exemplar stubs (every draft in every .run/drafts-t5* dir, best-of, match_one): 62/95 are genuine isolation-MATCH — the frontier's "match" statuses were honest, and all 11 batch-2 "walls" + the 3 _o0 giants have byte-correct bodies. A 6-iteration probe program (reconcile → splice into the REAL TU → full cpp|cc1|maspsx|as → relocation-masked byte-compare of the fn inside the TU object, .run/t6_reconcile_probe*.json) then decomposed the §41 wall into five mechanical defects of the v1 reconcile itself — fixed in tools/canon_sig_reconcile.py v3.1, taking the mechanically-bankable set 10 → 44 of 62 (4,254 ins, 13 giants ≥145; func_8013DD68 gate-validated byte-identical through make build):

  1. Scalar-typedef dups (typedef … u8;) must be stripped (C89 redef error) — same set match_one strips. Was 11 fns of "redef:u8/u16/s16/s8".
  2. Canonical truth = the PREPROCESSED TU's file scope (cpp + brace-depth-0 scan), not a token-scan: token-scanning counted macro/block-scope names as ambient (over-strip → undeclared) and missed the TU's own decl of the fn (self-conflicting types). NB: a DEFINE_ macro's extern lands at FILE scope when the macro is instantiated at file scope — and gcc-2.7.2 REMEMBERS block-scope extern types TU-wide, so both kinds bind later defs.
  3. Never strip a draft extern — BLOCK-SCOPE-MOVE it (types verbatim) when no decl is visible above the splice point. The draft's extern types are LOAD-BEARING (%lo-folding, access width, alignment: an ambient-type rewrite byte-drifted 18/62 — e.g. ((s16**)&u8_sym) derefs at alignment 1 → lwl/lwr). Block-scope decls are private and legal even when a DIFFERENT file-scope decl exists below (recover_giant's idiom, generalized). Visible-above + identical → drop; visible-above + different → ambient + cast-at-use (fn callees per §17a-1; data via access-casts — alignment caveat above routes the narrower-object cases to §33/reconcile_decls TU-retype instead).
  4. Colliding typedefs are RENAMED (Vec3 → Vec3_<addr>, attribute-tolerant), never layout-reconciled: type names emit no code, so the def side NEVER has a real layout problem (SVEC/Vec3/Prim/S8/ApplyMatrixSV walls all fell to the rename — 2 giants banked).
  5. Blanket use-site substitutions must skip decl lines ("decl lines are never cast" — cast_call_sites' rule; violating it emits extern void ((void(*)(…))f)(…) parse errors). Also: find the def on a COMMENT-MASKED copy (drafts' @stuck headers quote the sig and mis-anchor the rewrite).

The residue is three real classes (T6 curriculum tiers M3/M4/F): (a) arity conflicts with a visible typed prototype (a banked caller's macro declares void f(void*) arity-1, the byte-true def needs 3 params) — no draft transform can fix; the cure is the fix_arity_callers-class no-proto rewrite of the engine_core.h macro extern (extern s32 f();, byte-neutral for the loose callers), R22-gated — 6 fns. (b) stale TU decl types from earlier banked drafts (u8 vs s16* on D_801870B0 etc.) → reconcile_decls/§33 fleet-majority retype, then the drafts bank verbatim — 8 fns. (c) genuine per-fn residue — 4 fns (incl. func_80166994's real mixed-arity loose-typing entanglement).

The ×134 sweep law (Q5, 6/6 proven): sweep the RAW draft via family_remap.symbol_map, then re-run canon_sig_reconcile against EACH SIBLING's own TU, then gate. §41's "sweep fragility" was exactly the missing per-sibling re-reconcile (the ov077-reconciled text carries ov077-specific decisions). Sibling stubs live in the SAME split-file name fleet-wide (_after etc. — the whale-rollout structure); read the asm subdir off the sibling's INCLUDE_ASM line.

Refuted: the batch-3 "-O0 in-context byte-diff needs an -O0-specific reconcile" — all 3 _o0 giants (329/198/154 ins) probe BANKABLE at -O0 under v3.1 unchanged; the old diagnosis was v1's declaration perturbation, not a -O0 return-type law.

Probe-method notes (reusable): the in-TU masked byte-compare (insns_from_object(tu_o, fn) vs insns_from_s(splat_s)) is a fast, link-free gate proxy — but it is jal-symbol-blind (mask eats the target field), so harvest_verify stays the arbiter (G3/P9). And never hand-type a SHA: a mistyped --good-sha made a byte-perfect gate run report MISMATCH — read it from config/check.*.sha.

§41b — T7 execution: the object-only probe OVER-counts BANKABLE by two link/rodata classes (Phase 25 T7-M1, 2026-07-10)

Executing the §41a curriculum banked 37 of the 40 non-jumptable M1 exemplars byte-identical through the whole-binary gate (tools/t7_bank.py: reconcile-at-bank-time + harvest_verify, chunk-bet with per-round re-reconcile for cross-fn ambient mutation). The 7-fn gap between the T6 probe's "44 BANKABLE" and reality is two integration classes the T6 in-TU masked object-compare could not see — a sharper statement of "probe ≠ gate" (R14): the probe compiles to an OBJECT and masks jal/%hi/%lo, so it is blind to both rodata and link.

  1. Switch jump tables in rodata (4 fns: the 3 _o0 giants + func_8012ACE0). Their .text is byte-perfect — the unmasked diff is 100% j .L… / lui/addiu %hi/%lo(jtbl_…), all masked-EQ, zero real .text diffs — but the switch emits a jump table in rodata (jtbl_801D836C …) that the object-only compare never looked at, and the whole-binary SHA diverges there. This REFUTES the T6 curriculum's "Q3 -O0 reconcile REFUTED" claim (the probe said the _o0 giants bank; the gate says no) — the batch-3 "in-context byte-diff" finding STANDS. Route: the jump-table-in-rodata workflow (cookbook §8, the LZSS/§5a precedent — carve/match the jtbl_* rodata), F-band, NOT mechanical M1.

  2. Last-referencer link-wall (3 fns: func_8016D688/D_801D9C20, func_8016D1D8/D_801D9C20+D_801D9C60, func_80165240/D_8018977C). The fn is the ONLY asm referencer of a scratch data symbol; splat auto-generates that symbol into undefined_syms_auto.txt from the disassembly, so C-ifying the last referencer drops the symbol → ld: undefined reference to D_801D9C20. Compiles clean, fails at LINK (the object-only probe never links). Fix (M-linkwall tier): declare the symbol so ld resolves it — a manual undefined-syms entry or a splat data-symbol carve at that address (the bytes already live in the overlay image). Deferred pending the splat symbol-provisioning mechanics (don't guess an address into the byte-locked build). Reusable class: ANY bank that removes the last asm reference to an overlay-local data/scratch symbol.

Method upgrade for future curricula: an in-TU object probe is a necessary filter but NOT the gate — it misses rodata (jump tables, float/string pools) and all link-time resolution. Size a "mechanical" tier from the WHOLE-BINARY gate on a sample, or expect a ~15% object-probe over-count and treat the surplus as the two classes above. tools/t7_bank.py (reconcile-per-round + chunk-bisection) is the reusable M1 driver.

§41c — T7-M2: the ×134 def-side-wall sweep via per-sibling RE-reconcile (Phase 25, 2026-07-10; 4,389 banks, ~0 agent tokens)

§40's mechanical family_remap (symbol-remap a matched exemplar → sibling) banks 0 for the def-side-wall giants: the ov077-reconciled body carries ov077-specific block-scope-vs-ambient decisions, and each sibling's DIFFERENT decompile state (different fns banked above the splice) needs those decisions RE-COMPUTED. The Q5-proven fix, now tools/family_sweep.py --reconcile <rawdir>: per (exemplar, sibling), symbol-remap the RAW draft (source→sibling via family_remap.symbol_map) then re-run canon_sig_reconcile v3.2 against THAT sibling's TU, then the plain whole-binary byte-gate. engine_core.h is SHARED so the canonical sig is identical fleet-wide; only the per-overlay symbol names + the sibling's visible-above set change.

Result: 4,389 of 4,655 member-remaps banked (94%) across 133 overlays for the 35 M1 exemplars — fleet 72.29% → 73.58% (+1.29%), R22 clean-fleet 136/136, dedup-check 1813/0. The 266 misses are per-sibling loose-typing walls (the sibling banked a conflicting-type neighbor) → backlog. Cost: local cpp+build only, zero agent tokens. Cost note: the per-member re-reconcile runs cpp on the sibling TU prefix for visible_above — ~2 s/member (≈45 min staging for 4,655), then the group gate. _AMBIENT_CACHE/_VISIBLE_CACHE are cleared per sibling-TU inside the sweep (the sibling source is static during phase-1 staging, so the caches stay valid across exemplars for one TU). This is the endgame's economic engine for the def-side-wall giants: crack + reconcile ONE exemplar, sweep it ×134 mechanically.

§41b addendum — M4 "reconcile_decls" tier is ALSO a probe over-count: 0/8 mechanical (Phase 25 T7-M4, 2026-07-10)

The T6 curriculum's third "mechanical" sub-tier (M4: 8 object-probe BYTEDRIFT fns projected to bank via a reconcile_decls §33 TU-retype) is REFUTED by the whole-binary gate: 0/8 bank (reconcile_decls → canon_sig_reconcile → gate). Root cause, same R14 pattern as §41b's jumptable/linkwall: the T6 object-probe's "BYTEDRIFT" does NOT imply a data-type conflict. 4 of the 8 have reconcile_decls "touched 0" — no data decl even differs from the fleet-canonical — so their in-TU drift is pure codegen (scheduling ORDER: e.g. func_8013E83C reads D_80115118 before the prologue in the target; volatile-loss: func_801418F8's D_8011511A read-back; callee interactions). The other 4 have real data-decl differences but reconcile_decls' byte-neutral cast still perturbs the schedule. These 8 are F-band (byte-correct in ISOLATION — match_one c=0 — but drift 8–69 in the real TU) → permuter-ILS / §31, not a mechanical tier. reconcile_decls remains valid ONLY for a genuine data-TYPE conflict where the cast is schedule-invariant (its §33 giant proofs); it is not a driftfix.

Net honest tally of the T6 "mechanical" projection (58 fns / ~2.5 MB): truly mechanical = M1 37 + M3-clean 2 = 39 exemplars (banked + swept ×134 = ~4,694 fleet fns, fleet 72.29→73.66%). The other 19 were probe over-counts → F-band/specialist: 8 M4 (codegen drift) + 4 jumptable (rodata) + 3 linkwall (undefined-sym) + 4 M3-residue (arity/loose-typing). Lesson (reinforces §41b): size a "mechanical" tier from the WHOLE-BINARY gate on a full sample, never from an object-only probe — it can't see rodata, link, OR in-TU codegen perturbation. Expect ~⅓ of an object-probe "BYTEDRIFT/COMPILE-FAIL" bucket to be genuine per-fn work.

§41d — void→s32 is NOT always byte-neutral: gate the RAW draft FIRST (Phase 26 session 6, byte-proven)

R14 correction to the Phase-17 canonical convention. The canonical-sig form ("s32 return — void→s32 is byte-neutral, §3a-1") is false for a void body with no return statement: promoting the return type makes gcc-2.7.2 emit one extra instruction. Byte-proven on func_80182268 (31-ins jr, ov_SC01_077):

draft result
void func_80182268(void *a0) (raw) MATCH, 31 ins
s32 func_80182268(void *a0) (return type alone) DIFF, 32 ins
s32 func_80182268(s32 a0) (what canon_sig_reconcile emits) DIFF, 32 ins

The extra word is invisible in a leaf diff but lethal whole-binary: it pushed the isolated object's .text 4 bytes long, shifting every data symbol +4 → ~271,000 differing bytes and a 5-byte-longer image. match_one said MATCH; only the whole-binary gate caught it (G3/P9 again).

The rule (generalizing §19's sig_unify lesson): every recovery pass is a FALLBACK, never unconditional. canon_sig_reconcile exists to break the §41 def-side wall — it must not run on a draft that already compiles. jtbl_family_bank now gates raw → (on failure) reconciled, and canon_sig_reconcile only promotes the return type when a canonical extern actually demands it. Corollary: a function with no canonical decl anywhere (grep engine_core.h + the overlay .c) should be banked exactly as drafted.