feat(phase-30 S47-F2): auto-alias conflicting data externs (partial: 2/44); real cause recorded

The 44 data-symbol conflicting-types failures are NOT the cdFileLocTable duplicate-typedef class.
They are genuine per-view type differences: D_80078EB4 is s16 at 2,409 sites and u16 at 1,341;
D_800AE620 is Blk20/s32/Mat32. For data the declared type drives the load (lh vs lhu), so
canonicalising would rewrite thousands of already-banked sites' codegen. The answer is one type
PER VIEW — the §37 asm-label alias the fleet already hand-writes for D_800AE620 (9 sites).

Shipped: scope_data_externs now auto-aliases a conflicting extern —
    extern s16 aD80078EB4 __asm__("D_80078EB4");
keeping the draft's own type (byte-truth for that body) while the private C name makes collision
impossible and the asm label pins the emitted symbol, so codegen is unchanged. Fires only where
the TU declares that symbol with a DIFFERENT type text; same-type and already-aliased drafts are
untouched (4 controls, 2 of them negative). Applied on both scope paths — a staged draft's externs
arrive indented, so a demote-path-only fix reached 1 of 44.

R22 clean-fleet: check-all 213 passed / 0 failed of 213.

WHY ONLY 2 BANKED — the collision is DRAFT-vs-DRAFT, not draft-vs-TU. The failing draft declares
`extern s32 D_80114F24;` and ov_MAIN_012.c declares that symbol nowhere; the error lands at the
splice point. family_sweep stages every member of an (overlay, split) group into one TU before
gating, so two templated bodies with different views of one symbol collide with each other.
scope_data_fix sees one draft plus the pre-splice TU and structurally cannot see the others.
The real fix belongs in the staging loop, which knows the whole group: alias any data symbol
declared with >=2 distinct types across the drafts staged together. Not attempted here.

Three wrong inferences on this one task before reading a failing draft: scoped as the typedef
class; aliased only the demote path against the file's own comment; targeted the wrong collision.
This commit is contained in:
Drew T
2026-08-10 21:56:31 -06:00
parent b9033851cf
commit d4d35ec738
4 changed files with 229 additions and 6 deletions
+26
View File
@@ -426,6 +426,32 @@ sibling lacks. **Now the largest class, and a different mechanism from every dec
(D_801202A0 11 · func_8001534C 4) · `14` arity (too many/too few args) · `6` func_80145CEC.
**Every former unknown turned out cheap** — declarations, arity, redefinitions. Not one wall.
## ⚠ TASK F2 (data-symbol conflicts) — **PARTIAL: 2 of 44.** Mechanism correct, target wrong.
**NOT the cdFileLocTable class.** These are genuine PER-VIEW type differences, not duplicate
typedefs: `D_80078EB4` is `s16` at 2,409 sites and `u16` at 1,341; `D_800AE620` is
`Blk20`/`s32`/`Mat32`. For DATA the declared type drives the load (`lh` vs `lhu`), so canonicalising
would rewrite thousands of BANKED sites' codegen — the memcpy trap at 100× scale. The answer is one
type PER VIEW: the §37 asm-label alias, which the fleet already hand-writes for `D_800AE620` (9×).
**Shipped:** `scope_data_externs` auto-aliases a conflicting extern (`extern s16 aD80078EB4
__asm__("D_80078EB4")`) — keeps the draft's own type (byte-truth for that body), private C name
makes collision impossible, asm label pins the emitted symbol ⇒ codegen unchanged. Fires ONLY where
the TU declares that symbol with a different type text. 4 controls incl. 2 negatives. R22 **213/213**.
**WHY IT ONLY BANKED 2 — the collision is DRAFT-vs-DRAFT, not draft-vs-TU.** Evidence (finally
read, after two wrong inferences): the failing draft declares `extern s32 D_80114F24;` and
`ov_MAIN_012.c` declares that symbol **nowhere**; the error lands at line 24538 = the splice point.
The sweep stages EVERY member of an (overlay,split) group into one TU before gating, so two
templated bodies with different views of one symbol collide with each other. `scope_data_fix` sees
one draft + the PRE-SPLICE TU, so it structurally cannot see the others.
**→ The real fix belongs in the sweep's STAGING LOOP** (it knows the whole group): alias any data
symbol declared with ≥2 distinct types across the drafts being staged together. Not attempted.
**MY ERROR LEDGER ON THIS ONE TASK (3):** (1) scoped it as the typedef class — wrong; (2) put the
alias only on the demote path when the file's OWN comment says staged drafts arrive INDENTED — 1/44;
(3) the collision is not the one the mechanism targets — 2/44. Each caught by measurement, but I
burned three attempts on a 44-member class by inferring the shape instead of reading ONE failing
draft, which took 90 seconds when I finally did it.
## ▶ RESUME HERE — D (and the leftovers below)
**C is DONE.** For reference, the command was:
`.venv/bin/python tools/dedup_extend.py --binaries ov_SC03_107,ov_MAIN_012,ov_SC02_037`
+119 -1
View File
@@ -16564,7 +16564,125 @@ void func_80166654(void *a0) {
}
INCLUDE_ASM("asm/ov_MAIN_012/nonmatchings/ov_MAIN_012", func_80166690);
/* 12-byte sub-table record (stride 0xC) */
extern s32 func_80017758(void *a0, void *a1);
s32 func_80166690(s32 param_1, s32 param_2)
{
extern Blk20 aD800AE620 __asm__("D_800AE620");
extern u8 D_80184418[];
Frame_80166690 c;
Blk20 blk;
Rec_80166690 *sub;
Rec_80166690 *pv;
u8 *rec;
int i6;
int lt2;
int p2;
unsigned int uVar7, uVar8, uVar9;
int iVar3;
unsigned int w9, w7, w, t, w3b;
int w3;
short sVar10;
register unsigned int zr __asm__("$0");
blk = aD800AE620;
i6 = (short)param_1;
c.tag = 0x50000000;
rec = &D_80184418[i6 * 0xC4];
sVar10 = 0;
if (rec[0] == 0) {
return 0;
}
sub = (Rec_80166690 *)(rec + 4);
uVar9 = rec[1];
uVar8 = rec[3];
uVar7 = uVar8 >> 1;
iVar3 = rec[2];
w3 = iVar3 - 1;
iVar3 = w3 + zr;
if (w3 != -1) {
lt2 = i6 < 2;
p2 = (short)param_2;
do {
pv = &sub[(short)uVar9];
c.v0 = pv->h[0];
c.v1 = pv->h[1];
c.v2 = pv->h[2];
c.w0 = pv->h[3];
c.w1 = pv->h[4];
c.w2 = pv->h[5];
sVar10 = sVar10 + 1;
if (lt2) {
if (p2 == 0) {
t = uVar8 + zr;
c.c12 = t; c.c02 = t;
t = uVar7 + zr;
} else {
w = ((int)(uVar8 << 16) >> 17) + ((int)(uVar8 << 16) >> 19);
uVar7 = w + zr;
c.c12 = w; c.c02 = w;
t = uVar8 + zr;
}
c.c11 = t; c.c01 = t; c.c10 = t; c.c00 = t;
} else {
t = uVar8 + zr;
c.c10 = t; c.c00 = t;
t = uVar7 + zr;
c.c12 = t; c.c02 = t; c.c11 = t; c.c01 = t;
}
w9 = uVar9 - 1;
uVar9 = w9 + zr;
if ((int)(w9 << 16) < 0) {
uVar9 = 0xF;
}
pv = &sub[(short)uVar9];
c.v3 = pv->h[0];
c.v4 = pv->h[1];
c.v5 = pv->h[2];
c.w3 = pv->h[3];
c.w4 = pv->h[4];
uVar8 = uVar8 - 0x10;
w7 = uVar7 - 0x10;
uVar7 = w7 + zr;
c.w5 = pv->h[5];
if ((int)(w7 << 16) < 0) {
uVar7 = 0;
}
if (lt2) {
if (p2 == 0) {
t = uVar8 + zr;
c.c32 = t; c.c22 = t;
t = uVar7 + zr;
} else {
t = uVar7 + zr;
c.c32 = t; c.c22 = t;
t = uVar8 + zr;
}
c.c31 = t; c.c21 = t; c.c30 = t; c.c20 = t;
} else {
t = uVar8 + zr;
c.c30 = t; c.c20 = t;
t = uVar7 + zr;
c.c32 = t; c.c22 = t; c.c31 = t; c.c21 = t;
}
__asm__ __volatile__("" :: "r"(iVar3));
func_80017758(&c, &blk);
w3b = iVar3 - 1;
iVar3 = w3b + zr;
} while ((short)w3b != -1);
}
return (short)sVar10;
}
extern void func_801484B0(s32 a0, s32 a1);
extern void func_80148534(s32 a0, s32 a1);
+10 -1
View File
@@ -7078,7 +7078,16 @@ INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018540
INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_80185440);
INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_801854C0);
extern void func_8012A828(s32, void*);
extern short aD801CC358 __asm__("D_801CC358");
void func_801854C0(short *a0) {
*(short *)((char *)a0 + 0x2) = 0x1D;
*(short *)((char *)a0 + 0x34) = 0;
*(int *)((char *)a0 + 0x1c) = 0xf;
((void (*)(s32 *, s32))func_8012A828)((s32 *)a0, (int)&aD801CC358);
}
// @class: schedule
// @stuck: none — MATCH (181 ins, relocation-masked)
+74 -4
View File
@@ -93,6 +93,31 @@ def _body_open_brace(body, func):
_ASM_LABEL_RE = re.compile(r'__asm__\s*\(\s*"([^"]+)"\s*\)')
def _norm_ws(s):
return re.sub(r'\s+', ' ', s.strip())
def _decl_type_text(line, sym):
"""The declared TYPE of `sym` in one `extern` line — everything between `extern` and the
declarator, plus any array suffix. None if the line does not declare it."""
m = re.search(rf'^\s*extern\s+(.*?)\b{re.escape(sym)}\b\s*(\[[^\]]*\])?', line)
return (m.group(1) + (m.group(2) or '')) if m else None
def _file_scope_data_types(tu_text):
"""{sym -> declared type text} for every file-scope data extern ANYWHERE in the TU.
`_file_scope_data_syms` answers "declared ABOVE the insertion point", which is the right
question for the drop/demote decision and the WRONG one for conflict detection: a file-scope
declaration BELOW the body must still agree with a block-scope extern inside it."""
out = {}
for ln in FILE_EXTERN_RE.findall(tu_text):
d = DATA_SYM_RE.search(ln)
if d:
out.setdefault(d.group(0), _decl_type_text(ln, d.group(0)))
return out
def is_asm_alias(ln):
"""True for a §37/§84 ASM-LABEL ALIAS: `extern T ident… __asm__("SYM");` with ident != SYM.
@@ -121,9 +146,11 @@ def fix(body, tu_text, insert_pos, func):
qualifies, when the body declares no data externs, or when the opening brace can't be located."""
above = _file_scope_data_syms(tu_text[:insert_pos])
tu_types = _file_scope_data_types(tu_text)
demote = [] # (line_text, sym)
dropped = [] # syms the TU ALREADY declares above us — redundant, see below
keep_lines = []
aliased = [] # (sym, alias) — renamed at the end, both scope paths
for ln in body.split('\n'):
# BLOCK-SCOPE TOO, not just col-0 (Phase 29 SESSION-22). §8d demotes these externs on the way
# in, so by the time a sibling draft is STAGED they are already indented — and a col-0-only
@@ -135,6 +162,16 @@ def fix(body, tu_text, insert_pos, func):
if d and d.group(0) in above and not is_asm_alias(ln):
dropped.append(d.group(0))
continue
# THE ALIAS MUST REACH THIS BRANCH TOO (P30 S47). A staged draft's externs are ALREADY
# indented (see the note above), so they arrive here, not in the col-0 demote path —
# and a first cut that aliased only demoted lines fired on 1 member out of 44.
if d and not is_asm_alias(ln):
_mine, _theirs = _decl_type_text(ln, d.group(0)), tu_types.get(d.group(0))
if _mine and _theirs and _norm_ws(_mine) != _norm_ws(_theirs):
_al = f'aD{d.group(0)[2:]}'
ln = (re.sub(rf'\b{re.escape(d.group(0))}\b', _al, ln, count=1)
.rstrip().rstrip(';') + f' __asm__("{d.group(0)}");')
aliased.append((d.group(0), _al))
keep_lines.append(ln)
continue
if FILE_EXTERN_RE.match(ln): # col-0 extern (match => anchored at col 0)
@@ -161,17 +198,50 @@ def fix(body, tu_text, insert_pos, func):
dropped.append(d.group(0))
continue
keep_lines.append(ln)
if not demote and not dropped:
if not demote and not dropped and not aliased:
return body, []
def _rename(txt):
"""Point every USE at the alias; never touch the asm label (it names the real symbol)."""
for _s, _a in aliased:
txt = re.sub(rf'\b{re.escape(_s)}\b', _a, txt)
txt = txt.replace(f'__asm__("{_a}")', f'__asm__("{_s}")')
return txt
stripped = '\n'.join(keep_lines)
if not demote: # drops only — no block to place
return stripped, dropped
return _rename(stripped), dropped
at = _body_open_brace(stripped, func)
if at is None: # can't place them safely -> leave the body alone
return body, []
block = ''.join(f' {ln}\n' for ln, _ in demote)
return stripped[:at] + '\n' + block + stripped[at:], [s for _, s in demote] + dropped
# AUTO-ALIAS A DEMOTED EXTERN THE TU DECLARES WITH A DIFFERENT TYPE (P30 S47).
# Demoting to block scope does NOT avoid a conflict: C requires a block-scope `extern` to agree
# with any file-scope declaration of the same object in the TU, wherever it sits. And for DATA
# the type is not cosmetic — it drives the load: `s16` vs `u16` is `lh` vs `lhu`. The fleet
# genuinely reads one address at several widths (§16 loose typing): D_80078EB4 is `s16` at 2,409
# sites and `u16` at 1,341, D_800AE620 is `Blk20`/`s32`/`Mat32`. Canonicalising would rewrite
# thousands of BANKED sites' codegen, so the answer is not one type — it is one type PER VIEW.
# That is the §37 asm-label alias, and the fleet already hand-writes it for exactly this symbol:
# extern Mtx8_8017C910_8017C910 aD800AE620 __asm__("D_800AE620"); (9 sites)
# Emitting it automatically keeps the draft's own type — byte-truth for THIS body — while the
# private C name makes a collision impossible. Codegen is unchanged: the asm label fixes the
# emitted symbol, so the same load reaches the same address.
# Scoped deliberately: only fires where the TU actually declares the symbol with a DIFFERENT
# type text, so a member that never had a conflict is untouched.
block_lines = []
for ln, sym in demote:
mine, theirs = _decl_type_text(ln, sym), tu_types.get(sym)
if mine and theirs and _norm_ws(mine) != _norm_ws(theirs):
alias = f'aD{sym[2:]}'
block_lines.append(re.sub(rf'\b{re.escape(sym)}\b', alias, ln, count=1)
.rstrip().rstrip(';') + f' __asm__("{sym}");')
aliased.append((sym, alias))
else:
block_lines.append(ln)
block = ''.join(f' {b}\n' for b in block_lines)
return _rename(stripped[:at] + '\n' + block + stripped[at:]), \
[s for _, s in demote] + dropped
def main():