Files
BFM-decomp/tools/match_one.py
T
Drew T 876dc7f053 feat(phase-28 T5): resident 21 -> 14 stubs (7 banked, 90.34%) + fix match_one's fake isolation
Ultracode wave: 16 isolated drafters over the resident's non-jtbl stubs (the 5 jtbl deferred —
they need the rodata-island carve, §53). Drafts only; the whole-binary byte-gate arbitrated after.

- BANKED 7/16, byte-gated: func_800CEFD0(77) func_800D0D7C(45) func_800D1B80(22) func_800D1E28(37)
  func_800D1FC8(62) func_800D29F8(172) func_800D2D10(39).
  Resident REAL 122 -> 129, stubs 21 -> 14, byte-ident 124/145 (85.52%) -> 131/145 (90.34%).
  FLEET instr 9017152 -> 9017606 (+454 ins). R22 make clean && extract-all && check-all ->
  140 passed, 0 failed of 140 (the first R22 was killed by a terminal crash and RE-RUN, not assumed).
  Ground truth on 14 agrees 3 ways: source grep, splat-emitted stub .s count, progress.py.

- §52b's LAW, MEASURED AGAIN INDEPENDENTLY: the agents self-reported 11 match_one MATCH; the
  whole-binary gate banked 7 (64%). All 4 blocked MATCHes died on `conflicting types`
  (D_8010EDEC / D_80115110 / func_800D1984 / cdFileLocTable) — the loose-typing def-side wall, NOT
  codegen. gate_stage's recovery banked 0/5 on them. A match_one MATCH is a CANDIDATE (G3/P9).

- FIX — match_one's isolation was FAKE, and its own docstring was the false spec. It promises
  "Fully isolated (own temp dir) so many run in PARALLEL with no shared build -- a real asm-differ
  loop for an agent to iterate against", while `--work` defaulted to the SHARED '.run/match': every
  concurrent caller compiled into the same t.c/t.o. FOUND BY AN AGENT MID-WAVE, the only way it can
  be found — it read another agent's function out of its own scratch ("found another agent's
  func_800D2650 in my t.c") and reported it. Every other agent steered by a loop that could hand it
  someone else's compile: a CONFIDENT WRONG verdict, worse than a crash. Default is now a private
  .run/match/<fn>.<pid>; the default IS the promise. (Some agents had already worked around it by
  passing --work themselves.) The byte-gate was never at risk — it is the sole arbiter — but the
  iteration loop the agents steer by absolutely was.

- The 14 remaining: 5 PLUMBING (loose-typing) + 4 DIFF (genuine codegen: func_800D2650 close=4,
  func_800CFAD0 close=5, func_800D0E30 close=12, func_800D27DC close=48) + 5 jtbl deferred.
  Dossier next (T5b) — the agents' per-function residual analyses are the durable asset (R30).
2026-07-16 01:41:57 -06:00

114 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Per-function matching check: compile ONE function's C standalone (the pinned triple), mask
relocations (jal/j 26-bit + HI16/LO16 immediates, like psyq_identify), and compare to the target
bytes in its splat .s. Fully isolated (own temp dir) so many run in PARALLEL with no shared build --
a real asm-differ loop for an agent to iterate against.
Prints "MATCH (N ins)" on a byte-exact (relocation-masked) match, else a per-instruction diff.
gcc-2.7.2 -O2 compiles each global function independently (no cross-TU inlining of externs), so a
standalone match == the whole-binary byte-gate for that function -- MODULO in-TU declaration
conflicts (def-side loose-typing) and -O0 subsegments; ALWAYS finish on the whole-binary gate (G3/P9).
The masking + objdump live in tools/masked_diff.py (shared with the permuter's MaskedScorer). NOTE
(Phase 24 T2): objdump is now `-drz`, NOT `-dr` -- `-z` keeps runs of nops so GTE-heavy seeds are no
longer under-counted (func_80132784 now reads its true 400 ins, not 384).
python3 tools/match_one.py func_800D1DB0 # uses .run/drafts3|2|/<fn>.c
python3 tools/match_one.py func_800D1DB0 --c /path/to.c # explicit C file
"""
import subprocess, re, sys, os, argparse
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import masked_diff
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('fn')
ap.add_argument('--c', help='C file (externs + the function def). Default: search .run/drafts3|2|/')
ap.add_argument('--asm-subdir', default='asm/resident/nonmatchings/resident')
ap.add_argument('--work', default=None,
help='scratch dir. Default: a PRIVATE per-invocation dir (.run/match/<fn>.<pid>). '
'It used to default to the SHARED ".run/match", which silently broke the one '
'property this tool advertises — see below.')
ap.add_argument('--o0', action='store_true',
help='compile at -O0 (for the _o0 split subsegments: ov_SC01_077_o0.c, whale _o0b — '
'their target bytes are -O0; an -O2 compile can never match them, Makefile:445)')
a = ap.parse_args()
# PRIVATE SCRATCH BY DEFAULT (Phase-28 T5). This tool's own docstring promises "Fully isolated (own
# temp dir) so many run in PARALLEL with no shared build -- a real asm-differ loop for an agent to
# iterate against." That was FALSE: --work defaulted to the shared '.run/match', so every concurrent
# caller compiled into the SAME t.c/t.o. A 16-agent wave found it the only way it can be found — one
# agent read another's function out of its own scratch file ("found another agent's func_800D2650 in
# my t.c") and said so. Every other agent in that wave iterated against a loop that could silently
# hand it someone else's compile, which is worse than a crash: it produces a CONFIDENT WRONG verdict.
# The docstring was the spec; the default contradicted it; nothing checked. Now the default IS the
# promise. (The whole-binary byte-gate was never at risk — it is the sole arbiter, G3/P9 — but the
# iteration loop agents steer by absolutely was.)
if not a.work:
a.work = os.path.join('.run/match', f'{a.fn}.{os.getpid()}')
CPP = 'mipsel-linux-gnu-cpp'; CC1 = 'tools/bin/gcc-2.7.2-psx/cc1'
MASPSX = 'tools/maspsx/maspsx.py'; AS = 'mipsel-linux-gnu-as'; PY = '.venv/bin/python'
CPPFLAGS = '-lang-c -Iinclude -undef -Wall -fno-builtin -Dmips -D__GNUC__=2 -D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C'.split()
CC1FLAGS = ('-quiet %s -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker'
% ('-O0' if a.o0 else '-O2')).split()
ASFLAGS = '-Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O1 -G0'.split()
cfile = a.c
if not cfile:
for d in ('.run/drafts3', '.run/drafts2', '.run/drafts'):
if os.path.exists('%s/%s.c' % (d, a.fn)):
cfile = '%s/%s.c' % (d, a.fn); break
if not cfile or not os.path.exists(cfile):
print('FAIL: no C file for', a.fn); sys.exit(2)
wd = '%s/%s' % (a.work, a.fn); os.makedirs(wd, exist_ok=True)
src = open(cfile).read()
# strip inline scalar-typedef redefinitions (common.h provides them; C89 rejects the dup).
# The T4 primitive splits multi-typedef lines the old regex couldn't cross (42 discarded MATCHes).
src = masked_diff.strip_scalar_typedefs(src)
if '#include "common.h"' not in src:
src = '#include "common.h"\n' + src
open('%s/t.c' % wd, 'w').write(src)
def pipe(cmd, data=None):
return subprocess.run(cmd, input=data, capture_output=True)
p = pipe([CPP] + CPPFLAGS + ['%s/t.c' % wd])
if p.returncode: print('CPP FAIL\n' + p.stderr.decode()[-1500:]); sys.exit(1)
p = pipe([CC1] + CC1FLAGS, p.stdout)
if p.returncode: print('CC1 FAIL\n' + p.stderr.decode()[-1800:]); sys.exit(1)
p = pipe([PY, MASPSX, '--aspsx-version=2.56', '--expand-div'], p.stdout)
if p.returncode: print('MASPSX FAIL\n' + p.stderr.decode()[-1500:]); sys.exit(1)
p = pipe([AS] + ASFLAGS + ['-o', '%s/t.o' % wd], p.stdout)
if p.returncode: print('AS FAIL\n' + p.stderr.decode()[-1500:]); sys.exit(1)
# masked compare: my compiled object vs the resolved splat .s (mask driven by my object's relocs)
mine = masked_diff.insns_from_object('%s/t.o' % wd, a.fn)
tgt = masked_diff.insns_from_s('%s/%s.s' % (a.asm_subdir, a.fn))
if not mine:
print('FAIL: my object has no function', a.fn, '(compile produced nothing?)'); sys.exit(1)
n = max(len(mine), len(tgt))
diffs = []
for i in range(n):
mw = mine[i]['word'] if i < len(mine) else None
mask = masked_diff.mask_for(mine[i]['word'], mine[i]['reloc_kind']) if i < len(mine) else 0xFFFFFFFF
me = (mw & mask) if mw is not None else None
tg = (tgt[i]['word'] & mask) if i < len(tgt) else None
if me != tg:
diffs.append((i,
('%08x %s' % (mine[i]['word'], mine[i]['mnem'])) if i < len(mine) else '--',
('%08x %s' % (tgt[i]['word'], tgt[i]['mnem'])) if i < len(tgt) else '--'))
if not diffs and len(mine) == len(tgt):
print('MATCH (%d ins) %s' % (len(mine), a.fn))
sys.exit(0)
print('DIFF %s mine=%d ins, target=%d ins, %d mismatched' % (a.fn, len(mine), len(tgt), len(diffs)))
print(' idx | MINE | TARGET')
for i, me, tg in diffs[:40]:
print(' %3d | %-28s | %s' % (i, me, tg))
sys.exit(1)