feat(phase-29): func_8014D820 banked (41,952 ins) + conform_decls scalar-narrowing guard

WAVE 2 (9 never-drafted exemplars, ultracode): 9/9 returned, 5 MATCH / 4 near, 2.25M tokens.

BANKED: func_8014D820 (304 ins ×138) — and its agent ROOT-CAUSED the failure I left undiagnosed.
It was never an assembler problem: cc1 exit 33, `conflicting types for 'Ent'` vs
engine_types.h:434, surfaced by the recipe's `set -o pipefail` and MISATTRIBUTED to `as` because
`as` is the last stage in the pipe (Makefile:560). Fixed by moving V4/Desc/Ent to BLOCK scope —
byte-neutral and collision-proof across all 138 member TUs. Vindicates flagging it to the agent as
UNVERIFIED rather than passing my own guess forward as fact (§88e).
R22 clean-fleet 140/140.

NEW GUARD — SCALAR NARROWING IS NOT CALLER-NEUTRAL (byte-proven, and it cost 3 gate cycles):
conform_decls treated all decl type changes alike. A POINTER change is caller-neutral (func_80179B74
conformed 1,600 sites s16*/short* -> u16* and stayed byte-identical fleet-wide). A SCALAR WIDTH
change is NOT: narrowing `s32 a0` -> `u16 param_1` changes argument promotion at every call site.
MEASURED on func_80175DA8: decls reverted -> gate says PLUMBING; conform applied -> gate says DIFF.
The conform did not fix the draft, it changed the CALLERS. Now warned explicitly (not refused — the
draft's sig is still byte-truth for the callee and the gate arbitrates), with the instruction that a
DIFF after this conform means examine the callers (§17a-1 pair), not the body.
Verified the guard discriminates: fires on func_80175DA8 (s32->u16), silent on func_80179B74.

STILL OPEN from wave 2: func_80176218 + func_80175AB8 (DATA-symbol conflicts, D_80078EB4 /
D_8011F7BC -> reconcile_decls) · func_80175DA8 + func_80135EB0 (need the §17a-1 caller pair, not a
bare conform) · 4 near-misses with precise residuals recorded (func_80176734 129 length-drift,
func_80140958 49 inverted-hoist, func_80177B5C 19 sched tie, func_8017C974 83 -> permuter).
This commit is contained in:
Drew T
2026-07-27 15:38:15 -06:00
parent 42e9eaba55
commit 7d9dd6deca
4 changed files with 2042 additions and 1688 deletions
+1679 -1680
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -4,16 +4,16 @@
# cross-binary collapsible-byte leverage: docs/duplicates.cross.md.
# THREE progress metrics (all matter — see the labels):
FLEET fn-count byte-ident: 317487 / 353720 = 89.76% (REAL+LINKED+empties; FUNCTION-count, ×134-inflated — one crack counts per overlay)
FLEET instr-weighted : 10841039 / 13141652 = 82.5% (shipped .text across main + resident + 138 overlays; the decomp.dev-DISPLAY number)
FLEET distinct-code(uniq): 3979994 / 5634875 = 70.6% (65725/87459 unique fns; the DISTINCT-RE number)
FLEET fn-count byte-ident: 317488 / 353720 = 89.76% (REAL+LINKED+empties; FUNCTION-count, ×134-inflated — one crack counts per overlay)
FLEET instr-weighted : 10841343 / 13141652 = 82.5% (shipped .text across main + resident + 138 overlays; the decomp.dev-DISPLAY number)
FLEET distinct-code(uniq): 3980298 / 5634875 = 70.6% (65726/87459 unique fns; the DISTINCT-RE number)
MAIN game-code weighted : 436 / 60201 = 0.7% (INCLUDED in the fleet numbers above since 2026-07-22 — roadmap §1 metrics contract; LINKED-excluding Ghidra sig dated 2026-06-14; caveat is R34: no independent second oracle for a PS-X EXE, NOT drift)
(fleet EXCLUDING main, for continuity with pre-2026-07-22 readings: 10840603 / 13081451 = 82.9%)
(fleet EXCLUDING main, for continuity with pre-2026-07-22 readings: 10840907 / 13081451 = 82.9%)
FLEET REAL substantive : 315632 (of which dedup-shared 239530 via 1886 groups / 239604 instances)
FLEET REAL substantive : 315633 (of which dedup-shared 239530 via 1886 groups / 239604 instances)
FLEET LINKED PsyQ objs : 959
FLEET NON_MATCHING : 7 (0 in any default build — G4)
FLEET INCLUDE_ASM stubs : 36226
FLEET INCLUDE_ASM stubs : 36225
FLEET matchable : 353720
| binary | REAL | shared | LINKED | byte-ident | matchable | byte-ident % |
@@ -28,7 +28,7 @@ FLEET matchable : 353720
| ov_SC01_008 | 2256 | 1734 | 0 | 2258 | 2426 | 93.1% |
| ov_SC01_009 | 2282 | 1735 | 0 | 2283 | 2507 | 91.1% |
| ov_SC01_074 | 2260 | 1735 | 0 | 2262 | 2425 | 93.3% |
| ov_SC01_077 | 2440 | 1702 | 0 | 2442 | 2585 | 94.5% |
| ov_SC01_077 | 2441 | 1702 | 0 | 2443 | 2585 | 94.5% |
| ov_SC01_080 | 2298 | 1738 | 0 | 2298 | 2512 | 91.5% |
| ov_SC01_084 | 2305 | 1738 | 0 | 2310 | 2579 | 89.6% |
| ov_SC02_000 | 2358 | 1773 | 0 | 2358 | 2683 | 87.9% |
+323 -1
View File
@@ -1809,7 +1809,329 @@ void func_8014D738(void)
DEFINE_func_8014D790() /* dedup: shared engine-core @0x8014D790 (src/shared) */
INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_after", func_8014D820);
// @class: schedule
// @stuck: none — MATCH (304/304), symcheck SYMS-OK 12/12, and the whole-TU cc1 stage now compiles CLEAN
//
// ══════════════════════════════════════════════════════════════════════════════
// SESSION-22 RE-DERIVATION + THE ROOT-CAUSE FIX FOR THE "ASSEMBLER-STAGE" FAILURE
// ══════════════════════════════════════════════════════════════════════════════
// The SESSION-21 draft did reach match_one MATCH (re-verified this session, 304/304)
// and symcheck SYMS-OK — but the whole-binary build died with an opaque `Error 33`
// that was logged as "an ASSEMBLER-stage failure I did not finish diagnosing".
//
// IT IS NOT AN ASSEMBLER FAILURE. Stage-by-stage isolation of the real pipeline
// (cpp | cc1 | maspsx | as) over a scratch copy of src/ov_SC01_077/ov_SC01_077_after.c
// with this function spliced in place of its INCLUDE_ASM gives:
// cpp rc=0
// cc1 rc=33 <-- ".../tu.c:1901: conflicting types for `Ent'"
// "src/shared/engine_types.h:434: previous declaration of `Ent'"
// maspsx rc=0
// as rc=0 (only benign "used $at without .set noat" warnings)
// `33` is CC1's exit code, surfaced by the recipe's `set -o pipefail` (Makefile:560).
// Because `as` is the LAST stage in that pipe, make attributes the failure to the
// assembler — the same misread that made func_8012AAAC's "opaque Error 33" look
// assembler-shaped until it was isolated stage by stage.
//
// ROOT CAUSE: the draft declared `typedef struct Ent { ... } Ent;` at FILE scope.
// `Ent` is already a shared engine type (src/shared/engine_types.h:434,
// typedef struct { u16 guard; u16 pad; u16 field; u8 rest[14]; } Ent;
// a 20-byte struct), pulled into every overlay TU through
// src/ov_SC01_077/../shared/engine_core.h. gcc-2.7.2 rejects the redefinition
// outright (an ERROR, not the "type mismatch with previous external decl"
// WARNING that the rest of this TU is full of), so cc1 aborts.
//
// THE FIX, and why this shape: all three helper types are now declared at BLOCK
// scope inside func_8014D820 itself. That is strictly better than renaming to
// `Ent_8014D820` at file scope, because this function is an h_seq family EXEMPLAR
// with 138 members: a file-scope type name has to stay unique against 138 distinct
// overlay TUs *and* against whatever engine_types.h grows next, while a block-scope
// typedef can never collide with anything, in any TU, ever. It also matches the
// fleet convention — src/shared/engine_core.h contains ZERO `typedef struct`
// (all 0 of them); every DEFINE_ macro body uses engine_types.h types or local ones.
// Typedefs emit no code and create no allocno, so the move is byte-neutral:
// re-verified MATCH (304 ins) after the change, and the local DECLARATION ORDER
// below is untouched (§76 — declaration order is the dominant regalloc lever here).
//
// ══════════════════════════════════════════════════════════════════════════════
// THE BODY (SESSION-21 derivation, each lever re-verified this session)
// ══════════════════════════════════════════════════════════════════════════════
// func_8014D820 (304 ins, ov_SC01_077, h_seq family exemplar, 138 members).
// The whole residual off the SESSION-18 close=9 seed was a sched1 PERMUTATION
// inside ONE basic block (idx 79..101, the pos/desc set-up between the `slti 0x400`
// guard and the `currentLocationId` test): identical multiset of insns, identical
// register assignment, wrong positions. §66d-5 exactly — classed WIDTH [structural]
// at every waypoint and not structural at all.
//
// The lever (reusable): that block is 12 statements over a small dependency poset
// (load x0/y0/z0 -> add -> store pos.* / desc.*), so source ORDER is the only
// control and the space of linear extensions is enumerable. A dependency-respecting
// sweep found MATCH at
// x0; pos.x; y0; pos.y; z0; tx; pos.z; tz; desc.x; desc.z; ty; desc.y
// i.e. all three loads and both pos stores first, then the two adds, then the desc
// stores, with the y accumulator (`ty`) computed LAST. No pin, no barrier, no
// permuter run. Eleven hand attempts on this window (SESSION-18) were inert or
// worse — hand-reading a 12-statement schedule permutation is the wrong tool;
// brute-forcing the linear extensions is the right one.
//
// Load-bearing levers (each re-verified by removal this session):
// * the `a0v` launder (`__asm__("" : "=r"(a0v) : "0"(a0))`) — drop => 9 mismatched
// * the `a2` $7 pin — drop => 5
// * the `a2x -> a2` launder (vs a plain copy) — drop => 1
// * `new_var3` (the s16 stash of dx feeding ratan2) — drop => 2
// * `new_var2` (the desc.y + 0x10 accumulator) — drop => 2
// The `a2` pin's live range ENDS before the first `jal` (last use is
// `desc.z = a2[2] - a1[2]`), so it is §42e-CORRECTION / §74-safe to propagate.
//
// SYMBOLS (§58 rule 1/2): all five data externs use the canonical splat spelling and
// the engine_core.h type — `u8 D_801202A0[]`, `u8 D_801152A8[]`, `s16 D_801152AC`,
// `u8 D_8018876C[]`, `s16 currentLocationId`. symcheck: SYMS-OK, 12 symbols agree.
//
// The one deliberate divergence (§27 step 2 / rule 3): func_80135A4C's canonical
// extern is 4-arg but this call site sets only $a0-$a2, so the canonical decl is
// kept verbatim (it is also the TU's own spelling at ov_SC01_077_after.c:1651/1709)
// and the ARITY is cast at the call site.
//
// BANKING NOTE for the orchestrator (carried forward from SESSION-18, still true):
// 1 `self_decl_hdr` axis remains — DEFINE_func_8014D790's body declares
// func_8014D820, so that macro in engine_core.h needs the §85 return-axis widen
// (already applied). With the `Ent` collision gone the TU's cc1 stage is clean.
extern s16 currentLocationId;
extern u8 D_801202A0[];
extern u8 D_8018876C[];
extern u8 D_801152A8[];
extern s16 D_801152AC;
extern s32 ratan2(s32 a0, s32 a1);
extern s32 func_80135A4C(s32 a0, s32 a1, s32 *a2, s32 a3);
extern s32 func_80012A60(s32 a0, s32 a1);
extern void func_800139C8(s32 a0, void *a1, void *a2);
extern s32 func_80135888(s32 a0, s32 a1, s32 a2, s32 a3);
extern s32 func_8014DCE0(s32 a0, s32 a1, s32 a2);
extern s32 func_80133784(s32 a0, void *a1, s32 a2);
s32 func_8014D820(s32 a0, u16 *a1, u16 *a2x)
{
/* BLOCK-SCOPE types — see the header note. `Ent` at file scope collides with
src/shared/engine_types.h:434 and kills the TU's cc1 stage (exit 33). */
typedef struct
{
u16 x;
u16 y;
u16 z;
u16 w;
} V4;
typedef struct
{
u16 x;
u16 y;
u16 z;
u16 w;
s32 pad[4];
s32 f18;
s32 f1C;
s32 f20;
} Desc;
typedef struct EntD820
{
u16 f0;
u16 f2;
u16 f4;
u16 x;
u16 f8;
u16 y;
u16 fC;
u16 z;
u8 p10[0x10];
s32 f20;
u8 p24[0x34];
s32 f58;
u16 f5C;
u16 f5E;
u8 p60[0x10C - 0x60];
} Ent;
int new_var2;
s16 new_var3;
register u16 *a2 __asm__("$7");
V4 out[3];
V4 pos;
Desc desc;
Ent *ent;
Ent *p;
s32 dx;
s32 dz;
s32 ex;
s32 ez;
s32 r1;
s32 r2;
s32 r3;
s32 ang;
s32 x0;
s32 y0;
s32 z0;
s32 tx;
s32 tz;
s32 ty;
s32 r;
s32 t;
s32 u;
s32 a0v;
__asm__ __volatile__("" : "=r"(a2) : "0"(a2x));
t = a2[0];
u = a1[0];
dx = t - u;
__asm__ __volatile__("" : "=r"(a0v) : "0"(a0));
new_var3 = (s16) dx;
t = a2[2];
u = a1[2];
dz = t - u;
if (((s16) (dx | dz)) == 0)
{
goto fail;
}
desc.x = dx;
desc.y = a2[1] - a1[1];
desc.z = a2[2] - a1[2];
ent = *((Ent **) (a0v + 0x170));
if (ent->f0 == 0)
{
goto fail;
}
if ((ent->f5C & 0x1000) == 0)
{
goto fail;
}
if (ent->f58 == 0)
{
goto fail;
}
if (((s32 (*)(s32, s32, s32)) func_80135A4C)(ent->f20, ent->f58, (s32) a1) == 0)
{
goto fail;
}
ex = ent->x - (*((u16 *) (a0v + 6)));
ez = ent->z - (*((u16 *) (a0v + 0xE)));
r1 = (s16) ratan2((s16) dz, new_var3);
r2 = (s16) ratan2((s16) ez, (s16) ex);
r3 = (s16) func_80012A60(r1, r2);
if (0x400 <= r3)
{
goto fail;
}
desc.f20 = 0;
desc.f1C = 0;
desc.f18 = 0;
x0 = ent->x;
pos.x = x0;
y0 = ent->y;
pos.y = y0;
z0 = ent->z;
tx = x0 + desc.x;
pos.z = z0;
tz = z0 + desc.z;
desc.x = tx;
desc.z = tz;
ty = (y0 + desc.y) + 8;
desc.y = ty;
if (currentLocationId != 0x3008)
{
for (p = (Ent *) D_801202A0; p < (Ent *) (D_801202A0 + 0x6480); p++)
{
if (p == ent)
{
continue;
}
if (p->f0 == 0)
{
continue;
}
if (p->f5C == 0)
{
continue;
}
if (p->f58 == 0)
{
continue;
}
func_80135888(p->f20, p->f58, (s32) (&pos), (s32) (&desc));
}
}
else
{
ang = ratan2(((s16) x0) - ((s16) tx), ((s16) z0) - ((s16) tz)) & 0xFFF;
func_800139C8(ang, D_8018876C, &out[0]);
func_800139C8((s16) (ang - 0x155), D_8018876C, &out[1]);
func_800139C8((s16) (ang + 0x155), D_8018876C, &out[2]);
out[0].x += pos.x;
out[0].y = pos.y;
out[0].z += pos.z;
out[1].x += pos.x;
out[1].y = pos.y;
out[1].z += pos.z;
out[2].x += pos.x;
out[2].y = pos.y;
out[2].z += pos.z;
for (p = (Ent *) D_801202A0; p < (Ent *) (D_801202A0 + 0x6480); p++)
{
if (p == ent)
{
continue;
}
if (p->f0 == 0)
{
continue;
}
if (p->f5C == 0)
{
continue;
}
if (p->f58 == 0)
{
continue;
}
if (func_8014DCE0((s32) p, (s32) (&pos), (s32) (&out[0])) != 0)
{
__builtin_memcpy((void *) ((s32) (&desc)), (void *) ((s32) (&pos)), 8);
}
}
}
r = func_80133784(0, &pos, (s32) (&desc));
if ((r == 0) || ((r == 0x2000) && (((*(u16 *) D_801152A8) | ((u16) D_801152AC)) == 0)))
{
ent->x = desc.x - desc.f18;
ent->y = desc.y;
ent->z = desc.z - desc.f20;
desc.x -= pos.x;
desc.y -= pos.y;
desc.z -= pos.z;
*((u16 *) (a0v + 6)) = a1[0] + desc.x;
new_var2 = desc.y;
new_var2 = new_var2 + 0x10;
*((u16 *) (a0v + 0xA)) = a1[1] + new_var2;
*((u16 *) (a0v + 0xE)) = a1[2] + desc.z;
}
else
{
*((u16 *) (a0v + 6)) = a1[0];
*((u16 *) (a0v + 0xA)) = a1[1] + 0x10;
*((u16 *) (a0v + 0xE)) = a1[2];
}
return 1;
fail:
*((s32 *) (a0v + 0x170)) = 0;
return 0;
}
#include "common.h"
+33
View File
@@ -155,6 +155,39 @@ def main():
+ (" …" if len(set(callers)) > 3 else ""), file=sys.stderr)
return 2
# ---- THE SCALAR-NARROWING WARNING (Phase 29 SESSION-21, byte-proven) ----
# A POINTER type change in a decl is caller-neutral (passing a pointer is passing a pointer —
# func_80179B74 conformed 1,600 sites s16*/short* -> u16* and stayed byte-identical fleet-wide).
# A SCALAR WIDTH change is NOT: narrowing `s32 a0` -> `u16 param_1` changes argument promotion,
# so callers emit different code. Measured on func_80175DA8 — with the decls reverted the gate
# says PLUMBING (`conflicting types`), with the conform applied it says **DIFF**. The conform
# did not fix the draft; it changed the callers.
#
# This is NOT a refusal: the draft's signature is still byte-truth for the CALLEE, and the
# whole-binary gate is the arbiter. It is a warning that the usual "decls are free" intuition
# does not hold here, so a DIFF after this conform means re-examine the CALLERS (the §17a-1
# pair — keep the caller decl compatible and cast at the call site) rather than the body.
NARROW = {"u8": 1, "s8": 1, "char": 1, "u16": 2, "s16": 2, "short": 2}
old_shapes = {strip_names(re.search(rf"\(([^;]*)\)\s*;", f).group(1)) for f in forms
if re.search(rf"\(([^;]*)\)\s*;", f)}
new_shape = strip_names(params)
narrowing = []
for old in old_shapes:
for o, n in zip([x.strip() for x in old.split(",")], [x.strip() for x in new_shape.split(",")]):
if "*" in o or "*" in n:
continue # pointer change: caller-neutral
nb = NARROW.get(n.replace("unsigned ", "u").replace("signed ", "s"))
if nb and o not in ("", "void") and o != n:
narrowing.append(f"{o} -> {n}")
if narrowing:
print(f"\n⚠ SCALAR-NARROWING in the parameter list ({', '.join(sorted(set(narrowing)))}). "
f"Unlike a pointer-type change this is NOT caller-neutral — argument promotion at "
f"every call site changes, so callers emit different code (byte-proven on "
f"func_80175DA8: PLUMBING before the conform, DIFF after). Applying is still valid — "
f"the draft's signature is byte-truth for the CALLEE and the whole-binary gate "
f"arbitrates — but if the gate then reports DIFF, examine the CALLERS (§17a-1: keep "
f"their decl compatible and cast at the call site), not the body.")
# §85 precondition — only matters when the RETURN type changes.
ret_changes = any(not re.match(rf"extern\s+{re.escape(ret)}\s", f) for f in forms)
if ret_changes: