Files
BFM-decomp/cookbook/C0158.md
T

7.5 KiB
Raw Blame History

§147 — The three-stratum FRAME LAW, and four "stop searching" verdicts (P30 S42, func_8017C294, serial run)

Five levers from one serial crack. It did not bank (NEAR 12/246, zero structural divergence, and permuter_ils plateaus at exactly 12) — but the knowledge transfers, which is why the run was serial.

A. The frame has THREE strata, and stratum 3 is unreachable from C

gcc-2.7.2 lays out the frame in order: (1) declared locals, in declaration order, ascending from the outgoing-args top · (2) reload spill slots · (3) a trailing block the function's ?: chains allocate and never reference. §136-6 ("slots assigned in DECLARATION order") describes only stratum 1.

Symptom → verdict: if the target's mystery slot sits at the TOP of the frame, adjacent to the saved-register area, it is stratum 3 — and NO declaration-order, filler-array, volatile, or inner-block edit can reach it. Stop looking for the missing local.

Measurement recipe (30 seconds, deterministic): delete the min/max tail, recompile, re-read .frame … # vars=. The drop IS stratum 3's size. (Here: exactly 96 bytes.) Run this BEFORE drafting anything large with a min/max tail — it converts an unbounded "which local am I missing?" hunt into a yes/no.

B. A ?: on MEMORY operands costs ~16 bytes of invisible frame; on REGISTER operands, zero

minx = MIN(outp[0][0], outp[1][0]);              /* memory operands -> +16 bytes of frame */
a = outp[0][0]; b = outp[1][0]; minx = a<b?a:b;  /* register operands -> 0 bytes */

They are not interchangeable. The memory form is also what emits the target's lhu+lh double-read of one stack slot (the frame-size counterpart to §136-9). So when a draft's frame is a multiple of 16 too large around a min/max chain, COUNT THE ?:s before inventing a dead local (§83c's trap, seen from the other side). Corollary proven here: a zero-temp tail is unreachable when the target re-reads its operands — 219 ins (if/else) and 234 ins (operands bound to locals) vs 246.

C. Inner-block declaration does NOT delay slot allocation — BYTE-REFUTED

expand_function_start walks the whole BLOCK tree, so { … T x; … } lands at the same stratum-1 offset as a function-scope declaration. Do not spend a cycle on it.

D. A lone $t8/$t9 in the target is RELOAD SCRATCH — reproduce the spill, don't pin the register

MIPS defines no REG_ALLOC_ORDER (regalloc.md K3), so plain allocation never reaches $24/$25 unless everything below is busy. An inherited draft here pinned register short *dst2 __asm__("$24") to force lw $t8; the pin then pushed mfhi off $t8 onto $t9 — a diff the pin itself created. The correct lever was structural: delete the volatile "out" local and let the a1 parameter spill naturally — reload picks $t8 for both store and reload for free, and mfhi $t8 comes right too. Generalises §17/§72: before pinning a high register, check whether the target's value is a SPILL and reproduce the spill instead.

E. A qty_compare TIE is not spelling-reachable — recognise it and stop

QTY_CMP_PRI = log2(nrefs)·nrefs·size / (death − birth). When two quantities have equal ref counts and live ranges differing by one insn, the register grant flips with statement order and both orders cost the same. Here (mw; xw; mh; yh) buys the target's emission order and mh → $a0 but transposes w/h; (mw; mh; xw; yh) buys the registers and loses the order — both exactly 5.

Tell: structure exactly right, exactly ONE register PAIR transposed, and the alternative ordering transposes a DIFFERENT pair. Swept here: 72 statement permutations × 4 declaration orders × 7 s16/s32 retypings × ?:-MAX spellings × ref-count shifts × $v0/$v1 pins, plus the permuter — floor unchanged in every direction. Worth ~an hour to recognise early.

Consequence for the family (a real scheduling decision)

The blocker is a frame-layout fact about the body, not a per-overlay symbol thing, so all 15 siblings will hit it identically: the same draft remapped reaches 12/246 on each and none will bank. Do not spend the 15 until stratum 3 is explained. When it closes, family_remap carries all 16 in one pass.

Symptom lines for the index: "a mystery stack slot at the top of the frame" · "frame is a multiple of 16 too large" · "a lone $t8/$t9 in the target" · "exactly one register pair transposed" · "permuter and hand-search plateau at the same number".

⚠️ §147 CORRECTED BY THE BYTES (P30 S43) — A and E REFUTED, B re-explained. 12 → 2.

The function §147 was written from (func_8017C294, and its 246-ins twin at func_8017CE58 in ov_SC02_000/003) was re-attacked with ~70 named probes plus two permuter basins. Three of this section's verdicts were wrong, and the "stop searching" advice cost this project a parked family.

  • A — "stratum 3, unreachable from C" is REFUTED. There is no stratum 3. gcc-2.7.2's frame is declared locals, then reload spill slots in strictly increasing pseudo-regno order. The mystery 0x108 slot is an ORDINARY reload spill whose pseudo simply has the highest regno — because loop.c created it: writing the loop as an index loop (for (i=0;i<4;i++) over pos[i]/mat[i]/outp[i]) makes maybe_eliminate_biv_1/emit_iv_add_mult build the limit INSIDE the loop, landing the pseudo high; a pointer walk puts it in a low-regno expand pseudo at the BOTTOM of the reload block. That is the whole difference, and it is fully source-reachable — which is why every prior draft needed a fake volatile pEnd + dead[7] to counterfeit the offset. (121 → 54.)
  • B — the unreferenced slot block is NOT ?:-on-memory frame cost. It is combine-orphaned sign-extension intermediates: (ashift (subreg (reg:HI)) 16) pseudos that combine folds into an lh, leaving (use (reg)) + REG_DEAD at a CODE_LABEL (combine.c:10839), so alter_reg still hands each an 8-byte slot that emits nothing. Ablation: min chains cost 4 slots each, max chains only 2 (cse1 elides two conversions per max pass), clamps/base-folds 0.
  • E — the qty_compare TIE is breakable. __asm__("" : : "r"(w)) after mw = w — §148-C's zero-emission ref slider — flips w/h back onto $v0/$v1. (30 → 25.) (Note §150 separately shows this class is often variable-identity, not an allocator tie at all — check pseudo COUNT first.)
  • D applied properly still holds and is the second-biggest lever: drop the volatile out local and the $24 pin, store through the a1 parameter and let reload spill it — reload then picks $t8 for both store and reload for free. (54 → 30, length exact.)

Net: the recorded floor of 12 was a floor of the ANALYSIS, not of the function — it is now 2/246 (sw $a1 / lw $t8 at one stack offset), permuter-confirmed from both basins, and one draft covers four instances. The residual is a cse1 elision-count fact (target 16 orphan slots, draft 12), not an allocator or spelling one.

The process lesson, which is the expensive part: §147's "stop searching" verdicts parked func_8017C294 AND held back its 15 siblings pending an explanation of a stratum that does not exist. A confident negative verdict in this cookbook is a claim like any other — date it, name the evidence, and re-measure it before letting it park work. (Same shape as §146: re-measure a wall before you respect it.)