mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
e903713b71
Final S50 state: 307 instances banked, stubs 12,468 -> 12,161, fleet 95.3% instr / 90.0% distinct / 96.65% fn-count. R22 clean rebuild 4x, check-all 213/213 every time. - tools/aprop_autodraft.py + tools/draft_prechecks.py: seed body + symbol_map + a MINIMAL synthesized preamble. The seed's decl layer never travels — that layer is family_sweep's dominant failure (331 of 458 S49 verdicts). 256 banked at zero agent tokens, against the ~20M the same work would have cost as a wave. - Macro seeds (567 of 1196 members, all 3737 de-macroize) take the DEFINITION only; the block stays the decl source. Pasting it whole measured 28% vs inline's 68% — func_8016AB6C's macro is 1,891 lines of which 108 are the function. - IMM is a second engine, not a wall: T2a's imm_map_tier1 resolves a per-location LITERAL like symbol_map resolves a per-location SYMBOL. 131 of 275 IMM members resolve. - draft_prechecks negative-controlled against ALL 205 banked drafts: zero false positives, catches 39 of 67 known failures. That control found two bugs in the checks themselves — C89 `f()` declares UNSPECIFIED parameters (not zero), and a member's own definition read as a call to itself. Conservative by design: a pre-check that discards good drafts is worse than one that lets a few builds fail. - The A-prop pool is now priced exactly: PURE 437/37,376 ins, IMM 275/8,849, STRUCT 238/4,259. - Cookbook §171a; SETUP rows; CURRENT_PHASE S50 FINAL checkpoint.
119 lines
5.0 KiB
Python
119 lines
5.0 KiB
Python
"""Static pre-checks that turn a BUILD-time draft failure into a GENERATION-time skip.
|
|
|
|
Every class here was measured as a real failure in the S50 autodraft runs, and every one is
|
|
decidable without compiling anything. That is the whole point: a build spent to learn something a
|
|
regex knows is a build wasted, and at ~1 min/group over ~100 groups the waste is the run.
|
|
|
|
1. ARITY DISAGREEMENT — `too few arguments to function 'func_80146C3C'`.
|
|
The destination TU already declares a callee with a different parameter count than the seed's
|
|
body passes. Neither emitting the seed's decl (conflicting types) nor relying on the
|
|
destination's (too few arguments) can work; the member needs a real edit.
|
|
|
|
2. UNDEFINED DATA — `undefined reference to 'D_801E9C44'`.
|
|
The draft references a symbol nothing in the DESTINATION binary defines. The seed's own data
|
|
symbol survived into the draft because it sits at no relocation slot symbol_map covers.
|
|
"""
|
|
import glob
|
|
import os
|
|
import re
|
|
|
|
|
|
def _decl_arity(text, sym):
|
|
"""Parameter count of the first declaration/definition of `sym` in `text`, or None."""
|
|
m = re.search(rf'^[ \t]*(?:extern\s+)?[A-Za-z_][\w \*]*\b{re.escape(sym)}\s*\(([^;{{)]*)\)',
|
|
text, re.M)
|
|
if not m:
|
|
return None
|
|
args = m.group(1).strip()
|
|
if args == '':
|
|
return None # C89: `f()` declares UNSPECIFIED parameters, not zero — never a conflict
|
|
if args == 'void':
|
|
return 0
|
|
return len([a for a in args.split(',') if a.strip()])
|
|
|
|
|
|
def _call_arity(body, sym):
|
|
"""Argument count at the first CALL of `sym` in `body`, or None. Depth-aware so a nested call
|
|
or a comma inside a cast does not miscount."""
|
|
m = re.search(rf'\b{re.escape(sym)}\s*\(', body)
|
|
if not m:
|
|
return None
|
|
depth, args, cur = 0, [], ''
|
|
for ch in body[m.end() - 1:]:
|
|
if ch == '(':
|
|
depth += 1
|
|
if depth == 1:
|
|
continue
|
|
elif ch == ')':
|
|
depth -= 1
|
|
if depth == 0:
|
|
args.append(cur)
|
|
break
|
|
if depth == 1 and ch == ',':
|
|
args.append(cur)
|
|
cur = ''
|
|
continue
|
|
if depth >= 1:
|
|
cur += ch
|
|
return len([a for a in args if a.strip()])
|
|
|
|
|
|
def arity_conflicts(body, dest_text, syms, self_name=None):
|
|
"""Symbols the body CALLS with an argument count the destination's own declaration refuses."""
|
|
bad = []
|
|
for s in syms:
|
|
if not s.startswith('func_') or (self_name and s.lower() == self_name.lower()):
|
|
continue # the member's own definition is not a call to itself
|
|
d, c = _decl_arity(dest_text, s), _call_arity(body, s)
|
|
# CONSERVATIVE by measurement: flag only when the destination declares NAMED parameters and
|
|
# the body passes fewer. Every true positive had that shape (`dest decl 1 vs call 0`) and
|
|
# every false positive the other one (`dest decl 0 vs call 1`) — where the `0` comes from a
|
|
# `(void)` my parser reads out of a declaration it should not have matched. A pre-check that
|
|
# throws away good drafts is worse than one that lets a few builds fail.
|
|
if d is not None and c is not None and d > c:
|
|
bad.append(f"{s}(dest decl {d} vs call {c})")
|
|
return bad
|
|
|
|
|
|
_DEF_CACHE = {}
|
|
|
|
|
|
def defined_in_binary(binary, sym):
|
|
"""True if `sym` is defined anywhere in the destination binary — its asm (a dlabel/glabel, or
|
|
a symbol file entry) or its C. Cached per binary: the asm scan is the expensive part."""
|
|
key = binary
|
|
if key not in _DEF_CACHE:
|
|
defs = set()
|
|
for p in glob.glob(f"asm/{binary}/**/*.s", recursive=True):
|
|
for m in re.finditer(r'^(?:dlabel|glabel|jlabel)\s+([A-Za-z_]\w*)', open(p).read(), re.M):
|
|
defs.add(m.group(1))
|
|
for p in glob.glob(f"config/symbols.{binary}*.txt"):
|
|
for m in re.finditer(r'^([A-Za-z_]\w*)\s*=', open(p).read(), re.M):
|
|
defs.add(m.group(1))
|
|
_DEF_CACHE[key] = defs
|
|
return sym in _DEF_CACHE[key]
|
|
|
|
|
|
def undefined_data(binary, dest_text, syms, self_name=None, body=""):
|
|
"""Referenced symbols nothing in the destination binary defines, and the destination TU does
|
|
not define locally either.
|
|
|
|
A symbol the DRAFT ITSELF defines is not undefined — its own function name above all, which
|
|
has no `glabel` left in asm precisely BECAUSE it is now C. Skipping that was 38 of 38 false
|
|
positives in the negative control."""
|
|
out = []
|
|
for s in syms:
|
|
if self_name and s.lower() == self_name.lower():
|
|
continue
|
|
if re.search(rf'^[^\n=;]*\b{re.escape(s)}\b[^\n;]*\)\s*\{{', body, re.M) or \
|
|
re.search(rf'^[ \t]*(?:const\s+|static\s+)*[A-Za-z_][\w \*]*\b{re.escape(s)}\b'
|
|
rf'(?:\s*\[[^\]]*\])?\s*=', body, re.M):
|
|
continue
|
|
if defined_in_binary(binary, s):
|
|
continue
|
|
if re.search(rf'^[ \t]*(?:const\s+|static\s+)*[A-Za-z_][\w \*]*\b{re.escape(s)}\b[^\n;]*[=;]',
|
|
dest_text, re.M):
|
|
continue
|
|
out.append(s)
|
|
return out
|