diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 062476fec9..09b0a4a476 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -29782,3 +29782,68 @@ into the CURRENT file under the lock; a block that moved on disk since the calle conflict and is refused (R43). `harvest_verify` snapshots/restores only its binary's block. The gate's parallelism (`-j N`) was the amplifier: every whole-file write or restore of overlays.mk by one binary's gate clobbered another's. + +## §306 — A HAZARD `nop` IN FRONT OF A DIV-RESULT STORE IS A STATEMENT-ORDER DEFECT: THE INDEPENDENT TRAILING STATEMENT MUST BE WRITTEN *BEFORE* THE DIVISION-CONSUMING ONE (P31 S62 T4; byte-proven func_8017E7D0) + +**THE TELL.** A `LENGTH-DRIFT +1` whose extra instruction is a bare `nop` sitting immediately before the `sw` that stores a division result, while an adjacent, data-independent global store (`lui $at,%hi(G)` / `sh $zero,%lo(G)($at)`) appears AFTER that `sw` in your draft and BEFORE it in the target: + + mine: … mflo $a0 ; nop ; sw $a0,0x48($s0) ; lui $at ; sh $zero,%lo(G)($at) + target: … mflo $a0 ; lui $at ; sh $zero,%lo(G)($at) ; sw $a0,0x48($s0) + +Do not touch registers, pins, barriers or the permuter — the whole residual is one statement in the wrong place. + +**THE MECHANISM (two halves; the second is source-verified).** (1) cc1 sees `div` as ONE insn carrying the long `imuldiv` latency, so its consumer store is not ready for many cycles and the scheduler fills the shadow — but only from insns already available in LUID (= source) order. An independent statement written BEFORE the division statement sinks into that shadow; one written AFTER it is not pulled back into it. (2) The visible `nop` is **not gcc's** — maspsx splices it: after the `--expand-div` expansion, `_handle_nop_before_next_instruction` (`tools/maspsx/maspsx/__init__.py:642-675`, called at `:1137`) emits `nop # DEBUG: Reuse of ''` whenever the instruction following the expanded `mflo` reads the quotient register. So an empty shadow costs exactly one instruction, and *any* insn that does not read the quotient kills it. **Corollary (source-read, not A/B'd):** that same rule exempts a next instruction which uses `$at` while `nop_at_expansion` is false — which our pinned `--aspsx-version=2.56` gives (`maspsx.py:99-104`) — so a division result stored *straight to a global* through `lui $at` / `%lo(SYM)($at)` never pays this nop at all. + +**THE C SHAPE.** Swap the two independent trailing statements so the one with NO dependence on the division comes first: + +```c +/* target order — shadow filled, no nop (54 ins) */ +D_8019F70C = 0; +*(s32 *)(a0 + 0x48) = -D_80188A34 / ((s16)D_80188A2A[0] / 2); + +/* wrong order — empty shadow, maspsx nop (55 ins) */ +*(s32 *)(a0 + 0x48) = -D_80188A34 / ((s16)D_80188A2A[0] / 2); +D_8019F70C = 0; +``` + +**BYTE EVIDENCE.** `func_8017E7D0` (ov_SC06_016, `_jr_8017C8D0`, 54 ins). Target tail `asm/ov_SC06_016/nonmatchings/ov_SC06_016_jr_8017C8D0/func_8017E7D0.s:50-53` = `mflo $a0 / lui $at,%hi(D_8019F70C) / sh $zero,%lo(D_8019F70C)($at) / sw $a0,0x48($s0)`, under the signed `--expand-div` form (`break 7` / `break 6`, §228-3). v1 (division store first): `mine=55, target=54, 10 mismatched, class LENGTH-DRIFT [structural]`, first mismatch `idx45: 00000000 nop | 3c01801a lui $at,%hi(D_8019F70C)`. v2 = the same file with ONLY those two statements swapped: `MATCH (54 ins)`; `--json` verify `{"status":"match","closeness":0,"nins":54,"residual":[]}`. One function, both directions measured. + +*(Extends **§16Xb** (L13142) — the `mult`→`mflo` window read at the div's shadow: §16Xb's tell is a ZERO-drift reorder and its lever is "move the statement that FOLLOWS the multiplying statement"; here the drift is +1 and the filler comes from the statement BEFORE, so §16Xb's prescription points the wrong way. Instance of **§2-T2** (L78). Kin of the **§253/§165-06** note (L25420: postfix `(*p)++` keeps the store after the `mfhi`/`bnez` pair, bare `++` sinks it before the div) and of **§50-E** (L3616), which prices global-store REORDER the other way (an assembler-merged `lui $at`). **§179-B rule 4** (L17393) owns this same maspsx nop splicer for hand-written asm; this entry is its C-side face.)* + +## §306a — T4 DISTILL ADDENDA (P31 S62; four byte-proven refinements to existing laws, verified against the book by an independent verifier; each names its parent §) + +**→ parent: §255 "AND CASE-BODY PLACEMENT"** + +**Addendum (P31 S62 T4, func_800CAF9C):** third measured exemplar, and it lands at the *smallest tree that has a root* — the regime this paragraph's DFS sentence was written from. `func_800CAF9C` (md_MAIN_015, 105 ins, byte-gate green) dispatches `{0x41, 0x53, 0x73}`; `balance_case_nodes`' `i == 3` arm roots on the median, so `beq $v1,0x53` is the FIRST test (`asm/md_MAIN_015/nonmatchings/md_MAIN_015/func_800CAF9C.s` @800CAFD4) — yet **the root's body sits SECOND**: case 0x41's body is `.L800CB008`, the shared `case 0x53: case 0x73:` body is `.L800CB034`. DFS-root-first is false here, and §199-G (L21011) says why it must be: `expand_end_case` takes `before_case = get_last_insn()` *after* every body is already in the stream (`stmt.c:4749`) and `reorder_insns` (`stmt.c:5054-5056`) hoists only the *header* in front of them — no pass ever permutes bodies. Read ADD-9's "two regimes" as one law plus misread ascending-order exemplars: **body order is source-clause order, always**; what survives of §255's sentence is its other half, that physical arm order does not name case VALUES. +**Byte evidence.** Draft 1 spelled `case 0x53: case 0x73:` before `case 0x41:` — the compare chain (`lbu`/`beq 0x53`/`slti 0x54`/`beq 0x41`/`j`) was byte-identical through index ~9, then a total cascade from the first body onward: `near`, closeness 71, **47 of 104** residual. The ONLY edit was moving the two clauses (0x41 first); draft 2 → MATCH, 105/105, `residual: []`. **Diagnostic tell:** a residual that begins *exactly* at the first case body while the dispatch chain is already byte-clean ⇒ reorder the case CLAUSES in source; do not touch polarity, empty cases, or the tree. **⚠ Bound:** this instance alone cannot separate source order from *ascending* order (0x41 < 0x53) — ADD-9's `func_8017F328` swap probe is what discriminates; this card's contribution is killing root-first at 3 nodes. + +**→ parent: §16Xy** + +**Addendum (P31 S62 T4, func_8017FB38):** Fourth byte-instance, and a new cell — the cast is **narrower than the load**: `lh $v0,0x2C($a0) ; beqz ; addu $a0,$v0,$zero ; andi $a0,$a0,0xFF ; jal func_80016450` (0x8017FBC8, ov_SC07_000). §16Xy's table carries `(u16)`-on-`s16` and `(u8)`-on-`s8`; the mixed `(u8)`-on-an-`lh` cell behaves identically, so read the law as *narrow memory load + narrower-or-opposite-signed cast at an SImode use*, not as a width match. +**The crack re-derived §16Xy's ablation row for row without finding it** — `v0 & 0xFF` → `andi $a0,$v0,0xff`, no copy (closeness 10, residual [36]); `u8 t = v0;` → neither instruction, residual `nop`; only the two-instruction form matches. It then reached MATCH by the expensive road: `register s32 a0r __asm__("$4")` plus `__asm__("" : "=r"(a0r) : "0"(a0r))` between the copy and the mask. **That launder is §165-04/§165-35's instrument** (the `"=r"`/`"0"` pair forces the value through an SImode register operand, blocking combine's fold into the `lh`) — whose "honest scope: ONE A/B pair, body later abandoned" note now has its second byte-proven instance. So the transcript's "mechanism unknown at the RTL-pass level" is answered by §165-03: the stranded SImode extension is a conflict-free orphan pseudo whose preferred class converges to `ST_REGS`, and `alter_reg` slots it. +**Route to the declaration first, not the pin.** `s16 s = *(s16*)(a0+0x2C); if (s != 0) func_80016450((u8)s, 1);` buys the same pair with no `register __asm__` — which forfeits the family (§37/§162p3) and, per §164-49, can sell you a schedule. The pin here was **never solo-ablated** against the barrier (§266), so cite the barrier and treat `$4` as an unproven rider. +**Frame reconciliation — first function needing §16Xy's and §167-10's counters SUMMED.** Target `.frame` is 0x30 with `sw $ra,0x28` and args=16 ⇒ `vars = 24`, with zero `$sp` references anywhere in the body: three narrow-copy sites × 8 — two §167-10 compare-then-re-read-and-store-back copies (`addu $v1,$v0,$zero` at 0x8017FB58 and 0x8017FBA0) plus this §16Xy cast-to-call copy. The draft hand-shipped `frame_pad[6]` for exactly those 24 bytes. **Falsifiable prediction:** spell all three sites as `s16` locals and the 24 bytes mint themselves — the pad then over-shoots to 0x48 and FAILs, the §162i1 footgun §167-10 already names. +*Index gap:* `cookbook-index.md` L19 routes "`andi` folded away in your output but present in the target" to §1/I2 + §12 only. When the missing instruction is the **copy** and not the mask, the route is §16Xy → §165-02 → §167-10. + +**→ parent: §176-F row 3** + +**Addendum (P31 S62 T4, func_8017FB38):** row 3's tell reproduced verbatim on a second, independent function — `ov_SC07_000:func_8017FB38`, `IMM-OFFSET/6`, closeness 1, again **44 instructions**, again a lone `beqz $v0` whose *only* difference is the local-label immediate (mine `0x10400008` vs target `0x1040000E`; target asm `asm/ov_SC07_000/nonmatchings/ov_SC07_000_jr_8017BEBC/func_8017FB38.s:30`, the `beqz $v0,.L8017FBD8` at `8017FB9C`). + +**The dropped conditional edge has a SECOND C-level cause, and its fix is the mirror of row 3's.** Row 3's cause was a trailing statement written *after* a guard's closing brace (fix: move it in). This one is a **missing early `return;`**: the `t == 0` path was left to fall through into a later, logically-redundant `if (field2C != 0) { func_80016450(field2C & 0xFF, 1); }` instead of terminating, so that shared guard — not the epilogue — became what the `beqz` reaches. Spelling the bail-out explicitly, `} else { t = field2C; if (t == 0) { return; } t -= 0x10; … }`, gave **MATCH 44/44** with every other byte unchanged. + +**Read the SIGN of the immediate; it names the defect.** `match_one` emits residuals as `[i, mine, target]` (`tools/match_one.py:222`), so the two displacements are directly comparable. **Mine NEARER than the target's ⇒ my C falls into a downstream block the target's C skips — add the `return;`.** **Mine FARTHER ⇒ I terminate a path the target lets fall through — drop the terminator, or move the trailing statement inside the guard (row 3).** And the `delta` is a ruler, not noise: it is the instruction distance between the two candidate landing points (here 6, `+0x88` *inside* the shared guard → `+0xA0` the epilogue), so you can count straight to the block in question instead of searching the `.s`. + +**⚠ Do NOT bank the strong form "gcc-2.7.2 never merges or elides a later guard whose earlier predecessor would also fail it."** The bytes refute it here: the fall-through draft did not land on the shared guard's *label*, it landed one instruction **inside** it (`+0x88`, past the `lh $v0,0x2C($a0)` reload jump1 knew was redundant given `$v0 == 0`) — which is exactly why the delta is 6 and not the guard block's full 7. The law is about **which block the edge enters**, not about guard elision. (Complements §225-2 / §225-8, which put early-return-vs-`if`-block on the prologue/epilogue and `j`-vs-branch axes with *large* residuals; this axis is the count-neutral one. §176-G's bound still applies: the same edge error is silent when the arm exit is a `j`.) + +**→ parent: §211** + +**Addendum (P31 S62 T4, func_8017F608):** §211's half **(b)** — *"hoisting it above the guard lengthens the pseudo's live range across the guard block, flipping the local-alloc contest"* — fires with **no loop, no induction pseudo, and no instruction-count or delay-slot change at all**, so §211's own BOUNDARY ("it only bites when the guard/branch actually has a fillable slot and the two induction pseudos actually contend") is too narrow: the contest can be a single **constant** against local-alloc's generic-temp default. `func_8017F608` (ov_SC02_028, `jr_8017D898`, 99 ins) sat at closeness 2, `REGALLOC-PERM/$v0>$a0`, on exactly one pair — mine `li $v0,0x600` / `sh $v0,0x10($sp)` vs target `addiu $a0,$zero,0x600` / `sh $a0,0x10($sp)` — with §211's half (a) **already satisfied** (both builds put the `addiu` in the guard's own delay slot; only the colour differed). The fix: hoist the clamp constant into a named local **above** the guard and make the guard test the local, not the literal — `s16 clampval = 0x600; if (sp10.v[0] < clampval) { sp10.v[0] = clampval; }` → `match`, 99/99, residual `[]`; the stuck form declared the local *inside* the taken arm and tested the raw literal (closeness 2). +**THE TELL IS IN THE TARGET, NOT IN YOUR DIFF.** Read the *neighbouring* guard's delay slot: `asm/ov_SC02_028/nonmatchings/ov_SC02_028_jr_8017D898/func_8017F608.s` holds `8017F680 addu $a0,$v1,$zero` (outer `slti …,0x700` slot) and `8017F68C addiu $a0,$zero,0x600` (inner `slti …,0x600` slot) — the constant **re-defines the same register an earlier guard's slot already loaded**, which is one named local with two defs spanning both guards, never a store-time temp. A fresh temp takes `$v0` by local-alloc's copy-suggestion default (§186c, as corrected by §194-F). This is §208's pseudo-set principle run in the **merge** direction (§208 splits one expression into two locals to buy two registers; this merges a compare operand and a store source into one local to buy one register), and it is a second counter-example to §137's "source-level levers are a dead end for REGALLOC-PERM": the edit changes the pseudo SET, which §137's R/L arithmetic cannot see. +**BOUNDS (read these before quoting the recipe).** (1) The A/B moved **two things at once** — declaration position *and* the compare's operand — so per §266 neither half is independently citable; probe decl-above-with-literal-test before treating "test the local, not the literal" as the load-bearing clause. (2) The rematerialisation story is the drafter's hypothesis, not read out of a `-dl`/`-dg` dump; what is byte-proven is the source edit → MATCH. (3) The drafter's side claim that the same hoist "also flipped several BRANCH-POLARITY residuals earlier in the same draft" is unquantified and unverified — treat as a lead, not a law. (4) **Tried and failed, worth as much:** `register s32 clampval __asm__("$4")` on the same body REGRESSED 2 → 62 with **+1 ins** (99 → 100) — another row for §257/§268's pin ledger, and a straight confirmation of §176-B: on a 2-instruction REGALLOC-PERM, reach for the naming/scope lever first and the pin never. + +**Harness note banked with these (P31 S62 T4).** 22 of 60 probe agents reported that the pack's +warm-start body was a DIFFERENT function: drafts are stored by function name, and the same name at the +same address in another overlay is usually unrelated code. `api_agent.prior_draft` now accepts a +candidate only when the symbols it references overlap the target `.s`'s relocations (law 1c); the +haiku arm re-run on its misses with the filtered packs banked 2 more. Read "warm start" as "a body +whose symbols are this .s's symbols", never as "a body with this name".