phase10: merge 21 — 468 distinct bodies / 477 regions (7 from the milestone)

+4 bodies (worker A claims 19-22, all first-attempt). Candidate gate MATCH before
promotion. make check green: regions=477 AGREE, differing_bytes=0 MATCH.

THE STACK-SWITCH IDIOM IS NOW PROVEN TWICE, BYTE-EXACT, WITH NO OVERRIDE.
0x8006B9E0 matches on the first attempt with no maspsx override, no clobbers and no
operand declarations -- the three-macro form reproduces it exactly, including the
bare filler nop between the second restore and the epilogue's lw ra. Contrasting it
with A's 0x800BC658 near-match (same macros, off by 2 nops) isolates the variable:
0x8006B9E0's two calls have EMPTY delay slots while 0x800BC658's non-fast-path calls
set up an argument cc1 schedules into the slot. So the asm is not the variable -- an
ARGUMENT MOVE AT THE CALL SITE is. That narrows the 0x800BC658 remainder.

TWO NAMED-LOCAL FINDINGS, BOTH DIRECTIONS NOW OBSERVED (the family has seven
instances across the team):
  - 0x8005E538: the HANDLE must be a named local loaded before the first call.
    Inline, cc1 keeps only the object pointer in a callee-saved register and
    RELOADS the handle after the first call (76 vs 88 bytes). Generalisation: a value
    that must survive an intervening call has to be a named local.
  - 0x800F66B8 is the MIRROR CASE and a useful NEGATIVE: d[0] and d[1] are each
    loaded twice with neither held in a register, and the two loads of d[0] go to
    DIFFERENT registers. Naming a local would have been WRONG. So naming a value can
    be byte-required and NOT naming it can be byte-required; the diagnostic is the
    load count, not a rule about locals.

Inline asm in src/func_8006B9E0.c is documented per the Phase 10 convention
(header states the observed sequence, the reason and the limits).
This commit is contained in:
Christopher Williams
2026-09-24 08:02:57 -04:00
parent 39f2091c8a
commit 80b8b33eb4
6 changed files with 1298 additions and 1029 deletions
+1025 -1029
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -79,6 +79,7 @@
0x800262EC 0x800262F8 src/func_800262E0.c
0x800262F8 0x80026304 src/func_800262F8.c
0x800263A8 0x800263E8 src/func_800263A8.c
0x80026650 0x800266A8 src/func_80026650.c
0x800267C0 0x800267CC src/func_800267C0.c
0x800268C4 0x800268F4 src/func_800268C4.c
0x80026C2C 0x80026C7C src/func_80026C2C.c
@@ -197,6 +198,7 @@
0x80058288 0x800582AC src/func_80058288.c
0x8005E29C 0x8005E340 src/func_8005E29C.c
0x8005E3D0 0x8005E3F4 src/func_8005E3D0.c
0x8005E538 0x8005E590 src/func_8005E538.c
0x8005E79C 0x8005E820 src/func_8005E79C.c
0x8005ED6C 0x8005EDBC src/func_8005ED6C.c
0x800645CC 0x80064664 src/func_800645CC.c
@@ -220,6 +222,7 @@
0x8006B1CC 0x8006B214 src/func_8006B1CC.c
0x8006B66C 0x8006B6BC src/func_8006B66C.c
0x8006B778 0x8006B7C0 src/func_8006B778.c
0x8006B9E0 0x8006BA3C src/func_8006B9E0.c
0x8006BC08 0x8006BC34 src/func_8006BC08.c
0x8006BC34 0x8006BC74 src/func_8006BC34.c
0x8006EC94 0x8006ECD4 src/func_8006EC94.c
@@ -385,6 +388,7 @@
0x800F5B40 0x800F5B70 src/func_800F5B40.c
0x800F6330 0x800F6364 src/func_800F6330.c
0x800F6570 0x800F6594 src/func_800F6570.c
0x800F66B8 0x800F6710 src/func_800F66B8.c
0x800F75D0 0x800F760C src/func_800F75D0.c
0x800F7990 0x800F79C0 src/func_800F7990.c
0x800F79C0 0x800F79F0 src/func_800F79C0.c
1 # Code-region registry: one C region per matched function.
79 0x800262EC
80 0x800262F8
81 0x800263A8
82 0x80026650
83 0x800267C0
84 0x800268C4
85 0x80026C2C
198 0x80058288
199 0x8005E29C
200 0x8005E3D0
201 0x8005E538
202 0x8005E79C
203 0x8005ED6C
204 0x800645CC
222 0x8006B1CC
223 0x8006B66C
224 0x8006B778
225 0x8006B9E0
226 0x8006BC08
227 0x8006BC34
228 0x8006EC94
388 0x800F5B40
389 0x800F6330
390 0x800F6570
391 0x800F66B8
392 0x800F75D0
393 0x800F7990
394 0x800F79C0
+62
View File
@@ -0,0 +1,62 @@
/*
* func_80026650 — 88 bytes at 0x80026650..0x800266A8
*
* Drains a singly-linked list held in a field of the argument, calling one routine
* per node, then clears the head.
*
* Original words:
* 27BDFFE0 addiu sp,sp,-32
* AFB10014 sw s1,20(sp)
* 00808821 addu s1,a0,zero ; s1 = p
* AFBF0018 sw ra,24(sp)
* AFB00010 sw s0,16(sp)
* 8E250000 lw a1,0(s1) ; a1 = *p (head)
* 00000000 nop
* 10A00007 beq a1,zero,0x8002668C ; empty -> clear and return
* 00000000 nop
* 8CB00008 lw s0,8(a1) ; (L2) s0 = node->field_8
* 0C009958 jal 0x80026560
* 02202021 addu a0,s1,zero ; (delay) func_80026560(p, node)
* 02002821 addu a1,s0,zero ; node = next
* 14A0FFFB bne a1,zero,0x80026674 ; more -> body
* 00000000 nop
* AE200000 sw zero,0(s1) ; (END) *p = 0
* 8FBF0018 lw ra,24(sp)
* 8FB10014 lw s1,20(sp)
* 8FB00010 lw s0,16(sp)
* 27BD0020 addiu sp,sp,32
* 03E00008 jr ra
* 00000000 nop
*
* Same family callee and same node layout as src/func_800A6B38.c, but with no payload
* store and a `*p = 0` at the single exit. The head is read ONCE before the loop and
* the loop's back edge branches to the BODY start with the next node moved into `a1`
* immediately before it, so this is a `while` whose condition is re-tested on the
* loop variable rather than a re-read of `*p`.
*
* The call's second argument is the CURRENT NODE: `a1` holds it at the `jal` (it was
* loaded into `a1` at the loop head and is only reassigned AFTER the call). Note that
* src/func_80012E84.c passes an int through the same register, so the second
* parameter's TYPE is not recoverable — only its register and its width are.
*
* LIMITS: the function name, the callee, the node offsets (0 for the head, 8 for the
* link) and the list direction are hypotheses read from the instruction shape; only
* the bytes are evidence. `p` is a pointer to the head pointer. func_80026560 is this
* family's unregistered callee; its map is (pointer, one more register) and the
* second slot's type is unknown.
*/
extern void func_80026560(void *p, void *value);
void func_80026650(int **p)
{
int *node = *p;
while (node != 0) {
int *next = *(int **)((char *)node + 8);
func_80026560(p, node);
node = next;
}
*p = 0;
}
+71
View File
@@ -0,0 +1,71 @@
/*
* func_8005E538 — 88 bytes at 0x8005E538..0x8005E590
*
* Two-call update: fills a 16-byte stack record from two fields of one sub-object,
* passes it with a cached handle to a second routine, and writes that result back
* into the handle field.
*
* Original words:
* 27BDFFD0 addiu sp,sp,-48
* AFB10024 sw s1,36(sp)
* 00808821 addu s1,a0,zero ; s1 = a0
* AFBF0028 sw ra,40(sp)
* AFB00020 sw s0,32(sp)
* 8E220020 lw v0,32(s1) ; v0 = a0->field_20
* 00000000 nop
* 8C440000 lw a0,0(v0) ; a0 = v0[0]
* 8C4500E4 lw a1,228(v0) ; a1 = v0[57]
* 8E300B20 lw s0,2848(s1) ; s0 = a0->field_b20 (the cached handle)
* 0C01A01E jal 0x80068078
* 27A60010 addiu a2,sp,16 ; (delay) a2 = &rec
* 02002021 addu a0,s0,zero ; a0 = the cached handle
* 0C02AB27 jal 0x800AAC9C
* 27A50010 addiu a1,sp,16 ; (delay) a1 = &rec
* AE220B20 sw v0,2848(s1) ; a0->field_b20 = result
* 8FBF0028 lw ra,40(sp)
* 8FB10024 lw s1,36(sp)
* 8FB00020 lw s0,32(sp)
* 27BD0030 addiu sp,sp,48
* 03E00008 jr ra
* 00000000 nop
*
* Frame arithmetic fixes the record at 16 bytes: `s0` is at 0x20 and `ra` at 0x28, so
* the local between the outgoing argument area and the saves is exactly 0x10 bytes.
* The second call's result is both stored back into the handle field AND left in `v0`
* at the epilogue, so the function RETURNS it.
*
* THE HANDLE MUST BE A NAMED LOCAL LOADED BEFORE THE FIRST CALL. Written inline in
* the second call (`func_800AAC9C(*(int *)((char *)a0 + 0xB20), rec)`) cc1 keeps only
* the OBJECT POINTER in a callee-saved register and RELOADS the handle after the first
* call, giving a 40-byte frame with one save (76 bytes vs 88). Naming it —
* `int handle = *(int *)((char *)a0 + 0xB20);` before the first call — makes cc1 load
* it early and keep it across the call, which is why the original saves TWO registers
* in a 48-byte frame: `s1` for the object pointer and `s0` for the handle. This is
* the same "an intermediate named local is byte-required" family as the branchless
* boolean in src/func_80099D14.c.
*
* Both calls take three and two arguments respectively: the first is
* `f(v0[0], v0[57], &rec)` and the second `g(handle, &rec)`.
*
* LIMITS: the function name, both callees, the offsets 0x20, 0, 0xE4 and 0xB20, and
* the record's size are hypotheses read from the instruction shape; only the bytes are
* evidence. Neither callee is registered and both are referenced by address-named
* spellings. The record's fields are never read here, so its layout is not recoverable
* from this body.
*/
extern int func_80068078(int a0, int a1, void *a2);
extern int func_800AAC9C(int a0, void *a1);
int func_8005E538(int a0)
{
char rec[16];
int *sub = *(int **)((char *)a0 + 0x20);
int handle = *(int *)((char *)a0 + 0xB20);
int r;
func_80068078(*(int *)sub, *(int *)((char *)sub + 0xE4), rec);
r = func_800AAC9C(handle, rec);
*(int *)((char *)a0 + 0xB20) = r;
return r;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* func_8006B9E0 — 92 bytes at 0x8006B9E0..0x8006BA3C
*
* Runs two routines, each on the alternate scratchpad stack, in sequence.
*
* Original words:
* 27BDFFE8 addiu sp,sp,-24
* AFBF0010 sw ra,16(sp)
* 3C011F80 lui at,0x1F80
* AC3D03FC sw sp,1020(at) ; save sp at 0x1F8003FC
* 3C1D1F80 lui sp,0x1F80
* 37BD03DC ori sp,sp,0x3DC ; sp = 0x1F8003DC
* 0C01AB81 jal 0x8006AE04
* 00000000 nop ; (delay)
* 3C1D1F80 lui sp,0x1F80
* 8FBD03FC lw sp,1020(sp) ; restore sp
* 3C011F80 lui at,0x1F80
* AC3D03FC sw sp,1020(at)
* 3C1D1F80 lui sp,0x1F80
* 37BD03DC ori sp,sp,0x3DC
* 0C01AB95 jal 0x8006AE54
* 00000000 nop ; (delay)
* 3C1D1F80 lui sp,0x1F80
* 8FBD03FC lw sp,1020(sp)
* 00000000 nop
* 8FBF0010 lw ra,16(sp)
* 27BD0018 addiu sp,sp,24
* 03E00008 jr ra
* 00000000 nop
*
* WHY THIS FILE CONTAINS INLINE ASSEMBLY. The two stack-switch idioms write the
* STACK POINTER itself, and the register the C would have to reassign is one cc1
* controls for its own purposes — the inexpressibility is structural, not a
* scheduling or allocation tie-break. This is the developer-sanctioned extension of
* the Phase 8 convention (inline asm for shapes C provably cannot express), recorded
* in docs/MATCHING_CONVENTIONS.md; the integer logic and all scheduling still come
* from cc1 and maspsx. The sibling src/func_800BC658.c uses the same three macros.
*
* The alternate stack is 0x1F8003DC and the saved pointer lives at 0x1F8003FC; the
* restore is `lui sp,0x1F80` / `lw sp,1020(sp)`, i.e. it uses `sp` as the base after
* pointing it at 0x1F800000. Both calls have an empty delay slot filled with a `nop`,
* so neither callee receives any argument this body sets up.
*
* LIMITS: the function name, the two callees, the scratchpad addresses 0x1F8003DC and
* 0x1F8003FC and the sequence are hypotheses read from the instruction shape; only
* the bytes are evidence. Neither callee is registered and both are referenced by
* address-named spellings. The bare `nop` after the second restore is a filler between
* the asm and the epilogue's `lw ra` and is reproduced by the statement layout, not
* by an explicit instruction.
*/
#define SF3_SAVE_SP() \
__asm__ __volatile__("lui $at,0x1f80\n\t" \
"sw $sp,1020($at)")
#define SF3_SET_SP() \
__asm__ __volatile__("lui $sp,0x1f80\n\t" \
"ori $sp,$sp,0x3dc")
#define SF3_REST_SP() \
__asm__ __volatile__("lui $sp,0x1f80\n\t" \
"lw $sp,1020($sp)")
extern void func_8006AE04(void);
extern void func_8006AE54(void);
void func_8006B9E0(void)
{
SF3_SAVE_SP();
SF3_SET_SP();
func_8006AE04();
SF3_REST_SP();
SF3_SAVE_SP();
SF3_SET_SP();
func_8006AE54();
SF3_REST_SP();
}
+60
View File
@@ -0,0 +1,60 @@
/*
* func_800F66B8 — 88 bytes at 0x800F66B8..0x800F6710
*
* Fills three words of a descriptor from two 16-bit arguments and two shift
* expressions over the descriptor's own first two words, then calls on with the
* second shift.
*
* Original words:
* 27BDFFE8 addiu sp,sp,-24
* 3084FFFF andi a0,a0,0xffff ; a0 &= 0xffff
* 30A5FFFF andi a1,a1,0xffff ; a1 &= 0xffff
* AFBF0010 sw ra,16(sp)
* ACC40008 sw a0,8(a2) ; d[2] = a0
* 8CC40000 lw a0,0(a2) ; a0 = d[0]
* 8CC20004 lw v0,4(a2) ; v0 = d[1]
* 24030004 addiu v1,zero,4 ; v1 = 4
* ACC5000C sw a1,12(a2) ; d[3] = a1
* 8CC50000 lw a1,0(a2) ; a1 = d[0] (RELOADED)
* 00831804 sllv v1,v1,a0 ; v1 = 4 << d[0]
* 00431021 addu v0,v0,v1 ; v0 = d[1] + v1
* 2442FFFC addiu v0,v0,-4 ; v0 -= 4
* ACC20010 sw v0,16(a2) ; d[4] = v0
* 24020001 addiu v0,zero,1 ; v0 = 1
* 8CC40004 lw a0,4(a2) ; a0 = d[1] (RELOADED)
* 0C03D109 jal 0x800F4424
* 00A22804 sllv a1,v0,a1 ; (delay) a1 = 1 << d[0]
* 8FBF0010 lw ra,16(sp)
* 27BD0018 addiu sp,sp,24
* 03E00008 jr ra
* 00000000 nop
*
* `d[0]` and `d[1]` are each loaded TWICE and neither value is held in a register
* across the second use, so the source reads the fields again rather than storing
* them in locals: `d[4] = d[1] + (4 << d[0]) - 4;` and `f(d[1], 1 << d[0]);`. The two
* loads of `d[0]` go to different registers (`a0` for the first shift, `a1` for the
* second), which is why the second shift's amount arrives in `a1` — a named local
* would have collapsed both loads into one.
*
* The store order is d[2], d[3], d[4], and the two `andi`s are hoisted above the
* frame's `sw ra`.
*
* LIMITS: the function name, the callee, the descriptor offsets (0, 1, 2, 3, 4), the
* 16-bit masks and the constants 4 and 1 are hypotheses read from the instruction
* shape; only the bytes are evidence. Both arguments are masked to 16 bits with `andi`
* so both are narrower than int; `sllv` takes the shift amount from a register, so
* neither shift is by a constant. func_800F4424 is not registered and is referenced by
* its address-named spelling. The function sets no result, so it is `void`.
*/
extern void func_800F4424(int a0, int a1);
void func_800F66B8(int a0, int a1, int *d)
{
a0 &= 0xffff;
a1 &= 0xffff;
d[2] = a0;
d[3] = a1;
d[4] = d[1] + (4 << d[0]) - 4;
func_800F4424(d[1], 1 << d[0]);
}