#!/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, _jr_] (func alone -> its own object) [f_end, c, _after_](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 --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: /* */ 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()