From 6bb3d240fabe19c2c7ef5b1a0290390f841bc5d4 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:44:13 -0600 Subject: [PATCH] =?UTF-8?q?feat(p31=20s67):=20harvest=20=C2=A7325-=C2=A733?= =?UTF-8?q?1=20+=20gate=5Fwave.py=20(split=20jtbl/parallel,=20both=20lanes?= =?UTF-8?q?=20concurrent)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HARVEST — the s67o1/s67m1 wave banked 7 cookbook sections: * §325 a shared small constant stored twice in the pre-loop block is a LOCAL-ALLOC $s-occupant that steals the argument allocno's register — pin the ARGUMENT-derived local, not the constant (pinning the constant reached only closeness 15). byte-proven func_80184F18. * §326 spelling two reads of the same halfword differently (sym[i] vs *(s16*)(base+i*4+2)) yields different address rtx and DEFEATS address-CSE, restoring separate %hi/%lo groups. func_8017FAAC. * §327 a range test must be HImode: with s32 + a (u16) cast gcc PROVES the mask redundant and drops the andi — a real -1 length drift that reads as a schedule. +3 levers. func_8017EC34. * §328 NEW LAW: the volatile alias must be an aliased OBJECT; `*(volatile s32*)&sym` unfolds %lo into a separate addiu (+1 ins). func_80181B8C. * §329 fold-const narrows `(int)s16 & 0xFFF` onto the RAW HImode pseudo, breaking the sign-extend/mask register tie; a zero-byte `s32 e = t;` widening temp restores it (30 rows -> 0). * §330 the NEIGHBOUR-SHAPE lever, four independent instances in one wave — copy an already-banked in-TU function's SPELLING before any codegen reasoning (one dissolved 18 REGALLOC-PERM rows in a single compile). Corollary: a warm start from another binary is often worth LESS than the neighbour 20 lines away. * §331 OPEN GAP, recorded as unsolved: no lever eliminates an UNWANTED DUPLICATE copy at a branch-target block head (main/func_80013154, closeness 12, ~16 iterations, 5 approaches refuted). TOOLIFY — tools/gate_wave.py: split the batch on the per-draft jtbl predicate, run parallel_gate and the serial jtbl lane CONCURRENTLY. Measured this session: 4 binaries in 103s wall through parallel_gate (87/87/88/102s each) vs ~6 min serially; I had gated all 16 serially to protect ONE jtbl draft, ~1 hour. The split precedes the run because a jtbl worker does NOT fail cleanly — it re-extracts through the worktree's asm/ symlink and writes the MAIN tree while other workers read it. Its own negative control found two defects in it before first use: * listdir counted gate_stage's _xform output dirs (-cn/-cast/-rc/-sd, written as SIBLINGS inside the drafts root) as binaries: 20 "binaries" for a 16-binary wave. Now validated against progress.BINARIES and refused loudly (R32/R43). * a post-hoc control over BANKED functions cannot reproduce a split (has_jtbl has no stub to read); re-controlled against a live draft set, where it correctly routes the two functions the gate had independently reported CARVE-REFUSED. --- docs/cookbook-index.md | 44 +++++++--- docs/matching-cookbook.md | 84 +++++++++++++++++++ tools/gate_wave.py | 165 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 tools/gate_wave.py diff --git a/docs/cookbook-index.md b/docs/cookbook-index.md index bfda58fc10..86024c00c8 100644 --- a/docs/cookbook-index.md +++ b/docs/cookbook-index.md @@ -2,7 +2,7 @@ > **Generated by `tools/cookbook_index.py` — do not hand-edit** (R33). Regenerate after adding a cookbook section. > -> `docs/matching-cookbook.md` is ~716 KB / 972 sections. Grepping it blind is how three P30 wave-1 agents each "discovered" an idiom that was already written down. **Start here, then read the section.** A section appears under every symptom it addresses. +> `docs/matching-cookbook.md` is ~716 KB / 979 sections. Grepping it blind is how three P30 wave-1 agents each "discovered" an idiom that was already written down. **Start here, then read the section.** A section appears under every symptom it addresses. **How to use:** name what you SEE in the diff (a stolen delay slot, an extra `la`, a swapped register pair, a `conflicting types` error), find that symptom below, read those sections first. If nothing fits, THEN grind — and add a section when you win. @@ -34,7 +34,7 @@ ## By symptom -### delay slots & branches (58) +### delay slots & branches (59) - **§3-T4** — Branch polarity: invert the source condition to flip gcc's chosen branch L90 - **§5a** — Cross-jump tail-merge — gcc collapses two byte-identical blocks the original kept separate (FIX FOUND) L211 @@ -94,6 +94,7 @@ - **Addendum** — REGALLOC-PERM store in a cross-jump shared tail defeats §137's barrier (func_8017EDE8) L30630 - **Addendum** — LENGTH-DRIFT nop clears when offset math precedes the symbol launder (func_80039B20) L30634 - **Addendum** — Return-0 statement order: extra j+move vs delay-slot fusion (func_8018BB50) L30721 +- **§331** — OPEN GAP: NO LEVER ELIMINATES AN *UNWANTED DUPLICATE* COPY AT A BRANCH-TARGET BLOCK HEAD (P31 S67; main/func_80013154, closeness 12, NOT solved) L31059 ### instruction scheduling (70) @@ -168,7 +169,7 @@ - **Addendum** — Volatile zero-byte asm slider scrambles the prologue schedule (func_801814B0) L30747 - **Addendum** — §194-A addendum — the bare/colon-less fence measured NULL and only the "memory"-clobber fo (func_8017E464) L30761 -### register allocation & pins (115) +### register allocation & pins (117) - **§10** — Closing the regalloc/scheduling hard tail by hand (LZSS, Phase 7 session F — the full close) L854 - **Residual** — A — commutative `|`/`&`/`+` result lands in the wrong source-operand register L875 @@ -285,8 +286,10 @@ - **Addendum** — LENGTH-DRIFT −1 on a coalesced-away copy: a CALLER-SAVED SOURCE pin can resurrect it, boun (func_8017D72C) L30714 - **Addendum** — The dying-pinned-register reuse on a sign-extend chain: route every later read through a s (func_80186868) L30743 - **Addendum** — Register-pinned helper pointer local regresses closeness; use plain local (func_80013CFC) L30757 +- **§325** — REGALLOC: A SHARED SMALL CONSTANT STORED TWICE IN THE PRE-LOOP BLOCK IS A *local-alloc* $s-OCCUPANT THAT STEALS THE ARGUMENT ALLOCNO'S REGISTER — PIN THE ARGUMENT-DERIVED LOCAL, NOT THE CONSTANT (P31 S67; byte-proven ov_SC03_119/func_80184F18, 153 ins) L30987 +- **§329** — fold-const NARROWS `(int)s16_var & 0xFFF` ONTO THE *RAW HImode PSEUDO*, BREAKING THE SIGN-EXTEND/MASK REGISTER TIE — A ZERO-BYTE WIDENING TEMP RESTORES IT (P31 S67; byte-proven ov_SC01_084/func_80183244, 157 ins) L31034 -### CSE / redundancy / rematerialization (33) +### CSE / redundancy / rematerialization (36) - **§46** — The `func_80178D40` crack (890 ins ×134, the heaviest core in the game): four LOOP-STRUCTURE levers cheap-Opus found by reading loop.c/jump.c/cse.c (Phase 26 session 8, 2026-07-13) L3347 - **§83d** — CSE's quantity budget is WHOLE-FUNCTION, so a local rewrite cannot fix a local symptom L6486 @@ -321,8 +324,11 @@ - **REFUTED** — claim — REFUTED — the `j`-slot store is an ASPSX macro-expansion hop, not a cse split; maspsx hard (func_8005DBD8) L30589 - **§NNN** — REFUTED: A `sw %lo(SYM)($at)` IN A JUMP'S DELAY SLOT WITH ITS `lui $at` STRANDED ABOVE THE JUMP IS **ASSEMBLER** MACRO-EXPANSION, NOT A cse SPLIT — OUR PIPELINE CANNOT EMIT IT FROM ANY C, AND THE +1 IS A `maspsx` GAP (P31 S66 round2, `func_8005DBD8`, byte-probed) L30591 - **Addendum** — Volatile zero-byte asm slider scrambles the prologue schedule (func_801814B0) L30747 +- **§326** — DEFEATING ADDRESS-CSE: SPELL TWO READS OF THE SAME HALFWORD DIFFERENTLY AND GCC CANNOT SHARE THE ADDRESS (P31 S67; byte-proven ov_SC01_077/func_8017FAAC, 154 ins) L31000 +- **§327** — A RANGE TEST MUST BE HImode: WITH `s32` + A `(u16)` CAST GCC *PROVES* THE MASK REDUNDANT AND DROPS THE `andi` (P31 S67; byte-proven ov_SC02_039/func_8017EC34, 154 ins) L31009 +- **§328** — THE VOLATILE ALIAS MUST BE AN *OBJECT*, NOT A CAST: `*(volatile s32*)&sym` UNFOLDS %lo INTO A SEPARATE `addiu` (P31 S67; byte-proven ov_SC07_007/func_80181B8C, 156 ins, NEW LAW) L31024 -### loops & induction variables (33) +### loops & induction variables (34) - **§3-T1** — Loop pointer: top-of-body for `addu` induction, not constant-folded `addiu` L71 - **§34** — The `func_80138ED0` giant crack: gcc-2.7.2's **3-qty sort bug** + the **zero-byte asm allocation toolkit** + the **giv-init fence** (Phase 24 T5; Opus→close=21, Fable5→MATCH ×134) L2473 @@ -357,6 +363,7 @@ - **§297** — ONE GIV SERVING MIXED-WIDTH LOADS *AND* A STORE NEEDS STRUCT-MEMBER SPELLING; CAST/INDEX SPELLINGS RE-SPLIT THE PSEUDO (P31 S61; wave m0a, `func_800347C8`, main, byte-proven 31/31) L29499 - **§312** — A BARE RELATIONAL IN AN `if` GIVES THE `slt` NO TARGET: ASSIGN THE COMPARISON INTO THE ALREADY-ALLOCATED VARIABLE TO CHOOSE ITS DESTINATION REGISTER, AT ZERO INSTRUCTION COST (P31 S64 t5o-t5r; byte-proven func_80180DCC) L30159 - **Addendum** — Loop sentinel wrongly hoisted out of a call-containing loop (func_801835B0) L30493 +- **§325** — REGALLOC: A SHARED SMALL CONSTANT STORED TWICE IN THE PRE-LOOP BLOCK IS A *local-alloc* $s-OCCUPANT THAT STEALS THE ARGUMENT ALLOCNO'S REGISTER — PIN THE ARGUMENT-DERIVED LOCAL, NOT THE CONSTANT (P31 S67; byte-proven ov_SC03_119/func_80184F18, 153 ins) L30987 ### structs, block moves & memcpy (78) @@ -439,7 +446,7 @@ - **Addendum** — Masked-OR field-merge plateaus at close=10; bitfield store clears it (func_8017FD64) L30700 - **§321** — FILE-SCOPE DUPLICATE ANONYMOUS-STRUCT TYPEDEFS ARE A HARD ERROR; THE SAME TEXT AT BLOCK SCOPE IS A WARNING (P31 S66; byte-proven func_80180728, func_8017F9F8, func_8017E07C) L30845 -### types, signedness & load/store width (84) +### types, signedness & load/store width (86) - **§3-I1** — Unsigned range check: `(x - lo) < (hi-lo)` → `addiu`+`sltiu` L41 - **§3-I2** — Byte mask forces `andi` even after `lbu` L47 @@ -525,6 +532,8 @@ - **§320** — THE §43 "RETURN-TYPE FLIP PAIR" IS **NOT** TU-EDIT-REQUIRED: THREE DRAFT-ONLY ESCAPES (P31 S66; byte-proven func_800CCBC0, func_800D30D0, func_800D2A24) L30786 - **§321** — FILE-SCOPE DUPLICATE ANONYMOUS-STRUCT TYPEDEFS ARE A HARD ERROR; THE SAME TEXT AT BLOCK SCOPE IS A WARNING (P31 S66; byte-proven func_80180728, func_8017F9F8, func_8017E07C) L30845 - **§323** — CARRYING FILE-SCOPE TYPES BETWEEN SPLIT TUs: DEDUPE BY NAME, SKIP HEADER-PROVIDED, AND THE GUARD TRAVELS WITH THE BLOCK (P31 S67; ov_SC02_000, two defects fixed, one open) L30896 +- **§327** — A RANGE TEST MUST BE HImode: WITH `s32` + A `(u16)` CAST GCC *PROVES* THE MASK REDUNDANT AND DROPS THE `andi` (P31 S67; byte-proven ov_SC02_039/func_8017EC34, 154 ins) L31009 +- **§328** — THE VOLATILE ALIAS MUST BE AN *OBJECT*, NOT A CAST: `*(volatile s32*)&sym` UNFOLDS %lo INTO A SEPARATE `addiu` (P31 S67; byte-proven ov_SC07_007/func_80181B8C, 156 ins, NEW LAW) L31024 ### declarations, prototypes & K&R (101) @@ -888,7 +897,7 @@ - **§323** — CARRYING FILE-SCOPE TYPES BETWEEN SPLIT TUs: DEDUPE BY NAME, SKIP HEADER-PROVIDED, AND THE GUARD TRAVELS WITH THE BLOCK (P31 S67; ov_SC02_000, two defects fixed, one open) L30896 - **§324** — THE ARITY-SELF WALL NEEDS *BOTH* HALVES: A K&R DEFINITION **AND** A NO-PROTO TU DECL; EITHER ALONE STILL FAILS (P31 S67; rtu-proven ov_SC01_005/func_8017FBCC, 14 instances measured) L30949 -### build graph, splat & the harness (175) +### build graph, splat & the harness (176) - **§4** — Flag/toolchain gotchas L190 - **Build** — mechanism — per-file opt override (splat resegmentation) L307 @@ -1065,8 +1074,9 @@ - **Addendum** — Addendum to §167-12 — the single-operand volatile keepalive, re-confirmed at a gated MATCH (func_8017F498) L30485 - **§NNN** — REFUTED: A CODE BLOCK SHARED BY TWO *DIFFERENT* SWITCH STATEMENTS IS ORDINARY FORWARD `cross_jump`, NOT A SHAPE C CANNOT REACH — §164-11 ALREADY BANKS IT FROM PLAIN NESTED SWITCHES (P31 S66 round3, `func_8001B0D4`; ⚠ **UNPROVEN** — gate-REFUSED draft, closeness 68→45, never MATCHed) L30640 - **§323a** — R53, TWICE IN ONE HOUR: A FAILED BUILD LEAVES THE PREVIOUS BINARY, AND `sha1sum` READS GREEN (P31 S67) L30926 +- **§328** — THE VOLATILE ALIAS MUST BE AN *OBJECT*, NOT A CAST: `*(volatile s32*)&sym` UNFOLDS %lo INTO A SEPARATE `addiu` (P31 S67; byte-proven ov_SC07_007/func_80181B8C, 156 ins, NEW LAW) L31024 -### process, measurement & doctrine (116) +### process, measurement & doctrine (117) - **§8e** — The jtbl ALIGNMENT LAW + the pad-spec filter — multi-table .rodata spans (Phase 29, byte-proven; `.run/probe_jtbl/verdict.md`) L549 - **§3-The** — mechanism: game-code dedup is SOURCE-LEVEL, not an object swap (R-D1, the key lesson) L945 @@ -1184,8 +1194,9 @@ - **§322** — A PROBE THAT ANSWERS A *NECESSARY BUT NOT SUFFICIENT* QUESTION WILL PRICE BLOCKED WORK AS FREE: RUN THE REAL PLANNER WHEN THE PLANNER IS PURE (P31 S67; measured, 96 of 159 open jtbl functions) L30866 - **§323a** — R53, TWICE IN ONE HOUR: A FAILED BUILD LEAVES THE PREVIOUS BINARY, AND `sha1sum` READS GREEN (P31 S67) L30926 - **§324** — THE ARITY-SELF WALL NEEDS *BOTH* HALVES: A K&R DEFINITION **AND** A NO-PROTO TU DECL; EITHER ALONE STILL FAILS (P31 S67; rtu-proven ov_SC01_005/func_8017FBCC, 14 instances measured) L30949 +- **§328** — THE VOLATILE ALIAS MUST BE AN *OBJECT*, NOT A CAST: `*(volatile s32*)&sym` UNFOLDS %lo INTO A SEPARATE `addiu` (P31 S67; byte-proven ov_SC07_007/func_80181B8C, 156 ins, NEW LAW) L31024 -### (unbucketed — title matched no symptom vocabulary) (296) +### (unbucketed — title matched no symptom vocabulary) (297) - **§3-How** — to use this L30 - **§1** — Idiom catalog (asm pattern → C that produces it) L39 @@ -1483,6 +1494,7 @@ - **Addendum** — Integer-space address arithmetic is a THIRD no-movable spelling, and a distant `SYM[0]` re (func_8017D89C) L30704 - **RETRIEVAL** — FAILURES this round L30765 - **§323b** — A SCRIPT THAT PARSES argv AT IMPORT CANNOT BE SHARED; EXTRACT THE PREDICATE, DO NOT COPY IT (P31 S67) L30935 +- **§330** — THE NEIGHBOUR-SHAPE LEVER IS THE CHEAPEST FIRST MOVE, AND IT IS UNDER-USED (P31 S67; four independent instances in one 20-function wave) L31042 ## All sections, in order @@ -2459,6 +2471,13 @@ - **§323a** — R53, TWICE IN ONE HOUR: A FAILED BUILD LEAVES THE PREVIOUS BINARY, AND `sha1sum` READS GREEN (P31 S67) L30926 - **§323b** — A SCRIPT THAT PARSES argv AT IMPORT CANNOT BE SHARED; EXTRACT THE PREDICATE, DO NOT COPY IT (P31 S67) L30935 - **§324** — THE ARITY-SELF WALL NEEDS *BOTH* HALVES: A K&R DEFINITION **AND** A NO-PROTO TU DECL; EITHER ALONE STILL FAILS (P31 S67; rtu-proven ov_SC01_005/func_8017FBCC, 14 instances measured) L30949 +- **§325** — REGALLOC: A SHARED SMALL CONSTANT STORED TWICE IN THE PRE-LOOP BLOCK IS A *local-alloc* $s-OCCUPANT THAT STEALS THE ARGUMENT ALLOCNO'S REGISTER — PIN THE ARGUMENT-DERIVED LOCAL, NOT THE CONSTANT (P31 S67; byte-proven ov_SC03_119/func_80184F18, 153 ins) L30987 +- **§326** — DEFEATING ADDRESS-CSE: SPELL TWO READS OF THE SAME HALFWORD DIFFERENTLY AND GCC CANNOT SHARE THE ADDRESS (P31 S67; byte-proven ov_SC01_077/func_8017FAAC, 154 ins) L31000 +- **§327** — A RANGE TEST MUST BE HImode: WITH `s32` + A `(u16)` CAST GCC *PROVES* THE MASK REDUNDANT AND DROPS THE `andi` (P31 S67; byte-proven ov_SC02_039/func_8017EC34, 154 ins) L31009 +- **§328** — THE VOLATILE ALIAS MUST BE AN *OBJECT*, NOT A CAST: `*(volatile s32*)&sym` UNFOLDS %lo INTO A SEPARATE `addiu` (P31 S67; byte-proven ov_SC07_007/func_80181B8C, 156 ins, NEW LAW) L31024 +- **§329** — fold-const NARROWS `(int)s16_var & 0xFFF` ONTO THE *RAW HImode PSEUDO*, BREAKING THE SIGN-EXTEND/MASK REGISTER TIE — A ZERO-BYTE WIDENING TEMP RESTORES IT (P31 S67; byte-proven ov_SC01_084/func_80183244, 157 ins) L31034 +- **§330** — THE NEIGHBOUR-SHAPE LEVER IS THE CHEAPEST FIRST MOVE, AND IT IS UNDER-USED (P31 S67; four independent instances in one 20-function wave) L31042 +- **§331** — OPEN GAP: NO LEVER ELIMINATES AN *UNWANTED DUPLICATE* COPY AT A BRANCH-TARGET BLOCK HEAD (P31 S67; main/func_80013154, closeness 12, NOT solved) L31059 --- @@ -3443,3 +3462,10 @@ Notes routinely quote that as a section id. This table resolves it. Grep bait: ` | L30926 | §323a | R53, TWICE IN ONE HOUR: A FAILED BUILD LEAVES THE PREVIOUS BINARY, AND `sha1sum` READS GRE | | L30935 | §323b | A SCRIPT THAT PARSES argv AT IMPORT CANNOT BE SHARED; EXTRACT THE PREDICATE, DO NOT COPY I | | L30949 | §324 | THE ARITY-SELF WALL NEEDS *BOTH* HALVES: A K&R DEFINITION **AND** A NO-PROTO TU DECL; EITH | +| L30987 | §325 | REGALLOC: A SHARED SMALL CONSTANT STORED TWICE IN THE PRE-LOOP BLOCK IS A *local-alloc* $s | +| L31000 | §326 | DEFEATING ADDRESS-CSE: SPELL TWO READS OF THE SAME HALFWORD DIFFERENTLY AND GCC CANNOT SHA | +| L31009 | §327 | A RANGE TEST MUST BE HImode: WITH `s32` + A `(u16)` CAST GCC *PROVES* THE MASK REDUNDANT A | +| L31024 | §328 | THE VOLATILE ALIAS MUST BE AN *OBJECT*, NOT A CAST: `*(volatile s32*)&sym` UNFOLDS %lo INT | +| L31034 | §329 | fold-const NARROWS `(int)s16_var & 0xFFF` ONTO THE *RAW HImode PSEUDO*, BREAKING THE SIGN- | +| L31042 | §330 | THE NEIGHBOUR-SHAPE LEVER IS THE CHEAPEST FIRST MOVE, AND IT IS UNDER-USED (P31 S67; four | +| L31059 | §331 | OPEN GAP: NO LEVER ELIMINATES AN *UNWANTED DUPLICATE* COPY AT A BRANCH-TARGET BLOCK HEAD ( | diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index b2fdfc1f6a..5f2d0a2b5c 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -30983,3 +30983,87 @@ wall ledger, 8-20 → long-budget permuter. Do not price this rung as 14 banks. **BLAST RADIUS.** The TU-side half edits shared-within-binary source, so it needs the same journal/revert discipline as the arity pre-pass: keep the no-proto only where it bought a match, revert it everywhere else, and let the whole-binary gate arbitrate (G3/P9). + +## §325 — REGALLOC: A SHARED SMALL CONSTANT STORED TWICE IN THE PRE-LOOP BLOCK IS A *local-alloc* $s-OCCUPANT THAT STEALS THE ARGUMENT ALLOCNO'S REGISTER — PIN THE ARGUMENT-DERIVED LOCAL, NOT THE CONSTANT (P31 S67; byte-proven ov_SC03_119/func_80184F18, 153 ins) + +The twin gave the whole shape in one compile (147 -> 24 residual); the last 24 were a pure $s2/$s3 +swap that no pin-free dial reaches. WHY: a CSE'd HImode constant written twice in block 0 +(`desc.v[0].vz = 3` ×2) is a SINGLE-BLOCK, call-crossing quantity, so **local-alloc** assigns it a +callee-saved register BEFORE global-alloc runs, and `find_free_reg` always takes the first free one — +pushing the argument allocno from $s2 to $s3 and cascading. There is no 4th block-0 call-crossing +local that could die before the `$a2` copy is born, so no source-level reordering reaches it. + +**THE LEVER, AND ITS DIRECTION IS THE POINT:** pin the ARGUMENT-DERIVED local +(`register s32 u __asm__("$18")` on the uv-base local), NOT the constant. Pinning the constant to +$19 only reached closeness 15. §175's caller-saved-pin correctness trap does not apply to $s2. + +## §326 — DEFEATING ADDRESS-CSE: SPELL TWO READS OF THE SAME HALFWORD DIFFERENTLY AND GCC CANNOT SHARE THE ADDRESS (P31 S67; byte-proven ov_SC01_077/func_8017FAAC, 154 ins) + +The target keeps three separate `lui/addu/l*` $at groups where gcc wanted one shared base. Writing +one read as `D_8018A9B2[i]` and the other as `*(s16*)(D_8018A9B0 + i*4 + 2)` yields DIFFERENT address +rtx for the same location, so address-CSE cannot merge them and each keeps its own %hi/%lo pair. +Same function also needed §186: moving the `0x1A +=` before the `0x16 -= 0x40` put the if-arm's last +store in $v1 vs the else-arm's $v0, blocking the jump2 tail-merge that was costing 2 instructions +(cross-jump runs AFTER scheduling, so only a post-schedule register difference blocks it). + +## §327 — A RANGE TEST MUST BE HImode: WITH `s32` + A `(u16)` CAST GCC *PROVES* THE MASK REDUNDANT AND DROPS THE `andi` (P31 S67; byte-proven ov_SC02_039/func_8017EC34, 154 ins) + +`s32 q` + `(u16)q` lets fold-const derive `q = 0x7F ± (s16 x >> 1)` and conclude the mask cannot +change the value, so the `andi` never emits — a REAL −1 length drift that reads like a schedule +difference and sends you hunting the scheduler. Declare `u16 q` and test `q >= 0x2F && q < 0x80`. +Three more levers from the same function, all reusable: +* **§194-H in-place AND.** `v1 &= 0x2000;` inside `if (v1 != 0)` — only the IN-PLACE form reuses the + hard reg, which is what lets dbr speculate it into the guard's delay slot. +* **§3-T2 chain order.** Put the LONG chain (`p->0xA += D_8019B5E6[i]`) BEFORE the short store + (`p->0x34 = s1+1`); sched1 re-swaps the two `sh`s itself, and the leading `lh` forces the genuine + nop delay slot (`may_trap_p` on a MEM blocks dbr) while freeing $a0 for the table value. +* **Build the 2nd argument INLINE in the call** with a `(u16)` cast. As a preceding statement + (`u16 code = ...; & 0xFFFF`) its sll/ori/or chain outranks `move $a0,$s1` in sched priority and + steals the beqz delay slot. + +## §328 — THE VOLATILE ALIAS MUST BE AN *OBJECT*, NOT A CAST: `*(volatile s32*)&sym` UNFOLDS %lo INTO A SEPARATE `addiu` (P31 S67; byte-proven ov_SC07_007/func_80181B8C, 156 ins, NEW LAW) + +Four identical `lui/lw D_801C79A0; j LDDC; addiu` tails needed TWO levers together: +(1) `register __asm__("$2")` pins on cases 0 and 3 — cc1 otherwise homes them in $v1, missing the +depth-2 merge onto `.L80181DDC` and deep-merging case 0 into case 3 at the shared `jal`; and +(2) an `__asm__`-aliased **volatile OBJECT** view of `D_801C79A0` for case 3's reload only, to block +the back-merge into case 1's identical tail. +**THE LAW:** the volatile must be a declared aliased object (`extern volatile s32 vD_x __asm__("D_801C79A0");`). +Spelling it `*(volatile s32*)&D_801C79A0` unfolds the %lo into a separate `addiu` and costs +1 ins. + +## §329 — fold-const NARROWS `(int)s16_var & 0xFFF` ONTO THE *RAW HImode PSEUDO*, BREAKING THE SIGN-EXTEND/MASK REGISTER TIE — A ZERO-BYTE WIDENING TEMP RESTORES IT (P31 S67; byte-proven ov_SC01_084/func_80183244, 157 ins) + +gcc-2.7.2 narrows the AND onto the raw HImode pseudo (`andi $s2,$a1`), so the sign-extended value and +the masked value land in different registers and the difference cascades — measured 27 register rows +plus 3 schedule rows from this one cause. FIX: `s32 e = t;` — a named SImode temp that forces ONE +sign_extend, so local-alloc coalesces the fraction onto $s1 (`andi $s1,$s1`) while `sh $a1,0xDC` +keeps the raw register. 30 residual rows -> 0. (The neighbour §194-E gave the TU shape first: 70->30.) + +## §330 — THE NEIGHBOUR-SHAPE LEVER IS THE CHEAPEST FIRST MOVE, AND IT IS UNDER-USED (P31 S67; four independent instances in one 20-function wave) + +Before any codegen reasoning, read an ALREADY-BANKED function in the same TU and copy its SPELLING, +not just its structure. Measured this wave: +* `md_MAIN_031/func_800CAE0C` — the neighbour 6 lines away contained the same 3-call body verbatim + (same symbols, same `(void*)` casts). Copied its call spelling -> MATCH on first compile. +* `ov_SC01_084/func_8017EF28` — the neighbour's memory-to-memory house form + (`*(u16*)(p+0xE2) = *(u16*)(p+0xE2) + 0x70`) replaced a reused `int v` local and dissolved ALL 18 + $v0/$v1 REGALLOC-PERM rows in ONE compile — no §137 arithmetic, no pins. +* `ov_SC01_084/func_80183244` — neighbour gave the exact TU shape, 70 -> 30, nins exact. +* `main/func_8002A088` + `func_8002A7B4` — mirroring the already-banked in-TU twin `func_8002A2D4`'s + local shape (separate i/off locals, subtraction inlined as the call arg) produced the correct 0x18 + frame; the warm-start body had been 4-off on frame size alone. +**The corollary that costs functions when ignored:** a warm-start body from ANOTHER binary is often +worth less than the neighbour 20 lines away. `func_8017FAAC`'s warm start was a different function +entirely and was discarded. + +## §331 — OPEN GAP: NO LEVER ELIMINATES AN *UNWANTED DUPLICATE* COPY AT A BRANCH-TARGET BLOCK HEAD (P31 S67; main/func_80013154, closeness 12, NOT solved) + +nins matches 43/43. An inline-asm-forced `sy=y` copy needed at the "ax!=ay" block head gets +DUPLICATED by gcc into two hardregs: `$t1` in the bne's delay slot (correct) and a redundant `$t3` at +the block's own head (wrong), cascading into idx10-16, a register-role swap with `nx` at idx25-28, +and `$t3`-vs-`$t1` at the final subu. Tried and REFUTED: 5 sy-placement variants, register pins +($9/$8/$10), an explicit-register asm constraint (cc1-fail), and substituting `(s16)sy` for `ay` in +the later comparisons (regresses to 46 ins). ~16 iterations. +§164-36/§31-lever-2 cover asm at a branch-target block head + delay-slot fill, but every listed lever +FILLS AN EMPTY SLOT; none REMOVES A DUPLICATE. This is a genuine hole in the knowledge base — record +it as such rather than spending another wave slot on it blind. diff --git a/tools/gate_wave.py b/tools/gate_wave.py new file mode 100644 index 0000000000..cb188e39a0 --- /dev/null +++ b/tools/gate_wave.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""gate_wave.py — gate a whole wave the fast way: split on jtbl, run both lanes CONCURRENTLY. (P31 S67) + +WHY THIS EXISTS (measured, S67, and it cost an hour). `parallel_gate` cannot host a jtbl-bearing +draft: `harvest_verify`'s carve runs `make extract`, and a worker's worktree `asm/` is a SYMLINK to +the main tree (`parallel_gate.py:77` states the invariant — "a gate never writes them, only +`make extract` does"). Knowing that, I gated a 16-binary wave SERIALLY to protect the ONE jtbl +function in it. Numbers from that run: + + 4 binaries through parallel_gate (4 workers) : 103 s wall (87 / 87 / 88 / 102 s each) + the same 4 serially : ~6 min + the full 16 serially : ~1 hour, for 1 jtbl draft + +Applying a true rule to the wrong scope is the defect. The predicate is per-DRAFT and it is cheap: +`jtbl_carve --probe` runs the real planner (since S67), so the split costs one probe per target. + +WHY THE SPLIT PRECEDES THE RUN, rather than triaging failures afterwards. A jtbl draft in a +parallel worker does NOT fail cleanly — it re-extracts through the shared `asm/` symlink and writes +the MAIN tree while nine other workers read it. "Run everything parallel and re-run what failed" +can therefore poison the whole batch instead of isolating one draft. Split first, then triage what +the filter missed (R32: verify the pre-filter against the outcome; never assume it was complete). + + tools/gate_wave.py --drafts .run/S67_ov --workers 8 --commit [--r22] + //.c — the layout gate_stage and parallel_gate both take +""" +import argparse +import concurrent.futures as cf +import json +import os +import re +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(HERE) +sys.path.insert(0, HERE) +PY = os.path.join(REPO, ".venv/bin/python") +_JTBL_RE = re.compile(r"jtbl_[0-9A-Fa-f]{8}") + +import corpus # noqa: E402 +import progress # noqa: E402 + + +def sh(cmd, **kw): + return subprocess.run(cmd, cwd=REPO, capture_output=True, text=True, **kw) + + +def has_jtbl(binary, fn): + """Does this function's target .s reference a jump table? + + The SAME predicate `harvest_verify._fn_has_jtbl` uses to decide whether to carve — one + definition of the question, so the router and the gate cannot disagree (R33/R34). + """ + st = next((s for s in corpus.stubs(binary).values() if s.symbol == fn), None) + if st is None: + return False # already banked; the gate will no-op it + try: + return bool(_JTBL_RE.search(open(os.path.join(REPO, st.asm_path), errors="replace").read())) + except OSError: + return False + + +def split(drafts_root): + """(parallel_plan, serial_items) — parallel is a plan.json for parallel_gate; serial is + [(binary, [fn...])] for gate_stage.""" + par, ser = [], [] + known = set(progress.BINARIES) + unknown = [] + for binary in sorted(os.listdir(os.path.join(REPO, drafts_root))): + d = os.path.join(REPO, drafts_root, binary) + if not os.path.isdir(d): + continue + # THE LADDER LEAVES ITS OWN OUTPUT DIRS BESIDE THE DRAFTS. `gate_stage._xform` writes + # `-cn`, `-cn-cast`, `-cn-cast-rc`, `-cn-cast-rc-sd` as SIBLINGS inside this root, + # so a plain listdir reports 20 "binaries" for a 16-binary wave and would gate transformed + # intermediates as if they were fresh work. Validate the name against the corpus and refuse + # what is not a binary (R32: assert the denominator; R43: refuse, never mishandle). + if binary not in known: + unknown.append(binary) + continue + fns = sorted(f[:-2] for f in os.listdir(d) if f.endswith(".c")) + if not fns: + continue + jt = [f for f in fns if has_jtbl(binary, f)] + # A binary goes SERIAL if ANY of its drafts carries a table: the gate runs per binary, and + # one carving draft is enough to make the whole worker unsafe. + (ser if jt else par).append((binary, fns, jt)) + if unknown: + print(" skipped %d non-binary dir(s) (ladder intermediates): %s" + % (len(unknown), " ".join(sorted(unknown)[:6])), file=sys.stderr) + plan = [{"binary": b, "drafts": os.path.join(REPO, drafts_root, b)} for b, _, _ in par] + return plan, ser, par + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--drafts", required=True, help="//.c") + ap.add_argument("--workers", type=int, default=8) + ap.add_argument("--commit", action="store_true") + ap.add_argument("--r22", action="store_true") + ap.add_argument("--source-tag", default="wave") + ap.add_argument("--plan-only", action="store_true") + a = ap.parse_args() + + plan, ser, par = split(a.drafts) + n_par = sum(len(f) for _, f, _ in par) + n_ser = sum(len(f) for _, f, _ in ser) + print("split: %d binaries / %d drafts PARALLEL · %d binaries / %d drafts SERIAL (jtbl)" + % (len(plan), n_par, len(ser), n_ser)) + for b, fns, jt in ser: + print(" serial %-16s %s (jtbl: %s)" % (b, " ".join(fns), " ".join(jt))) + if a.plan_only: + return 0 + if not plan and not ser: + print("REFUSING: no drafts found under %s" % a.drafts, file=sys.stderr) + return 2 + + plan_path = os.path.join(REPO, ".run/gate_wave_plan.json") + with open(plan_path, "w") as fh: + json.dump(plan, fh, indent=1) + + def run_parallel(): + if not plan: + return "(no parallel lane)" + cmd = [PY, "tools/parallel_gate.py", "--plan", ".run/gate_wave_plan.json", + "--workers", str(a.workers)] + if a.commit: + cmd.append("--commit") + r = sh(cmd, timeout=14400) + return (r.stdout or "") + (r.stderr or "") + + def run_serial(): + out = [] + for b, _, _ in ser: + cmd = [PY, "tools/gate_stage.py", "--binary", b, + "--drafts", os.path.join(a.drafts, b), "--source-tag", a.source_tag] + if a.commit: + cmd.append("--commit") + r = sh(cmd, timeout=7200) + out.append("[serial] %s rc=%d %s" % (b, r.returncode, (r.stdout or "").strip()[-200:])) + return "\n".join(out) + + # BOTH LANES AT ONCE (Drew, S67: no lane should idle). They are safe together — parallel_gate's + # workers are isolated by construction and it adopts a file only if the main tree's copy still + # matches the pinned baseline, REFUSING rather than clobbering if the serial lane moved it. + with cf.ThreadPoolExecutor(max_workers=2) as ex: + fp, fs = ex.submit(run_parallel), ex.submit(run_serial) + print(fp.result()) + print(fs.result()) + + if a.r22: + print("[gate_wave] R22 clean-fleet ...") + r = sh(["make", "clean"], timeout=3600) + r = sh(["make", "extract-all"], timeout=14400) + r = sh(["make", "check-all"], timeout=14400) + tail = ((r.stdout or "") + (r.stderr or "")).strip().splitlines()[-3:] + print("\n".join(tail)) + if r.returncode: + print("R22 FAILED — do not report banks as done (R22/R50)", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main())