Files
BFM-decomp/tools/jr_isolate.py
T
Drew T d1dd29d815 feat(phase-26): §8b multi-jtbl same-subseg — contiguous MERGE (built) + isolation scaffold
Session-4 same-subseg handling (the de-risk preamble's harder half; byte-proof of
the merged build + isolation deferred to Stage 2 with concrete cores):

- jtbl_carve.py: MERGE adjacent same-subseg carves into one spanning .rodata piece
  (a code object emits its jtbls contiguous, so two matched jr-fns in one subseg are
  byte-correct iff their jtbls abut). BOUND-FIX: a new jtbl's end is bounded by the
  next raw dlabel OR the next existing carve start (an already-carved adjacent jtbl
  is gone from the data asm -> raw dlabels over-extend it -> false "non-contiguous").
  Config-proven (func_80171B4C 801D8C48 merges with func_801734BC 801D8C68). NO-OP
  for family-1/cross-subseg (single carve per subseg) -> committed configs unaffected.
- jr_isolate.py (scaffold, NOT yet functional): the non-contiguous case — split a fn
  into its own code subseg (whale _o0b precedent) so its jtbl carves independently.
  BLOCKED on split_src_region, which can't partition the overlay .c (global canonical-
  sig extern layer + per-fn callee-externs + DEFINE_func macros + @class annotations,
  ~922 non-address items). Stage-2 build item (overlay-.c-aware source split).
- cookbook §8b (the --order sandwich + the two same-subseg cases + the blocker);
  CURRENT_PHASE session-4 checkpoint updated with the Stage-2 unblock decision.
2026-07-12 22:06:17 -06:00

116 lines
5.5 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 (Phase-26 session 4): the config split + `func_subseg`-derived carve are ready, but this is BLOCKED on
`split_src_region.py`, which cannot partition the overlay `.c` — those files carry non-address top-level items
(the Phase-17 global canonical-sig extern layer + per-function callee-extern blocks + `DEFINE_func_X()` dedup
macros + `// @class` annotations) that its "one item = one address" model chokes on (~922 unresolved in
ov_SC01_077_after.c). Finishing this needs an overlay-`.c`-aware source split (header = includes + the global
extern layer; attach leading extern-decl blocks to the following function-block). See cookbook §8b. Stage-2 item.
"""
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}"
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()