Files
BFM-decomp/tools/scope_data_externs.py
T
Drew T 1ab9905368 feat(phase-26): §8d scope_data_externs — the ×133 sweep blocker fixed; func_8015AE2C banked ×134
- ROOT CAUSE (R14 — the session-7 diagnosis was half right): the isolated region builds [ OK ]
  WITHOUT the body, so §8b isolation was never implicated. `family_remap.gather_externs` prepends
  carried decls at FILE scope; D_801812A4 is a fn-ptr dispatch table the sibling declares FOUR
  incompatible ways at BLOCK scope inside its own later functions, so the carried file-scope decl
  ESTABLISHES A GLOBAL THE TU NEVER HAD and every later block-scope extern must now agree with it.
  Byte-proven asymmetry: BLOCK(int)->BLOCK(struct*)->FILE(void*) builds; FILE(void*)->BLOCK(int)
  errors. It was the ONLY hard error in the build — all 27 carried function externs were fine raw.

- THE FIX (demote, don't reconcile): tools/scope_data_externs.py emits a carried D_ extern at BLOCK
  scope inside the function body when the TU has no file-scope decl of it above the insertion point.
  Byte-neutral (an extern emits no code; type + access opcodes unchanged) and never worse than raw,
  so it needs no oracle, no type comparator, no fn-ptr parser. Restores fidelity — the original
  declares these symbols at block scope in exactly this way. Wired into jtbl_family_bank as the
  `scoped` stage: raw -> scoped -> recovered -> reconciled (scoped is the base for the later stages).

- reconcile_decls is the WRONG instrument for this class, twice: its oracle answers "what does the
  FLEET call this symbol" when the question is "what can THIS TU see", and its DATA_DECL_LINE_RE
  cannot parse `extern void (*D_x[])(void *);` — silently skipping the very symbols that were
  failing (the phase's third silent-skip bug, after find_site braces + overlay_files splits).

- R17 TRIAGE RULE, first real test, held: `conflicting types` = the compiler REFUSED TO COMPILE =
  a C front-end diagnostic = our Python. Reading cse.c/global.c would have taught nothing.

- RESULT: func_8015AE2C (562 ins, reach 134) swept 133/133 siblings, 0 failures. R22 clean-fleet
  136/136 BYTE-IDENTICAL (534 changed src files); dedup-check 1813 validated / 0 failed; 0
  NON_MATCHING (G4). instr-weighted 63.0 -> 63.6%; distinct-code 39.1 -> 40.5% (+256 unique fns /
  +79,957 ins) — one core, ~0 agent tokens.

- knowledge captured during the producing session (R30/R31/R21): cookbook §8d, decision-log
  2026-07-13 session 8, SETUP tool-inventory row; CURRENT_PHASE session-8 checkpoint.
2026-07-13 20:45:15 -06:00

140 lines
7.0 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
"""scope_data_externs.py — place a templated body's DATA externs at the scope the target TU can accept
(Phase 26, cookbook §8d). The missing recovery step that unblocks the heavy-jr ×N family sweeps.
THE BUG IT FIXES (byte-proven on func_8015AE2C -> ov_SC01_000)
-------------------------------------------------------------
`family_remap.gather_externs` carries the exemplar's decls for every symbol the body references and
prepends them at FILE scope (family_remap.py, remap_hseq). For a per-location DATA symbol that the
target TU declares only at BLOCK scope — inside its OWN later functions, loosely typed — that carried
file-scope decl ESTABLISHES A GLOBAL DECLARATION the TU never had, and every later block-scope `extern`
of that symbol must now agree with it. They don't (the engine is loosely typed), so gcc-2.7.2 rejects
the TU. The asymmetry, both halves byte-proven by the whole-binary gate:
BLOCK(int) -> BLOCK(struct Ent *) -> FILE(void *) ... builds [ OK ] (the region, stub state)
FILE(void *) -> BLOCK(int) -> ... conflicting types for `D_801812A4' (ERROR)
Example: ov_SC01_000's `D_801812A4` is a fn-ptr dispatch table declared FOUR incompatible ways in one
region — `(int)` and `(struct Ent_8015CD20 *)` at block scope inside func_8015C128 / func_8015CD20, then
`(void *)` at file scope. That compiles. Prepend the body's `extern void (*D_801812A4[])(void *);` above
them and it does not.
THE FIX
-------
Emit the carried extern at BLOCK scope (inside the function body) whenever the TU has NO file-scope decl
of that symbol above the insertion point. Then it declares no global, nothing below it can conflict, and
the TU's own decl environment is preserved exactly. This is the §8c principle ("splitting a TU means
rebuilding its declaration environment") applied to templating: a carried decl must not CHANGE the
environment — and it restores fidelity, because the original source declares these symbols at block
scope in precisely the same way (m2c/Ghidra emit per-function externs there).
BYTE-NEUTRAL: an `extern` declaration emits no code, and moving it changes neither the symbol nor the
declared type, so every access keeps its opcode. Only name lookup moves. The whole-binary byte-gate
(G3/P9) remains the sole arbiter — a wrong placement just fails the gate and the sibling reverts.
NEVER WORSE THAN THE STATUS QUO: if the symbol DOES have a file-scope decl above, we leave the line
alone (identical spelling = a legal duplicate; a differing spelling is the §41 reconcile class and is
an error at file scope either way, so demoting could not have saved it — the `reconciled` stage is the
fallback there).
SCOPE: `D_` data externs only. Function externs stay at file scope so `cast_call_sites` /
`canon_sig_reconcile` (which parse file-scope decls) keep working on them; gcc tolerates mismatched
function decls with a pedwarn, so they are not the failing class.
Usage (library — the sweep calls fix() directly; ×N per family, so no subprocess):
from scope_data_externs import fix
body, moved = fix(body, tu_text, insert_pos, "func_8015AE2C")
CLI (diagnostics):
tools/scope_data_externs.py --body .run/_repro_body.c --tu src/ov_SC01_000/ov_SC01_000_jr_8015AE2C.c \
--func func_8015AE2C [--out fixed.c]
"""
import argparse
import re
import sys
# a col-0 (file-scope) `extern ...;` on one line. Leading whitespace => block scope, which is what we
# emit and never need to re-place.
FILE_EXTERN_RE = re.compile(r'^extern\b[^;{}\n]*;', re.M)
DATA_SYM_RE = re.compile(r'\bD_[0-9A-Fa-f]{6,8}\b')
def _file_scope_data_syms(text):
"""Every D_ symbol declared by a col-0 `extern` in `text`. The symbol a decl DECLARES is its first
D_ token — true for both `extern u8 D_x[];` and the fn-ptr-array form `extern void (*D_x[])(void *);`
(the form reconcile_decls' regex cannot parse, which is why that tool silently skipped this class)."""
out = set()
for m in FILE_EXTERN_RE.finditer(text):
d = DATA_SYM_RE.search(m.group(0))
if d:
out.add(d.group(0))
return out
def _body_open_brace(body, func):
"""Index of the newline ending the line that opens `func`'s body — i.e. where block-scope decls go.
Handles both the ANSI form and the K&R form (mandatory whenever a zero-arg engine_core.h thunk calls
the function), whose param decls sit between the signature and the `{`."""
sig = re.search(rf'^[^\n]*\b{re.escape(func)}\s*\(', body, re.M)
if not sig:
return None
brace = re.compile(r'^\s*\{\s*$', re.M).search(body, sig.end())
return brace.end() if brace else None
def fix(body, tu_text, insert_pos, func):
"""Demote the body's file-scope DATA externs that the TU does not already declare at file scope
above `insert_pos`. Returns (new_body, [moved symbols]). A no-op (body unchanged) when nothing
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])
demote = [] # (line_text, sym)
keep_lines = []
for ln in body.split('\n'):
if FILE_EXTERN_RE.match(ln): # col-0 extern (match => anchored at col 0)
d = DATA_SYM_RE.search(ln)
if d and d.group(0) not in above:
demote.append((ln, d.group(0)))
continue # drop from the file-scope preamble
keep_lines.append(ln)
if not demote:
return body, []
stripped = '\n'.join(keep_lines)
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]
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--body', required=True, help='the remapped body (file-scope externs prepended)')
ap.add_argument('--tu', required=True, help='the target TU (.c) the body is spliced into')
ap.add_argument('--func', required=True)
ap.add_argument('--at', type=int, default=None,
help='char offset of the insertion point (default: the INCLUDE_ASM stub for --func)')
ap.add_argument('--out')
a = ap.parse_args()
body, tu = open(a.body).read(), open(a.tu).read()
pos = a.at
if pos is None:
m = re.search(rf'INCLUDE_ASM\("[^"]*",\s*{re.escape(a.func)}\);', tu)
if not m:
sys.exit(f'no INCLUDE_ASM stub for {a.func} in {a.tu} (pass --at)')
pos = m.start()
new, moved = fix(body, tu, pos, a.func)
print(f'file-scope data syms above the insertion point: {len(_file_scope_data_syms(tu[:pos]))}')
print(f'demoted to block scope: {len(moved)} {moved}')
if a.out:
open(a.out, 'w').write(new)
print(f'wrote {a.out}')
if __name__ == '__main__':
main()