mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
fix(phase-26a): A9a — canon_sig_reconcile sees fn-ptr dispatch tables (cdecl supplement)
The def-side-wall recovery tool, live on the ×134 economic-engine paths (jtbl_family_bank,
family_sweep --reconcile-raw, bank_exemplar, t7_bank, scope_data_externs, family_remap). The
audit's two HIGH findings: its tu_ambient/visible_above classifier regexes and _reconcile_data's
_DATA_EXTERN_RE have a type class `[\w \*]` that cannot hold a `(`, so they are 100% blind to
fn-ptr DATA decls — `extern void (*D_x[])(void);`, the per-overlay jump-table DISPATCH arrays
jtbl_family_bank exists to bank. A fn-ptr symbol thus landed in NO bucket, fell out of `visible`,
and _reconcile_data block-moved the draft's extern into a guaranteed `conflicting types`.
FIX (R33-adjacent, but SUPPLEMENT not wholesale-replace — the proven regex stays byte-identical):
- tu_ambient / visible_above: keep the v3.2 regex classification UNCHANGED, then supplement from
cdecl.tu_scope with ONLY the fn-ptr symbols the regex drops (_fnptr_data). A full delegation was
tried and rejected — cdecl normalizes funcs formatting (void*→void *, param names, and a real
return-ptr hazard u8*→u8) which rippled 728k value-changes into the callee string-compare; the
audit flagged fn-ptr DATA, not funcs, so the blast radius is held to exactly that.
- _reconcile_data: a fn-ptr pre-pass (_FNPTR_DATA_RE) — visible ⇒ STRIP (ambient serves; a
call-through `D_x[i]()` is decl-INDEPENDENT indirect codegen, so NO access-cast: casting a
fn-ptr would mangle it into `((u8*)D_x)[i]()`, the dormant transform reconcile_tu documents);
not visible ⇒ block-move verbatim. Plus the F2 fix (bail on a `(` in the CODE, not a comment).
- R32 coverage assertion: a fn-ptr data extern may never survive the pass.
PROVEN ADDITIVE (regression baseline .run/audit/a9a_*):
- classifier snapshot over 1683 TUs: REGRESSIONS 0, ADDITIONS 69,798 (all data fn-ptr).
- reconcile() OLD-vs-NEW over 434 real drafts against home TUs: 0 regressions.
- src/ UNTOUCHED (a tool change moves no bytes); the whole-binary byte-gate (G3/P9) remains the
sole arbiter — a wrong reconcile fails to bank, never falsely banks.
This commit is contained in:
@@ -50,11 +50,27 @@ Usage:
|
||||
--out .run/t5b2-recon/func_8013B274.c --tu src/ov_SC01_077/ov_SC01_077_a.c \
|
||||
[--sig 'void func_8013B274(s32 a0, s32 a1, void *a2)']
|
||||
"""
|
||||
import re, os, argparse, subprocess
|
||||
import re, os, argparse, subprocess, importlib.util
|
||||
|
||||
EC = 'src/shared/engine_core.h'
|
||||
ET = 'src/shared/engine_types.h'
|
||||
|
||||
|
||||
def _load(mod, rel):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
mod, os.path.join(os.path.dirname(os.path.abspath(__file__)), rel))
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
return m
|
||||
|
||||
|
||||
# THE declaration oracle (Phase 26-A §51g): a total recursive-descent parser of C's declarator
|
||||
# grammar. It classifies fn-ptr / sized-array / multi-declarator decls the tu_ambient/visible_above
|
||||
# regexes were structurally blind to — the class that put fn-ptr DISPATCH TABLES in NO bucket, so
|
||||
# `visible_above` under-reported them and _reconcile_data block-moved the draft's extern into a
|
||||
# guaranteed `conflicting types` (docs/tooling-audit.md, the two HIGH findings on this file).
|
||||
cdecl = _load('cdecl', 'cdecl.py')
|
||||
|
||||
CPP = ['mipsel-linux-gnu-cpp', '-lang-c', '-Iinclude', '-undef', '-Wall', '-fno-builtin',
|
||||
'-Dmips', '-D__GNUC__=2', '-D__OPTIMIZE__', '-Dpsx', '-D_PSYQ', '-D_MIPSEL', '-D_LANGUAGE_C']
|
||||
|
||||
@@ -135,9 +151,25 @@ _AMBIENT_CACHE = {}
|
||||
_VISIBLE_CACHE = {}
|
||||
|
||||
|
||||
def _fnptr_data(tu_path, above=None):
|
||||
"""cdecl SUPPLEMENT: the fn-ptr DATA dispatch tables (`extern void (*D_x[])(void);`) the
|
||||
tu_ambient/visible_above regexes are structurally blind to — their type class `[\\w \\*]`
|
||||
cannot hold a `(`, so these land in NO bucket, `visible_above` under-reports them, and
|
||||
_reconcile_data block-moves the draft's extern into a guaranteed `conflicting types`
|
||||
(docs/tooling-audit.md, the two HIGH findings). Additive: name -> (fnptr-type, is_array),
|
||||
over exactly the symbols the regex dropped."""
|
||||
out = {}
|
||||
for name, d in cdecl.tu_scope(tu_path, above=above).items():
|
||||
if d.storage != 'typedef' and d.kind in ('fnptr', 'fnptr_array'):
|
||||
out[name] = (d.type, bool(d.is_array)) # 'void (*)(void)' / 'void (*[])(void)'
|
||||
return out
|
||||
|
||||
|
||||
def visible_above(tu_path, fn):
|
||||
"""v3: the set of file-scope names (funcs+data) declared ABOVE fn's INCLUDE_ASM stub —
|
||||
what is actually in scope at the splice point. Drives the block-scope-vs-ambient branch."""
|
||||
what is actually in scope at the splice point. Drives the block-scope-vs-ambient branch.
|
||||
v3.3 (Phase 26-A): UNION the cdecl fn-ptr-data names the regex is blind to, so a dispatch
|
||||
table can no longer fall out of `visible` and trigger a wrong block-scope move."""
|
||||
key = (tu_path, fn)
|
||||
if key in _VISIBLE_CACHE:
|
||||
return _VISIBLE_CACHE[key]
|
||||
@@ -162,13 +194,16 @@ def visible_above(tu_path, fn):
|
||||
names.add(nm.group(1))
|
||||
finally:
|
||||
os.path.exists(tmp) and os.remove(tmp)
|
||||
names |= set(_fnptr_data(tu_path, above=fn)) # v3.3: + the fn-ptr dispatch tables
|
||||
_VISIBLE_CACHE[key] = names
|
||||
return names
|
||||
|
||||
|
||||
def tu_ambient(tu_path):
|
||||
"""The TU's REAL file-scope namespace: {'funcs': name->decl, 'data': name->(type,is_arr),
|
||||
'typedefs': name->normalized def}. Function DEFINITIONS count as decls (sig authority)."""
|
||||
'typedefs': name->normalized def}. Function DEFINITIONS count as decls (sig authority).
|
||||
v3.3 (Phase 26-A): the fn-ptr DATA symbols the regex drops into no bucket are supplemented
|
||||
from cdecl — additive, so every symbol the proven regex classified is byte-identical."""
|
||||
if tu_path in _AMBIENT_CACHE:
|
||||
return _AMBIENT_CACHE[tu_path]
|
||||
funcs, data, typedefs = {}, {}, {}
|
||||
@@ -195,6 +230,9 @@ def tu_ambient(tu_path):
|
||||
if dm:
|
||||
data[dm.group(2)] = (base + (' ' + dm.group(1) if dm.group(1) else ''),
|
||||
dm.group(3) is not None)
|
||||
for name, dt in _fnptr_data(tu_path).items(): # v3.3: + the fn-ptr dispatch tables
|
||||
if name not in data and name not in funcs and name not in typedefs:
|
||||
data[name] = dt
|
||||
_AMBIENT_CACHE[tu_path] = dict(funcs=funcs, data=data, typedefs=typedefs)
|
||||
return _AMBIENT_CACHE[tu_path]
|
||||
|
||||
@@ -320,6 +358,10 @@ def _reconcile_callees(draft, ambient_funcs, fn, visible):
|
||||
plist = ', '.join(ptypes) if ptypes else ''
|
||||
cast = f'(({dret}(*)({plist})){name})'
|
||||
draft = _sub_skip_decls(draft, r'\b' + re.escape(name) + r'\s*\(', cast + '(')
|
||||
# R32 coverage: every fn-ptr data extern must have been consumed (stripped or block-moved) by
|
||||
# visit_fnptr above — never silently emitted verbatim into a TU that declares it differently.
|
||||
assert not _FNPTR_DATA_RE.search(draft), \
|
||||
'canon_sig_reconcile: a fn-ptr data extern survived _reconcile_data (silent skip = a defect)'
|
||||
return draft, moved
|
||||
|
||||
|
||||
@@ -339,6 +381,13 @@ _DATA_EXTERN_RE = re.compile(r'^[ \t]*extern\s+([^;\n()]*?)\s*\b'
|
||||
r'([A-Za-z_]\w*(?:\s*\[[^\]]*\])?(?:\s*,\s*\**[A-Za-z_]\w*(?:\s*\[[^\]]*\])?)*)'
|
||||
r'\s*;[ \t]*(?:/\*[^\n]*\*/)?[ \t]*\n', re.M)
|
||||
|
||||
# fn-ptr DATA externs (dispatch tables): `extern void (*D_x[])(void);` / `extern s32 (*D_x)(s32);`.
|
||||
# _DATA_EXTERN_RE's type class `[^;\n()]*?` forbids the `(`, so it is 100% blind to these (F1, 436
|
||||
# draft lines) — the class that IS the per-overlay jump-table dispatch arrays jtbl_family_bank banks.
|
||||
_FNPTR_DATA_RE = re.compile(
|
||||
r'^[ \t]*extern\s+[^;\n]*?\(\s*\*\s*([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\)\s*\([^;]*\)'
|
||||
r'\s*;[ \t]*(?:/\*[^\n]*\*/)?[ \t]*\n', re.M)
|
||||
|
||||
|
||||
def _reconcile_data(draft, ambient_data, visible):
|
||||
"""v3: per data-extern name —
|
||||
@@ -351,9 +400,24 @@ def _reconcile_data(draft, ambient_data, visible):
|
||||
to the reconcile_decls/§33 TU-retype tier, not this transform)."""
|
||||
casts, moved = [], []
|
||||
|
||||
# v3.3: fn-ptr DATA externs (dispatch tables) FIRST — _DATA_EXTERN_RE below cannot see them.
|
||||
# visible -> STRIP (the ambient/engine_core.h decl serves; a call-through `D_x[i]()` is
|
||||
# decl-INDEPENDENT indirect codegen, so NO access-cast — casting a fn-ptr would mangle
|
||||
# `D_x[i]()` into `((u8*)D_x)[i]()`, the dormant transform the reconcile_decls audit named).
|
||||
# not visible -> block-move the draft's decl verbatim (its type is exact + expires at the `}`).
|
||||
def visit_fnptr(m):
|
||||
name = m.group(1)
|
||||
if name not in visible:
|
||||
moved.append(_norm(m.group(0).strip()))
|
||||
return ''
|
||||
|
||||
draft = _FNPTR_DATA_RE.sub(visit_fnptr, draft)
|
||||
|
||||
def visit(m):
|
||||
base = m.group(1).strip()
|
||||
if not base or '(' in m.group(0):
|
||||
# v3.3: bail on a `(` in the CODE, not in a trailing comment — F2: an ordinary scalar extern
|
||||
# was silently abandoned because a drafter wrote '(' in prose (87 corpus lines).
|
||||
if not base or '(' in m.group(0).split('/*')[0]:
|
||||
return m.group(0)
|
||||
kept = []
|
||||
for piece in m.group(2).split(','):
|
||||
|
||||
Reference in New Issue
Block a user