Files
BFM-decomp/tools/reconcile_tu.py
T
Drew T 4aae5e7589 fix(phase-26a): A3d — retire the fleet-majority oracle: it was WRONG for the TU 16% of the time, on both banking paths
R33 applied to the worst finding in the audit: this oracle was not fixed, it was RETIRED.

    reconcile_decls asks "what does the FLEET call this symbol?"
    C asks           "what does THIS TRANSLATION UNIT declare?"

The engine is loosely typed -- the same address is legitimately declared with incompatible types in
different overlays -- so a single fleet-wide answer is WRONG FOR SOME TU BY CONSTRUCTION. And it is
worse than a silent skip: it writes an ACTIVELY WRONG declaration into the draft, which then
collides with the very TU it was meant to conform to.

MEASURED across ov_SC01_077's 12 TUs, against what cpp says each TU really declares:

    the fleet oracle AGREES with the TU ................ 2883
    the fleet oracle CONFLICTS with it (cc1 REJECTS)  ..  548    <- 16%
    the TU declares it, the oracle has NO answer ......   357

and it was LIVE ON BOTH BANKING PATHS:
  * gate_stage      -- rewrote 60 of 196 drafts in the current batch
  * jtbl_family_bank -- EVERY SIBLING of the ×134 family sweep, the project's economic engine.
    A poisoned decl means that sibling silently does not bank, and the loss is invisible: the sweep
    simply reports a smaller number. The irony is exact -- that function's own docstring already
    knew the conflicting symbols are PER-OVERLAY, which is precisely why a FLEET oracle could never
    have been right.

reconcile_tu.py (written in Phase 26 but NEVER WIRED) now supersedes it, rebuilt on cdecl:
  * ask cpp what the TU declares (macro-injected DEFINE_func_* externs included -- a raw scan
    cannot see them, §8c / §51g LAW 7);
  * ask cc1 whether the draft's decl can coexist (cdecl.compatible, validated against the real
    gcc-2.7.2 front end on 1,485 live pairs -- NOT the C standard, NOT modern gcc; §51g LAW 9);
  * NOT declared -> leave the draft alone (its extern types are load-bearing: %lo-folding, access
    width, alignment); compatible -> nothing; CONFLICTING -> the TU wins + cast at every USE so the
    draft's intended access survives byte-for-byte;
  * derives WHICH TU from corpus.stubs() rather than a hand-passed --src-file (§51g LAW 10).
  * handles the fn-ptr kind NATIVELY -- which is why it supersedes rather than patches: teaching
    reconcile_decls' parser to see `extern void (*D_x[])(void);` would have ARMED its fn-ptr-blind
    data_access_subs to rewrite a call-through `D_x[i]()` into `((u8 *)D_x)[i]()`. Fixing the regex
    would have detonated a dormant bug.

AND THE NULL RESULT, AGAIN, REPORTED AS SUCH (P9/R14): on the 196 never-banked historical drafts the
new oracle banks EXACTLY AS MANY AS THE OLD ONE -- zero. That tail fails on CODEGEN, not on decl
plumbing. The two disagree on 45 of 196 drafts and the outcome does not move. This is a CORRECTNESS
fix (548 wrong declarations removed from two live pipelines, protecting all FUTURE drafts and every
future family sweep), not a banking win, and it is not being sold as one. Three nulls in one session.

reconcile_decls.py is kept as EVIDENCE, marked RETIRED, with no live caller.

  R22 clean-fleet: make clean + extract-all + check-all -> 136 passed, 0 failed of 136
  src/ untouched (0 changes)   reconcile_tu: 0 coverage defects over 196 drafts
  NOTE: the family-sweep path gets its real exercise at Task 8 -- watch the per-sibling bank rate.
2026-07-14 12:53:14 -06:00

191 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""reconcile_tu.py — conform a draft's DATA declarations to what the TARGET TU can actually SEE.
THE SUCCESSOR TO reconcile_decls.py, and the reason is structural, not a typo.
reconcile_decls asks "what does the FLEET call this symbol?"
C asks "what does THIS TRANSLATION UNIT declare?"
Those are different questions, and only the second one has an answer. This engine is loosely typed:
the same address is legitimately declared with incompatible types in different overlays, so a single
fleet-wide answer is **wrong for some TU by construction** — and it is worse than a silent skip,
because it hands back an ACTIVELY WRONG declaration that then collides with the very TU it was
supposed to conform to. Measured on ov_SC01_077's 12 TUs (Phase 26-A):
the fleet oracle AGREES with the TU's own declaration ...... 2883
the fleet oracle CONFLICTS with it (cc1 REJECTS the result) . 548 <- 16%
the TU declares it and the fleet oracle has NO answer ....... 357
and it is live: it rewrote 60 of 196 drafts in the current batch.
WHAT THIS DOES INSTEAD
======================
* Ask cpp what the TU actually declares (`cdecl.tu_scope` — so macro-injected `DEFINE_func_*`
externs are visible; a raw text scan cannot see them, §8c / cookbook §51g LAW 7).
* Ask cc1 whether the draft's declaration can coexist with it (`cdecl.compatible`, validated
against the real gcc-2.7.2 front end on 1,485 live pairs — NOT against the C standard and NOT
against modern gcc, which give different answers; §51g LAW 9).
* NO visible declaration -> no conflict is possible -> LEAVE THE DRAFT ALONE. Its extern types are
load-bearing (%lo-folding, access width, alignment all key off the declared type), and
scope_data_externs (§8d) will demote it to block scope so it establishes no global that the TU's
own later block-scope externs would have to agree with.
* A visible, COMPATIBLE declaration -> nothing to do.
* A visible, CONFLICTING declaration -> **the TU wins** (it is the environment; we are the guest).
Rewrite the draft's decl to the TU's, and CAST AT EVERY USE so the access the draft intended is
preserved exactly. gcc-2.7.2 folds a compile-time cast of a known symbol, so the emitted bytes
are unchanged — and the whole-binary byte-gate (G3/P9) remains the sole arbiter either way.
WHICH TU? DERIVED, NEVER HAND-PASSED (§51g LAW 10)
==================================================
`--src-file` was an OPTIONAL flag defaulting to `src/<ov>/<ov>.c`. ov_SC01_077 has **263 open stubs
across 12 TUs and only 13 of them are in the main .c**, so 95% of drafts were being reconciled
against a translation unit that would never compile them. The INCLUDE_ASM line is self-describing;
`corpus.stubs()` reads it. Ask, don't assume.
Usage:
tools/reconcile_tu.py --overlay ov_SC01_077 --in <drafts> --out <dir> [--src-file <tu.c>]
from reconcile_tu import fix; body, notes = fix(body, tu_path, fn)
"""
import argparse
import glob
import importlib.util
import os
import re
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _load(mod, rel):
spec = importlib.util.spec_from_file_location(mod, os.path.join(REPO, rel))
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
cdecl = _load('cdecl', 'tools/cdecl.py')
corpus = _load('corpus', 'tools/corpus.py')
def tu_for(overlay, fn, override=None):
"""The TU this draft is spliced into — DERIVED from the corpus (§51g LAW 10)."""
if override:
return override
try:
st = corpus.stubs(overlay).get(int(fn[5:], 16))
if st:
return os.path.join(REPO, st.path)
except Exception:
pass
return os.path.join(REPO, f'src/{overlay}/{overlay}.c')
# ---- the byte-neutral access casts ---------------------------------------------------------------
def _elem(d):
"""The element/base type the DRAFT believes the symbol has."""
return (d.base + (' *' if d.kind == 'ptr' else '')).strip()
def _cast_sub(d, tu):
"""(regex, repl) reproducing the DRAFT's intended access to the symbol under the TU's storage.
gcc folds a compile-time cast of a known symbol, so the emitted access is unchanged.
NOTE the fn-ptr arms. reconcile_decls' `data_access_subs` has no fn-ptr kind, so the moment its
parser is taught to SEE `extern void (*D_x[])(void);` it would happily rewrite a call-through
`D_x[i]()` into `((u8 *)D_x)[i]()` — a dormant transform that fixing the parser would ARM. This
tool handles the kind natively, which is why it supersedes that one rather than patching it."""
sym = d.name
rx = re.compile(rf'(&?)\b{re.escape(sym)}\b(\s*\[)?')
c_arr = tu.kind in ('array', 'fnptr_array')
e = _elem(d)
if d.kind == 'fnptr': # draft: void (*D_x)(P)
acc = f'(({e} (*)({", ".join(d.params or [])})){sym})'
elif d.kind == 'fnptr_array': # draft: void (*D_x[])(P)
acc = f'(({e} (**)({", ".join(d.params or [])})){"" if c_arr else "&"}{sym})'
elif d.kind == 'array': # draft: E D_x[]
acc = f'(({e} *){"" if c_arr else "&"}{sym})'
else: # scalar / struct / pointer
acc = f'(*({e} *)&{sym})' if not c_arr else f'(*({e} *){sym})'
return rx, lambda m: f'{m.group(1)}{acc}{m.group(2) or ""}'
def fix(body, tu_path, fn):
"""Conform the draft's DATA decls to the TU. Returns (new_body, notes)."""
full = cdecl.tu_scope(tu_path) # CONFLICT domain: a decl BELOW still conflicts
above = set(cdecl.tu_scope(tu_path, above=fn)) # ORDER, for cc1's no-prototype rule
plan, notes = {}, []
for st in cdecl.split_statements(body):
try:
ds = cdecl.parse(st.text)
except cdecl.CDeclError as e:
notes.append(f'!! UNPARSED (a coverage defect, not a no-op): {st.text[:60]} -> {e}')
continue
for d in ds:
if d.is_definition or d.kind == 'func' or d.storage == 'typedef':
continue
tu = full.get(d.name)
if tu is None:
continue # not declared here -> no conflict is possible
a, b = (tu, d) if d.name in above else (d, tu)
if cdecl.compatible(a, b):
continue # cc1 accepts both -> leave the draft's types
plan[d.name] = (tu, d, st)
notes.append(f'{d.name}: draft {d.type!r} vs TU {tu.type!r} -> TU wins + cast at use')
if not plan:
return body, notes
out, done = [], set()
for line in body.split('\n'):
hit = next((s for s, (_t, _d, st) in plan.items()
if st.text.split('\n')[0].strip() == line.strip() and s not in done), None)
if hit: # the draft's decl line -> the TU's declaration
done.add(hit)
indent = re.match(r'^[ \t]*', line).group(0)
out.append(indent + plan[hit][0].declaration())
continue
if re.match(r'\s*(extern|typedef)\b', line):
out.append(line) # never cast inside a declaration line
continue
for _s, (tu, d, _st) in plan.items(): # cast every USE to the draft's intended view
rx, rep = _cast_sub(d, tu)
line = rx.sub(rep, line)
out.append(line)
return '\n'.join(out), notes
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--overlay', default='ov_SC01_077')
ap.add_argument('--in', dest='indir', required=True)
ap.add_argument('--out', dest='outdir', required=True)
ap.add_argument('--src-file', dest='src_file', default=None,
help='override the derived TU (normally unnecessary — it is derived per draft)')
ap.add_argument('--strict', action='store_true', help='exit non-zero on a coverage defect')
a = ap.parse_args()
override = a.src_file and os.path.join(REPO, a.src_file)
os.makedirs(os.path.join(REPO, a.outdir), exist_ok=True)
drafts = touched = syms = defects = 0
for p in sorted(glob.glob(os.path.join(REPO, a.indir, '*.c'))):
fn = os.path.basename(p)[:-2]
new, notes = fix(open(p).read(), tu_for(a.overlay, fn, override), fn)
open(os.path.join(REPO, a.outdir, os.path.basename(p)), 'w').write(new)
drafts += 1
n = sum(1 for x in notes if not x.startswith('!!'))
defects += sum(1 for x in notes if x.startswith('!!'))
if n:
touched += 1
syms += n
print(f'drafts: {drafts}; reconciled: {touched} draft(s), {syms} data symbol(s); '
f'coverage defects: {defects}')
if a.strict and defects:
sys.exit(1)
if __name__ == '__main__':
main()