mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
2c4637450b
Wave AA: 69 cards / 6,539 ins / 8 gate groups, 33 siblings, snapshotted 69/69. All 69 passed the S46 validity gate pre-draft — the first wave drawn through it since it was wired into build_wave_atlas, and the honest population negative control for that gate (zero false refusals; a poisoned card is refused OUT-OF-RANGE). decl_prior now contributes 165 DATA rows to the cards (0 on every prior wave, the §204-E fix). The 49 uncopyable pointer-to-function spellings are marked `fleet_note` rather than dropped: the consensus is real information, the rendering is not valid C. NOT fixed at the source — gate_main.typesig is the banking driver's conflict oracle, so the safe fix is a display-only pretty-printer in decl_prior, and no index rebuild is needed since the tuple is what the index stores.
218 lines
11 KiB
Python
218 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""decl_prior.py — the FLEET's consensus declaration for every symbol, as a wave-card field.
|
|
|
|
WHY THIS EXISTS (P31 S54, cookbook §196). Wave-V token accounting: drafting is 88.3% of a wave's
|
|
output tokens, repair 8.4%, reconcile 3.2% -- so the leverage is in what a drafter has to GUESS
|
|
before its first compile, not in the plumbing afterwards. Two of those guesses are answered
|
|
somewhere in the tree and nowhere on the card:
|
|
|
|
* CALLEE ARITY AND RETURN TYPE. §195-A proved there is no positive tell in the asm: an argument
|
|
that dies at the call is allocated straight into $aN, so its only def is a plain load and every
|
|
use reads $aN -- the walk cannot distinguish it from a scratch register, in EITHER direction.
|
|
The prescribed procedure is a two-arity A/B, i.e. an extra compile per ambiguous callee. But
|
|
some other TU in the fleet has usually already banked a function that calls it, and that
|
|
declaration is evidence.
|
|
* GLOBAL TYPE. The Reconcile phase and every CONFLICTING-EXTERN drop exist because N drafters
|
|
independently invent a spelling for one D_ symbol. The fleet has usually settled it already.
|
|
|
|
This is the same shape of fix as §193-A (`seed_ref`) and §194-E (`tu_ref`): the answer was in the
|
|
tree, the card just never carried it. It is fully deterministic -- no agent, no tokens.
|
|
|
|
THE EVIDENCE HIERARCHY, strongest first (each row is emitted with its count so the agent can weigh):
|
|
1. DEF -- a banked DEFINITION's own signature. The function exists; this is its real shape.
|
|
2. TU -- the destination TU's own declaration. Authoritative for THIS draft by wave law 2
|
|
regardless of what the fleet says, because that is the file it must compile in.
|
|
3. FLEET -- the modal `extern` spelling across all other TUs, with its count and its rivals.
|
|
|
|
Usage:
|
|
decl_prior.py --build # (re)build .run/decl_prior.json from src/**/*.c
|
|
decl_prior.py --query func_8012BEE8 # what the fleet says about one symbol
|
|
decl_prior.py --for-asm <file.s> [--tu <path>] # every symbol a target references
|
|
"""
|
|
import argparse, collections, glob, json, os, re, sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import cdecl
|
|
import gate_main as gm
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
INDEX = os.path.join(REPO, '.run', 'decl_prior.json')
|
|
|
|
# Symbols reach a .s in exactly three shapes (§194-E: an uppercase-word regex reads the comment
|
|
# column's hex WORDS as symbol names -- 34 "symbols" for one function, 31 of them hex).
|
|
# §204-E: the `\b` must bind to the `jal` arm ONLY. Prefixing the whole alternation demands a
|
|
# word boundary immediately before `%`, and in a .s that position always follows a space -- so
|
|
# the %hi/%lo arm could never match and the card's promised GLOBAL-TYPE row was 0 of 1,210 across
|
|
# four waves. NC on the 75 wave-Z targets: jal 306 -> 306 (zero regressions), data 0 -> 299.
|
|
_ASM_SYM = re.compile(r'(?:\bjal\s+(\w+)|%[hl][io]\(([\w+]+)\))')
|
|
_DEF = re.compile(r'^[A-Za-z_][\w \t\*]*?\b(\w+)\s*\(([^;{]*)\)\s*\{', re.M)
|
|
|
|
|
|
def asm_symbols(path):
|
|
"""{symbol} referenced by a target .s, from its jal targets and %hi/%lo operands."""
|
|
try:
|
|
raw = _ASM_SYM.findall(open(path, errors='replace').read())
|
|
except OSError:
|
|
return set()
|
|
return {(a or b).split('+')[0] for a, b in raw if (a or b)}
|
|
|
|
|
|
OVERLAY_WINDOW = 0x80170000 # >= this, a func_ADDR name is per-overlay and NOT fleet-unique
|
|
|
|
|
|
def _binary_of(path):
|
|
"""src/<binary>/<file>.c -> <binary>; src/800.c and friends -> 'main'."""
|
|
rel = os.path.relpath(path, os.path.join(REPO, 'src'))
|
|
parts = rel.split(os.sep)
|
|
return parts[0] if len(parts) > 1 else 'main'
|
|
|
|
|
|
def _is_overlay_window(sym):
|
|
"""A func_ADDR / D_ADDR symbol in the overlay load window is a DIFFERENT object per overlay."""
|
|
m = re.match(r'(?:func|D)_([0-9A-Fa-f]{8})$', sym)
|
|
return bool(m) and int(m.group(1), 16) >= OVERLAY_WINDOW
|
|
|
|
|
|
def build(verbose=True):
|
|
"""symbol -> {'def': {binary: [sig, n]}, 'ext': [[sig, n], ...]}.
|
|
|
|
DEFS ARE KEYED BY BINARY (P31 S55, cookbook §201 — §150-B applied to this index). Overlay
|
|
functions are named by VRAM address and 134 overlays load at the same window, so a bare
|
|
`defs[name]` counter mixes N UNRELATED functions: measured over the tree, 3,911 of 9,861
|
|
symbols with a definition are defined in more than one binary, 1,219 of those disagree on
|
|
ARITY, and 818 of the disagreements are a top-two TIE that `most_common` broke by sorted-file
|
|
order — i.e. the lowest-numbered overlay silently won. On wave Y's five binaries the card
|
|
printed 65 DEF rows for overlay-window symbols and 26 of them (40%) were another overlay's
|
|
function. Byte-proven cost: applying one such row's arity to `func_8017E83C` took it from
|
|
MATCH (114 ins) to 113 ins / 83 mismatched."""
|
|
defs = collections.defaultdict(lambda: collections.defaultdict(collections.Counter))
|
|
exts = collections.defaultdict(collections.Counter)
|
|
files = sorted(glob.glob(os.path.join(REPO, 'src', '**', '*.c'), recursive=True))
|
|
for p in files:
|
|
b = _binary_of(p)
|
|
txt = cdecl._mask(open(p, errors='replace').read()) # R33: the one masking oracle
|
|
for d in gm.DECL.findall(txt):
|
|
s = gm.sym_of(d)
|
|
if s:
|
|
exts[s][str(gm.norm_sig(gm.typesig(d)))] += 1
|
|
for m in _DEF.finditer(txt):
|
|
name, params = m.group(1), m.group(2)
|
|
if name in ('if', 'for', 'while', 'switch', 'return', 'sizeof'):
|
|
continue
|
|
ret = txt[max(0, m.start()):m.start(1)].strip()
|
|
defs[name][b][str(gm.norm_sig(gm.typesig('%s %s(%s)' % (ret, name, params))))] += 1
|
|
out = {}
|
|
for s in set(defs) | set(exts):
|
|
row = {}
|
|
if defs.get(s):
|
|
row['def'] = {b: list(c.most_common(1)[0]) for b, c in defs[s].items()}
|
|
if exts.get(s):
|
|
row['ext'] = [[sig, n] for sig, n in exts[s].most_common(3)]
|
|
out[s] = row
|
|
os.makedirs(os.path.dirname(INDEX), exist_ok=True)
|
|
json.dump({'symbols': out, 'files': len(files)}, open(INDEX, 'w'))
|
|
if verbose:
|
|
print(f"decl_prior: {len(files)} files -> {len(out):,} symbols "
|
|
f"({sum(1 for r in out.values() if 'def' in r):,} with a banked DEFINITION) -> {INDEX}")
|
|
return out
|
|
|
|
|
|
def load():
|
|
if not os.path.isfile(INDEX):
|
|
return build(verbose=False)
|
|
return json.load(open(INDEX))['symbols']
|
|
|
|
|
|
def tu_decls(tu_path):
|
|
"""symbol -> sig for the destination TU's OWN declarations (wave law 2: authoritative here)."""
|
|
out = {}
|
|
try:
|
|
txt = cdecl._mask(open(tu_path, errors='replace').read())
|
|
except (OSError, TypeError):
|
|
return out
|
|
for d in gm.DECL.findall(txt):
|
|
s = gm.sym_of(d)
|
|
if s:
|
|
out[s] = str(gm.norm_sig(gm.typesig(d)))
|
|
return out
|
|
|
|
|
|
def _uncopyable(sig):
|
|
"""True when a rendered signature cannot be pasted into C as-is (pointer-to-function shapes)."""
|
|
t = str(sig)
|
|
return '(*' in t or '* void' in t or '*,' in t
|
|
|
|
|
|
def for_asm(asm_path, tu_path=None, idx=None, limit=14, binary=None):
|
|
"""[{sym, tu, def, fleet, rivals}] for the symbols a target references. Rows the destination TU
|
|
already declares are marked `tu` and need no fleet evidence -- law 2 settles them.
|
|
|
|
`binary` is the TARGET's binary and is required for a trustworthy `def` row on overlay-window
|
|
symbols (§201); without it, such rows are withheld with a `def_absent` reason."""
|
|
idx = idx if idx is not None else load()
|
|
mine = tu_decls(tu_path) if tu_path else {}
|
|
rows = []
|
|
for s in sorted(asm_symbols(asm_path)):
|
|
r = idx.get(s)
|
|
if not r and s not in mine:
|
|
continue
|
|
row = {'sym': s}
|
|
if s in mine:
|
|
row['tu'] = mine[s]
|
|
if r and r.get('def'):
|
|
# §201: a DEF row is authoritative only from the TARGET'S OWN binary when the symbol
|
|
# lives in the overlay window — otherwise it is another overlay's function wearing the
|
|
# same address-derived name (§150-B / §164-77: a fleet plurality carries ZERO authority
|
|
# for a per-overlay symbol). Resident/shared/main symbols are fleet-unique and fine.
|
|
d = r['def']
|
|
if binary and binary in d:
|
|
row['def'] = d[binary][0]
|
|
elif not _is_overlay_window(s):
|
|
sig, n = sorted(d.values(), key=lambda x: -x[1])[0]
|
|
row['def'] = sig
|
|
row['def_n'] = n
|
|
elif len(d) == 1 and not binary:
|
|
row['def'] = list(d.values())[0][0]
|
|
else:
|
|
# Say WHY there is no def row rather than silently omitting it.
|
|
row['def_absent'] = ('%d other binar%s define this address; none is yours'
|
|
% (len(d), 'y' if len(d) == 1 else 'ies'))
|
|
if r and r.get('ext'):
|
|
row['fleet'] = r['ext'][0][0]
|
|
row['n'] = r['ext'][0][1]
|
|
# A POINTER-TO-FUNCTION decomposes into a spelling nobody can paste --
|
|
# ('void (*', ('* void',)) is really `void (*D_x)(void *)`. The CONSENSUS is still
|
|
# real information ("19 TUs agree this symbol is a function pointer"), so the row stays;
|
|
# what must not happen is an agent copying that tuple into C and burning iterations on
|
|
# the compile error. §204-E surfaced 49 such rows on wave AA (0 on every earlier wave,
|
|
# because the %hi/%lo arm had never fired) -- the renderer limitation is older than the
|
|
# fix that revealed it. Marked, not dropped, until the index's type formatter is rebuilt.
|
|
if _uncopyable(row['fleet']):
|
|
row['fleet_note'] = ('FUNCTION POINTER — the fleet agrees on the SHAPE but this '
|
|
'rendering is not valid C. Write the spelling yourself from '
|
|
'the .s; do NOT paste this tuple.')
|
|
if len(r['ext']) > 1:
|
|
row['rivals'] = ['%s x%d' % (sig, n) for sig, n in r['ext'][1:]]
|
|
rows.append(row)
|
|
# A row the TU already settles is not news; put the genuinely-informative ones first.
|
|
rows.sort(key=lambda x: (('tu' in x), -(x.get('n') or 0)))
|
|
return rows[:limit]
|
|
|
|
|
|
if __name__ == '__main__':
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('--build', action='store_true')
|
|
ap.add_argument('--query')
|
|
ap.add_argument('--for-asm')
|
|
ap.add_argument('--tu')
|
|
a = ap.parse_args()
|
|
if a.build:
|
|
build()
|
|
elif a.query:
|
|
print(json.dumps(load().get(a.query, {}), indent=1))
|
|
elif a.for_asm:
|
|
for r in for_asm(a.for_asm, a.tu):
|
|
print(json.dumps(r))
|
|
else:
|
|
ap.print_help()
|