diff --git a/config/regions.tsv b/config/regions.tsv index d388c79..48e8fa8 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -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 diff --git a/phase-ends/logs/Phase12.md b/phase-ends/logs/Phase12.md index 7e3c6e4..f2b18b9 100644 --- a/phase-ends/logs/Phase12.md +++ b/phase-ends/logs/Phase12.md @@ -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. diff --git a/src/func_800582AC.c b/src/func_800582AC.c new file mode 100644 index 0000000..5e7cf78 --- /dev/null +++ b/src/func_800582AC.c @@ -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; + } +} diff --git a/src/func_800A82D0.c b/src/func_800A82D0.c new file mode 100644 index 0000000..251bcd4 --- /dev/null +++ b/src/func_800A82D0.c @@ -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; +}