Files
BFM-decomp/tools/lint_symbol_refs.py
T
Drew T a7394f44dc feat(phase-31): S78 #12 — the 800c3 "wall" band is LIBPAD 4.2.1 + LIBAPI 4.2: 46 names applied; integrate wired by subseg range; renames via ApplySymbols
- provenance: the psx loader's per-version PsyQ signature sets place PADENTRY/PADCMD/PADPORTD/
  PADSEQD (4.2), WAITRC2 (4.3), COUNTER/C114/FIRST/PAD/PATCH/CHCLRPAD (libapi 4.2) byte-exact in
  0x8005CE48-0x8005FC68 / 800c2 -> 12 of main's 29 stubs incl. all four §332 walls are Sony's
  DualShock library in reorder mode. 46 names -> symbols.us.txt (count 1081), band TUs, verbatim
  manifest, wave_exclude; firstfile/firstfile2 (4.2 naming); CdGetToc @0x800430B8 (was the Phase-21
  xdedup mislabel DecDCToutCallback). SETUP §5.1 corrected; psyq-worklist S78; cookbook §487;
  decision-log + accelerators S78; CHECKSUMS +Psy-Q_46.zip +PSYQ_SDevTC_v4.5.zip.
- psyq_integrate: --yaml maps stub<->objects by SUBSEG RANGE with an exact-tiling check and PRINTS
  the located-but-unwired residue (libgte: 13 objs / 1,264 ins) — main's LINKED build had been RED
  at HEAD since the S77 psyq_identify fix (22 libgte blocks merged to 3; gate worktrees take the
  stub fallback so it never showed); a library object's exported symbol whose recovered address the
  curated file names differently is --redefine-sym'd (R15; A66 firstfile->firstfile2).
- Ghidra: 47 MCP renames did NOT persist through the sentinel stop (R9 caught it) -> NEW
  tools/ghidra_scripts/ApplySymbols.java + tools/ghidra_apply_symbols.sh mirror the curated file
  headless with a real save: 73 renamed, R9-verified x4. SETUP inventory rows (R21).
- lint_symbol_refs: scans verbatim __asm__ bodies (`.ent\tfunc_X` is invisible to \b and to the
  string-masked scan); negative-controlled (red on the pre-fix TUs, green on the passing tree).
- R22: clean extract-all 212/212 + check-all green on the final config; main rebuilt byte-identical
  143dbb89 after the last src-only fix -> 213/213; tools-health OK.
2026-09-04 15:57:06 -06:00

132 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""lint_symbol_refs.py — catch stale func_<ADDR> refs that a symbols.us.txt rename left dangling.
The Phase-24 T5b/T5c breakage class: Phase-21 xdedup renamed 62 PsyQ functions to their curated
names (InitHeap, GetTPage, SysEnqIntRP …) in config/symbols.us.txt, but the committed source that
references them by the OLD `func_<ADDR>` name was never updated — both `INCLUDE_ASM(func_<ADDR>)`
stub refs (src/*.c) AND `func_<ADDR>(...)` calls in shared macros (src/shared/engine_core.h). A
GENUINELY-clean rebuild then fails (splat emits `<curated>.s` / provides the symbol only under the
curated name), but INCREMENTAL builds reuse stale .s/.o and mask it (the R22 failure mode). This
went undetected from Phase 21 → 23.
This lint flags every `func_<ADDR>` token in committed src/ whose address has a CURATED (non-`func_`)
name in the symbol files and NO `func_<ADDR>` symbol of its own — i.e. a reference splat/the linker
can no longer resolve under that name. Run it after any symbols rename and in `make report`.
Exit 0 = clean; exit 1 = stale refs found (printed as file:line func_<ADDR> -> curated).
"""
import re, glob, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cdecl # §134/R33: the ONE comment/string masking oracle
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# The files a real binary actually stacks (main/resident/overlays). The proto symbol files
# (symbols.proto-*.txt) are R13 UNVERIFIED cross-build data and are NEVER stacked into a build —
# reading them would invent phantom curated names. This is blind spot #2 (the audit read only 2 of
# these): a per-overlay rename must be seen or a genuine dangling ref hides.
def _symbol_files():
files = []
for p in sorted(glob.glob(os.path.join(REPO, "config/symbols*.txt"))):
if "proto" in os.path.basename(p):
continue
files.append(p)
return files
def load_symbols():
"""addr(lower hex) -> curated name (excludes the func_<ADDR>/D_<ADDR> auto-names); and the set
of addresses that DO have a literal auto-name symbol (those resolve under func_/D_ as written).
Covers both the func_ (code) and D_ (data) auto-name classes, across every REAL stacked file."""
curated, autosym = {}, set()
for p in _symbol_files():
for m in re.finditer(r'^([A-Za-z_]\w*)\s*=\s*(0x[0-9A-Fa-f]+)', open(p).read(), re.M):
nm, a = m.group(1), m.group(2).lower()
if nm.lower() in ("func_" + a[2:], "d_" + a[2:]):
autosym.add(a)
else:
curated.setdefault(a, nm)
return curated, autosym
# A `func_<ADDR>` / `D_<ADDR>` given an explicit asm label — `extern void func_8005C324(...)
# __asm__("memcpy");` — emits a reference to the LABEL, not to a `func_<ADDR>` symbol, so it resolves
# at link regardless of the rename. This is blind spot #3 (the audit's 43-then-262 false positives are
# entirely this class: engine_core.h's block-copy macro binds func_8005C324 to memcpy via __asm__).
_ASM_LABEL_RE = re.compile(
r'\b(?:func_|D_)([0-9A-Fa-f]{6,8})\b[^;{}\n]*?__asm__\s*\(\s*"[^"]+"\s*\)')
def asm_labeled_addrs(files):
out = set()
for cf in files:
for m in _ASM_LABEL_RE.finditer(open(cf, errors="replace").read()):
out.add("0x" + m.group(1).lower())
return out
def strip_comments_strings(src):
"""Blank comments + string/char-literal contents, via the ONE masking oracle (`cdecl._mask`).
§134 / R33 (P30 S39). This used to be a private char-by-char scanner — correct, but a SECOND
implementation of the masking `cdecl._mask` already owns, and the §134 class (a line-shape
decision made against unmasked text) had by then appeared in six tools precisely because each
one kept its own copy. One oracle cannot diverge from itself.
Behavioural note, verified before the swap: `_mask` blanks the quote DELIMITERS as well as the
content, where this scanner kept the quotes. That is irrelevant here — both blank the contents,
and every token this linter hunts (`func_<ADDR>`, `D_<ADDR>`, and the bare 2nd argument of
`INCLUDE_ASM("...", func_X)`) lives OUTSIDE the quotes either way. Gated on the linter's own
output being byte-identical across the change, not on the masks being byte-identical."""
return cdecl._mask(src)
def main():
curated, autosym = load_symbols()
# Blind spot #1: src/shared/*.h (engine_core.h — 10k+ func_/D_ tokens across every overlay) was
# never scanned, yet one dangling ref there breaks EVERY clean build at once.
files = sorted(glob.glob(os.path.join(REPO, "src/**/*.c"), recursive=True) +
glob.glob(os.path.join(REPO, "src/shared/*.h")))
asm_labeled = asm_labeled_addrs(files) # blind spot #3: __asm__("label") resolves the ref
stale = []
for cf in files:
rel = os.path.relpath(cf, REPO)
code = strip_comments_strings(open(cf, errors="replace").read())
for i, line in enumerate(code.splitlines(), 1):
for m in re.finditer(r'\b(?:func_|D_)([0-9A-Fa-f]{6,8})\b', line):
a = "0x" + m.group(1).lower()
if a in autosym or a in asm_labeled: # real auto-name OR bound to a label -> resolves
continue
if a in curated: # renamed to a curated name, no auto-name symbol -> DANGLING
stale.append((rel, i, m.group(0), curated[a]))
# Blind spot #4 (P31 S78): the §265 verbatim `__asm__("...")` bodies. `strip_comments_strings` blanks
# every string literal, so a renamed `func_<ADDR>` that survives INSIDE an asm body (`.ent\tfunc_X`,
# `.end\tfunc_X`, `.globl func_X`, `jal\tfunc_X`, `func_X:`) is invisible above — and gas then dies
# with `.size expression for func_X does not evaluate to a constant`. Scan the string bodies too,
# and do NOT rely on `\b`: the literal escape `\t` ends in the word char `t`, so `\tfunc_X` has no
# word boundary before the token (the exact miss that produced the first S78 red build).
for cf in files:
rel = os.path.relpath(cf, REPO)
raw = open(cf, errors="replace").read()
for sm in re.finditer(r'__asm__\s*\(((?:\s*"(?:[^"\\]|\\.)*")+)\s*\)', raw):
body = sm.group(1)
line0 = raw.count("\n", 0, sm.start()) + 1
for m in re.finditer(r'(?<![0-9A-Za-z_])(?:func_|D_)([0-9A-Fa-f]{6,8})(?![0-9A-Za-z_])', body):
a = "0x" + m.group(1).lower()
if a in autosym or a in asm_labeled: # blind spot #3 applies here too (memcpy binding)
continue
if a in curated:
stale.append((rel, line0, m.group(0) + " (inside __asm__ body)", curated[a]))
if stale:
print(f"[lint_symbol_refs] {len(stale)} STALE func_<ADDR> ref(s) — renamed in symbols, "
f"clean-build will FAIL (fix: rename to the curated name):")
for rel, i, tok, nm in stale:
print(f" {rel}:{i} {tok} -> {nm}")
return 1
print("[lint_symbol_refs] OK — no stale renamed func_<ADDR> refs in committed src/")
return 0
if __name__ == "__main__":
sys.exit(main())