Files
BFM-decomp/tools/jr_isolate.py
T
Drew T 57e5f970c8 docs(tools): four docstrings describing pre-session behaviour
* draw_waves Usage advertised [--no-main], which argparse never defined (the flags are
  --main / --only-main, and main is excluded by default), and omitted --exclude-file,
  which is now a PREREQUISITE that refuses a stale list.
* jr_isolate's STATUS block still declared the tool BLOCKED on split_src_region with the
  blocker unbuilt. Five defects were fixed this session and it runs the full chain to
  completion; what remains is a duplicate-definition class at assembly. Says so, and
  points at §431 as the cheaper route than finishing the item model.
* ld_interleave's layout diagram — the first thing anyone reads — showed the pre-S72
  three-piece island with 6324C.data.o. main's island is SEVEN pieces driven by --order;
  --front/--tail is the overlay form now.
* jtbl_rodata_pads described a stored-spec-only filter and advertised guards that no
  longer all exist; --derive serves main since S72.
2026-09-02 17:15:28 -06:00

136 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""Isolate a jr-function into its own code subseg (Phase-26 §8 multi-jtbl, the non-contiguous case).
Two matched jr-functions in ONE code object emit their jtbls CONTIGUOUS in that object's .rodata
(gcc source order). That is byte-correct only if their jtbls are adjacent in the overlay's .rodata
island. When an UNMATCHED jtbl sits between them, the object can't reproduce the island layout ->
jtbl_carve refuses (non-contiguous same-subseg). This tool splits the containing code subseg so the
target function becomes its own object (the whale `_o0b` precedent), preserving every other matched C
body (H5, via split_src_region.py trim/inject), after which its jtbl carves independently.
[s_off, c, SUB] -> [s_off, c, SUB] (functions < func)
[f_off, c, <ov>_jr_<addr>] (func alone -> its own object)
[f_end, c, <ov>_after_<addr>](functions >= func's end)
The carve subseg names are re-derived by jtbl_carve from each function's ADDRESS against the current
code subsegs, so after this split jtbl_carve automatically points every carve at its new object -- this
tool only restructures code subsegs; it never edits a `.rodata` carve. Idempotent (no-op if already
isolated). Self-contained: does the config split, the source trim/inject, and the re-extract.
jr_isolate.py <ov> --func func_XXXX
STATUS (P31 S73): the Phase-26 blocker is GONE. `split_src_region.py` could not partition an
overlay `.c` because it demanded an address for every top-level item; five defects were fixed
(address-less preamble runs coalesce FORWARD into the item below them; coalesce carries
(addr, name, text) captured BEFORE the merge; `item_name` strips comments; it anchors on a
DEFINITION, not on a leading `extern` — which used to return the name "void"; and it accepts
an INDENTED top-level definition). This tool now runs the full chain to completion on
`ov_SC02_005` — config split, source trim, re-extract, inject.
NOT DONE: the resulting object still fails to ASSEMBLE on a remaining duplicate-definition
class, so no overlay has been split with it yet. Before sinking more time into the item
model, consider cookbook §431 instead — cut the file verbatim at line boundaries and let the
COMPILER enumerate what crosses. That is what worked first time on main, and it does not
depend on this tool being correct.
"""
import argparse
import os
import re
import subprocess
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import jtbl_carve # cfg_path, func_subseg, overlay_vram_base, PIECE_RE
REPO = jtbl_carve.REPO
def func_end_vram(ov, func):
"""func's end vram = its last .text instruction's vram + 4 (from the nonmatchings .s)."""
sub = jtbl_carve.func_subseg(ov, func)
s = open(os.path.join(REPO, "asm", ov, "nonmatchings", sub, f"{func}.s")).read()
# instruction lines: /* <fileoff> <VRAM> <bytes> */ <mnemonic>
addrs = [int(m, 16) for m in re.findall(
r"/\*\s*[0-9A-Fa-f]+\s+([0-9A-Fa-f]{8})\s+[0-9A-Fa-f]{8}\s*\*/\s*\S", s)]
if not addrs:
sys.exit(f"jr_isolate: no instruction addresses in {func}.s")
return max(addrs) + 4
def sh(cmd, **kw):
return subprocess.run(cmd, cwd=REPO, **kw)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("ov")
ap.add_argument("--func", required=True, help="func_XXXXXXXX to isolate into its own code subseg")
a = ap.parse_args()
ov, func = a.ov, a.func
m = re.fullmatch(r"func_([0-9A-Fa-f]{8})", func)
if not m:
sys.exit("jr_isolate: --func must be func_XXXXXXXX")
faddr_hex = m.group(1)
base = jtbl_carve.overlay_vram_base(ov)
cfg = jtbl_carve.cfg_path(ov)
txt = open(cfg).read()
jr_name = f"{ov}_jr_{faddr_hex}"
after_name = f"{ov}_after_{faddr_hex}"
# IDEMPOTENCY MUST CHECK THE LAST STEP, NOT THE FIRST (P31 S72). This tool writes the CONFIG
# split, then splits the SOURCE, then re-extracts. Keying "already isolated" on the config
# alone means any failure in between leaves a half-applied tree in which the tool believes it
# is finished and short-circuits forever — measured here: the first run died in
# split_src_region, and the retry (after that bug was fixed) printed "already isolated" over a
# source file that had never been split. Require the SOURCE to exist too, and say which half
# is missing so the state is recoverable instead of mysterious.
src_c = os.path.join(REPO, "src", ov, f"{jr_name}.c")
if f", c, {jr_name}]" in txt and not os.path.exists(src_c):
sys.exit(f"jr_isolate {ov}: HALF-APPLIED — config/{os.path.basename(cfg)} already names "
f"`{jr_name}` but {os.path.relpath(src_c, REPO)} does not exist. Revert the config "
f"(git checkout -- {os.path.relpath(cfg, REPO)}) and re-run, or finish the source "
f"split by hand. Refusing rather than reporting success over a broken tree (R43).")
if f", c, {jr_name}]" in txt:
print(f"jr_isolate {ov}: {func} already isolated ({jr_name})")
return
sub = jtbl_carve.func_subseg(ov, func) # current containing code subseg
f_vram = int(faddr_hex, 16)
f_end = func_end_vram(ov, func)
f_off, f_end_off = f_vram - base, f_end - base
# find the [s_off, c, SUB] config line for the containing subseg
line_re = re.compile(rf"^([ \t]*)- \[(0x[0-9A-Fa-f]+),\s*c,\s*{re.escape(sub)}\]\s*(?:#.*)?$", re.M)
mm = line_re.search(txt)
if not mm:
sys.exit(f"jr_isolate: no `[..., c, {sub}]` line in {cfg}")
indent, s_off = mm.group(1), int(mm.group(2), 16)
if not (s_off <= f_off < f_end_off):
sys.exit(f"jr_isolate: {func} (0x{f_off:x}..0x{f_end_off:x}) not inside subseg {sub} (@0x{s_off:x})")
block = "\n".join([
f"{indent}- [{hex(s_off)}, c, {sub}]",
f"{indent}- [{hex(f_off)}, c, {jr_name}] # Phase-26 §8 jr isolation (jr_isolate.py)",
f"{indent}- [{hex(f_end_off)}, c, {after_name}]",
])
txt = line_re.sub(lambda _: block, txt, count=1)
open(cfg, "w").write(txt)
# source: keep functions < f_vram in SUB.c, move real-C >= f_end to a frag for the after file
sub_c = f"src/{ov}/{sub}.c"
frag = f".run/isolate_{ov}_{func}.frag"
syms = f"config/symbols.{ov}.txt"
sh([sys.executable, "tools/split_src_region.py", "--symbols", syms,
"trim", sub_c, hex(f_vram), hex(f_end), frag], check=True)
# regenerate: splat emits jr_name.c (func's stub) + after_name.c (stubs for >= f_end)
if sh(["make", "--no-print-directory", "extract", f"BINARY={ov}"]).returncode:
sys.exit(f"jr_isolate: extract failed for {ov}")
# restore the moved matched C into the freshly-generated after file
sh([sys.executable, "tools/split_src_region.py", "--symbols", syms,
"inject", f"src/{ov}/{after_name}.c", frag], check=True)
print(f"jr_isolate {ov}: {func} -> {jr_name}; remainder >= 0x{f_end:x} -> {after_name}")
if __name__ == "__main__":
main()