phase12: merge 26 (674 bodies) + the epilogue token is a per-row property invisible in a length check

This commit is contained in:
Christopher Williams
2026-09-24 19:14:00 -04:00
parent 9b9ba0c2ea
commit 4588107cd7
4 changed files with 168 additions and 0 deletions
+2
View File
@@ -12,8 +12,10 @@
# Matched so far:
0x80010810 0x8001084C src/func_80010810.c
0x80012780 0x8001278C src/func_80012780.c
0x8001278C 0x800127F0 src/func_8001278C.c maspsx=epilogue
0x800127F0 0x8001281C src/func_800127F0.c
0x8001281C 0x80012834 src/func_8001281C.c
0x80012834 0x8001289C src/func_80012834.c
0x8001289C 0x800128E4 src/func_8001289C.c
0x800128E4 0x80012918 src/func_800128E4.c
0x80012918 0x80012960 src/func_80012918.c
1 # Code-region registry: one C region per matched function.
12 # Matched so far:
13 0x80010810
14 0x80012780
15 0x8001278C
16 0x800127F0
17 0x8001281C
18 0x80012834
19 0x8001289C
20 0x800128E4
21 0x80012918
+50
View File
@@ -1299,3 +1299,53 @@ Not rewriting history for a number, but recording it, because **a wrong body cou
exactly the kind of figure a later session trusts without re-deriving**, and the phase's whole
projection rests on this number being the one the gate produced. The authority is
`config/regions.tsv` counted at the merge, and that said 681 regions / 672 bodies.
### The `maspsx=epilogue` TOKEN is a PER-ROW property and it is INVISIBLE in a length check (worker D)
D's `0x8001278C` (100 B) **requires** `--fill-epilogue`, and `0x80012834` (104 B) — **ten rows away in
the same band** — epilogues PLAIN and must **not** get the flag. This makes the token the second entry
on the list of things that may not be carried across rows, and it is worse than the first:
* the indexed-vs-pointer **loop form** at least shows up as a length difference;
* the **epilogue token** does not. Both shapes have the same instruction COUNT —
the original's `lw ra,16(sp) / move v0,zero / jr ra / addiu sp,sp,24` against cc1's
`lw ra,16(sp) / addiu sp,sp,24 / jr ra / nop` — so a missing token presents as **differing bytes at
the correct length**, which is precisely the presentation every worker on this roster has learned to
read as an *allocator or scheduler floor*. **A worker who trusts the shape will mis-classify a row
that a one-token claim would close.**
**D's procedure, which is the right one and is now the roster's:** *read the original's last three
instructions BEFORE choosing the flag, and carry the token only when the `addiu sp,sp,N` sits inside the
`jr ra` slot.* **15 registered regions carry `maspsx=epilogue` today**, each individually verified
byte-exact, so none is at risk — but the habit is.
### A measurable invariant for extents, and it answers D's delay-slot worry
D reported losing a reading because its own `--end` **excluded the `jr ra` delay slot**, and said the
failure "looked exactly like a one-instruction-short row" — which is the same symptom as my guessed-
extent error earlier the same hour.
I checked all 681 registered regions against the invariant and it holds **uniformly**:
> **a region's extent ends exactly 8 bytes past its LAST `jr $31`** — 681 of 681, zero exceptions.
That is worth having as a procedure because it is a **self-check a worker can run on an extent it
computed itself**: find the last `jr $31` in the extent, confirm `end - that_address == 8`, and if it is
not, the extent is wrong rather than the candidate. It is also the right rule for the multi-exit rows,
where "the last `jr $31`" and "the only `jr $31`" are different addresses.
### Worker D's third instance of "the counter survives unless the source has no counter"
`0x8001278C`'s loop variable is the **byte offset**, not an index: the original walks
`v0 = 180; do { store; v0 -= 20; } while (v0 >= 0)` with the address formed
`lui at,0x8012 / addu at,at,v0 / sw zero,12912(at)`. The natural indexed spelling
(`for (i = 9; i >= 0; i--) D_80123270[i].f0 = 0;`) makes cc1 keep **both** the counter and the derived
IV (`li v1,9`, `addiu v1,v1,-1`, `bgez v1`, and the delay slot becomes `addiu v0,v0,-20` instead of a
`nop`) — four instructions more. Spelling it as an explicit byte offset leaves cc1 with ONE variable,
which is the original. **Third instance this phase: the counter survives unless the source has no
counter to survive.**
Also from that row, D's **second** instance of the symbol-arithmetic procedure paying off: D first
mis-added 2300 and asked for a `0x801221E4` the binary never touches; `gp+12` is `0x80121944` and
`gp+2300` is **`0x80122234`**, and both were already registered. D's own rule now: *read the
displacement and compute the symbol, do not infer it from an adjacent symbol's name.*
+68
View File
@@ -0,0 +1,68 @@
/*
* func_8001278C — 100 bytes at 0x8001278C..0x800127F0
*
* Calls four helpers, zeroes the halfword at gp+12 (0x80121944), clears the first
* word of each of ten 20-byte records at 0x80123270 (walking a BYTE OFFSET down from
* 180), zeroes a SECOND halfword at gp+2300 (0x80122234), and returns 0.
*
* THREE levers, two of them reusable:
*
* 1. **`--fill-epilogue` (region token `maspsx=epilogue`).** The original's tail is
* `lw ra,16(sp) / move v0,zero / jr ra / addiu sp,sp,24` -- the frame release
* INSIDE the jump's delay slot. Without the flag cc1 emits
* `lw ra,16(sp) / addiu sp,sp,24 / jr ra / nop`, which is the same instruction
* COUNT and a different shape, so it shows up as differing bytes rather than a
* length mismatch. Any framed row whose `addiu sp,sp,N` sits in the `jr ra` delay
* slot needs this flag; the claim must carry the token.
* 2. **THE LOOP VARIABLE IS THE BYTE OFFSET, NOT AN INDEX.** The original iterates
* `v0 = 180; do { store; v0 -= 20; } while (v0 >= 0)` with the address formed as
* `lui at,0x8012 / addu at,at,v0 / sw zero,12912(at)`. Writing the natural
* indexed form -- `for (i = 9; i >= 0; i--) D_80123270[i].f0 = 0;` -- makes cc1
* KEEP BOTH the counter and the derived induction variable (`li v1,9`,
* `addiu v1,v1,-1`, `bgez v1`, and the delay slot filled with `addiu v0,v0,-20`
* instead of a `nop`), 4 instructions more. Spelling the walk as an explicit byte
* offset (`*(int *)((char *)D_80123270 + i)`, 180 down to 0 by 20) leaves cc1 with
* a single variable, which is what the original has.
* 3. **THE TWO HALFWORDS ARE DIFFERENT GLOBALS.** Decode the displacement rather than
* reusing the first symbol: gp+12 is 0x80121944 and gp+2300 (0x8FC) is 0x80122234.
* (Both are already registered WITH gp markers, so no symbol request was needed;
* my own first attempt mis-added 2300 and asked for a 0x801221E4 that the binary
* does not touch -- the assembler's displacement is the authority.)
*
* LIMITS: the ten-record/20-byte stride is inferred from the walk (`li v0,180`,
* `addiu v0,v0,-20`), not from a recovered struct; only the first word of each record
* is written, so the record size is a lower bound. The two halfwords are two
* separate symbols and nothing here relates them. The four callees are named for
* their addresses and their contracts are unestablished; two of them are called
* twice in a row and are NOT merged, which is only evidence that they are calls.
*/
typedef struct {
int f0;
char pad[16];
} Rec_80123270;
extern Rec_80123270 D_80123270[10];
extern short D_80121944;
extern short D_80122234;
void func_80026304(void);
void func_80016174(void);
void func_80012F24(void);
void func_80021BA8(void);
int func_8001278C(void)
{
int i;
func_80026304();
func_80016174();
D_80121944 = 0;
for (i = 180; i >= 0; i -= 20) {
*(int *)((char *)D_80123270 + i) = 0;
}
func_80012F24();
func_80021BA8();
func_80021BA8();
D_80122234 = 0;
return 0;
}
+48
View File
@@ -0,0 +1,48 @@
/*
* func_80012834 — 104 bytes at 0x80012834..0x8001289C
*
* If `*q` is non-zero, calls a helper with `p[37]` and `*q`; if the helper's low
* byte is non-zero that byte is returned. Otherwise stores a second helper's result
* back through `q` and returns 0. Matched on the FIRST spelling.
*
* WHAT THE BYTES PIN DOWN: the value of `*q` is loaded ONCE at the top, before the
* branch, and stays live in `a1` across it (the first call passes it without
* reloading), while the first call's FIRST argument is the VALUE at `p+148`
* (`lw a0,148(s1)`) and the second call's first argument is the ADDRESS `p+148`
* (`addiu a0,s1,148`) -- a value/address pair on the same field, which is the shape
* that makes the two calls a get/set pair rather than a repeated call. The second
* call's second argument is the POINTER `q` itself (`move a1,s0`), not `*q`. The
* `0xff` mask is computed once and reused for both the test and the return value
* (`andi v0,v0,0xff` then `bnez v0`), so `v & 0xff` must appear in BOTH the condition
* and the returned expression -- writing it once and returning `v` changes the
* emitted code.
*
* CONTRAST WORTH KEEPING: this function's epilogue is the PLAIN form
* (`addiu sp,sp,32` BEFORE `jr ra`), so it must NOT be built with `--fill-epilogue`.
* 0x8001278C, ten rows away, needs that flag. **The epilogue mode is a per-row
* property, like the loop form -- check which shape the original has before choosing
* the flag, and carry the token only when the frame release is inside the delay
* slot.**
*
* LIMITS: `p` is treated as an `int *` because the field is reached as `p[37]`
* (offset 148) and by `&p[37]`; the field's real type, the meaning of the two
* helpers, the role of `q` (an out-parameter that receives the second helper's
* result, and whose incoming value gates the first call), and what a non-zero low
* byte signifies are NOT recovered. The callees are named for their addresses. The
* frame is 32 bytes with `s0`/`s1`/`ra`, which is what two saved registers plus a
* call costs; nothing here identifies what `s0`/`s1` stand for beyond `q` and `p`.
*/
extern int func_800263E8(int a, int b);
extern int func_80026460(int *a, int *b);
int func_80012834(int *p, int *q)
{
if (*q != 0) {
int v = func_800263E8(p[37], *q);
if (v & 0xff)
return v & 0xff;
}
*q = func_80026460(&p[37], q);
return 0;
}