phase10: CLOSE — PhaseEnd, digest, verification record, cookbook 41-57

MILESTONE MET AND EXCEEDED: 484 distinct matched bodies / 493 registered regions
(target 475, from the 400 baseline) — +84 bodies. Developer confirmation of the
milestone was requested and given before any close record was written.

Closing checklist all green from clean:
  make clean && make all   exit 0
  cmp                      exit 0
  SHA-1 both files         e173426c157384ebf1b6caf8c6fea18a85a14af9
  make test                237 tests, OK
  make extents-verify      regions=493 disagreements=0 AGREE
  make gate                c_regions=493 differing_bytes=0 MATCH
  registry audit           493 rows, 0 overlaps, 0 unsorted, 0 bad extents,
                           0 missing sources, 484 distinct sources
  worklist                 listed=1193, excluded_already_registered=493
  negatives index          194 rows, address-ordered, 0 registered
  git status --short src/  empty (0 untracked files)
  firewall                 0 prohibited-root paths (591 tracked files)

New records:
  phase-ends/PhaseEnd_Phase10.md      the phase record
  docs/PHASE10_VERIFICATION.md        the verification record
  docs/MATCHING_COOKBOOK.md           findings 41-57 (57 total)
  phase-ends/CURRENT_PHASE.md         CLOSED, with the checklist itemised
  phase-ends/DIGEST.md                the Phase 10 digest entry

The headline finding is methodological (finding 41, THE SIZE-BAND LAW): the matched
corpus median is 48 bytes with 454/459 at <=200 B while the remaining levered rows
had a median of 456 B, and two independent measurements — one controlled — put the
small band at 1-2 attempts per row against 1-in-12 for 200-800 B.

The phase's character: five of the coordinator's own generalisations were bounded by
workers (rare-epilogue class, register-field diagnostic, polarity lever, goto
trigger, load-delay consumer form). The rules that survived are the ones that were
bounded.

Five incidents recorded rather than smoothed over; the candidate gate rejected three
batches and the tracked registry was never corrupted. Scope held: the blocked classes
stay excluded, no scheduler-changing flag was granted, and inline asm was extended
only to shapes C provably cannot express.

STOPPING HERE. Phase 11 does not begin in this session.
This commit is contained in:
Christopher Williams
2026-09-24 08:24:53 -04:00
parent 9aae442e64
commit 4a731de2d5
5 changed files with 661 additions and 4 deletions
+236
View File
@@ -683,3 +683,239 @@ taken in Phase 10. The two opt-in maspsx modes shipped in Phase 10 (`maspsx=nore
`maspsx=regread`) are implemented and default-off but **neither has been shown to close a region**:
on `0x800FFF60` R1 makes the length *worse* (140 → 128) because it also removes `nop`s the original
needs, and R2's predicate did not fire.
## Phase 10 — findings 41+ (coordinated tail squeeze, 2026-09-24)
Phase 10 took the corpus from 400 to **484 distinct bodies / 493 regions** with the full clean
audit green. Its headline result is methodological, and most of its levers are *two-sided* — each
acquired its limit by being tested against a row where it failed.
### 41. THE SIZE-BAND LAW — the phase's headline finding
**The matched corpus is 484 regions with a median size of 48 bytes and 454 of 459 at ≤200 B; the
remaining levered rows have a median of 456 B. Size is the strongest predictor of yield left in the
pool.** Two independent measurements, one of them controlled:
| source | ≤200 B | 200 B–800 B |
|---|---|---|
| worker C — same worker, same levers, same day, band the only variable | **1, 1, 1, 2** attempts | **1-in-12** |
| worker B2 — within a single session | **1, 2, 2** attempts (claims 5-7) | the two >200 B rows it spent the *most* time on: **1 deferred + 1 harness, zero matches** |
B2's version is the more persuasive because the confound of "different workers" is absent.
*Limit:* the band is where matching has been happening, not a claim that large bodies are
unmatchable — every measured residual had an identified mechanism. But when a worker-hour must be
placed, this is the number to use.
### 42. The commutative-operand lever, and its coupled allocation side-effect
`addu` takes the register of its **first dying source operand**, so `base + (index << 4)` and
`(index << 4) + base` allocate differently despite being mathematically identical. Diagnostic: count
right + a residual that is a small multiple of 4 in ONE basic block → compare **register fields** per
instruction before touching the expression shape.
**COUPLING (Phase 10).** On `0x80027744` (4 bytes) and `0x80050674` (1 byte, only the register fields
of one `addu`) the operand order and the register allocation are **coupled**: every spelling giving the
original's `addu` operand order also flips which value takes `v0` vs `v1`, because operand order
changes the pseudo creation order. `base + a1*4` gives the right allocation and the wrong destination;
`a1*4 + base` the reverse. So re-spelling the addition is **necessary but not sufficient** — look for a
shape that keeps the index in `v1` while the base is still live at the addition.
**AMENDMENT — A PERMUTED REGISTER SET CAN BE A SEMANTIC BUG (worker A).** The register-field
diagnostic was broadcast as "permuted registers = the commutative lever", and that generality is
**false**. On `0x800A5180` the source wrote `q[1]` where the original read `entry[1]` through the
table-entry pointer: right length, identical mnemonics and schedule, 11 differing bytes all in register
fields, and **not** an allocation tie-break. **Verify the DATAFLOW — which value each register actually
holds — before concluding tie-break.** Two different pointers swapping registers are indistinguishable
from register names alone.
### 43. The two-arm branch family — three triggers, and the first two are ordered
1. **POLARITY (try first).** For `a1 = (x < 2) ? a3 : saved`, both the natural order and the ternary
emit `beq v0,zero` with the arms swapped (right length, 5 differing bytes). Writing the
larger-than arm first — `if (x >= 2) a1 = saved; else a1 = a3;` — makes cc1 emit `bne v0,zero` with
`a1 = a3` in the **branch delay slot**, running on both paths and overwritten on the `>= 2` path:
the original exactly.
2. **A GUARD THAT MUST BRANCH *INTO* THE BODY NEEDS A `goto` (try second).** On `0x800A9C24` both
ordinary spellings produce the mirrored `beq v1,zero` at 96 differing bytes, and only
`if (a1 != 0) goto body; return -1; body:` gives the original's `bne v1,zero` with the `return -1`
block inline as the fall-through.
3. **SHARING ONE BLOCK NEEDS A NAMED `goto` LABEL.** On `0x800A6998` the two `func_800A6FAC(0)` calls
appear **exactly once** in the original, reached both by fall-through and by the `w != 0` branch.
`while`/`for` spellings either **invert the loop** (156 B) or make cc1 **cross-jump the two `== 0`
tests** so the x-load goes dead (124 B).
*Limit (worker A, `0x800256F0`):* trigger 2 is **necessary but not sufficient** — the branch went into
the body and the ORDINARY spelling still reproduced the arm placement correctly. Keep the two-step
order and do not expect a `goto` every time the layout looks that way.
### 44. The paired boolean rule — combine, or name, according to the original's shape
- **Original has a BRANCH on a boolean expression → combine the conditions into ONE `&&` chain.**
Three separate `if (...) return 0;` statements let cc1 fold the third guard into a branchless
`xor`/`sltiu` (124 B); one `&&` chain with a single trailing `return 0` keeps the branch.
(`0x800B67B8`; also `0x8006AA10`, where nested `if`s give two `li a3,1` blocks and 124 B
LENGTH-MISMATCH versus one shared `li` for the combined form.)
- **Original has the BRANCHLESS form → NAME the boolean.** `rec[6] = ((a3 & 0xff) != 0) << 1` is
branchy at 92 B; only `int flag = (a3 & 0xff) != 0;` then `rec[6] = flag << 1` gives the original's
`andi`/`sltu`/`sll` at 88. Six spellings measured.
*Limit:* on `0x800B67B8` a `goto fail; … fail: return 0;` spelling is **also** byte-identical, so the
distinguishing fact is **"one combined condition with one trailing `return`"**, not the operator.
### 45. The named-locals family — three directions, plus scope
The family has **five** distinct mechanisms. The three *directions* around one mechanism, all
byte-required, all measured:
| direction | row | what the source must do |
|---|---|---|
| names it → cc1 keeps it | `0x8005E538` | the handle must be a named local loaded before the first call, or cc1 keeps only the object pointer in a callee-saved register and **reloads** the handle after the call (76 vs 88 B) |
| re-reads it → cc1 re-reads it | `0x800F66B8` | `d[0]`/`d[1]` are each loaded twice with neither held in a register — naming a local would be **wrong** |
| re-reads it → cc1 CSEs it anyway | `0x800256F0` | the original re-reads a field after a call; cc1 CSE'd the two reads |
**Diagnostic: the load count, not a rule about locals.**
Plus: **order-pinning** (a named local fixes a store order), **reload-prevention** (a local is not
memory, so a store through a possibly-aliasing pointer cannot invalidate it), the **inverted** case
where the repeated expression is correct and the local is the mistake, and the **scoping** lever —
a local shared by two guard blocks gets its live ranges **coalesced**; declaring each in its own
brace-scoped block prevents that (`0x8008BA80`).
### 46. The per-site `gp` form, and its double-failure case
The gp marker is a **per-symbol** property in the harness but a **per-access** property in the
original, so a registry-gp-marked symbol may need a per-region `gp=-NAME` override. The strongest
evidence is a single worker's batch: `D_80121BFC` is read **gp-relatively** in one row
(`lw v1,708(gp)`) and **absolutely** in another (`lui v1,0x8012` + `lw v1,7164(v1)`).
**On `0x800A6998` BOTH spellings are wrong, for two independent reasons:** the **symbol** spelling
gives the gp-relative encoding the original does not use, while the **literal** spelling makes cc1
**cache the address in `s0`** so the frame grows 24 → 32 bytes with an extra saved register. The
per-site override is the only correct route.
### 47. The `-G` small-data threshold lever
A symbol whose **address is taken** is invisible to `-G0`: cc1 emits a two-instruction large-data `la`
and CSE-hoists it into a callee-saved register (candidate LONGER by ~12 with an extra saved register
holding an `la`). A `-G` that admits the symbol makes cc1 emit one `la` per use with no hoist. Rule:
**the symbol is admitted as small data exactly when `declared size <= G`.**
*Limit:* the harness's `-G0` + per-symbol gp marker model is exact for a direct `lw`/`sw`, which is why
400 bodies matched without any `-G` override; it is inexact only when the symbol's ADDRESS is taken.
Override-free routes were tested and closed (known-size declaration at `-G0` still mismatches;
`__attribute__((section(".sdata")))` still mismatches). **`-G8` is a working value, not a recovered
one** — the true `-G` is only bounded as `>= 4` by this row. The global `-G0` default is unchanged by
developer decision, so the existing corpus is untouched.
### 48. Signedness shows up as an encoding — four forms
- `addiu <negative>` = signed destination, `ori <positive>` = unsigned (`0x8010A940`: `0xC000` needs
`unsigned short`).
- A 1-byte residual on a `li`/`addiu` immediate whose **low byte is unchanged** is a signedness
difference, not a value difference (`0x80073F78`: `*(signed char *)` loads `-30`/`0xFFFFFFE2` where
`char *` loads `226`/`0x000000E2`).
- **Within one function**, two constants can need opposite signedness (`0x8008A758`: `0xC0FFFFU` needs
`ori`, `480` needs `addiu` — same immediate bits, one differing byte).
- The **loop-test** form: `sltiu` vs `slti` (opcode 0x0b vs 0x0a) means an `unsigned` counter
(`0x800504E4`).
### 49. A self-inflicted typo presents as a 1-byte diff — and compute-don't-eyeball is the fix
The harness's name-encoded symbol resolution is **faithful to what you typed**, so a mis-converted
displacement encodes the wrong address and presents as a mysterious 1-byte diff (`0x8008DB44`:
`addiu a2,a2,32140` vs `32172` — the source declared `D_80117DAC` for `D_80117D8C`). **When the
residual is one instruction's immediate, recompute the address from the instruction's OWN immediate
before theorising.** Four instances this phase; two of them were the same class read as decimals
(`0x80091370`'s -30200/-30172 are the `%lo` of `0x80138A08`/`0x80138A24`; `0x800ACA10`'s 28220 is
`0x6E3C`, not `0x6E7C`).
### 50. The scheduler class — `-fno-schedule-insns` is the one-compile confirmation
**Correct length + a residual that is a permutation of a few instructions + the unscheduled build
matching the original order ⇒ the residual is sched, not source shape** (worker C2). Measured on
`0x800A6C34` (16 differing) and `0x80016F80` (45 differing), both at correct length: the identical
source with `cc1 -quiet -O2 -G0 -fno-schedule-insns` produces the original's instruction ORDER
byte-for-byte.
**The trap is that the two effects are coupled and opposed:** on `0x80016F80` sched-OFF gives the
original's order but the wrong register allocation (`i→s0/p→s1` vs the original's `i→s1/p→s0`), while
sched-ON gives the right allocation and the wrong order. On `0x800A6C34` sched-OFF gives the original's
load/store order but reorg then fills the `jal` slot the original leaves empty for maspsx's `nop`.
**Neither alone matches.**
*Limit:* the override is for **diagnosis only**. Taking it would mean the corpus is no longer all built
by one compiler configuration. Record the row as a negative with the lever named.
### 51. Dead-store elimination, and when `volatile` is the answer
Two stores to the same address are DSE'd and cc1 deletes the first store **and its `andi`** (144 B vs
the correct 156) — and **a literal-address second store does NOT defeat the DSE**. Measured set
(`0x80016F80`): the repeated stores must be `volatile`; the ten zero stores must be `volatile` too or
the scheduler floats them above the mask block (68 → 45 differing); with both volatile the two `andi`s
stay adjacent only if the masked values are bound to **temporaries before the stores**; and the loop
bound must stay **non**-volatile or its load moves after the zero stores (160 B).
### 52. maspsx drops an explicit `#nop` marker it should honour
cc1 emits a bare `#nop` when it wants a load-delay nop. maspsx re-derived the need and could **overrule
cc1** for a **bare-symbol store** consumer: `uses_at('sw $2,D_801221C4')` is True (the store expands
via `$at`) while `nop_at_expansion` is False for ASPSX >= 2.30, so neither test fired and the nop was
dropped (`0x80107C5C` at 108 vs 112; `0x8003A9C8`). **Fixed in Phase 10** (tracked patch
`tools/patches/maspsx-phase10-r1r2.patch`): maspsx now honours the explicit marker. Verified
regression-free at 489 regions.
*Limit and lesson:* the predicate `line_loads_from_reg` **already returned True** for a store source —
the worker's first diagnosis (extend the predicate) would have been a **no-op patch**. **A named
mechanism is a hypothesis until it is traced, even when the observation is solid and reproducible.**
The two other opt-in modes shipped in the same patch (`maspsx=noreordernop`, `maspsx=regread`) remain
unproven: neither has been shown to close a region.
### 53. The record pointer must be the BASE, not an offset one
`p = (unsigned char *)(node[0] + 6)` with `p[-2]/p[-1]/p[0]` makes cc1's combine pass split it into
**two** pointers (`addiu` to base+6 AND base+5) — 124 B, LENGTH-MISMATCH. `p = node[0]` with
`p[4]/p[5]/p[6]` reproduces the original's single `addiu a1,v1,6` base with -2/-1/0 offsets.
*Diagnostic:* candidate LONGER by 4 with two `addiu`s from one base where the original has one →
suspect the base choice, not the stride.
### 54. A 2-D array's row stride is byte-load-bearing (a LENGTH-class lever)
`D_8013C078[a3][a4]` emits `sll a3,4` + `sll a4,2` + `add` and the length is correct (120 B);
`D_8013C078[a3 * 4 + a4]` folds the outer `*4` into a second `sll` and the row comes out **4 bytes
SHORT**. *Diagnostic:* when a scaled index is one `sll` short, the source is a 2-D array whose row
stride the compiler can use directly, not a flattened index.
### 55. The family lever's scope, measured on a large sample
The argument-map template **transfers**; the body shape **does not**. Worker C matched `0x80027E1C`
(frame -48) and `0x80099078` (frame -40) first-attempt from the frame-size-as-shape-signature map,
then the map stopped transferring: the -48 siblings `0x800BD5E8` (380 B) and `0x800BE828` (416 B) are
`mult`-heavy vector maths, the -40 sibling `0x80052424` (488 B) is a 4-argument nested branch tree, and
`0x80015FC8` (288 B) carries a `div` with `break` guards. **The remaining cluster rows are structurally
distinct bodies that merely CALL the same target.**
### 56. The `restores_unsaved` fragment class
A body that **restores a callee-saved register it never saves** cannot be a whole function: the
register was established by an enclosing prologue the derived extent cut off. These are `jal` targets
*inside* a real function, so the extent walk began mid-body — distinct from `bad_extent_start`, which
flags starts that are not function entries at all. **Verified disjoint from the corpus: 0 of 462
registered regions trip it; 11 worklist rows do.** Implemented in `sf3_triage` as a counted reason.
*Limit:* sufficient but not complete — it catches only rows that RESTORE an unsaved register, not rows
that merely read one, so the count is a lower bound.
### 57. Process rules the phase earned
- **Claim rows carry the source md5.** `src/func_XXXXXXXX.c` is a shared, unlocked namespace across
concurrent sessions, so a file can change **between a worker's verify and the coordinator's merge**.
A mismatch means re-verify rather than merge or reject. ("Verify the file at the path you are about
to claim" is necessary but **not sufficient** — the gap is between verify and merge.)
- **A variant is an experiment; `src/func_XXXXXXXX.c` is the claim.** Fold a winning spelling from a
scratch variant into the documented source before claiming.
- **Run the `src/` cleanup audit BEFORE the final report**, not after — a finding that is not also a
claim is invisible to the merge flow.
- **Read start and end straight out of the worklist row**, never reconstruct them from a hand dump: a
hand-derived range produces a LENGTH-MISMATCH indistinguishable from a real extent defect.
- **One attempt on a named lever, then classify.** A harness classification backed by a proof is worth
more than an assumption; the model case is `0x80023D40`, where cc1's duplicated `slti` in the `bgez`
slot is **correct code**, so no source shape can prevent it.
+104
View File
@@ -0,0 +1,104 @@
# Phase 10 — Verification Record
**Scope:** the checks that establish Phase 10's result. Every number below was produced by a command
run in this repository against the tracked state, not estimated.
**Result: 484 distinct matched bodies / 493 registered regions** (from the Phase 9 close state of
400 / 409), all gates green from a clean tree.
## The closing gate set (run from clean, in this order)
| Check | Command | Result |
|---|---|---|
| Clean rebuild | `make clean && make all` | exit 0 |
| Byte comparison | `cmp build/scus_946_40.rebuilt 'extracted/SCUS_946.40;1'` | exit 0 |
| Full-file SHA-1 (both) | `sha1sum` | `e173426c157384ebf1b6caf8c6fea18a85a14af9` |
| Synthetic suite | `make test` | **237 tests, OK** |
| Extent agreement | `make extents-verify` | `regions=493 disagreements=0 result=AGREE` |
| Whole-binary gate | `make gate` | `c_regions=493 differing_bytes=0 result=MATCH` |
| Registry audit | scripted | 493 rows, 0 overlaps, 0 unsorted, 0 bad extents, 0 missing sources, 484 distinct sources |
| Worklist regeneration | `make worklist` | `listed=1193`, `excluded_already_registered=493` (= the registry size) |
| Negatives reconciled | scripted | index 194 rows, address-ordered, 0 duplicates, 0 registered; **0 unregistered negatives survive into the worklist** |
| `src/` cleanliness | `git status --short src/` | **empty** — 0 untracked files |
| Registry ↔ tracked sources | scripted | 484 distinct registry sources = 484 tracked `src/` files |
| Firewall | `git ls-files` under prohibited roots | **0** (591 tracked files) |
## Per-merge verification (every merge, all 27)
Every merge followed the hardened flow and the **candidate gate ran before promotion in every case**:
1. `sf3_merge apply` → candidate regions + symbols (never the tracked registry)
2. `sf3_match gate` on the candidate, whole-binary, expecting the fixed SHA-1
3. only on `result=MATCH`: promote, then `make check`
4. reconcile negatives, regenerate the worklist, refilter partitions, commit, push
The gate rejected three batches during the phase, and **the tracked registry was never corrupted**:
| Batch | Failure | Cause | Resolution |
|---|---|---|---|
| merge 8 | `differing_bytes=1117197` | a region-option request staged in a separate file rather than in the claim row, so the region merged without its `gp` override | folded the option into the claim row, re-gated to MATCH |
| merge 19/20 | `differing_bytes=96` | the claimed `src/` path held a different spelling from the one verified (a bare variant, not the documented source) | returned to the worker; the documented source was restored and re-verified |
| merge 20 | `differing_bytes=96` again | a **concurrent write** to the same `src/` path by another session (a coordinator double-assignment) | md5-in-claim-row guard adopted; the row was reassigned to one owner |
## Audit cadence actually achieved
- **Full clean audit** (`make clean && make all`, `cmp`, SHA-1, registry audit, `git status --short src/`,
firewall) at the milestone and at the close, plus at the 3rd-merge interval.
- **Every merge**: candidate gate + `make check`.
- **Every merge**: worklist regeneration and partition refiltering, with the disjointness and
union-equals-worklist proof re-run (`intersection = ∅`, `union == worklist`).
- **Negative reconciliation** run repeatedly during the phase rather than only at the close, so no
worker finding depended on ignored staging surviving. The final extraction imported 28 negatives and
dropped 28 rows that had since been registered.
## Harness changes and their verification
| Change | Tracked as | Verified by |
|---|---|---|
| `maspsx` opt-in modes `noreordernop` / `regread` | `tools/patches/maspsx-phase10-r1r2.patch` | patch reproduces both modified files byte-identically from the pristine pinned checkout; `make check` green at 493 with the modes off |
| **maspsx honours cc1's explicit `#nop` marker** | same patch (default-on) | **`make check` green at 489 regions / 237 tests with every one of the 489 regions byte-identical**, so the fix is regression-free |
| `restores_unsaved` exclusion class | `tools/sf3_triage` + 5 new tests | measured disjoint from the corpus: **0 of 462 registered regions** trip it, 11 worklist rows do |
| per-region `cc1=` / `gp=` override plumbing | `tools/sf3_match` + 5 new tests | suite 229 → 237, all green |
`tools/maspsx/` is **git-ignored**, so an in-place edit would not survive a fresh clone. Every maspsx
change is therefore carried as a tracked patch and recorded in `docs/SETUP.md` with the apply command.
## Registry-option decisions (each byte-verified with a failing control)
| Row | Option | Without it | With it |
|---|---|---|---|
| `0x800A6BEC` | `cc1=-G8` | 84 B LENGTH-MISMATCH | 72 B, 0 differing |
| `0x80015D50` | `gp=-D_80121B88` | 104 B LENGTH-MISMATCH | 108 B, 0 differing |
| `0x8002D364` | `gp=-D_801226F4` | 140 B LENGTH-MISMATCH | 144 B, 0 differing |
| `0x800A6998` | `gp=-D_80121B88` | 120 B LENGTH-MISMATCH | 128 B, 0 differing |
| `0x80048128` | `gp=-D_80121BFC` | 88 B without the override is the MATCH; the symbol spelling is wrong | 88 B, 0 differing |
No option was granted on a hunch: each was reproduced by the coordinator with a control that fails
without it. **No scheduler-changing flag was granted at any point** — a flag that changed cc1's
scheduler would mean the corpus is no longer all built by one compiler configuration.
## Scoring honesty
The milestone is **bodies on the clean whole-binary gate**, never "keep matching". A classified but
unmatched residual counts as a **recorded negative, not progress**. The phase therefore reports:
- **484 matched bodies** (the milestone metric, target 475)
- **194 recorded negatives** in the tracked index, each with an address, size, status and class
- **2 harness rows with proven impossibility arguments** (`0x80023D40` — cc1's duplicated `slti` in the
`bgez` slot is *correct code*, so no source shape can prevent it; `0x800FBB20`)
- **the blocked classes unchanged**: trapping arithmetic, the `0x80012xxx` primitive-init family, and
the maspsx mutual exclusion all remain excluded with counted reasons, 0 of 493 registered regions
containing any of them
## Known limits of this verification
- The `restores_unsaved` and `classify_epilogue` scans are **lower bounds**: they catch rows that
restore an unsaved register / that have `jr ra` as the third tail word, not rows that merely read an
unsaved register or that restore two or more registers before the jump.
- `0x8010080C` remains a **false extent start** whose root cause (a `jal` target mid-body) is recorded
but not fixed in `sf3_extents`; a hand-edited extent would fail `make extents-verify` by design.
- `0x80107C5C` **matches** with the `#nop` fix (verified against worker B2's variant `X3.c`, 112 B, 0
differing) but its source is a bare variant with no header, so it is recorded as unblocked-and-one-
documented-source-away rather than claimed. It is not in the 484.
- The two opt-in maspsx modes shipped alongside the `#nop` fix remain **unproven**: neither has been
shown to close a region.
+61 -4
View File
@@ -1,10 +1,67 @@
# CURRENT_PHASE — Phase 10: Matching the Remaining Pool (Goal A — tail squeeze)
**Status:** ACTIVE (P10-T1 complete; P10-T2 ready to dispatch)
**Status:** **CLOSED** (2026-09-24) — milestone **MET at 484 distinct matched bodies / 493 regions**
(target 475, from the 400 baseline), with the developer's explicit confirmation of the milestone
given before any close record was written. PhaseEnd: `phase-ends/PhaseEnd_Phase10.md`. Verification:
`docs/PHASE10_VERIFICATION.md`. Ledger: `phase-ends/logs/Phase10.md`. **No further work is active in
this phase.**
**Created:** 2026-09-24 (coordinating session `01a0d302-0201`, after developer approval)
**Plan:** `phase-ends/Phase10_PLAN.md` — approved 2026-09-24, **Goal A: 475 distinct
matched bodies** from the Phase 9 close state of **400** (i.e. **+75 new bodies**),
measured as bodies on the coordinator's clean whole-binary gate.
**Plan:** `phase-ends/Phase10_PLAN.md` — approved 2026-09-24, **Goal A: 475 distinct matched bodies**.
## Final state
| | Phase 9 close | Phase 10 close |
|---|---|---|
| Distinct matched bodies | 400 | **484** |
| Registered regions | 409 | **493** |
| Synthetic tests | 229 | **237** |
| Tracked files | 506 | 591 |
All gates green from clean: `make clean && make all` exit 0, `cmp` exit 0, both SHA-1
`e173426c157384ebf1b6caf8c6fea18a85a14af9`, `make test` 237 OK, `make extents-verify`
`regions=493 disagreements=0 AGREE`, `make gate` `c_regions=493 differing_bytes=0 MATCH`, registry audit
493 rows / 0 overlaps / 0 bad extents / 0 missing sources / 484 distinct, worklist `listed=1193` with
`excluded_already_registered=493`, negatives index 194 rows address-ordered with 0 registered,
`git status --short src/` empty, 0 firewall violations.
## Sessions (all released)
| Role | Short id | Claims | Outcome |
|---|---|---|---|
| **Coordinator** | `01a0d302-0201` | — | 27 merges, all gated; close records written |
| Worker A | `01a0d302-18e3` | 26 | final handoff accepted |
| Worker B | `01a0d302-8a6f` | 23 | stopped on a yield basis at 47% (recorded as **not** the measured criterion) |
| Worker B2 | `01a0d338-485d` | 9 | replacement for B; stopped clean |
| Worker C | `01a0d302-fad3` | 22 | stopped at the measured 68% cap |
| Worker C2 | `01a0d344-0c94` | 4 | replacement for C; stopped on budget |
## Closing checklist — all items verified
| # | Item | State |
|---|---|---|
| 1 | Every worker: final report, drafts removed from `src/`, complete negatives list | done (A, B, B2, C, C2 all released with handoffs) |
| 2 | `git status --short src/` = 0 | **empty** |
| 3 | Registry audit: ordered, non-overlapping, extents exact, sources present | 493 rows, 0 violations, 484 distinct |
| 4 | All gates from clean | all exit 0 (see above) |
| 5 | Worklist regenerates with `excluded_already_registered` = registry size | `493` = `493` |
| 6 | Negative metadata extracted before ignored staging is lost | index 194 rows; 0 unregistered negatives survive into the worklist |
| 7 | Firewall and Git review | 0 prohibited-root paths; history audited before the first push |
| 8 | Milestone confirmation requested | **requested and given** |
| 9 | PhaseEnd, digest update, ledger, commit, stop | written; **stopped without beginning Phase 11** |
## Carried into Phase 11
- **The size-band law** (cookbook finding 41) as the dispatch rule: ≤200 B yields at 1–2 attempts per row
against 1-in-12 for 200 B–800 B.
- The **md5-in-claim-row guard** and the four process rules in cookbook finding 57.
- The **negatives index** (194 rows) as the queue-reconciliation input.
- The **tracked maspsx patch** (`tools/patches/maspsx-phase10-r1r2.patch`) with `SETUP.md` provenance —
`tools/maspsx/` is git-ignored, so the patch is the only reproducible carrier of those changes.
- Worker handoffs with ranked near-matches: worker A's 13 and worker C2's 4, each with an exact residual
and a named untried lever.
- Unresolved: the `0x8010080C` false extent start; the maspsx rare-epilogue mutual exclusion; the two
opt-in maspsx modes (unproven); the scheduler class; and `0x80107C5C` (matches, one documented source
away, not counted in the 484).
## Sessions
+95
View File
@@ -113,3 +113,98 @@ the dependent-claim rule, the leading-indicator rule, and compute-don't-
eyeball (extended to two-piece constants). Next: Phase 10 plan is written
(`phase-ends/Phase10_PLAN.md`) and requires explicit developer approval. No
changes to AGENTS.md.
## Phase 10 — Matching the Remaining Pool, Goal A (2026-09-24)
Phase 10 ran a tail squeeze from the Phase 9 close state and closed at **484 distinct matched bodies /
493 registered regions** (from 400 / 409 — **+84**, against a milestone of 475, so **+9 over target**),
verified on the coordinator's own clean whole-binary gate (`c_regions=493`, `differing_bytes=0`, SHA-1
`e173426c157384ebf1b6caf8c6fea18a85a14af9`). All gates exit 0 from clean: `make clean && make all`,
`cmp`, `make test` (237 tests, up from 229), `make extents-verify` (AGREE), `make gate` (MATCH); the
registry audit reports 493 ordered non-overlapping regions, 0 bad extents, 0 missing sources and 484
distinct sources; `git status --short src/` is **empty** (0 untracked files, so 484 registry sources =
484 tracked `src/` files); the worklist regenerates with `excluded_already_registered=493`; and 0 tracked
paths sit under any prohibited root (591 tracked files).
**The phase's headline finding is methodological: THE SIZE-BAND LAW.** The matched corpus has a median
size of 48 bytes with 454 of 459 at ≤200 B, while the remaining levered rows had a median of 456 B, so
size is the strongest predictor of yield left in the pool. Two independent measurements, one controlled
(same worker, same levers, same day, band the only variable): **1, 1, 1, 2 attempts per row on the
≤200 B band against 1-in-12 on the 200 B–800 B band**; and within one session, **1, 2, 2** on the small
band against the two >200 B rows it spent the most time on producing **1 deferred + 1 harness row and
zero matches**. Acting on this mid-phase — every dispatch file re-ranked to size-band-first — caught a
worker heading back into the band it had just measured as exhausted. It is cookbook finding 41.
**The phase's character is that every lever acquired a limit, and five of the coordinator's own
generalisations were bounded by workers** — more than in any prior phase. "Skip the rare-epilogue class"
was falsified (it is a three-way split and attemptable). "A permuted register set = the commutative
lever" was falsified (it can be a *semantic bug* — two pointers swapping registers look identical from
register names, so **verify the dataflow first**). "Residual at a two-arm branch → invert and swap" was
bounded (both ordinary spellings can mirror, so the guard may need a `goto`). "The `goto` trigger needs a
`goto`" was bounded (necessary but **not sufficient**). And a coordinator "negative result" on the
load-delay class was shown to have tested the wrong consumer form. **The rules that survived are the
ones that were bounded**, and that is the phase's most transferable process result.
**The rare-epilogue class (finding 35) is mechanistically closed.** GNU `as` in reorder mode fills a jump
delay slot with the immediately-preceding instruction but **refuses when doing so would place `jr ra` in
the load-delay slot of `lw ra`**, so the class splits on whether any instruction intervenes before the
frame release. The obvious fix was measured and closed: maspsx forces each function into
`.set noreorder`, so suppressing only its jump-slot `nop` restores the length but leaves the slot
**empty**; also suppressing the function-level `.set noreorder` makes `as` fill the epilogue *exactly*
right and then over-fill other slots for +4 bytes. A tracked post-pass modelling ASPSX's fill for one
site is the remaining route and was not taken.
**Harness work, all verified.** A new **`restores_unsaved` exclusion class** in `sf3_triage` (a body
restoring a callee-saved register it never saves cannot be a whole function; verified disjoint from the
corpus — 0 of 462 registered regions, 11 worklist rows — and it independently re-derived a defect another
worker had found by a different signal). A **maspsx fix honouring cc1's explicit `#nop` marker**
(developer-authorised): cc1 asked for a load-delay nop and maspsx overruled it for bare-symbol store
consumers, because `uses_at` is True for a macro store while `nop_at_expansion` is False for ASPSX
≥ 2.30; **verified regression-free at 489 regions, every one byte-identical**. Worker B2 found the gap
but its first diagnosis (extend the predicate) would have been a **no-op** — the predicate already
returned True — and the coordinator traced the real mechanism: **a named mechanism is a hypothesis until
it is traced, even when the observation is solid**. Because `tools/maspsx/` is **git-ignored**, every
maspsx change is carried as a **tracked patch** verified to reproduce the modified files byte-identically
from the pristine pinned checkout, with provenance in `docs/SETUP.md` — an in-place edit would not have
survived a fresh clone, and that was caught before it mattered. Per-region override plumbing was added
for the new modes (+5 tests).
**Five incidents were recorded rather than smoothed over, and none corrupted the tracked registry.**
A **coordinator double-assignment** (one row redistributed to one worker and chartered to another, so two
sessions wrote one file) produced the **md5-in-claim-row guard**, now standing protocol, because
`src/func_XXXXXXXX.c` is a shared unlocked namespace and a file can change *between a worker's verify and
the coordinator's merge* — so "verify the file at the path you are about to claim" is necessary but **not
sufficient**. A worker verified a variant and claimed a different file. A worker dropped a matched row
from staging by reporting it as a finding without listing it as a claim (caught by its own cleanup
audit — hence "run the audit BEFORE the final report"). A coordinator commit message silently lost a fact
to unescaped backticks. A coordinator-recorded *reason* was wrong for a tested lever (the test was inert
by construction) and was corrected after the worker pointed it out. The candidate gate rejected three
batches and the tracked registry was never touched.
**Scope held.** The blocked classes (trapping arithmetic, the `0x80012xxx` primitive-init family, the
maspsx mutual exclusion) stay excluded — 0 of 493 registered regions contains any of them — and the
phase's harness changes did not reopen them. **No scheduler-changing flag was granted** at any point:
`cc1=-G8` and `gp=-NAME` were granted because they are byte-required and each was verified with a failing
control, but a flag that changed cc1's scheduler would mean the corpus is no longer all built by one
compiler configuration; workers proposed it twice and were told no, and both recorded their rows as
negatives with the lever named. Inline asm was extended by developer decision from "instructions C cannot
*name*" to "shapes C provably cannot *express*", first instance the PSX scratchpad stack switch, proven
byte-exact twice. `git clean -x`/`-fdx` were never used, and the ROM firewall was audited across the
entire history before the first push to a new remote (largest blob in all of history: a 163 KB config
TSV).
**Two workers rotated mid-phase on measured criteria and two fresh sessions replaced them; fresh context
beat exhausted context both times.** Worker A produced 26 claims (24 first-attempt with no override),
worker B 23, worker C 22, worker B2 9, worker C2 4. One worker stopped on a *yield* basis at 47% and the
stop was recorded as explicitly **not** the measured criterion rather than dressed as a rotation.
Unresolved and recorded with evidence: the `0x8010080C` false extent start (`sf3_extents` deliberately
not changed — a hand-edited extent fails `make extents-verify` by design); the maspsx rare-epilogue
mutual exclusion; the two opt-in maspsx modes (`noreordernop`, `regread`) which are implemented,
default-off and regression-safe but **neither shown to close a region**; the scheduler class
(`-fno-schedule-insns` gives the original's order on at least two rows but the allocation or delay slot
then disagrees); `0x80107C5C`, which matches with the `#nop` fix but whose source is a bare variant with
no header, so it is recorded as unblocked-and-one-documented-source-away and **not counted in the 484**;
and 194 recorded negatives in the tracked index, each with an address, size, status and class. Cookbook
grew to 57 findings (41–57 new), `docs/PHASE10_VERIFICATION.md` records the verification, and
`phase-ends/logs/Phase10.md` is the ledger. No changes to AGENTS.md.
+165
View File
@@ -0,0 +1,165 @@
# PhaseEnd — Phase 10: Matching the Remaining Pool (Goal A, tail squeeze)
**Closed:** 2026-09-24, coordinator session `01a0d302-0201`.
**Milestone:** **475 distinct bodies** — **MET and exceeded at 484**, from the Phase 9 close state of
400. **484 distinct matched bodies / 493 registered regions.**
**Developer confirmation:** requested and given before any close record was written, per the plan.
## Result
| | Phase 9 close | Phase 10 close |
|---|---|---|
| Distinct matched bodies | 400 | **484** |
| Registered regions | 409 | **493** |
| Registered symbols | 380 | 386 |
| Synthetic tests | 229 | **237** |
| Tracked files | 506 | 591 |
**+84 distinct bodies**, against a milestone of +75. All gates green from clean:
```
make clean && make all exit 0
cmp exit 0
SHA-1 (both files) e173426c157384ebf1b6caf8c6fea18a85a14af9
make test 237 tests, OK
make extents-verify regions=493 disagreements=0 result=AGREE
make gate c_regions=493 differing_bytes=0 result=MATCH
registry audit 493 rows, 0 overlaps, 0 unsorted, 0 bad extents,
0 missing sources, 484 distinct sources
worklist listed=1193, excluded_already_registered=493
negatives index 194 rows, address-ordered, 0 duplicates, 0 registered
git status --short src/ empty (0 untracked files)
firewall 0 tracked paths under any prohibited root
```
## Roster and yield
| Session | Role | Claims | Outcome |
|---|---|---|---|
| `01a0d302-0201` | coordinator | — | merged and gated every claim; 27 merges |
| `01a0d302-18e3` | worker A | **26** | 24 first-attempt with no override; stopped with a complete handoff |
| `01a0d302-8a6f` | worker B | 23 | stopped on a *yield* basis at 47% (recorded as explicitly **not** the measured criterion) |
| `01a0d338-485d` | worker B2 | 9 | replacement for B; produced the md5 guard and the `#nop` finding |
| `01a0d302-fad3` | worker C | 22 | stopped at the measured 68% cap; produced the array-locals lever and the size-band experiment |
| `01a0d344-0c94` | worker C2 | 4 | replacement for C; produced the scheduler-class finding |
Two workers rotated mid-phase on measured criteria, and two fresh sessions replaced them. **Fresh
context beat exhausted context both times**, which is Phase 9's strongest lesson confirmed again.
## The headline finding: THE SIZE-BAND LAW
**The matched corpus has a median size of 48 bytes and 454 of 459 were at ≤200 B, while the remaining
levered rows had a median of 456 B. Size is the strongest predictor of yield left in the pool.**
Two independent measurements, one of them controlled:
| source | ≤200 B | 200 B–800 B |
|---|---|---|
| worker C — same worker, same levers, same day, band the only variable | **1, 1, 1, 2** attempts | **1-in-12** |
| worker B2 — within a single session | **1, 2, 2** attempts | the two >200 B rows it spent the *most* time on: **1 deferred + 1 harness, zero matches** |
This was acted on mid-phase: every worker's dispatch file was **re-ranked to size-band-first**, which
caught worker C heading back into the band it had just measured as exhausted. It is cookbook finding 41
and the number to use when placing a worker-hour in a future phase.
## The phase's character: every lever acquired a limit
Most of Phase 10's levers are **two-sided**, and each side was found by a worker testing the rule
against a row where it failed. Five of the coordinator's own generalisations were bounded this way —
more than in any prior phase:
| coordinator's rule | bounded by | the limit |
|---|---|---|
| "skip the rare-epilogue class" | worker B | the class is a three-way split, and attemptable |
| "a permuted register set = the commutative lever" | worker A | a permuted set can be a **semantic bug** — verify dataflow first |
| "residual at a two-arm branch → invert and swap" | worker B2 | both ordinary spellings can mirror; the guard may need a `goto` |
| "the `goto` trigger needs a `goto`" | worker A | necessary but **not sufficient** |
| "R2 did not fire, so the load-delay class is closed" | worker B2 | R2 only extends to `jr`/`jalr`; the store consumer was untested |
The rules that survived into the cookbook are the ones that were bounded. This is the checks-and-balances
structure working as designed, and it is the phase's most transferable process result.
## Harness work
- **`restores_unsaved` exclusion class** (`tools/sf3_triage` + 5 tests): a body restoring a callee-saved
register it never saves cannot be a whole function. Verified disjoint from the corpus (**0 of 462
registered regions**; 11 worklist rows). It independently re-derived a defect another worker had found
by a different signal.
- **maspsx: honour cc1's explicit `#nop` marker** (developer-authorised). cc1 asked for a load-delay nop
and maspsx overruled it for bare-symbol store consumers. **Verified regression-free: all 489 regions
byte-identical.** Found by worker B2, whose first diagnosis (extend the predicate) would have been a
**no-op** — the predicate already returned True. The coordinator traced the real mechanism.
- **Per-region override plumbing** for the new maspsx modes, plus 5 tests.
- **`tools/maspsx/` is git-ignored**, so every maspsx change is carried as a **tracked patch**
(`tools/patches/maspsx-phase10-r1r2.patch`) verified to reproduce the modified files byte-identically
from the pristine pinned checkout, with provenance in `docs/SETUP.md`. **An in-place edit would not
have survived a fresh clone** — this was caught before it mattered.
- **md5-in-claim-row guard**, adopted after a real concurrent-write collision: `src/func_XXXXXXXX.c` is
a shared, unlocked namespace, so a file can change between a worker's verify and the coordinator's
merge. A mismatch means re-verify rather than merge or reject.
## Incidents, recorded rather than smoothed over
1. **A coordinator double-assignment.** `0x800A9C24` was redistributed to worker B2 and then chartered
to worker C2, so two sessions wrote one file and a correct claim came back DIFF. **The error was the
coordinator's**; neither worker did anything wrong. It produced the md5 guard.
2. **A worker verified a variant and claimed a different file** (merge 20). Returned to its owner; the
rule "verify the file you are about to stage, at the path you are about to claim" was added to every
charter.
3. **A worker dropped a matched row from staging** by reporting it as a *finding* without listing it as
a claim. Caught by that worker's own cleanup audit; the rule "run the audit BEFORE the final report"
was added.
4. **A coordinator commit message silently lost a fact** to unescaped backticks. Amended and
force-pushed; commit messages now go through a file.
5. **A coordinator-recorded reason was wrong** for a tested lever (the test was inert by construction).
Corrected in the tracked index after the worker pointed it out.
Each was caught by the merge flow or by a worker, and none corrupted the tracked registry.
## Scope, safeguards, and what did NOT change
- **The blocked classes stay excluded**: trapping arithmetic, the `0x80012xxx` primitive-init family, and
the maspsx mutual exclusion. 0 of 493 registered regions contains any of them. The phase's harness
changes did **not** reopen them.
- **No scheduler-changing flag was granted.** `cc1=-G8` and `gp=-NAME` were granted because they are
byte-required and each was verified with a failing control; a flag that changed cc1's scheduler would
mean the corpus is no longer all built by one compiler configuration. Workers proposed it twice and
were told no.
- **Inline asm was extended** (developer decision) from "instructions C cannot *name*" to "shapes C
provably cannot *express*", with demonstrated triggers and per-file documentation. First instance: the
PSX scratchpad stack switch, proven byte-exact twice.
- **`git clean -x` / `-fdx` were never used.** No ROM-derived material entered a tracked path at any
point: the largest blob in the entire history is a 163 KB config TSV, and the firewall was audited
across all 200+ commits before the first push to the new remote.
## Unresolved, recorded with evidence
- **`0x8010080C` false extent start** — a `jal` target mid-body splits one real 252-byte function. Root
cause recorded; `sf3_extents` deliberately not changed (a hand-edited extent fails
`make extents-verify` by design, and a real fix alters the boundary rule all 493 regions derive from).
- **The maspsx rare-epilogue mutual exclusion** — finding 40 records the load-delay-hazard mechanism and
the measured failure of the obvious fix. The remaining route is a tracked post-pass modelling ASPSX's
fill for one site; developer-owned, not taken.
- **The two opt-in maspsx modes** (`noreordernop`, `regread`) are implemented, default-off and
regression-safe, but **neither has been shown to close a region**.
- **`0x80107C5C` matches** with the `#nop` fix but its source is a bare variant with no header, so it is
recorded as unblocked-and-one-documented-source-away, **not** counted in the 484.
- **The scheduler class** (`-fno-schedule-insns` gives the original's order on at least two rows, but the
allocation or delay slot then disagrees) is characterised and recorded, not solved.
- **194 recorded negatives** in the tracked index, each with an address, size, status and class, plus the
workers' full residual/spelling/untried-lever detail in the ledger and their handoffs.
## Carried into Phase 11
- The **size-band law** (finding 41) as the dispatch rule.
- The **md5 guard** and the four process rules in finding 57.
- The **negatives index** (194 rows) as the queue-reconciliation input, already proven to keep closed
negatives off the worklist head.
- The **tracked maspsx patch** and its `SETUP.md` provenance.
- Two workers' handoffs with ranked near-matches: worker A's 13 and worker C2's 4, each with an exact
residual and a named untried lever.
- The unresolved items above.
## Rules
No changes to `AGENTS.md`.