diff --git a/docs/SETUP.md b/docs/SETUP.md index 3e4d05f05f..9ad3b0dc46 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -973,6 +973,30 @@ fills fast). Nothing is leaking — but the host does not get the memory back on | `tools/restage_matching.py` | rebuilds a gate plan from `recover_integration --probe-only` verdicts, keeping only drafts that compile-and-MATCH in their REAL TU | when a binary banks 0 and you suspect one bad draft is failing its siblings' shared build. **Caveat measured S71:** that probe compiles but never LINKS or CARVES, so its MATCH is not a bank prediction | | `tools/weave_sweep.py` | the §406/§408 derived-selector sweep: scores each open stub's stored drafts, classifies the `sw $ra` disagreement from the residual, applies the clobber only to WEAVE-SUNK, `--lever-all` is the ablation control | as the template for "price a class by its RESIDUAL, not its SHAPE" — the sweep itself is a measured null (§408) | +### Tools added / changed 2026-09-03 (S76) — R21 record + +| tool | what it does | when you need it | +|---|---|---| +| `tools/gate_main_parallel.py` **(NEW)** | runs the REAL `gate_main` inside N git worktrees to discover which drafts pass, then hands the union to ONE authoritative `gate_main` in the real tree. Workers discover; only the final serial pass banks | a main slate large enough that serial bisection hurts. **Measured: one gate cycle is 16 s**, so MAX_STEPS=24 is ~6.4 min serial and ~90 s across four workers. Run `--negative-control` once per environment first | +| `tools/sync_tu_decls.py` **(NEW)** | banks a draft the gate refuses by copying the TU's OWN `extern` line for whatever symbol the gate names, re-gating, and repeating | a draft that is byte-correct but rejected on a declaration conflict. Refuses `self_decl_tu` (use `cast_self_callers --sync-decls`) and refuses a NEAR up front, since syncing declarations makes a body COMPILE, never MATCH | + +**Oracle corrections — re-read any verdict recorded before these:** +* `match_one` and `rtu_match` now route the Makefile's `REORDER_TUS` (`800c2 800c2_2 800c2_3 800c3`) + through `reorder_passthrough.py + as -O2`, the real build path. They previously modelled + `maspsx + as -O1` and manufactured a phantom §182/§188 epilogue wall for every function in those + four TUs. Same draft, `func_8005ECC0`: closeness **5 / 36 ins** before, **2 / 35** after. +* `oracle_reorder.py`'s docstring no longer says the shape is unreachable — for those four TUs the + second cell of its diagnostic IS the build path, so a 0 there means the draft will bank. +* `draw_waves --main` **drew ZERO main functions** until this session (`bins` came from `src/*` + DIRECTORIES and main has no `src/main/`), while printing a reassuring "refusing 49 LINKED subsegs". + Coverage is now asserted: `--main` yielding no main stubs exits 4. Also `--redraw-open`, because + the ledger records what was ATTEMPTED and a still-open stub is still work. +* A **verbatim-asm draft** is refused at three points — `gate_main`, `harvest_verify`, and + `api_agent.prior_draft` (which was OFFERING them as warm starts). 1,099 of the 704,375 `.c` files + in the draft store are §265 bodies under ordinary `.c` names. +* `verbatim_to_stub` refuses a **§179-C epilogue-less fragment** (no `jr $ra` of its own) — a stub + there is an unbankable target. Census: of main's verbatim blocks, 37 have `jr $ra` and 100 do not. + **Two gating rules that are now enforced in code, not remembered:** * `parallel_gate` **REFUSES `main`** — main's extract rewrites the linker script, so an incremental gate is a false PASS (§414). Use `tools/gate_main.py`: baseline assert → one clean rebuild per diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index c7842e4971..a1260894d3 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -35636,3 +35636,74 @@ as a whole-block schedule difference. Unpinning `o`/`col`/`sh23`/`abr` fixed the *(Also: the 850 single-prim block written as an inline `addPrim` with block-local temps, and the function defined with the TU's typed prototype `(Obj_80021D38*, u8*, DVec_80021D38*, u8*)` to avoid the §41 declaration wall.)* + +#### §462 — FOUR LEVERS FROM `main:func_80024054` (91 ins, 74/53/32 → 4) + +**Source: the S76 agent on `func_80024054`.** Four independent unlocks, none previously recorded; +the last one is the kind of rule that silently costs a whole attempt. + +**1. `array[var - K]` folds K into the symbol's LO16 / `lhu` displacement.** Naming an intermediate +`idx = var - K` does NOT stop it — the fold happens at the front end / in combine, before any +register assignment you could steer. The only thing that defeated it was a zero-byte opacity barrier +immediately after computing the index, one per use site: +`__asm__ __volatile__("" : "=r"(idx) : "0"(idx));` +Reach for this whenever the target loads from `sym+0` with a computed index and your build folds the +constant into the displacement instead. + +**2. The fused `sll 16; sra 15` sign-extend-and-scale wants the index declared `s16`, not `s32`.** +This confirms §241's recipe on a fresh case — worth knowing it reproduces rather than being a +one-off of that function. + +**3. A mask-then-compare LOCAL causes a cross-jump merge AND flips branch polarity.** Writing +`bits = val & 0xC000;` then `if (bits == …) else if …` merged two case tails into one shared block +and emitted `bne`-polarity branches where the target has `beq`. Dropping the local and switching on +the expression directly — `switch (val & 0xC000) { case 0x8000: … case 0xC000: … default: … }` — +fixed both at once and reproduced the target's forward-`beq` shape. **The temporary was the defect**; +this is the same family as §461's "laundering can be the defect", from the opposite direction. + +**4. 🔴 A POINTER PARAMETER'S SIGNEDNESS DECIDES HOW `-1` IS MATERIALIZED.** Declaring `arg1` as +`s16 *` rather than `u16 *` flips the fail-path constant from `ori $x, 0xffff` to `addiu $x, -1`, +matching the target — because gcc-2.7.2 canonicalizes the RHS constant against the **lvalue's** +signedness before choosing the load-immediate opcode. Nothing about the store's *value* changes, so +this is invisible in the C and shows up only as a one-instruction opcode difference. If a residual is +a lone `ori 0xffff` vs `addiu -1`, check the signedness of the pointer being written through before +touching anything else. + +**Left open:** one `DELAY-SLOT` residual — `addu $a3,$zero,$zero` is insn #0 in the target and lands +in the branch delay slot in every C variant. Two independent prior attempts hit the same wall; +five further variants (statement reorder, register pin, barriers either side of the load) each left +it unchanged or traded it for an equal residual elsewhere. Permuter-class, Law 3. + +#### §463 — 🔴 SPILL SLOTS ARE 8 BYTES, AND THE §41b "LOAD ABOVE THE PROLOGUE" WALL IS REFUTED + +**Source: the S76 agent on `main:func_8001FC08` (400 ins, 33 → 0 MATCH).** Three laws, and the +second one deletes a wall this file has been asserting. + +**1. A 4-BYTE GAP IN AN OTHERWISE 4-PACKED FRAME IS A SPILL SLOT, NOT A PAD.** `sp+0xC8` / `sp+0xD0` +in this target are not struct members — they are spilled pseudos. reload's `alter_reg` calls +`assign_stack_local(mode, size, -1)`, and `align == -1` means `BIGGEST_ALIGNMENT` (8) with +`CEIL_ROUND`, so **every 4-byte spill slot occupies EIGHT bytes**. That is exactly why the target's +two slots sit 8 apart with `0xCC`/`0xD4` untouched. Reading those gaps as padding — or as fields of a +struct you then invent — is a wrong model of the frame. Worth 11 instructions here, and modelling +them as spills is also what evicts both values from local-alloc so reload picks `$t0`. + +**2. §41b's "a global load cannot float above the RTL prologue" IS NOT A WALL — it is an `$a0` +ANTI-DEPENDENCE.** The parameter copy `addu $s0, $a0, $zero` *reads* `$a0`, which pins the load +below it. Get the value out of `$a0` and make the load the first statement, and it floats to idx 0 +on its own. **Both moves are required and either alone is worthless** — statement-first by itself +measured 33 → **50** (worse); combined with law 1 (which is what frees the register) it went +22 → 4. Before treating a "load above the prologue" residual as unreachable, check what reads the +argument register. + +**3. Which ARGUMENT POSITION a guard value is passed in decides its hard register.** Passing it as +arg 1 — `func_80021120(&L.cnt, L.lp)` — gives that pseudo a `qty_phys_copy_sugg` toward `$a1`, which +local-alloc's scan-from-`$v0` can never reach on its own. The sibling guards that do *not* pass it +stay in `$v0`, which is the control proving the mechanism rather than a coincidence. + +**Banker caveat for this function:** its `INCLUDE_ASM` is at `src/800.c:11168`, but the TU's own +`MTX_80020248` typedef (`:11180`) and the `D_80074818`/`D_80075018` externs (`:11191-2`) are twelve +lines BELOW it. Hoist that block above `:11168` or drop the draft's copy, or the duplicate typedef is +a hard C89 error at bank time. + +**Verified by hand (law 1c):** 26 `jal` targets and 16 HI16/LO16 relocs identical in name and order; +the four `D_1F800020` words are the scratchpad literal, byte-identical (`3c111f80` / `26310020`). diff --git a/docs/wave-playbook.md b/docs/wave-playbook.md index f0cb444959..344f83cf5e 100644 --- a/docs/wave-playbook.md +++ b/docs/wave-playbook.md @@ -433,6 +433,25 @@ said; a sibling `scratch_/` with candidates but no final draft is worth scor tier is exhausted, re-run its targets on the next tier ALONGSIDE the dying agents — never kill a running workflow to relaunch it differently. +### 6-S76. WHAT TO DO WHEN A GATE BANKS FAR LESS THAN IT STAGED + +Measured S76: the overlay gate banked **1 of 38** and main's first pass **0 of 49** — and none of it +was drafting quality. Classify before re-drafting anything: + +1. **Run `recover_integration --probe-only --no-propagate`.** It compiles each stranded draft in its + ACTUAL TU and splits them three ways. On main's 40: **CC1-FAIL 16 / DIFF 18 / MATCH 6**. Only the + CC1-FAIL group is plumbing; the DIFF group is real residual and no declaration work will save it. + **A `CC1-FAIL` says the declaration blocked COMPILATION — never that the body underneath is right.** +2. **For a CC1-FAIL, run `tools/sync_tu_decls.py --binary main --fn F --draft D --apply`.** It copies + the TU's own `extern` for whichever symbol the gate names, re-gates, repeats. Banked + `func_8005EB28` in one round and `func_8005EC00` in two. +3. **If it refuses with `self_decl_tu`**, the TU declares the function being banked, so the call + SITES must change too — that is `cast_self_callers --sync-decls`, and the `--undo-journal --keep` + afterwards is mandatory. +4. **Expect a CASCADE.** Every bank gives its TU a real definition, which then contradicts the stale + `extern` that every later draft in that TU still carries. A draft that was compatible before a + bank can be incompatible after it — re-run the sync rather than concluding the draft went bad. + ## 6. Gate — EVERYTHING PARALLEL. There is no serial lane. > **P31 S74 — after a gate that CARVED, the binary's `asm/` is stale until a re-extract.** The