Files
BFM-decomp/tools/family_remap.py
T
Drew T 6eea1ac6a4 fix(phase-30 S48-0b): _carry_typedefs' already-carried test was line-oriented
The test was `^\s*typedef\b[^\n]*\bNAME\b` — the name must sit on the SAME LINE
as the keyword. True of `typedef unsigned char u8;`, never true of the
multi-line form the preamble backscan actually carries:

    typedef struct Foo {   …   } Foo;

so every multi-line typedef already in the unit was carried a SECOND time and
the unit reached the gate with two definitions of one tag. canon_sig_reconcile
uniquifies draft tags, so the duplicate is exact: `redefinition of struct
Foo_8013C0F8`.

- Extract the block capture as `_typedef_blocks(lines)` and derive the
  already-carried set from it (R33: one parser, two callers), so the
  multi-line form is recognised exactly as the single-line form always was.

Measured on the 0b population: func_8013C0F8 (3 slots) carried Foo+Bar twice;
func_8013B83C dropped a redundant file-scope copy of a typedef its body
declares at block scope. 6 other families byte-identical output.
2026-08-11 16:00:02 -06:00

1201 lines
67 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Phase-25 family template-instantiation: mechanically remap a MATCHED exemplar's C to a byte-variant
h_norm SIBLING by positionally pairing their per-overlay symbol references.
The Phase-24 reframe found the unmatched frontier clusters into h_norm STRUCTURAL families (same code
shape across overlays, byte-shattered because each member references PER-OVERLAY symbols). T3 proved these
are NOT free `--tier h_norm` dedup (one C body can't name 134 overlays' different symbols). But two
h_norm-identical members have IDENTICAL instruction streams except in the masked reloc fields (jal target,
lui/lo address) — so: disassemble both overlay images at ADDR, pair the resolved reloc targets in
instruction order, and substitute the exemplar C's per-overlay symbol NAMES (`D_<addr>`/`func_<addr>`)
with the sibling's. Shared EXE/resident symbols map to themselves. The result is the sibling's C —
generated mechanically from ONE crack, for ~0 agent tokens. The whole-binary byte-gate stays the sole
arbiter (G3/P9): a wrong remap is rejected.
Phase-26 extends this to the looser h_seq family key (mnemonic skeleton, immediates may differ):
* reloc_targets propagates a pending lui-hi through add/addu index arithmetic (the gcc-2.7.2
indexed-global `lui;addu $idx;lw %lo($at)` idiom the pre-26 tracker dropped — the "reach-1 tail"
was largely this blind spot, not unique code);
* remap does a SINGLE-PASS simultaneous substitution (the old sequential re.sub corrupted chained /
permuted maps) and merges an optional imm_map (T2a per-member immediate edits);
* to_addr enables cross-address siblings (same engine fn at a different vram per overlay, T2b).
tools/family_remap.py --addr 0xADDR --from ov_SC01_077 --to ov_SC01_000 [--to-addr 0xADDR2] [--out draft.c]
"""
import struct, json, glob, re, sys, argparse, collections, functools, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cdecl # noqa: E402
VRAM = 0x80128158 # the location-overlay slot — DEFAULT only; per-alias truth is vram_of()
# lo-type ops whose rs is a hi-base (loads/stores incl. unaligned, addiu, ori)
LO_OPS = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x28, 0x29, 0x2A, 0x2B, 0x2E, 0x09, 0x0D}
_VRAM_CACHE = {}
def vram_of(ov):
"""The binary's load vram, DERIVED from its splat config (R33; the jtbl_carve pattern).
P30 S44: `VRAM` was a module constant, correct for the 138 location overlays (shared slot) and
silently WRONG for every other binary class — the resident (0x800CEDF8), the new md_* modules
(slots 0x800CAE08 / 0x800CCB1C / 0x801A00D8). All offset math (`addr - VRAM`) would read garbage
bytes for those without erroring — the R32 silent-skip shape, in the tool the whole family engine
stands on. Every splat config states its vram; read it once per alias."""
if ov not in _VRAM_CACHE:
p = f"config/splat.{ov}.yaml" # cwd-relative like img_path (callers run at repo root)
m = re.search(r"vram:\s*(0x[0-9A-Fa-f]+)", open(p).read())
if not m:
raise SystemExit(f"family_remap: no vram in {p} (R32 — refusing a default)")
_VRAM_CACHE[ov] = int(m.group(1), 16)
return _VRAM_CACHE[ov]
_PATH_CACHE = {}
def img_path(ov):
"""The binary's extracted payload, DERIVED from its splat config's `target_path` (R33).
NEVER reconstruct this path from the alias. The first 134 overlays put their code at PAC entry 0
(`0.4.dec`), but the four SC07 overlays the Phase-27 disc-completeness audit found put graphics at
entry 0 and CODE at entry 1 (`1.4.dec`). The old hardcoded `…/FILE_{fn}.dir/0.4.dec` guess returned
None for them, and None was SILENT: `_img` -> None -> `stream_words` -> None -> `classify_member`
-> ("LEN", []) — i.e. every SC07 member was dropped as "not templatable" and, worse, poisoned its
family's `diff_class` to MIXED (`family_hseq.py:141-143` counts LEN against n_templatable). A
scanner that mis-reports a whole overlay as structurally-different, silently, is exactly the
silent-skip defect class (R32) — and it is the SAME bug `new_overlay.sh` had (its hardcoded
`0.4.dec` glob hid these four overlays for a month), left uncorrected in a second tool.
`config/splat.<bin>.yaml` is what the BUILD reads, so it cannot drift from the bytes. This also
makes the resident work for free (its target_path is `MAIN.CD.dir/FILE_010.dir/1.1`).
Raises rather than returning None: a missing payload is a DEFECT, not a state (R32). Every caller
either opened it unguarded already (`reloc_targets`, `jr_isolate_all`) or silently mis-classified.
"""
if ov not in _PATH_CACHE:
cfg = f"config/splat.{ov}.yaml"
if not glob.glob(cfg):
raise FileNotFoundError(f"img_path({ov}): no {cfg} — is this binary onboarded?")
m = re.search(r"^\s*target_path:\s*(\S+)\s*$", open(cfg).read(), re.M)
if not m:
raise ValueError(f"img_path({ov}): {cfg} has no `target_path:` — cannot derive the payload")
p = m.group(1)
if not glob.glob(p):
raise FileNotFoundError(f"img_path({ov}): {cfg} names {p}, which does not exist — run `make extract BINARY={ov}`")
_PATH_CACHE[ov] = p
return _PATH_CACHE[ov]
def nins_of(ov, addr):
for line in open(f".run/sig.{ov}.jsonl"):
d = json.loads(line)
if int(d["addr"], 16) == addr:
return d["nins"]
return None
@functools.lru_cache(maxsize=None)
def fn_addrs(ov):
"""The FUNCTION start addresses in <ov> — the same sig set `nins_of` treats as the boundary
oracle (R33: one oracle, cached because symbol_map asks per reloc slot per member)."""
return frozenset(int(json.loads(l)["addr"], 16) for l in open(f".run/sig.{ov}.jsonl"))
@functools.lru_cache(maxsize=None)
def extern_fn_addrs():
"""FUNCTION start addresses of the ALWAYS-LINKED images (the resident + main).
WHY `fn_addrs(to_ov)` ALONE IS THE WRONG QUESTION (P30 S47, byte-witnessed on 611 rows).
The kind test below spells a reloc target `func_` iff the address is a function IN THE SIBLING'S
OWN SIG, else `D_`. But a body routinely calls OUTSIDE its own image: an overlay calls resident
helpers, an md_* module calls the overlay-range engine. Those addresses are absent from the
sibling's sig, so the test fell through to `D_` and emitted a DATA name for a FUNCTION —
`D_800183E0`, `D_800D1EBC`, `D_80171A1C` — none of which exist anywhere in src/ or the symbol
file, while `func_80171A1C` alone has 1,061 references. Result: `undefined reference` at LINK
time, 611 member-rows across 45 symbols, the largest single residue class in the sweep.
"Not in MY sig" means "not mine", NOT "is data" — so ask the shared images too before defaulting.
Measured: 0x800183E0 is a function only in main's sig; 0x800D1EBC only in the resident's;
0x80171A1C in 141 overlay sigs (so an md_* sibling hit the same fallthrough)."""
out = set()
for b in ("resident", "main"):
p = f".run/sig.{b}.jsonl"
if os.path.exists(p):
try:
out |= {int(json.loads(l)["addr"], 16) for l in open(p)}
except Exception: # a malformed sig must not break remapping
pass
return frozenset(out)
def reloc_targets(ov, addr, data=None):
"""ordered [(kind, resolved_addr)] for jal targets + lui/lo address loads, in instruction order.
Verified against splat .s ground truth (22/22 on func_80141100; 15/15 on func_801407F4 whose
indexed-global D[i] accesses the pre-Phase-26 tracker missed — see the add/addu hi-propagation).
`data` (optional) = the overlay image bytes, read once by the caller and passed to AVOID the
per-call `open(...).read()` when scanning many functions of one overlay (jr_inventory reads it
once, then calls this ~2,300× — 6s -> 0.1s). Omit it and the image is re-read fresh per call
(the default, so a re-extraction mid-process is always seen)."""
if data is None:
data = open(img_path(ov), "rb").read()
n = nins_of(ov, addr)
off = addr - vram_of(ov)
out, pend = [], {}
for k in range(n):
pc = addr + k * 4
w = struct.unpack_from("<I", data, off + k * 4)[0]
op = w >> 26
if op in (2, 3): # j / jal
out.append(("call", ((pc + 4) & 0xF0000000) | ((w & 0x03FFFFFF) << 2)))
elif op == 0x0F: # lui -> rt holds the hi
pend[(w >> 16) & 0x1F] = (w & 0xFFFF) << 16
elif op in LO_OPS:
rs = (w >> 21) & 0x1F
if rs in pend:
lo = w & 0xFFFF
if lo >= 0x8000:
lo -= 0x10000
out.append(("data", pend[rs] + lo))
del pend[rs]
pend.pop((w >> 16) & 0x1F, None) # rt overwritten
elif op == 0: # R-type
funct = w & 0x3F
rd = (w >> 11) & 0x1F
if funct in (0x20, 0x21): # add / addu: the indexed-global idiom
# gcc-2.7.2 emits `lui $at,%hi(D); addu $at,$at,$idx; lw ..%lo(D)($at)` for D[i] —
# the addu PRESERVES the hi anchor (index shifts the runtime value, not the symbol).
# Propagate the pending hi through the add so the following %lo still resolves D.
rs = (w >> 21) & 0x1F
rt = (w >> 16) & 0x1F
if rs in pend:
pend[rd] = pend[rs]
elif rt in pend:
pend[rd] = pend[rt]
else:
pend.pop(rd, None)
else:
pend.pop(rd, None) # rd overwritten with a non-address value
return out
# ---- Phase-26 shared word-diff classifier (family_hseq survey T1, imm engine T2a, sweep pre-filter T3) ----
# Two members of an h_seq family have IDENTICAL mnemonic sequences but may differ in reloc fields
# (symbol-remappable), in true immediates / shift amounts (imm-substitutable, T2a), OR in REGISTER
# allocation (NOT templatable — h_seq ignores registers). Classify each differing instruction so a
# family/member is bucketed PURE (reloc-only) / IMM / STRUCT-EXCLUDED (regalloc drift or collision).
_IMG_CACHE = {}
def _img(ov):
# img_path raises on a missing payload (R32), so this can no longer hand `None` downstream and
# manufacture a phantom LEN. `classify_member`'s None-guard now catches only a genuine
# `nins_of` miss (the address is not in that overlay's sig), which is a real state.
if ov not in _IMG_CACHE:
_IMG_CACHE[ov] = open(img_path(ov), "rb").read()
return _IMG_CACHE[ov]
def stream_words(ov, addr, nins):
"""the nins raw instruction words of a function from its overlay image (cached)."""
data = _img(ov)
if data is None:
return None
off = addr - vram_of(ov)
return [struct.unpack_from("<I", data, off + k * 4)[0] for k in range(nins)]
def reloc_indices(words):
"""set of instruction indices that participate in an ADDRESS reloc — jal, an addr-anchor lui
(confirmed by a %lo consumer, propagated through add/addu), and the %lo consumer itself."""
pend, jal, lui_addr, lo = {}, set(), set(), set() # pend: reg -> originating lui index
for k, w in enumerate(words):
op = w >> 26
if op in (2, 3):
jal.add(k)
elif op == 0x0F:
pend[(w >> 16) & 0x1F] = k
elif op in LO_OPS:
rs = (w >> 21) & 0x1F
if rs in pend:
lo.add(k)
lui_addr.add(pend[rs])
del pend[rs]
pend.pop((w >> 16) & 0x1F, None)
elif op == 0:
funct = w & 0x3F
rd = (w >> 11) & 0x1F
if funct in (0x20, 0x21):
rs, rt = (w >> 21) & 0x1F, (w >> 16) & 0x1F
if rs in pend:
pend[rd] = pend[rs]
elif rt in pend:
pend[rd] = pend[rt]
else:
pend.pop(rd, None)
else:
pend.pop(rd, None)
return jal | lui_addr | lo
def reg_fields(w):
"""the register + opcode-identity fields (everything EXCEPT the imm / jump-target / shift-amount).
Equal reg_fields with unequal words ⟹ the diff is purely immediate/target/sa (not regalloc)."""
op = w >> 26
if op == 0: # R-type: rs, rt, rd, funct (sa excluded)
return (0, (w >> 21) & 0x1F, (w >> 16) & 0x1F, (w >> 11) & 0x1F, w & 0x3F)
if op in (2, 3): # j / jal: target excluded
return (op,)
if op == 0x0F: # lui: rt only (hi excluded)
return (op, (w >> 16) & 0x1F)
return (op, (w >> 21) & 0x1F, (w >> 16) & 0x1F) # I-type: rs, rt (imm excluded)
_SHIFT_FUNCTS = (0x00, 0x02, 0x03) # sll srl sra (sa is the immediate); sllv/srlv/srav excluded
def classify_member(words_ex, words_sib):
"""returns (cls, positions): cls in {PURE, IMM, STRUCT, LEN}; positions = [(idx, kind)] for each
DIFFERING instruction, kind in {RELOC, IMM, IMM_SA, STRUCT}. Reuses the extended reloc tracker."""
if words_ex is None or words_sib is None:
return "LEN", []
if len(words_ex) != len(words_sib):
return "LEN", []
rel = reloc_indices(words_ex)
positions = []
for k, (a, b) in enumerate(zip(words_ex, words_sib)):
if a == b:
continue
if reg_fields(a) != reg_fields(b): # opcode/register/funct drift -> not templatable
positions.append((k, "STRUCT"))
elif k in rel:
positions.append((k, "RELOC"))
elif (a >> 26) == 0:
positions.append((k, "IMM_SA" if (a & 0x3F) in _SHIFT_FUNCTS else "STRUCT"))
else:
positions.append((k, "IMM")) # I-type non-address immediate (incl. const-lui hi)
if any(c == "STRUCT" for _, c in positions):
cls = "STRUCT"
elif any(c in ("IMM", "IMM_SA") for _, c in positions):
cls = "IMM"
else:
cls = "PURE"
return cls, positions
# ---- Phase-26 T2a: the diff-driven immediate engine (Tier 1) ----
# h_seq IMM families differ in a handful of true immediates (per-location constants). Extract only the
# DIFFERING imm/sa positions, and where a value is unambiguous (maps 1:1 and doesn't recur at a
# non-differing position) swap its C literal. Ambiguous/folded values are left UNRESOLVED (Tier 2 probe
# / agent queue). The whole-binary byte-gate arbitrates — a wrong swap simply fails.
_SIGNED_IMM = frozenset((0x08, 0x09, 0x0A, 0x0B, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26,
0x28, 0x29, 0x2A, 0x2B, 0x2E)) # addi/addiu/slti/sltiu + loads/stores
_UNSIGNED_IMM = frozenset((0x0C, 0x0D, 0x0E)) # andi/ori/xori
def imm_value(w, is_reloc):
"""the human immediate value at an instruction (signed where the field is), or None if this
position carries no plain substitutable immediate (reloc lo/hi, register op, lui const-hi)."""
if is_reloc:
return None
op = w >> 26
if op == 0:
return (w >> 6) & 0x1F if (w & 0x3F) in _SHIFT_FUNCTS else None
if op in _SIGNED_IMM:
v = w & 0xFFFF
return v - 0x10000 if v >= 0x8000 else v
if op in _UNSIGNED_IMM:
return w & 0xFFFF
return None # lui const-hi etc. -> Tier 2
def _c_literal_swap(unit, exv, sibv):
"""find a C integer-literal occurrence of exv and return (found_token, replacement_same_style),
or (None, None). Tries hex (both cases) then decimal; preserves sign + hex-case style."""
forms = ([f"0x{exv:x}", f"0x{exv:X}", str(exv)] if exv >= 0
else [f"-0x{-exv:x}", f"-0x{-exv:X}", str(exv)])
for t in forms:
if re.search(r'(?<!\w)' + re.escape(t) + r'\b', unit):
if "x" in t.lower():
digits = t.lstrip("-")[2:]
up = any(c.isalpha() and c.isupper() for c in digits)
rep = ("-" if sibv < 0 else "") + "0x" + (f"{abs(sibv):X}" if up else f"{abs(sibv):x}")
else:
rep = str(sibv)
return t, rep
return None, None
def _fmt_like(tok, v):
"""render v in the same style (hex/dec, case, sign) as the literal token `tok`."""
if "x" in tok.lower():
up = any(c.isalpha() and c.isupper() for c in tok.lstrip("-")[2:])
return ("-" if v < 0 else "") + "0x" + (f"{abs(v):X}" if up else f"{abs(v):x}")
return str(v)
def _ordinal_edits(unit, exv, valpos, diff_idx, sib_words):
"""ORDINAL (positional) resolution for an `asm-ambiguous` value — Phase 29 T87.
The value substitution in `imm_map_tier1` is BY VALUE over the whole body, so when the exemplar
uses the same literal at a position that differs AND at one that does not, it cannot tell the two
apart and (correctly) refuses rather than corrupt the fixed one. But the asm knows exactly which
occurrence moved: zip the value's asm positions against the C literal's occurrences IN ORDER and
rewrite only the occurrences whose instruction is in `diff_idx`.
Only fires when the two counts agree, which is the check that makes the order assumption safe
enough to try; gcc may still reorder, so the whole-binary byte-gate remains the sole arbiter
(G3/P9) — a wrong pairing is rejected, never banked.
Returns [(start, end, replacement)] on the ORIGINAL unit, or None if it cannot be resolved."""
asm_pos = [k for k, v in enumerate(valpos) if v == exv]
if not any(k in diff_idx for k in asm_pos):
return None
forms = ([f"0x{exv:x}", f"0x{exv:X}", str(exv)] if exv >= 0
else [f"-0x{-exv:x}", f"-0x{-exv:X}", str(exv)])
for t in forms:
spans = [mm.span() for mm in re.finditer(r'(?<!\w)' + re.escape(t) + r'\b', unit)]
if not spans:
continue
diff_pos = [k for k in asm_pos if k in diff_idx]
if len(spans) == len(asm_pos):
pairing = list(zip(spans, asm_pos)) # every asm use has a C token
elif len(spans) == len(diff_pos):
# FEWER C tokens than asm uses, and exactly as many as the DIFFERING uses: the extra asm
# occurrences are IMPLICIT — gcc synthesised them, so no C literal names them and a swap
# cannot corrupt them. The canonical case is an array index: `D_x[*(u16 *)(a0 + 0x2)]()`
# emits BOTH the `0x2` offset (per-member) and a fixed `sll ..,2` for the 4-byte stride.
# Pairing against ALL uses would never match and the member would refuse forever.
pairing = list(zip(spans, diff_pos))
else:
return None # counts disagree -> not safely pairable
edits = []
for (s, e), k in pairing:
if k in diff_idx:
sv = imm_value(sib_words[k], False)
if sv is None:
return None
edits.append((s, e, _fmt_like(t, sv)))
return edits or None
return None
def imm_map_tier1(unit, ex_words, sib_words):
"""Tier 1: {c_literal_token: replacement} for the unambiguous immediate diffs; plus the list of
UNRESOLVED (value, reason) that need the Tier-2 probe, and the unit with any ORDINAL edits
(§T87) already applied. Returns (imm_map, unresolved, unit)."""
rel = reloc_indices(ex_words)
valpos = [imm_value(w, k in rel) for k, w in enumerate(ex_words)]
diff_idx = {k for k in range(len(ex_words)) if ex_words[k] != sib_words[k]}
by_val = collections.defaultdict(set) # ex human-value -> {sib values}
handled = set()
unresolved = []
for k in sorted(diff_idx):
if k in rel: # reloc -> symbol_map's job
continue
ev = valpos[k]
if ev is None: # differing lui-hi / other -> Tier 2
unresolved.append((f"@{k}", f"non-imm diff op={ex_words[k] >> 26:#x}"))
continue
by_val[ev].add(imm_value(sib_words[k], False))
handled.add(k)
imm_map = {}
ord_edits = []
for exv, sibvs in by_val.items():
if len(sibvs) != 1 or None in sibvs:
unresolved.append((exv, "multi-target")); continue
sibv = next(iter(sibvs))
if any(valpos[k] == exv for k in range(len(ex_words)) if k not in diff_idx and valpos[k] is not None):
# value also used at a FIXED position -> a by-value swap would corrupt it. Try the
# ORDINAL resolution (T87) before giving up; it pairs asm positions to C occurrences.
e = _ordinal_edits(unit, exv, valpos, diff_idx, sib_words)
if e:
ord_edits.extend(e); continue
unresolved.append((exv, "asm-ambiguous")); continue
tok, rep = _c_literal_swap(unit, exv, sibv)
if tok is None:
unresolved.append((exv, "not-in-C")); continue
imm_map[tok] = rep
if ord_edits: # apply right-to-left: spans are on
for s, e_, rep in sorted(ord_edits, key=lambda x: -x[0]): # the ORIGINAL unit
unit = unit[:s] + rep + unit[e_:]
return imm_map, unresolved, unit
_TU_CACHE = {}
def _tu_text(ov):
if ov not in _TU_CACHE:
_TU_CACHE[ov] = "".join(open(f).read() for f in sorted(glob.glob(f"src/{ov}/{ov}*.c")))
return _TU_CACHE[ov]
def gather_externs(from_ov, from_addr, unit):
"""file-scope `extern` decls from the exemplar TU for every func_/D_ symbol the body references but
the extracted unit does NOT already declare. extract_unit only grabs the immediately-preceding
externs; a per-location body that indexes a global (`(*D_x[..])()`) references symbols declared once
at file scope elsewhere — templated into a sibling TU that never declared them, they're `undeclared`
at the gate. Carrying them (they get remapped downstream) fixes the decl class. Excludes the self
name. Duplicate-identical externs are legal C; a type-conflict is the §41 reconcile class (rare).
COMMENT-MASKED (Phase 29 T53). Every scan below runs over `cdecl._mask`ed text — comments and
string literals blanked, length-preserving, so offsets still index the original. Two same-class
defects this closes, both the §96 disease (matching by raw TEXT):
* a symbol named only in the body's PROSE counted as a reference. `func_80135260`'s header
comment mentions its sibling `func_80135D20` twice, so the scan demanded a decl for it and
printed "the sibling will not compile" on all 137 members — a 100%-false-alarm warning that
masked real causes for two tasks. T52 settled it empirically: all 132 banked anyway.
* a COMMENTED-OUT `extern` in the exemplar TU could be selected as the carried declaration and
spliced into the sibling as live code. Not yet observed; closed by construction.
The emitted text is sliced from the ORIGINAL by span — a masked decl is all blanks."""
self_sym = f"func_{from_addr:08X}"
unit_mask = cdecl._mask(unit)
refs = set(re.findall(r'\b(?:func_[0-9A-Fa-f]{8}|D_[0-9A-Fa-f]{8})\b', unit_mask)) - {self_sym}
tu = _tu_text(from_ov)
tu_mask = cdecl._mask(tu)
lines, seen, unresolved = [], set(), []
# STATEMENT-oriented, not line-oriented (Phase 26-A audit).
#
# The scan used `[^\n;{}]*`, which cannot cross a newline — so a WRAPPED comma-separated extern was
# invisible in BOTH directions: the first physical line has no ';' and the continuation line has no
# `extern`. src/ov_SC01_077/ov_SC01_077.c:271-272 declares NINE symbols that way:
# extern unsigned char D_801DAA78, D_801DAA79, ..., D_801DAA7C,
# D_801DAA7D, ..., D_801DAA80;
# and the exemplar that references them (func_8013D178) is a substantial family with 133 members.
# Every sibling was therefore staged with NO declaration for those symbols, failed to compile
# (`D_801DAA7D undeclared`), and — because the gate chunks by (overlay, split) — bisect-stormed the
# whole group with it. Booked as a compile failure, i.e. invisible.
_EXTERN_STMT = re.compile(r'^[ \t]*extern\b[^;{}]*?;', re.M | re.S)
# (masked_text, original_text) per statement: MATCH on the mask, EMIT from the original.
stmts = [(tu_mask[s.start():s.end()], tu[s.start():s.end()])
for s in _EXTERN_STMT.finditer(tu_mask)]
unit_stmts = [unit_mask[s.start():s.end()] for s in _EXTERN_STMT.finditer(unit_mask)]
# FILE-SCOPE DECLS FIRST (Phase 29 T61). This function's contract is "file-scope `extern` decls
# from the exemplar TU", but `^[ \t]*extern` also matches an INDENTED one — a block-scope decl
# living inside some OTHER function's body, which is not even in scope at the exemplar's own
# definition. Carrying it and re-emitting it at FILE scope in the sibling is how
# `extern void func_80155FF8(void *, u8);` (ov_SC01_077 L1213, inside another function) landed
# above the sibling's `DEFINE_func_80155FF8()` macro definition and collided with it —
# 137 members booked as a compile failure.
# Ordered, not filtered: a col-0 decl wins, and an indented one is still available as the
# fallback it always was, so a symbol declared ONLY block-scope keeps working exactly as before.
stmts.sort(key=lambda so: 0 if so[0].startswith('extern') else 1)
for sym in sorted(refs):
if any(re.search(rf'\b{sym}\b', s) for s in unit_stmts):
continue # already declared inside the unit
if sym in seen:
continue
hit = next((orig for masked, orig in stmts if re.search(rf'\b{sym}\b', masked)), None)
if hit is None:
unresolved.append(sym) # REPORTED, never silently dropped (R32)
continue
lines.append(" ".join(hit.split())) # collapse the wrap to one legal C line
seen.add(sym)
if unresolved:
print(f"[gather_externs] func_{from_addr:08X}: {len(unresolved)} referenced symbol(s) have NO "
f"file-scope decl in {from_ov} — the sibling will not compile: {unresolved[:6]}")
return lines
def remap_hseq_body(from_addr, from_ov, to_ov, to_addr, body):
"""the h_seq remap (symbol + immediate + cross-address self-rename) applied to a PROVIDED body text
(a RAW crack draft), NOT the extract_unit matched body — for the per-sibling reconcile path (§41c):
remap the raw draft to the sibling, then canon_sig_reconcile it against the sibling's own TU. No
extern-carry (reconcile handles ambient decls). Returns (remapped_text, info) or (None, error)."""
nins = nins_of(from_ov, from_addr)
ex_words = stream_words(from_ov, from_addr, nins)
sib_words = stream_words(to_ov, to_addr, nins)
cls, _ = classify_member(ex_words, sib_words)
if cls in ("STRUCT", "LEN"):
return None, f"member class {cls}"
m, err = symbol_map(from_addr, from_ov, to_ov, to_addr)
if err:
return None, err
imm_map, unresolved = ({}, [])
if cls == "IMM":
imm_map, unresolved, body = imm_map_tier1(body, ex_words, sib_words)
if unresolved:
return None, f"unresolved immediates: {unresolved}"
table = dict(m)
if from_addr != to_addr:
table[f"func_{from_addr:08X}"] = f"func_{to_addr:08X}"
table.update(imm_map)
body, off_notes = fix_derived_offsets(body, table) # §84, BEFORE the name substitution
return apply_remap(body, table), {"symbol_map": m, "imm_map": imm_map,
"derived_offsets": off_notes}
def remap_hseq(from_addr, from_ov, to_ov, to_addr=None):
"""h_seq family template: reloc symbol remap (§40b) + immediate substitution (T2a Tier 1) +
cross-address self-rename (T2b) + carried file-scope externs. Returns (draft, info) or
(None, error_str). info = {symbol_map, imm_map, unresolved, n_externs, cf}. A member with register
drift (STRUCT) or unresolved immediates is refused (the caller skips it — byte-gate would reject)."""
if to_addr is None:
to_addr = from_addr
nins = nins_of(from_ov, from_addr)
ex_words = stream_words(from_ov, from_addr, nins)
sib_words = stream_words(to_ov, to_addr, nins)
cls, _ = classify_member(ex_words, sib_words)
if cls in ("STRUCT", "LEN"):
return None, f"member class {cls} (not templatable)"
unit, cf = extract_unit(from_ov, from_addr)
if not unit:
return None, f"no matched unit for func_{from_addr:08x} in {from_ov}"
m, err = symbol_map(from_addr, from_ov, to_ov, to_addr)
if err:
return None, err
imm_map, unresolved = ({}, [])
if cls == "IMM":
imm_map, unresolved, unit = imm_map_tier1(unit, ex_words, sib_words)
if unresolved:
return None, f"unresolved immediates (Tier-2): {unresolved}"
externs = gather_externs(from_ov, from_addr, unit)
if externs:
unit = "\n".join(externs) + "\n" + unit
table = dict(m)
if from_addr != to_addr:
table[f"func_{from_addr:08X}"] = f"func_{to_addr:08X}"
table.update(imm_map)
unit, off_notes = fix_derived_offsets(unit, table) # §84, BEFORE the name substitution
return apply_remap(unit, table), {"symbol_map": m, "imm_map": imm_map, "unresolved": unresolved,
"n_externs": len(externs), "cf": cf,
"derived_offsets": off_notes}
def symbol_map(addr, from_ov, to_ov, to_addr=None):
"""{exemplar_name: sibling_name} for the per-overlay symbols (positional zip). None,err if not clean.
to_addr defaults to addr (same-address h_norm sibling); pass it for a cross-address (T2b) sibling."""
if to_addr is None:
to_addr = addr
ex, tg = reloc_targets(from_ov, addr), reloc_targets(to_ov, to_addr)
if len(ex) != len(tg):
return None, f"reloc-count mismatch {len(ex)}!={len(tg)} (not an h_norm-clean pair)"
m = {}
for (ke, ae), (kt, at) in zip(ex, tg):
if ke != kt:
return None, "reloc-kind mismatch (not h_norm-clean)"
if ae != at:
# Name the symbol by WHAT THE ADDRESS IS, not by HOW IT WAS LOADED (Phase 26-A audit).
#
# The prefix used to come from the reloc KIND: `func_` for a `jal`, `D_` for anything else.
# But reloc_targets labels every lui/%lo pair "data" — and a FUNCTION's address taken via
# lui/%lo (an address-taken callback) is exactly that shape. splat's own .s proves it:
# `%lo(func_8017E1D4)` occurs 7 times in ov_SC01_077's asm. For all 7 the map got a
# `D_<ADDR>` key while the exemplar's C writes `func_<ADDR>`, so apply_remap's word-bounded
# substitution matched NOTHING and silently no-op'd. The sibling body then kept the
# EXEMPLAR's function pointer, the whole-binary gate rejected it, and the loss was booked
# as a BYTE failure — indistinguishable from a genuine compiler wall. (3 families x 37
# still-stubbed members, live.)
#
# Belt-and-braces: for a data-kind reloc emit BOTH keys. Addresses are unique and
# apply_remap is a single simultaneous pass, so only the token that actually appears in the
# C can ever match — the extra key is free and cannot mis-substitute.
# ...and that rule has to govern the TARGET side too (Phase 29 T82). The block below
# used to spell the sibling's symbol from the EXEMPLAR's kind — `func_` if the exemplar's
# slot was a function, `D_` if data. For a CROSS-ADDRESS family the two sides need not
# agree: `func_80174784`'s callback slot is the FUNCTION `func_801747CC`, while member
# `func_8017CFD4`'s same slot is the DATA symbol `D_80182688`. The map emitted
# `func_80182688` — a name for an address that is not a function — so the body
# materialized the wrong symbol and the whole-binary gate rejected all 251 members.
#
# It reads as a compiler wall because the two oracles disagree in the most misleading
# possible way: `rtu_match` MASKS HI16/LO16, so a wrong %hi/%lo symbol still reports
# MATCH (measured: `MATCH (10 ins)` on a member the fleet gate refused). Spell the
# target by what the target address IS in the SIBLING's overlay.
# ...and ask the ALWAYS-LINKED images too, not just the sibling's own sig — a body
# calls outside its image constantly, and "absent from my sig" is not "is data"
# (see extern_fn_addrs: 611 undefined-reference rows across 45 symbols).
# THREE ORACLES, STRONGEST FIRST — and never a blanket `D_` fallback:
# 1. the SIBLING'S OWN sig: authoritative for its image, and the reason the T82 case
# above still works (a slot that is a function in the exemplar and DATA in the
# member is decided here, by the member's own boundaries).
# 2. the ALWAYS-LINKED images (resident + main): a body calls outside its image
# constantly, and their address ranges cannot collide with an overlay's.
# 3. the EXEMPLAR REACHED IT BY `jal`: a call target is a function BY DEFINITION, so
# for an external address neither sig covers (an md_* module calling the
# overlay-range engine — 0x80171A1C, 112 rows) this is evidence, not a guess.
# Only a non-call reloc whose target no oracle claims falls to `D_`.
if at in fn_addrs(to_ov) or at in extern_fn_addrs() or ke == "call":
tgt = f"func_{at:08X}"
else:
tgt = f"D_{at:08X}"
if ke == "call":
m[f"func_{ae:08X}"] = tgt
else:
m[f"D_{ae:08X}"] = tgt
m[f"func_{ae:08X}"] = tgt
return m, None
# A machine-generated FILE-SCOPE decl layer terminates here; extract_unit's backward preamble
# walk must not cross it (the layer belongs to the file, not to the function beneath it).
_DECL_LAYER_END = re.compile(r"end (?:§8b carried decl layer|canonical-sig layer)")
_DEFINE_FN = re.compile(r'^\s*#define\s+([A-Za-z_]\w*)\s*\(')
def _carry_macros(lines, start, end, unit_text):
r"""The file-scope function-like `#define` macros the unit BODY references but does not itself
contain — returned in file order, to be prepended to the unit (Phase-27 T5).
A GTE-heavy per-location function calls C inline-asm macros (`gte_ldv0(...)`, `gte_rtps()`, …)
that are `#define`d at FILE scope, above the function, NOT pulled from common.h (common.h carries
the *assembler* gte_macros.inc for the INCLUDE_ASM path; the C macros live only in the exemplar's
.c). extract_unit's backward preamble walk stops at the first `#define`/continuation line, so it
dropped them — and a sibling staged without them saw every GTE op as an implicit-declaration CALL.
That is TWO failures in one: the sibling fails to compile, AND — if the body carries a caller-saved
register pin — the phantom call pushes cc1's sched1 into `create_reg_dead_note`'s abort
(sched.c:2725), the SIGABRT long mis-recorded as the §42e "pin-crash wall" (`.run/giants/
pin_crash_sigabrt.md`: the wall is this staging drop, not a compiler limit — a pinned family
stages 133/133 clean once its macros ride along). Carried macros never reach dedup_propagate's
engine_core.h lift (that path builds from its own body, not extract_unit), so no `#define` is ever
embedded inside a `DEFINE_func_*()` macro."""
carried, i = [], 0
while i < len(lines):
m = _DEFINE_FN.match(lines[i])
if not m:
i += 1
continue
j = i
while j < len(lines) and lines[j].rstrip().endswith("\\"):
j += 1
inside = start <= i and j <= end
if not inside and re.search(rf'\b{re.escape(m.group(1))}\s*\(', unit_text):
carried.append("\n".join(lines[i:j + 1]))
i = j + 1
return carried
_TD_CLOSE = re.compile(r'^\}\s*[A-Za-z_]\w*\s*(?:\[[^\]]*\])?\s*;')
def _typedef_block_start(lines, j, limit=400):
"""Index of the `typedef` line OPENING the multi-line typedef whose CLOSING line is `lines[j]`
(`} Ang2_8012B77C;`), else None — Phase-30 S6b, defect D5.
extract_unit's preamble backscan reads one line at a time, so a multi-line typedef presents its
CLOSING line first: `}` is not one of the accepted prefixes, the walk stops, and the type never
travels. The sibling then gets `Ang2_8012B77C undeclared` — MEASURED as 17 zero-crack families /
24,332 templatable ins, including the 275-member `func_80128C98` (`CdFileLoc`). The old note here
said such functions "route through the engine_types.h lift"; that lift is a separate, heavier
mechanism (a fleet-shared header edit, blast-radius T2) and it was never run for these, so in
practice the class was simply dropped. Carrying the block is blast-radius T0 (draft-only) and
`harvest_verify` already removes a typedef the sibling TU also provides
(`cdecl.strip_provided_typedefs`), so a duplicate cannot break the sibling.
Only a block that literally BEGINS with `typedef` is carried: a plain struct VARIABLE (`struct
{…} someGlobal;`) closes identically but defining it in the sibling would be a duplicate global."""
if not _TD_CLOSE.match(lines[j].lstrip()):
return None
depth = 0
for k in range(j, max(-1, j - limit), -1):
depth += lines[k].count('}') - lines[k].count('{')
if depth == 0:
if re.match(r'^\s*typedef\b', lines[k]):
return k
# brace-on-its-own-line form: `typedef struct` / `{` / … / `} Foo;`
if lines[k].strip() == '{' and k and re.match(r'^\s*typedef\b', lines[k - 1]):
return k - 1
return None
return None
def _def_head_at(ln, paren_idx, more=()):
"""Does the `func_…(` occurrence whose '(' is at `paren_idx` head a DEFINITION on this line?
The old test was "the line does not end in `;`", which is wrong whenever a line holds BOTH a
declaration and a definition (Phase 29 T65). ov_SC01_077_jr_80154C24.c:1346 is
extern void func_80156044(int, int); int func_80155FF8(int, int) { __asm__ … }
and the line does not end in `;`, so `func_80156044` — which appears only in the DECLARATION —
was taken as a definition head. extract_unit therefore lifted the neighbouring wrapper instead of
the real definition seven lines below, and every sibling got `redefinition of func_80155FF8`.
The correct question is what follows the parameter list: `;` is a declaration, `{` (or end of
line, i.e. the brace-on-its-own-line form) is a definition.
`more` = the following source lines, so the question can still be answered when the parameter list
WRAPS. The old fallback answered "param list continues past this line -> an ANSI definition", which
is false for a wrapped multi-line DECLARATION (Phase-30 S6a, defect D2):
extern void func_801466F0(s32 a0, s32 a1, s32 a2, s32 a3,
s32 sp5, s32 sp6, s32 sp7, s32 sp8);
ov_SC01_077_after.c:457 is exactly that, and it was accepted as a definition head — so extract_unit
returned a 16-line fragment containing NO function body (a neighbouring `DEFINE_func_*()`, an
`#include`, and a comment whose `{ u16, … }` example closed the forward brace scan). Every sibling
of that family failed, and the family read as a 0/137 compiler wall (§110/R35). With `more`, the
list closes on the continuation line and the same `;` vs `{` test gives the right answer."""
depth = 0
for seg in (ln[paren_idx:],) + tuple(more):
for i, ch in enumerate(seg):
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if depth == 0:
rest = seg[i + 1:].strip()
rest = re.sub(r'^(/\*.*?\*/|//.*)\s*', '', rest)
if rest == '' or rest.startswith('{'):
return True
if rest.startswith(';'):
return False
return '{' in rest
# The parameter list never closed. Given lookahead that means malformed/unknown input, and
# "unknown" must not be reported as a definition (D2). With no lookahead we genuinely cannot
# tell, so keep the historical answer for any caller not yet passing `more`.
return not more
def _foreign_defs(unit_text, addr):
"""Every `func_XXXXXXXX` this unit DEFINES other than its target (Phase 29 T65, §110).
Definition vs declaration is the same test extract_unit uses on the way in: a declaration ends in
`;` once trailing comments are stripped. `DEFINE_func_X()` macro instantiations are not matched —
there is no word boundary before `func_` inside `DEFINE_func_`, which is exactly what we want,
since a macro instantiation defines nothing the sibling does not already have."""
self_sym = f"func_{addr:08X}".lower()
out = []
_mlines = cdecl._mask(unit_text).split("\n")
for _li, ln in enumerate(_mlines):
for m in re.finditer(r'\bfunc_[0-9A-Fa-f]{8}\s*\(', ln):
name = m.group(0).split("(")[0].strip()
if name.lower() == self_sym:
continue
# The text before the name must look like a DECLARATION HEAD (a return type), which
# excludes call sites (`iVar4 = func_X(` / a bare `func_X(a);` statement). Split on the
# last `;` first, so a definition sharing its line with a preceding declaration — the
# wrapper form that caused this whole bug — is still seen.
pre_tail = ln[:m.start()].rsplit(';', 1)[-1]
if not re.fullmatch(r'\s*[A-Za-z_][\w \*]*', pre_tail):
continue
# Lookahead (D2): a WRAPPED declaration continues past its line, so without `more` this
# test used to answer "definition" and _foreign_defs would refuse a perfectly good unit.
if not _def_head_at(ln, ln.index('(', m.start()), _mlines[_li + 1:_li + 9]):
continue # a declaration, not a def
if name not in out:
out.append(name)
return out
def _alias_decl_for(lines, addr):
"""(alias_ident, decl_line_index) when the function is defined under the §37/§73 ASM-LABEL ALIAS
form, else (None, None).
A body whose byte-true signature conflicts with the fleet-canonical decl (BOTH §73 axes at once —
return AND params) is banked zero-touch by giving the definition a different C identifier and
binding the emitted SYMBOL with a GNU asm label:
extern void func_8016191C(void *a0, s32 a1); <- the fleet canon, in engine_core.h
int aF8016191C(int param_1, unsigned int param_2) __asm__("func_8016191C");
int aF8016191C(int param_1, unsigned int param_2) { ... }
`extract_unit` matched only a definition head literally NAMED func_<ADDR>, so it was structurally
blind to this form and returned None — which every caller reads as "not matched". MEASURED
(Phase 30): that blindness was the WHOLE of the h_seq sweep's 137 "no matched unit" skips — one
exemplar (func_8016191C @ ov_SC01_077) × 137 same-address members, 3,288 ins, every member still
an INCLUDE_ASM stub and otherwise sweep-ready. The tool, not the compiler (R35).
The alias DECLARATION must travel with the unit: without it the sibling TU emits the symbol
`aF8016191C` and the function never lands at func_<ADDR>."""
# S33: the matcher used to be `rx.match(ln)` PER LINE, so it was blind to a WRAPPED alias
# declaration — the §134 multi-line class, the third tool it has surfaced in. Measured before
# fixing (R37): 91 asm-label alias decls exist fleet-wide, the per-line form matched 90, and the
# single miss was func_801466F0 (137 members / 3,288 ins written off as "no matched unit"). So
# this is a ONE-FUNCTION fix, not the class the S6b note implied — recorded so a future session
# does not scope against it. Match over the JOINED text and map the offset back to a line index;
# the RETURNED index must stay the DECL's FIRST line, because extract_unit carries from there.
rx = re.compile(rf'(?:^|\n)([ \t]*[A-Za-z_][\w \*]*?\b([A-Za-z_]\w*)\s*\([^;]*?\)\s*'
rf'__asm__\s*\(\s*"func_{addr:08X}"\s*\)\s*;)', re.I | re.S)
text = "\n".join(lines)
for m in rx.finditer(text):
ident = m.group(2)
if ident.lower() == f"func_{addr:08x}":
continue
# offset -> 0-based index of the line the DECLARATION STARTS on
j = text.count("\n", 0, m.start(1))
return ident, j
return None, None
def extract_unit(ov, addr):
"""the matched inline def + its contiguous preceding extern/blank/comment lines, from the overlay src.
func_<addr> names are UPPERCASE-hex in src (func_8013DBE4); match case-insensitively to be safe.
Also resolves the §37/§73 ASM-LABEL ALIAS definition form (see `_alias_decl_for`): the accepted
definition name becomes the alias identifier, and the alias declaration line is carried into the
unit so the sibling still emits the func_<ADDR> symbol."""
plain = re.compile(rf'^\s*[A-Za-z_][\w \*]*\bfunc_{addr:08X}\s*\(', re.I)
for cf in sorted(glob.glob(f"src/{ov}/{ov}*.c")):
_raw = open(cf).read()
lines = _raw.split("\n")
# Length- AND line-preserving comment/string blank-out (R33: cdecl._mask is the ONE masking
# oracle). Used for the forward brace scan below, so a `{ u16, … }` inside a documentation
# comment can never close a function body (Phase-30 S6a, the D2 post-mortem).
masked = cdecl._mask(_raw).split("\n")
alias_ident, alias_ln = _alias_decl_for(lines, addr)
# RE-DERIVE per file: `pat` must never leak an earlier file's alias into a later one.
pat = (re.compile(rf'^\s*[A-Za-z_][\w \*]*\b{re.escape(alias_ident)}\s*\(', re.I)
if alias_ident else plain)
for i, ln in enumerate(lines):
if alias_ident and i == alias_ln:
continue # the alias DECLARATION is not the definition
# A DECLARATION ends in `;` — but m2c writes them with a trailing comment
# (`M2C_UNK func_80178D40(s32, s32); /* extern */`), so the raw line ends in `*/` and the
# old `endswith(";")` guard let it through as a DEFINITION. The forward brace-scan then ran
# past it and swallowed the NEXT function's body, handing remap_hseq a garbage unit (5 of 35
# substantial-family exemplars — all still INCLUDE_ASM stubs). The whole-binary gate rejected
# every one, so no wrong match was ever banked (G3/P9) — but the family engine burned a
# build per sibling on them and any extract_unit-based readiness analysis was wrong.
# Strip trailing comments before the `;` test. (Phase 26 session 8, R14.)
code = re.sub(r'(/\*.*?\*/|//.*)\s*$', '', ln).rstrip()
_m = pat.search(ln)
if _m and "INCLUDE_ASM" not in ln and not code.endswith(";") \
and _def_head_at(ln, ln.index('(', _m.end() - 1), lines[i + 1:i + 9]):
j = i - 1
# Grab the fn's own preceding decls: externs, comments, single-line typedefs
# (Phase-26 §8: jr-function bodies define local `typedef struct {…} Foo_<addr>;` that
# must template with the body, else the sibling sees `Foo undeclared`) — AND, since
# Phase-30 S6b (D5), MULTI-LINE typedefs, via `_typedef_block_start` (the old note
# here said they "route through the engine_types.h lift"; measured, they routed
# nowhere — 17 families / 24,332 ins were simply dropped).
# STOP at a machine-generated FILE-SCOPE decl layer (the §8b carried layer that
# jr_isolate_all prepends to an isolated region, or the Phase-17 canonical-sig layer):
# those belong to the FILE, not to the first function under them. Absorbing one makes
# the unit drag ~140 unrelated externs into every sibling — several naming types the
# sibling's TU lacks — and the whole family gate-fails (Phase 26 session 6).
while j >= 0 and (lines[j].strip() == "" or j == alias_ln or
lines[j].lstrip().startswith(("extern", "//", "/*", "*", "typedef")) or
_typedef_block_start(lines, j) is not None):
if _DECL_LAYER_END.search(lines[j]):
break
# D5: a multi-line typedef closes with `} Foo;`. Jump to its `typedef` line so the
# WHOLE block is carried, then keep walking above it.
_tds = _typedef_block_start(lines, j)
if _tds is not None:
j = _tds - 1
continue
# A LINE THAT STARTS WITH `extern` CAN STILL HOLD A DEFINITION (Phase 29 T65).
# ov_SC01_077_jr_80154C24.c:1346 is
# extern void func_80156044(int, int); int func_80155FF8(int, int) { __asm__ … }
# — a declaration AND a complete handwritten inline-asm function on ONE line. The
# scan absorbed it as a "preceding decl", so the unit for func_80156044 carried a
# SECOND function definition, and every sibling that already defines that function
# via its shared DEFINE_ macro got `redefinition of func_80155FF8`. 137 members,
# booked as a compile failure. A pure declaration never contains `{`; a single-line
# `typedef struct {…} Foo;` legitimately does, and is still carried.
#
# …AND NEITHER DOES A COMMENT (Phase-30 S6a, defect D1). The guard fired on any
# DOCUMENTATION line that merely mentions a brace — `* => { u16, u16, s32 }` — so
# the carry stopped in the MIDDLE of a block comment and the sibling received a
# dangling ` * …` fragment plus an unmatched `*/`: `parse error before 'the'`.
# Comment text cannot hold a definition, so exempt it. (A `/*…*/ int f() {` line
# is code with a leading comment, so only an UNCLOSED `/*` counts as comment-only.)
_ls = lines[j].lstrip()
_comment_only = _ls.startswith(("*", "//")) or (_ls.startswith("/*") and "*/" not in lines[j])
if "{" in lines[j] and not _ls.startswith("typedef") and not _comment_only:
break
j -= 1
start = j + 1
# R32 BACKSTOP for D1: never hand a sibling a DANGLING comment fragment. If the carried
# preamble opens inside a block comment (a `*/` with no `/*` before it) the backscan was
# truncated by something we have not accounted for — drop the fragment and say so, rather
# than emit ` * …` lines as code and let 137 siblings fail as a phantom "wall".
_pre = "\n".join(lines[start:i])
_close = _pre.find("*/")
if _close != -1 and (_pre.find("/*") == -1 or _pre.find("/*") > _close):
_drop = _pre[:_close + 2].count("\n") + 1
print(f"[extract_unit] func_{addr:08X} in {ov}: carried preamble opened INSIDE a "
f"block comment — dropping {_drop} dangling line(s) (D1 backstop)")
start = min(start + _drop, i)
depth, started, end = 0, False, i
for k in range(i, len(lines)):
depth += masked[k].count("{") - masked[k].count("}")
if "{" in masked[k]:
started = True
if started and depth <= 0:
end = k
break
unit_text = "\n".join(lines[start:end + 1])
if alias_ident and not (start <= alias_ln <= end):
# The backscan walks PAST the alias line (so the fn's own preceding externs are
# carried, exactly as for a plain definition) and normally sweeps it into
# [start:end]. If some layout kept it out, carry it explicitly and FIRST — the asm
# label is what binds this body to the func_<ADDR> symbol in the sibling TU.
unit_text = lines[alias_ln].strip() + "\n" + unit_text
extra = _foreign_defs(unit_text, addr)
if extra:
# R32: a unit that defines a function OTHER than its target cannot be templated —
# the sibling already has that function (inline, or via its shared DEFINE_ macro),
# so splicing produces `redefinition of …`. Refuse LOUDLY rather than hand the
# sweep a unit that will fail 137 times and read as a compiler wall.
print(f"[extract_unit] func_{addr:08X} in {ov}: unit also DEFINES {extra} — "
f"refusing (a multi-definition unit cannot template; §110)")
return None, None
macros = _carry_macros(lines, start, end, unit_text) # gte_* etc. the body needs (T5)
if macros:
unit_text = "\n".join(macros) + "\n" + unit_text
tds = _carry_typedefs(lines, unit_text) # types above a #define (S43)
if tds:
unit_text = "\n".join(tds) + "\n" + unit_text
return unit_text, cf
if alias_ident:
# R32: we PROVED the function is defined here (the asm label binds the symbol) but could
# not find the alias identifier's definition. Falling through to _macro_unit would report
# "not matched" — the silent skip this whole fix exists to delete. Refuse loudly instead.
print(f"[extract_unit] func_{addr:08X} in {ov}: {cf} declares the asm-label alias "
f"`{alias_ident}` but no definition of it was found — refusing (R32).")
return None, None
return _macro_unit(addr)
_TD_NAME_RE = re.compile(r'^\s*typedef\b.*?([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*;', re.S)
def _carry_typedefs(lines, unit_text):
"""Carry any file-scope typedef the extracted unit actually USES but did not carry.
P30 S43. The preamble backscan's accept-set is blank/`extern`/comment/`typedef`, so it **halts at
the first `#define`** and never reaches typedefs declared above the macro block. `_carry_macros`
then re-attaches the macros, which hides the truncation — the unit looks complete and is not.
Measured on the 0x80182FD4 family: 16/16 gte macros carried, **0/10 typedefs**, silently; the
staged sibling failed as `` `Vec8_80182FD4' undeclared `` and the sweep booked it
`PLUMBING: parse error before 'unsigned'` (the error surfacing at the next token), which read as
an intractable wall. It banked instantly once the type was carried by hand.
Additive by construction: only typedefs whose NAME appears as a word in the unit and which are not
already present are prepended, so a unit that was already complete is byte-unchanged. Whether the
gap is fatal depends on whether the TARGET TU happens to declare the type itself — which is why
this failed loudly for some families and silently succeeded for others."""
defs = _typedef_blocks(lines)
# ALREADY-CARRIED test, by BLOCK not by line (P30 S48). The old test was
# `^\s*typedef\b[^\n]*\bNAME\b`, which requires the name on the SAME LINE as the keyword — true
# of `typedef unsigned char u8;`, never true of the multi-line form the backscan actually
# carries: `typedef struct Foo {` … `} Foo;`. So every multi-line typedef the preamble already
# held was carried a SECOND time, and the unit reached the gate with two definitions of one tag:
# `redefinition of struct Foo_8013C0F8` (canon_sig_reconcile uniquifies the tag, so the
# duplicate is exact). Measured across the 44 jr families of the 0b sweep: 1 family, 3 slots.
carried = {n for n, _ in _typedef_blocks(unit_text.split("\n"))}
# TRANSITIVE closure: a carried typedef may itself name another typedef (measured — carrying
# `Vec8_80182FD4` alone then failed on `SVECTOR_8016E7C8`, which it references). Iterate to a
# fixpoint, emitting DEPENDENCY-FIRST so C89 sees each name before its use.
out, seen, want = [], set(carried), unit_text
for _ in range(12): # depth guard; real chains here are 2-3
added = False
for name, blk in defs:
if name in seen:
continue
if re.search(r'\b' + re.escape(name) + r'\b', want):
seen.add(name)
out.insert(0, blk) # dependency-first: later finds precede earlier ones
want += "\n" + blk
added = True
if not added:
break
return out
def _typedef_blocks(lines):
"""[(name, full_block)] for every typedef in `lines` — single- or multi-line."""
defs = []
for k, ln in enumerate(lines):
if not ln.lstrip().startswith("typedef"):
continue
# BRACE-AWARE block capture. A `;`-terminated scan stops INSIDE the struct — the first
# member line (`SVECTOR_8016E7C8 v[4];`) ends in `;` — yielding a truncated, unclosed
# typedef that cc1 reports at the member's type name. Close on the brace depth instead,
# then take the `;` that follows `}` (P30 S43, measured).
blk, j, depth, opened = ln, k, ln.count("{") - ln.count("}"), "{" in ln
while j + 1 < len(lines) and j - k < 60 and (
(opened and depth > 0) or (not opened and ";" not in blk)):
j += 1
blk += "\n" + lines[j]
depth += lines[j].count("{") - lines[j].count("}")
opened = opened or "{" in lines[j]
while opened and ";" not in blk.rsplit("}", 1)[-1] and j + 1 < len(lines) and j - k < 60:
j += 1
blk += "\n" + lines[j]
m = _TD_NAME_RE.match(blk)
if m:
defs.append((m.group(1), blk))
return defs
def _macro_unit(addr):
r"""Fallback: reconstruct the unit from its SHARED `DEFINE_func_<ADDR>()` macro.
A function can be MATCHED with no inline definition in ANY .c — once dedup_propagate lifts it, the
body lives only as a macro in src/shared/engine_core.h and every overlay carries a bare
`DEFINE_func_X()` instantiation. extract_unit globbed only `src/<ov>/<ov>*.c`, so for those it
returned None — and the caller reads None as "not matched".
MEASURED (Phase 26-A audit, CRITICAL): 93 of the 218 h_seq exemplars the manifest calls MATCHED are
phantom for exactly this reason, carrying 2,157 candidate members of which **1,834 are still
INCLUDE_ASM stubs that are PURE/IMM-clean, symbol_map-clean and unpinned** — i.e. they would be
staged and byte-gated today, and were instead dropped before the first build. And because these are
the functions dedup lifted precisely BECAUSE their reach is high, the loss is concentrated in the
largest families.
This is an exact INVERSE of dedup_propagate.make_macro(), which built the macro from `externs +
def block` as: `#define DEFINE_func_<ADDR>() \` then, per line, 4 spaces + line.rstrip() + ` \`
(no continuation on the last). So the reconstruction IS the unit extract_unit wants — it is derived
from the generator, not re-guessed from the text.
NOTE for callers: the returned path is a HEADER, not a TU. family_sweep must keep using the
SIBLING's src_rel for its reconcile/scope_data step."""
head = f"#define DEFINE_func_{addr:08X}() \\"
for hp in sorted(glob.glob("src/shared/*.h")):
lines = open(hp).read().split("\n")
for i, ln in enumerate(lines):
if ln.rstrip() != head:
continue
body, j = [], i + 1
while j < len(lines):
raw = lines[j]
cont = raw.rstrip().endswith("\\")
stripped = re.sub(r'\s*\\$', '', raw) # drop the line-continuation
body.append(stripped[4:] if stripped.startswith(" ") else stripped)
j += 1
if not cont:
break
return "\n".join(body), hp
# There is a SECOND shared-body mechanism, and a macro-only fallback would be blind to it: a DIRECT
# definition in a shared header, #included per overlay rather than instantiated as a macro. The whale
# (func_80144B9C, 770 ins, -O0) is shared exactly that way — config/dedup.us.yaml records it as
# `func: func_80144B9C # ... shared via a HEADER #included in each overlay's -O0 <ov>_o0b.c (NOT a
# DEFINE_ macro)`. Costs nothing today (the whale is fully banked), but the shape is armed for the
# next shared-header function, and "the tool silently says NOT MATCHED" is precisely the failure
# class this audit exists to remove. Be TOTAL over both mechanisms.
# COLUMN 0 only. A shared header is mostly MACRO BODIES, and make_macro indents every body line by
# exactly 4 spaces — so anchoring at column 0 excludes them by construction. That matters: a macro
# body line like ` extern void func_80144B9C(void); \` would otherwise be read as a DEFINITION,
# because the trailing line-continuation means the line does not end in ';' and the decl guard never
# fires. That is exactly the declaration-read-as-definition bug fixed at commit:0552 — do not re-open
# it. The whale's direct definition (`void func_80144B9C(void) {`) is at column 0, as any real
# file-scope definition must be.
pat = re.compile(rf'^[A-Za-z_][\w \*]*\bfunc_{addr:08X}\s*\(', re.I)
for hp in sorted(glob.glob("src/shared/*.h")):
lines = open(hp).read().split("\n")
for i, ln in enumerate(lines):
code = re.sub(r'\s*\\$', '', ln) # drop a macro line-continuation FIRST
code = re.sub(r'(/\*.*?\*/|//.*)\s*$', '', code).rstrip()
if not (pat.search(ln) and "INCLUDE_ASM" not in ln and not code.endswith(";")):
continue
depth, started, end = 0, False, i
for k in range(i, len(lines)):
depth += lines[k].count("{") - lines[k].count("}")
if "{" in lines[k]:
started = True
if started and depth <= 0:
end = k
break
return "\n".join(lines[i:end + 1]), hp
return None, None
_DERIVED_OFF_RE = re.compile(r'&\s*(D_([0-9A-Fa-f]{8}))\s*\+\s*(0[xX][0-9A-Fa-f]+|\d+)')
def fix_derived_offsets(unit, table):
"""§84 — recompute `&D_<ex> + <lit>` literals against the MEMBER's own symbol addresses.
A crack may reach a symbol via a DIFFERENT symbol plus a literal offset, so gcc cannot CSE the two
%hi/%lo pairs into one:
(*(S9 *)&D_801DAA78) = *(S9 *)(&D_801DA998 + 0x20); /* same addr as &D_801DA9B8 */
The literal is NOT a constant of the algorithm — it is the DISTANCE BETWEEN TWO PER-OVERLAY
SYMBOLS, and that distance differs per overlay. Substituting the NAMES while carrying the literal
through emits a wrong %lo, and `match_one` CANNOT SEE IT (it masks HI16/LO16) — only the
whole-binary gate does, as a ONE-BYTE diff. Measured: exemplar 0x801DA998+0x20 == 0x801DA9B8, but
member 0x801A5778+0x20 == 0x801A5798 while the true member symbol is 0x801A5790 (delta 0x18).
`unit` is the EXEMPLAR body (pre-substitution) and `table` the exemplar->member symbol map, so both
endpoints resolve here. Returns (unit', notes); notes carries (base, old, new) per site, with
new=None where an endpoint is NOT a mapped symbol — REPORTED, never silently carried (R32: a
silent skip is a defect, and a silent skip is exactly how this survived)."""
notes = []
def repl(m):
base_ex, hexs, lit_s = m.group(1), m.group(2), m.group(3)
lit = int(lit_s, 16) if lit_s.lower().startswith('0x') else int(lit_s)
target_ex = "D_%08X" % (int(hexs, 16) + lit)
base_mem, target_mem = table.get(base_ex), table.get(target_ex)
if base_mem is None or target_mem is None:
notes.append((base_ex, lit, None)) # not a symbol-to-symbol distance -> leave as-is
return m.group(0)
new_lit = int(target_mem[2:], 16) - int(base_mem[2:], 16)
notes.append((base_ex, lit, new_lit))
if new_lit == lit:
return m.group(0)
return m.group(0).replace(lit_s, ("0x%X" % new_lit) if new_lit >= 0 else ("-0x%X" % -new_lit))
return _DERIVED_OFF_RE.sub(repl, unit), notes
def apply_remap(unit, table):
"""single-pass simultaneous substitution of {source_token: replacement} over unit — each source
token is matched once against the ORIGINAL text, so chained/permuted maps (D_A->D_B, D_B->D_C, or
an immediate value permutation 0x10->0xA & 0x4->0x10) are correct where the old sequential re.sub
corrupted them. Tokens are word-bounded (`\\b`); longest-first avoids any prefix ambiguity."""
if not table:
return unit
keys = sorted(table, key=len, reverse=True)
rx = re.compile(r'\b(?:' + '|'.join(re.escape(k) for k in keys) + r')\b')
return rx.sub(lambda mm: table[mm.group(0)], unit)
def remap(addr, from_ov, to_ov, to_addr=None, imm_map=None):
"""returns (draft_text, symbol_map) or (None, error_str).
Same-overlay-address remap by default; pass to_addr for a cross-address (T2b) sibling — the self
name func_<ADDR> is then remapped to func_<TO_ADDR> too. imm_map (T2a) merges per-member immediate
/literal token substitutions into the SAME single pass. The returned map is the per-overlay symbol
map only (self-rename + imm edits excluded), preserving the pre-Phase-26 contract."""
if to_addr is None:
to_addr = addr
m, err = symbol_map(addr, from_ov, to_ov, to_addr)
if err:
return None, err
unit, cf = extract_unit(from_ov, addr)
if not unit:
return None, f"no matched unit for func_{addr:08x} in {from_ov}"
table = dict(m)
if addr != to_addr: # self-rename: definition + any recursion
table[f"func_{addr:08X}"] = f"func_{to_addr:08X}"
if imm_map:
table.update(imm_map)
unit, _off_notes = fix_derived_offsets(unit, table) # §84, BEFORE the name substitution
unit = apply_remap(unit, table)
return unit, m
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--addr", required=True)
ap.add_argument("--from", dest="frm", required=True)
ap.add_argument("--to", required=True)
ap.add_argument("--to-addr", dest="to_addr", default=None,
help="sibling address if different from --addr (cross-address T2b family)")
ap.add_argument("--out", default=None)
a = ap.parse_args()
addr = int(a.addr, 16)
to_addr = int(a.to_addr, 16) if a.to_addr else None
draft, m = remap(addr, a.frm, a.to, to_addr)
if draft is None:
print(f"REMAP FAIL (func_{addr:08x} {a.frm}->{a.to}): {m}")
sys.exit(1)
out = a.out or f".run/remap_{a.to}_{(to_addr or addr):08x}.c"
open(out, "w").write(draft + "\n")
print(f"remapped {len(m)} per-overlay symbol(s) {a.frm}->{a.to}: {m}")
print(f"-> {out}")
if __name__ == "__main__":
main()