feat(phase-29): caller pair banked (57,822 templ ins) via K&R defs — §92's remedy corrected (§99)

func_80175AB8 + func_80175DA8 both banked. R22 clean-fleet 140 passed / 0 failed of 140.

§92 SAID these need "the §17a-1 caller pair, NOT a bare conform" — the diagnosis was right (conforming
a narrow param changes argument promotion at every call site, measured PLUMBING -> DIFF) but the
remedy was the expensive one. The actual fix touches NO declaration: convert the DEFINITION to K&R,
where a narrow param PROMOTES to int (C89 6.3.2.2) and is therefore already compatible with the
fleet's existing `s32` prototype, while still emitting narrow-param codegen. §43 applied to the def
side. T0 draft-only, ZERO blast radius, versus a 524-site fleet conform.

THREE reconcile_tu BUGS SURFACED, ONE OF THEM MINE:
(a) BLIND TO BLOCK SCOPE. split_statements is depth-0 BY DESIGN, and §8d deliberately demotes data
    externs into the function body — so the tool saw one statement and no declarations, printing
    "reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0" for a draft cc1 rejected with
    `conflicting types for D_8011F7BC`. A silent skip (R32). Fixed: descend one level.
(b) MY BUG, introduced by (a): descending into ANY `{` also enters struct/union/enum definitions, so
    MEMBERS parse as declarations and get conformed — `u32 code;` became the TU's
    `typedef void (*code)(unsigned short*);` INSIDE the struct, and `p->code` became
    `p->(*(u32 *)&code)`. Caught by DIFFING THE TOOL'S OUTPUT AGAINST ITS INPUT before trusting it;
    the byte-gate would have said PLUMBING and explained nothing. Guard: function bodies only.
(c) LATENT since the tool was written: _cast_sub matched bare identifiers and rewrote MEMBER ACCESSES
    as globals. Unreachable until (a) existed. Guard: (?<![.\w])(?<!->).

cookbook §99.
This commit is contained in:
Drew T
2026-07-27 21:27:24 -06:00
parent 3d01aaea8c
commit 5f1fc5b5eb
4 changed files with 390 additions and 4 deletions
+49
View File
@@ -7142,3 +7142,52 @@ defining TU's own decl); definition + register pin intact; both drafts re-banked
> binary **succeeded** — because it reused objects the clean run rebuilds. *An incremental pass does
> not refute a clean-tree failure.* Every step of this diagnosis came from reading the real cc1/ld
> error after a genuinely clean rebuild, never from reasoning about what the tool "should" do.
## §99 — The narrow-param wall is a DEF-side problem with a ZERO-blast-radius fix: convert the definition to K&R (Phase 29 SESSION-22, `func_80175AB8` + `func_80175DA8`)
**§92 said these two need "the §17a-1 caller pair, NOT a bare conform".** The diagnosis was right —
conforming the fleet's `void f(s32)` declarations to the byte-true `void f(s16)` narrows the
parameter, changing argument promotion at **every** call site, so the callers emit different code
(§92 measured exactly that: PLUMBING before the conform, **DIFF** after). But the prescribed remedy
was the expensive one. The actual fix touches **no declaration at all**:
```c
void func_80175AB8(param_1) /* K&R: the narrow param PROMOTES to int (C89 6.3.2.2), */
s16 param_1; /* so this is ALREADY compatible with the fleet's `s32` */
{ /* prototype — while still emitting the narrow-param codegen. */
```
That is §43 applied to the **definition** side instead of the declaration side. Cost: a draft-only
edit (**T0**, zero blast radius) versus a **524-site fleet conform**. Both banked, `d19c9580`,
R22 140/140 — **57,822 templated instructions for two draft-local rewrites.**
> **The law:** when the byte-true signature has a NARROW scalar parameter and the fleet declares it
> wide, do NOT move the declarations. Move the DEFINITION to K&R and let C's promotion rule make the
> existing prototype correct. Conform only when the disagreement is a POINTER shape (caller-neutral,
> §85) or an arity/return change.
### Two `reconcile_tu` bugs found underneath, one introduced while fixing the other
**(a) It was blind to BLOCK-SCOPE declarations.** `split_statements` is depth-0 **by design**, so for
a draft whose body is one function definition it returns exactly ONE statement and every declaration
inside is invisible — and §8d (`scope_data_externs`) *deliberately demotes the data externs to block
scope*. C still requires a block-scope `extern` to agree with a file-scope declaration in scope, so
the conflict is real: the tool printed `reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0`
for a draft cc1 then rejected with `conflicting types for D_8011F7BC`. Fixed by descending one level.
**(b) DESCENDING INTO ANY `{` CORRUPTS STRUCTS — a bug I introduced with (a) and caught by diffing
the tool's own output against its input.** Struct MEMBERS parse as declarations and get "conformed":
```
- u32 code; /* 0x04 */ -> typedef void (*code)(unsigned short*);
- p->code = *(u32 *)src; -> p->(*(u32 *)&code) = *(u32 *)src;
```
Guard: descend only into a FUNCTION body (a parameter list before the brace, and not a
`typedef|struct|union|enum` head).
**(c) And it exposed a LATENT one:** `_cast_sub`'s regex matched a bare identifier, so it rewrote
**member accesses** as if they were the global. Broken since the tool was written; only reachable
once block-scope descent started finding such names. Guard: `(?<![.\w])(?<!->)`.
> **Process note worth keeping:** (b) was caught because the transform's output was diffed against
> its input *before* the result was trusted — not by a gate. The byte-gate would have reported
> PLUMBING and told me nothing about *why*, and the corrupted draft looked plausible.
+22
View File
@@ -6136,3 +6136,25 @@ closure-scope error) — on the tool that banked 543 members today. **Reverted,
Restructuring a proven tool with blind string replaces at the end of a long session is how a working
thing gets broken. Left as a specified next-session task; `sweep_parallel.py` already exists and is
proven, so the work is *wiring*, not invention.
## ✅ T22 — the caller pair BANKED (57,822 templ ins) with ZERO fleet edits; §92's remedy corrected
**`func_80175AB8` + `func_80175DA8` both banked.** R22 clean-fleet **140/140**.
**§92 prescribed the expensive remedy.** Its diagnosis was right (conforming a narrow param changes
argument promotion at every call site → DIFF), but the fix needs **no declaration touched**: convert
the DEFINITION to **K&R**, where the narrow param promotes to `int` and is therefore already
compatible with the fleet's existing `s32` prototype — §43 applied to the def side. **T0, zero blast
radius, versus a 524-site fleet conform.** → cookbook **§99**.
**Three `reconcile_tu` bugs surfaced, and ONE WAS MINE:**
- **(a)** blind to **block-scope** declarations (`split_statements` is depth-0 by design, and §8d
*deliberately demotes* data externs into the function body) → reported `0 reconciled / 0 coverage
defects` for a draft cc1 rejected with `conflicting types for D_8011F7BC`. Fixed: descend one level.
- **(b) MY BUG, introduced by (a):** descending into ANY `{` also enters struct definitions, so
members parse as declarations and get conformed — it rewrote `u32 code;` into the TU's
`typedef void (*code)(...)` INSIDE the struct and mangled `p->code` → `p->(*(u32 *)&code)`.
**Caught by diffing the tool's output against its input before trusting it** — the gate would only
have said PLUMBING. Guard: descend only into a function body.
- **(c)** latent since the tool was written: `_cast_sub` matched bare identifiers, rewriting **member
accesses** as globals. Only reachable once (a) existed. Guard: `(?<![.\w])(?<!->)`.
+271 -2
View File
@@ -2998,9 +2998,278 @@ INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_801758F
INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_801759D8);
INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80175AB8);
// @class: regalloc-order
// @stuck: none — MATCH (188/188), symcheck SYMS-OK (22 symbols)
//
// func_80175AB8 — ov_SC01_077 h_seq exemplar (188 ins, ×138 members).
// Zero file-scope footprint (§28/§43): typedef + every extern is block-scoped so the body
// lifts as one unit for dedup_propagate / family_sweep.
//
// FOUR byte-proven levers found here (all newly measured this session, closeness 15 -> 0):
//
// L1 base-pointer opacity WITHOUT killing the birthing boost (sched.c birthing_insn_p).
// $s2 must hold &D_8011F7F0 while `arr = base - 0x48` stays a SEPARATE addiu, so the
// symbol must not constant-fold. The obvious fence `__asm__("" : "=r"(base) : "0"(base))`
// works but sets `base` TWICE => reg_n_sets[base]==2 => birthing_insn_p() returns 0 =>
// adjust_priority() skips the max_priority boost => the `la` is scheduled at the very top
// instead of after `sll $s3,$v0,2`. Fix: fence a SINGLE-set pseudo —
// __asm__("" : "=r"(base) : "0"(&D_8011F7F0));
// `base` is now set once (by the asm) and the address temp once (by the `la`), so BOTH keep
// the birthing boost and land where the target puts them. (15 -> 5 mismatches.)
//
// L2 a volatile STORE is a memory barrier that register moves may cross; a
// `__asm__ __volatile__("" ::: "memory")` is a FULL barrier that they may not.
// sched.c's ASM_OPERANDS case adds a dependence on every reg's last set/use for a volatile
// asm, so the memory fence pinned `sw $s1,0x18($s4)` BEFORE the call's `addu $a0,$s1,$zero`.
// Writing the store as `*(volatile u32 *)&slot[6] = ...` keeps the store anchored (dropping
// the fence entirely let it sink 11 slots) while letting the arg move hoist past it.
// (5 -> 3 mismatches.) The SECOND memory fence (before func_80024054) is still required.
//
// L3 local-alloc.c block_alloc ties operand 0 to the first input operand that DIES
// (combine_regs). `sum = r1 + r2` therefore inherits r1's quantity — and r1 crosses a call,
// so the whole quantity is forced call-preserved ($s0) and the `move $5,ext` copy-suggestion
// is ignored. Neither variable reuse nor an extra post-addu use of r1 breaks the tie
// (sched1 runs first and re-sinks the use). A pin whose live range crosses no call does:
// register s32 sum __asm__("$5");
// (§44-correction: a pin that does not span a `jal` is ×N-safe.) (3 -> 2 mismatches.)
//
// L4 pinning `sum` to $a1 makes gcc emit it before the $a0 arg move; pinning the $a0 arg too
// (`register u32 *a0v __asm__("$4"); a0v = p;`) restores the target order. (2 -> 0.)
//
// Also note the deliberate MIXED addressing of the same objects: (*(u16 *)&D_8011F7BC)/(*(u16 *)&D_8011F7BE) are read
// absolutely at the 3rd call (lui/lhu) and $s2-relatively (-0x34/-0x32) at the 4th/6th/7th. That
// is what the target does; writing either form uniformly breaks it.
#include "common.h"
void func_80175AB8(param_1)
s16 param_1;
{
typedef struct { s32 g0; s32 pad[2]; } S_AF634; /* size 0x0C */
extern S_AF634 D_800AF634[];
extern S_AF634 D_800AF638[];
extern u8 D_8011F7F0;
extern u16 D_8011F7B4;
extern u16 D_8011F7B6;
extern s16 D_8011F7BC;
extern s16 D_8011F7BE;
extern s32 D_8011F804;
extern u16 D_8011F80A;
extern u16 D_8011F824;
extern u8 D_8011F832;
extern u8 D_8011F83A;
extern u8 *D_8018A23C[];
extern u32 *func_80176D94(void *param_1, u32 param_2, s16 param_3_);
extern u32 *func_801770E0(void *param_1, u32 param_2, s16 param_3_);
extern u32 func_801783D0(s32 a0, s32 a1);
extern u32 *func_80177EA4(u32 *param_1, s32 param_2, u32 param_3, s32 param_4);
extern u32 *func_80177B5C(u32 *a0, s32 a1, s32 a2, s32 a3, s32 a4);
extern void func_80177940(u32 *p, u32 a_, u32 b_, u32 c_);
extern u32 *func_80178298(u32 *param_1, u8 *param_2, short param_3, short param_4);
extern s32 func_80024054(u8*, u8*);
extern s32 func_8005A600(s32, s32, s32, s32, s32);
u8 local[24];
u8 *base;
u8 *arr;
u32 *slot;
u32 *p;
register u32 *a0v __asm__("$4");
u32 uv;
s32 k;
s16 q;
s32 r1;
s32 a2v;
register s32 sum __asm__("$5");
s32 r2;
s32 idx;
s16 sv;
p = (u32 *)(D_800AF638[param_1].g0 + D_800AF634[param_1].g0 * 4);
__asm__("" : "=r"(base) : "0"(&D_8011F7F0)); /* L1 */
arr = base - 0x48;
slot = (u32 *)(param_1 * 4 + (s32)arr);
*(volatile u32 *)&slot[6] = (u32)p; /* L2 */
p = func_80176D94(p, (s16)(D_8011F7B4 - 0x71), (s16)(D_8011F7B6 + 0x51));
p = func_801770E0(p, (s16)(D_8011F7B4 - 0x71), (s16)(D_8011F7B6 + 0x65));
p = func_80177EA4(p, func_801783D0(D_8011F804, 0),
(s16)((*(u16 *)&D_8011F7BC) + 0x39), (s16)((*(u16 *)&D_8011F7BE) + 0x51));
uv = D_8011F824;
k = 0x3E7;
if (uv < 1000) {
k = uv;
}
sv = (s16)func_801783D0(k, 4);
p = func_80177B5C(p, sv, D_8011F832,
(s16)(*(u16 *)(base - 0x34) + 0x39),
(s16)(*(u16 *)(base - 0x32) + 0x65));
q = (s16)D_8011F80A / 15;
a2v = (q & 3) * 15;
r1 = func_801783D0((s32)(q << 16) >> 18, 8);
r2 = func_801783D0(a2v, 0);
a0v = p; /* L4 */
sum = r1 + r2; /* L3 */
p = ((u32 * (*)(u32 *, s32, s32, s32))func_80177940)(
a0v, (s16)sum,
(s16)(*(u16 *)(base - 0x34) + 0x76),
(s16)(*(u16 *)(base - 0x32) + 0x65));
idx = D_8011F83A & 0x7F;
D_8011F83A = idx;
__asm__ __volatile__("" ::: "memory");
((void (*)(s32, u8 *))func_80024054)(((s32 *)D_8018A23C)[idx], local);
p = func_80178298(p, local,
(s16)(*(u16 *)(base - 0x34) + 0x39),
(s16)(*(u16 *)(base - 0x32) + 0x5B));
func_8005A600((s32)p, 0, 0, 0x15, 0);
*p = (((u32)p - 0x14) & 0xFFFFFF) | 0x2000000;
slot[8] = (u32)p;
p += 5;
D_800AF634[param_1].g0 += ((s32)p - (s32)slot[6]) >> 2;
}
// @class: schedule
// @stuck: none — MATCH (231 ins)
#include "common.h"
extern u8 D_8011F7F0;
extern u8 D_800B9A13;
typedef struct { s32 g0; s32 pad[2]; } S_AF634; /* size 0x0C */
extern S_AF634 D_800AF634[];
extern S_AF634 D_800AF638[];
extern u16 D_8018A1DC[];
extern u16 D_8018A22C[];
extern u16 D_8018A238;
extern u8 D_8018A2B8[];
extern u8 D_8018A2CC[];
extern s32 D_8018A2E4[];
extern u8 D_800D43D4;
extern u8 D_800D4414;
extern u8 D_800D45D4;
extern u32 *func_8017742C(u32 *a0, s32 a1, s32 a2);
extern s32 func_8005A600(s32 a0, s32 a1, s32 a2, s32 a3, s32 a4);
extern void func_800183E0(s32 a0);
typedef struct {
u32 tag; /* 0x00 */
u32 code; /* 0x04 */
u16 x; /* 0x08 */
u16 y; /* 0x0A */
u32 uv; /* 0x0C */
u32 wh; /* 0x10 */
} Sp_80175DA8; /* 0x14 */
void func_80175DA8(param_1)
u16 param_1;
{
u8 *base = &D_8011F7F0;
u8 *s = base - 0x48;
u16 *src = D_8018A1DC;
Sp_80175DA8 *p;
s16 i;
s32 arg;
s32 t;
s32 fl;
u16 v;
p = (Sp_80175DA8 *)(D_800AF638[(s16)param_1].g0 + D_800AF634[(s16)param_1].g0 * 4);
*(Sp_80175DA8 **)(s + (s16)param_1 * 4 + 0x28) = p;
i = 0;
do {
p->tag = ((u32)(p - 1) & 0xFFFFFF) | 0x4000000;
p->code = *(u32 *)src;
src += 2;
if (i < 2) {
p->x = *src++ + *(u16 *)(s + 0xC);
p->y = *src++ + *(u16 *)(s + 0xE);
} else if (i == 2) {
p->x = *src++ + *(u16 *)(s + 0x10);
p->y = *src++ + *(u16 *)(s + 0x12);
} else {
p->x = *src++ + *(u16 *)(s + 0x14);
p->y = *src++ + *(u16 *)(s + 0x16);
}
p->uv = *(u32 *)src;
src += 2;
p->wh = *(u32 *)src;
src += 2;
p++;
i++;
} while (i < 5);
*(u8 *)(s + 7) = D_800B9A13;
p = (Sp_80175DA8 *)func_8017742C((u32 *)p,
(s16)(*(u16 *)(s + 0x10) - 0x98),
(s16)(*(u16 *)(s + 0x12) + 9));
func_8005A600((s32)p, 0, 0, 0x16, 0);
p->tag = ((u32)(p - 1) & 0xFFFFFF) | 0x2000000;
*(Sp_80175DA8 **)(s + (s16)param_1 * 4 + 0x30) = p;
p++;
{
s32 acc = D_800AF634[(s16)param_1].g0;
D_800AF634[(s16)param_1].g0 =
acc + (((s32)p - *(s32 *)(s + (s16)param_1 * 4 + 0x28)) >> 2);
}
p = *(Sp_80175DA8 **)(s + (s16)param_1 * 4 + 0x28);
if (base[0x48] != 0) {
*((u8 *)p + 0xD) = 0;
} else {
*((u8 *)p + 0xD) = 0xA0;
}
{
u8 *q1 = *(u8 **)(s + (s16)param_1 * 4 + 0x28);
*(u16 *)(q1 + 0x22) = 0x6CD6;
if (base[0x48] & 0x80) {
*(u16 *)(q1 + 0x20) = D_8018A238;
arg = (s32)&D_800D45D4;
} else {
u16 *tt = D_8018A22C;
s32 k = base[0x48];
if (k != 0) {
k--;
tt += k;
}
*(u16 *)(q1 + 0x20) = *tt;
arg = D_8018A2E4[base[0x48]];
}
}
func_800183E0(arg);
{
u8 *q2 = *(u8 **)(s + (s16)param_1 * 4 + 0x28);
t = (s32)(*(u16 *)(base + 0x2E) << 16);
if (t != 0) {
q2[0x49] = D_8018A2CC[t >> 20];
} else {
q2[0x49] = 0xA0;
}
}
{
u8 *q3 = *(u8 **)(s + (s16)param_1 * 4 + 0x28);
v = *(u16 *)(base + 0x40);
if (v < 100) {
q3[0x5D] = D_8018A2B8[v / 5];
} else {
q3[0x5D] = 0;
}
}
fl = *(s16 *)(base + 0x1E) & 0x8000;
/* §5a zero-byte sched fence: without it sched1 hoists the `la D_800D43D4`
into the lh's load-delay slot, dropping the target's nop (-1 ins). */
__asm__("");
arg = (s32)&D_800D43D4;
if (fl != 0) {
arg = (s32)&D_800D4414;
}
func_800183E0(arg);
}
INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80175DA8);
INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80176144);
+48 -2
View File
@@ -95,7 +95,11 @@ def _cast_sub(d, tu):
`D_x[i]()` into `((u8 *)D_x)[i]()` — a dormant transform that fixing the parser would ARM. This
tool handles the kind natively, which is why it supersedes that one rather than patching it."""
sym = d.name
rx = re.compile(rf'(&?)\b{re.escape(sym)}\b(\s*\[)?')
# `(?<![.\w])(?<!->)` — a MEMBER ACCESS is not this symbol. Without it, a struct field that
# happens to share a global's name is rewritten at every use: `p->code` became
# `p->(*(u32 *)&code)`, which is not even valid C. Latent since the tool was written and only
# reachable once block-scope descent started finding such names (Phase 29 SESSION-22).
rx = re.compile(rf'(&?)(?<![.\w])(?<!->){re.escape(sym)}\b(\s*\[)?')
c_arr = tu.kind in ('array', 'fnptr_array')
e = _elem(d)
@@ -110,16 +114,58 @@ def _cast_sub(d, tu):
return rx, lambda m: f'{m.group(1)}{acc}{m.group(2) or ""}'
def _draft_statements(body):
"""Every declaration-bearing statement in the draft, at FILE scope AND at BLOCK scope, as
(Stmt-with-ABSOLUTE-spans, is_inner).
WHY (Phase 29 SESSION-22, byte-witnessed on func_80175AB8). `split_statements` is depth-0 BY
DESIGN, so for a draft whose body is one function definition it returns exactly ONE statement —
the definition — and every declaration inside it is invisible. That is precisely where the data
externs live: §8d (`scope_data_externs`) DEMOTES them to block scope on purpose, because a
block-scope extern declares no global for the TU's own later decls to collide with.
But C still requires a block-scope `extern` to be compatible with a file-scope declaration of the
same identifier that is in scope — so the conflict is real, and this tool was structurally blind
to it: it reported `reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0` for a draft
cc1 then rejected with `conflicting types for D_8011F7BC`. A silent skip that reads exactly like
"nothing to do" (R32).
One level of descent is enough and is deliberate: the externs sit at the top of the function
body, not inside nested blocks, and recursing further would start parsing arbitrary code."""
out = []
for st in cdecl.split_statements(body):
out.append((st, False))
m = re.search(r'\{', cdecl._mask(st.text))
if not m:
continue
# DESCEND ONLY INTO A FUNCTION BODY. Descending into ANY `{` also enters struct/union/enum
# definitions, whose MEMBERS then parse as declarations and get "conformed" against the TU —
# byte-witnessed while writing this: a member `u32 code;` was rewritten to the TU's
# declaration of `code` (`typedef void (*code)(unsigned short*);`) INSIDE the struct, and the
# member ACCESS `p->code` became `p->(*(u32 *)&code)`. A function definition is distinguished
# by a parameter list before the brace and by not being a tag/typedef definition.
head = st.text[:m.start()]
if re.match(r'\s*(typedef|struct|union|enum)\b', head) or ')' not in head:
continue
off = st.start + m.end() # first byte INSIDE the body
inner_txt = body[off:st.end]
for s2 in cdecl.split_statements(inner_txt):
out.append((cdecl.Stmt(s2.text, off + s2.start, off + s2.end), True))
return out
def fix(body, tu_path, fn):
"""Conform the draft's DATA decls to the TU. Returns (new_body, notes)."""
full = cdecl.tu_scope(tu_path) # CONFLICT domain: a decl BELOW still conflicts
above = set(cdecl.tu_scope(tu_path, above=fn)) # ORDER, for cc1's no-prototype rule
plan, notes = {}, []
for st in cdecl.split_statements(body):
for st, inner in _draft_statements(body):
try:
ds = cdecl.parse(st.text)
except cdecl.CDeclError as e:
if inner:
continue # ordinary CODE inside a function body — not a declaration, not a defect
notes.append(f'!! UNPARSED (a coverage defect, not a no-op): {st.text[:60]} -> {e}')
continue
for d in ds: