phase12: merge 23 (667 bodies) + five multi-exit rows ARE gate-permitted for cc1bin

MERGE 23: +2 bodies from worker D's un-attempted band, both re-verified from fresh --work dirs:
0x800582AC (64 B) and 0x800A82D0 (64 B). Gate c_regions=676, differing_bytes=0, MATCH.

DISPATCH CORRECTION, and it came from checking a claim rather than forwarding it. D reported three
multi-exit rows as unclosable because 'my multi-return probes share ONE exit under all ten cc1 builds'.
Measured from the binary: ALL SEVEN unregistered multi-exit rows are >=2 jr $31, which is the gate's
precondition for naming an alternative cc1 -- so the restriction PERMITS the lever on every one, with
four merged rows as precedent.

And C's measured row refutes the probe conclusion directly: 0x801008DC is 3 exits and the default gives
the correct 88 bytes with 58 DIFFERING bytes because it merges all three returns, while
cc1bin=gcc-2.8.1-psx gives 88/0/MATCH. The merge is SHAPE-dependent, and a synthetic probe speaks only
for the shape it probes. I made exactly this error earlier in the phase with a probe-dependent
mechanism written into tools/sf3_match.

Rows routed: A gets 0x80107CCC (3 exits, A has the diagnosis), B gets 0x800FF6DC / 0x80100998 /
0x80107D7C, D gets 0x800FFBBC.

Also recorded: D's self-caught harness hazard -- its run helper leaves the LAST variant on disk, so its
first verification of 0x800A82D0 ran against the wrong variant and report.tsv's md5 described it. D
re-verified from a fresh directory and made the re-write explicit in its procedure. This is the failure
mode that would produce a FALSE MATCH CLAIM rather than a missed one, and it is invisible from outside
because the report's own md5 agrees with the wrong file.
This commit is contained in:
Christopher Williams
2026-09-24 19:01:41 -04:00
parent bf7b6ceac2
commit 58ff408f9f
4 changed files with 182 additions and 0 deletions
+2
View File
@@ -273,6 +273,7 @@
0x80057DFC 0x80057E04 src/func_80057DFC.c
0x80058230 0x80058288 src/func_80058230.c
0x80058288 0x800582AC src/func_80058288.c
0x800582AC 0x800582EC src/func_800582AC.c
0x8005A33C 0x8005A474 src/func_8005A33C.c
0x8005E17C 0x8005E1E4 src/func_8005E17C.c
0x8005E29C 0x8005E340 src/func_8005E29C.c
@@ -474,6 +475,7 @@
0x800A745C 0x800A74BC src/func_800A745C.c
0x800A74BC 0x800A74D0 src/func_800A74BC.c
0x800A8224 0x800A82D0 src/func_800A8224.c
0x800A82D0 0x800A8310 src/func_800A82D0.c
0x800A8920 0x800A8984 src/func_800A8920.c
0x800A8B48 0x800A8B8C src/func_800A8B48.c
0x800A9C24 0x800A9CC4 src/func_800A9C24.c
1 # Code-region registry: one C region per matched function.
273 0x80057DFC
274 0x80058230
275 0x80058288
276 0x800582AC
277 0x8005A33C
278 0x8005E17C
279 0x8005E29C
475 0x800A745C
476 0x800A74BC
477 0x800A8224
478 0x800A82D0
479 0x800A8920
480 0x800A8B48
481 0x800A9C24
+52
View File
@@ -1097,3 +1097,55 @@ is a precise statement of a boundary that four workers have been circling all ph
* **`p += i + start; p->p0 = 0;` rather than `p[i + start].p0 = 0;`** — the subscript form writes the
address sum into the OFFSET's register and rotates the loop; accumulating into a pointer makes the
sum's destination the BASE register (the original's `addu v1,v1,v0`).
### FIVE UNREGISTERED MULTI-EXIT ROWS ARE GATE-PERMITTED FOR `cc1bin`, AND ONE WORKER IS SKIPPING THREE OF THEM
**This is the largest single dispatch correction of the phase and it came from checking a claim I was
about to accept.** Worker D reported that three rows in its band (`0x800FF6DC`, `0x80107CCC`,
`0x80100998`) are the multi-exit class, and that its measurement stands: *"my multi-return probes share
ONE exit under all ten cc1 builds in `tools/old-gcc`, framed and frameless, so no source spelling can
produce them with this toolchain"* — therefore D skips them and the escalation is the coordinator's.
That conclusion is **over-generalised from synthetic probes, and the phase's own record refutes it.**
I measured the exit counts directly from the binary:
| row | `jr $31` | gate permits `cc1bin`? | whose pool |
|---|---|---|---|
| `0x80107CCC` | **3** | yes | A (claimed) |
| `0x800FF6DC` | 2 | yes | B |
| `0x80100998` | 2 | yes | B |
| `0x80107D7C` | 2 | yes | B |
| `0x800FFBBC` | 2 | yes | D |
| `0x80010418` | 2 | yes | — |
| `0x800FD220` | 4 | yes | — |
**All seven are ≥2, which is the gate's precondition for naming an alternative cc1 — so the restriction
PERMITS the lever on every one of them**, and the precedent is four merged rows
(`0x800FF43C`, `0x800FF47C`, `0x80108578`, `0x801008DC`).
**And C's measured row contradicts D's probes directly.** `0x801008DC` is 3 exits and DID NOT MATCH on
the default: 2.7.2 gives the correct 88 bytes with **58 differing bytes because it merges all three
returns into one shared epilogue** — and `cc1bin=gcc-2.8.1-psx` gives 88 / 0 / MATCH. So "the ten cc1
builds all merge multi-returns" is false as stated: **the merge is shape-dependent, and a synthetic
probe can only speak for the shape it probes.** I made exactly this error earlier in the phase (I wrote
a probe-shape-dependent mechanism into `tools/sf3_match` as a finding and had to correct it), which is
why the claim was checked rather than forwarded.
**Action taken: A told to try `cc1bin` on `0x80107CCC` (where A has the full diagnosis — every spelling
merges the two identical return-0 blocks, which is a COMPILER behaviour and therefore the right lever);
B told about its three; D told about its one and that its probe conclusion does not generalise.**
### Worker D's harness hazard, self-caught, and it is the dangerous kind
D reported an error in its own verification with the correction for the log: **its first verification of
`0x800A82D0` ran against the wrong variant left on disk by its `run` helper** (the helper leaves the
LAST variant written), so `report.tsv`'s md5 described the 68-byte variant while the claim was the
64-byte one. D caught it, re-verified from a fresh `--work` dir at 64/0, and made explicit in its
procedure that **the final verification must always re-write the matched source first.**
**Recorded because this is the failure mode that would produce a FALSE MATCH CLAIM rather than a missed
one**, and it is invisible from the outside: a report's own md5 would agree with the wrong file. D found
it in its own work and reported it unprompted, which is the only reason it is in the record at all.
**Every merge I perform re-verifies from a fresh `--work` directory against the file named in the
claim**, which is what makes the roster's numbers safe against this class of slip — the gate is the
backstop, but re-derivation from a clean directory is the primary defence.
+62
View File
@@ -0,0 +1,62 @@
/*
* func_800582AC — 64 bytes at 0x800582AC..0x800582EC
*
* Clears the first field of 42 records in the 44-byte-stride table at 0x8013186C
* and sets a parallel table of 42 words at 0x80131F84 to -1.
*
* Matched on the FOURTH spelling, and the lever is the ORDER OF THE
* INITIALISATIONS, which is the order of the DECLARATIONS:
*
* move a1,zero ; i = 0
* li a2,-1 ; n = -1 <-- BETWEEN the two
* lui/addiu a0,0x80131F84 ; q = D_80131F84
* move v1,zero ; the loop's induction variable
*
* Written the obvious way -- `q = D_80131F84;` first, then a `for (i = 0; ...)`,
* with the -1 as a LITERAL in the body -- cc1 emits the counter and the pointer
* first and then materialises -1 LAST (a constant used only inside the loop is
* hoisted to the preheader, after the pointer init): 18 differing bytes, then 11
* once the pointer/`i` order is fixed. Declaring
*
* int i = 0; int n = -1; int *q = D_80131F84;
*
* and storing the VARIABLE `n` puts the `li a2,-1` exactly between the other two,
* because C executes declaration initialisers in declaration order. **A named
* variable for a value that never changes is the lever whenever a hoisted constant
* lands in the wrong place.**
*
* THE TWO TABLES ARE ACCESSED DIFFERENTLY, and the bytes show it: the 44-byte table
* is INDEXED (`D_8013186C[i].f0 = 0`), so cc1's strength reduction creates the
* `v1` induction variable (`addiu v1,v1,44` in the loop's branch delay slot) and
* re-materialises the base per iteration (`lui at,0x8013 / addu at,at,v1 /
* sw zero,6252(at)`); the word table is WALKED (`*q++ = n`, `addiu a0,a0,4`). A
* single base register for both tables, or indexing both, does not reproduce this.
*
* LIMITS: the tables are named for their addresses and typed from the strides
* (44 = the record size the `addiu v1,v1,44` proves; 4 = a word). Nothing here
* establishes what the records are, what field 0 means, why one table is cleared
* and the other set to -1, or that the two tables are related at all -- only that
* both are touched 42 times, which is the `slti v0,a1,42`. The `int` element type
* of the second table is the 4-byte store; it could be a pointer array with the
* same instruction. The `i = 0` declaration order matters only for the register
* pair (a1/a0); with the pointer declared first cc1 swaps them.
*/
typedef struct {
int f0;
char pad[40];
} Rec_8013186C;
extern Rec_8013186C D_8013186C[42];
extern int D_80131F84[42];
void func_800582AC(void)
{
int i = 0;
int n = -1;
int *q = D_80131F84;
for (; i < 42; i++) {
D_8013186C[i].f0 = 0;
*q++ = n;
}
}
+66
View File
@@ -0,0 +1,66 @@
/*
* func_800A82D0 — 64 bytes at 0x800A82D0..0x800A8310
*
* `lookup(id)`: the low byte indexes a 28-byte-stride table at 0x8013B878; if the
* record's byte at offset 21 equals `id >> 8` the record's ADDRESS is returned, but
* only when the index is below 64, and zero otherwise.
*
* Matched on the SIXTH spelling. Two levers, both about how a single value is
* returned:
*
* 1. **THE EARLY RETURN IS THE NEGATED ONE.** Written the natural way --
* if ((id >> 8) == p->b21)
* return (idx < 64) ? (unsigned int)p : 0;
* return 0;
* cc1 emits the value path, then a `j` to the shared epilogue, and a separate
* zero path: 68 bytes. Written with the test NEGATED and the zero returned
* FIRST --
* if ((id >> 8) != p->b21)
* return 0;
* return (idx < 64) ? (unsigned int)p : 0;
* the zero is set in the BRANCH DELAY SLOT (`move v0,zero`, which runs on both
* paths) and the value path falls straight through into the single `jr ra`:
* 64 bytes, MATCH. Note the delay slot writes v0 -- the RETURN register -- which
* is what makes the fall-through value computation land in v0 directly instead
* of in a temporary that is moved at the end (that shape is 5 differing bytes:
* the value in a2 plus a `move v0,a2`).
* 2. **THE INDEX IS SIGNED.** `int idx` gives the original's `slti v0,a1,64`;
* `unsigned int idx` gives `sltiu` (1 differing byte, and the wrong one).
*
* The value itself is a BRANCHLESS masked select -- `slti` / `negu` / `and` -- which
* is cc1's form for `(idx < 64) ? (unsigned int)p : 0`. Splitting it into an `if`
* branches and does not match; the ternary IS the source shape here.
*
* The address arithmetic is `idx * 28`, emitted as `sll 3` / `subu` / `sll 2`
* (x8, -x, x4), and the record base is added with the OFFSET as the first operand
* (`addu v1,v0,v1`), which is what `&table[idx]` produces.
*
* LIMITS: the table is named for its address; the 28-byte stride is proved by the
* `sll/subu/sll` sequence and the offset 21 by the `lbu`, but the field's meaning is
* NOT recovered, and the record could be a longer object of which only these two
* fields are touched. The `idx < 64` bound is the only evidence for the table's
* length; nothing proves 64 is a size rather than a validity mask. `id`'s two
* halves are treated as (low byte, next byte) because that is what the `andi`/`srl`
* pair does -- whether the game calls them an index and a tag is unknown. The
* function's return type is written `unsigned int` because the returned value is an
* ADDRESS masked by a boolean; a pointer return type produces the same bytes.
*/
typedef struct {
char pad[21];
unsigned char b21;
char pad2[6];
} Rec_8013B878;
extern Rec_8013B878 D_8013B878[64];
unsigned int func_800A82D0(unsigned int id)
{
int idx;
Rec_8013B878 *p;
idx = id & 0xff;
p = &D_8013B878[idx];
if ((id >> 8) != p->b21)
return 0;
return (idx < 64) ? (unsigned int)p : 0;
}