Files
Drew T 2c4637450b chore(phase-31): S56 checkpoint — 127 banked, wave AA built and staged (not launched)
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.
2026-08-18 15:44:55 -06:00

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()