Files
Drew T 827295e241 tools+docs(phase-33.5): task 13.5 — the tools audit + the two dictionaries: tools/tool_census.py (two agreeing enumerations of 327 tool files; docstring/SETUP row/consumers/class derived from the tree; the authored half in config/tool_dictionary.tsv — phase · portability · the NEED each tool answers · what · adapts · verdict — with coverage asserted both ways) → docs/tool-index.md (need-keyed, KEEP-GEN, Reference-index row, wiki + how-to pointers), the kit's tools/MANIFEST.md regenerated (header states live 293 + superseded 28 = 321 rows), and the two verbatim corpora in-tree (Drew, confirmed S91): decomp-architect/corpus/tools/<phase>/ (302 copies + 28 superseded pointers + INDEX) and corpus/cookbook/ (the cookbook, its symptom index, the codegen map, a front page stating what transfers per compiler) — sha1-equal to their sources by tool_census --check in tools-health, regenerated by make kit-corpus; kit_lint exempts the corpus dirs (verbatim evidence) but syntax-checks them; G66 (consult the tool dictionary first) + G67 (translate an inherited idiom through its pass) + two memory seeds (34 at install); SETUP Step 6 installs docs/knowledge-corpus.md and checks the manifest against its own stated total; the ops-setup dictionary rows; the intake's Phase 7 cites G66/G67 and Phase 10 + Part C name the raw-cast → declared-symbol step; templates/layout-contract.md (the five-tool probe, a draft for the split). The review under Drew's criterion: 93 no-consumer tools (one Opus agent's draft, verified: 0 defects, every successor live, 0 live consumers, 0 collisions; four one-off verdicts overturned to STILL-NEEDED) → 34 retired by git mv to tools/sunset/ (28 superseded, 6 one-offs; README review table; SETUP rows moved; Archive-index group). Run 4 (fresh throwaway, the final kit): stopped on my Step-6 check (321 vs the live 293) → both sides derived → resumed → PASS 10/10, manifest 56 == 56, 4 commits, guardrails held (the one foreign path was the timeline regenerated by the detached tools-health). tools-health OK; doc_links --strict rc 0; audit_public OK over 6,842 paths; the purge probe PASSED (Phase 34's gate open). decision-log "P33.5 S91" + accelerators "P33.5 S91" banked; log + checkpoint (NEXT = task 14, xHigh, fresh session)
2026-09-07 22:09:15 -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()