From abbedcee8b458d0eebc96ff2a99675af1d3f47a2 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:59:56 -0600 Subject: [PATCH] =?UTF-8?q?fix(phase-26a):=20A9a=20=E2=80=94=20canon=5Fsig?= =?UTF-8?q?=5Freconcile=20sees=20fn-ptr=20dispatch=20tables=20(cdecl=20sup?= =?UTF-8?q?plement)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/canon_sig_reconcile.py | 72 ++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/tools/canon_sig_reconcile.py b/tools/canon_sig_reconcile.py index 06935515a..1374f7a5a 100644 --- a/tools/canon_sig_reconcile.py +++ b/tools/canon_sig_reconcile.py @@ -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(','):