feat(phase-26): §8b overlay-src parser (404/404) + jr isolation tool + the gcc-scoping wall finding

- tools/overlay_src_split.py: overlay-.c-aware partition (header = includes + Phase-17
  canonical-sig layer; per-address items = preamble + body; robust def/decl/K&R/DEFINE_func/
  SETTER/RETCONST classification). Fleet-validated 404/404 overlay .c, 341,902 items —
  round-trip exact / 0 unresolved / 0 non-monotonic. The Stage-2 isolation unblock.
- tools/jr_isolate_all.py: multi-cut jr resegment (config split at jr boundaries, source
  repartition + INCLUDE_ASM path repoint, banked-jr carve repoint, -O0 skip, ambient decl
  carry). SINGLE-cut isolation byte-identical (func_8013FFD8 -> d19c9580, R22).
- FINDING (decision-log 2026-07-13): full 54-jr isolation of the dense _after object hits
  gcc-2.7.2 block-scope-extern TU-persistence (func_801734BC/D_80126B3E declared only in
  engine_core.h DEFINE_func macros); mechanical TU-split breaks it. Fix = declaration-
  completion from a global symbol->type map (Drew-approved next step; lazy per-core).
- baseline intact (ov_SC01_077 rebuilds d19c9580); no config/src/binary change committed.
  CURRENT_PHASE session-5 checkpoint + decision-log R31. db.*.gbf = R23 noise, not staged.
This commit is contained in:
Drew T
2026-07-13 12:00:26 -06:00
parent d1dd29d815
commit 234788dfc4
4 changed files with 802 additions and 1 deletions
+47
View File
@@ -420,3 +420,50 @@ INCLUDE_ASM rodata) cannot arbitrate a match whose difference lives in that regi
light tail because it "feels productive." Wiki lesson: when a newly-built mechanism has an
un-built sub-case that the expensive targets will hit, force that sub-case out on the cheap
targets first — de-risking and building-the-missing-piece are the same move.
### 2026-07-13 — the jr-core ISOLATION wall: mechanical TU-splitting breaks gcc-2.7.2's lenient scoping
- **Context + belief:** Stage 2 of the multi-jtbl campaign (heavy jr cores → template ×134) needs each
matched jr-function ALONE in its own code subseg so its jtbl carves without a same-subseg collision.
Drew's steer: build the **scalable "isolate-ALL-jr-per-sibling" upfront resegment** (one-shot multi-cut
per overlay) so every Stage-2 core bank is a trivial fill during the closing Fable5 window. Belief going
in: this is mechanical source-splitting — partition the overlay `.c` at jr boundaries, repoint config +
carves, rebuild byte-identical.
- **What was built + PROVEN:** `tools/overlay_src_split.py` — an overlay-`.c`-aware partition (header =
includes + Phase-17 canonical-sig layer; each addressed item = its preamble + body; robust
definition/declaration/K&R/`DEFINE_func`/`SETTER`/`RETCONST` classification). **Fleet-validated 404/404
overlay `.c`, 341,902 items, round-trip exact / 0 unresolved / 0 non-monotonic.** `tools/jr_isolate_all.py`
— multi-cut resegment (config split at jr boundaries, source repartition + INCLUDE_ASM path repoint,
banked-jr carve repoint, -O0-object skip). **SINGLE-cut isolation byte-identical** (isolate func_8013FFD8
in the simple `main` object → clean `make build` = `d19c9580`, R22).
- **What FAILED (byte-verified):** the FULL 54-jr isolation on ov_SC01_077 hits a **long tail of C-scoping
edge cases**, culminating in the decisive one: **`func_801734BC` uses `D_80126B3E` with no local decl;
`D_80126B3E` is declared `extern s16` ONLY inside `DEFINE_func` macros in `engine_core.h`.** The original
`_after.c` compiles because **gcc-2.7.2 lets a block-scope `extern` (from an earlier `DEFINE_func` macro
expansion) persist to file scope for the rest of the TU** — splitting `_after` separates the core from the
earlier macro that declares the symbol → `undeclared`. Earlier tail members (all fixed incrementally, in
order): block-scope externs must not be hoisted (per-fn type shadows — `D_80115118` is `unsigned short`
in most funcs but the struct `S115118` in one); file-scope decl ORDERING across a cut (`D_80115110` used
above its in-region decl); **ambient decl context** (a region needs the file-scope decls that lived in
earlier regions of the object — solved: prepend, original order, shadow-safe because a file-scope-declared
symbol can't carry a *different*-typed block shadow or the original wouldn't compile); file-local-typed
externs (`extern Vec8 D_…;`) can't hoist above their typedef.
- **The why (root):** these overlay TUs are hand-matched against a compiler that treats a block-scope
`extern` as declaring the symbol for the WHOLE TU. Mechanical splitting into per-jr TUs breaks that
invisible cross-function dependency, and the dependency is carried through **`DEFINE_func`/`SETTER` macro
expansions in `engine_core.h`**, not just visible col-0 decls — so no amount of *col-0* ambient-carry
fixes it.
- **The candidate fix (not yet built):** **declaration-completion** — build a global symbol→type map from
`engine_core.h`'s macro `extern`s + all overlay col-0 decls, and for each region emit a file-scope
`extern <type> <sym>;` for every `D_`/`func_` symbol the region USES, EXCLUDING type-inconsistent symbols
(the `D_80115118` shadow set, kept block-scope in bodies). This makes every region self-contained
regardless of where the original declared the symbol. Est. ~40–60 LOC on top of the proven parser; the
whole-binary byte-gate arbitrates. **Owner decision pending (Drew): invest in declaration-completion vs
a different Stage-2 approach** — surfaced this session before sinking more time (P5a: repeated failures,
distinct root cause each).
- **Hindsight / for the wiki:** "mechanical source split" of matching-decomp overlay code is NOT mechanical
— the C is written against a specific compiler's lenient scoping (block-scope-extern TU persistence,
macro-injected decls, per-function type shadows). Splitting a TU means REBUILDING each fragment's full
declaration environment from a global symbol map, not relocating text. The parser (structure) was the
easy 20%; the declaration environment (semantics) is the 80%. Prove the mechanism on the SIMPLE object
first (it passed) but budget for the dense object's scoping tail before committing to upfront-×134.
+18 -1
View File
@@ -24,7 +24,23 @@ The Phase-25 h_seq reframe: the "unique tail" is really per-location families
- [ ] **Task 11 — Step-D residue map** `[xHigh]` — true singletons (~0.27M ins) + 5 behemoths → Phase-27 input doc. NO execution.
- [ ] **Task 12 — PhaseEnd** `[Max — Tier 1; R27 prompt]` — P7 walk, milestone demo, gate 2, `PhaseEnd_Phase26.md`, worklog → `logs/Phase26.md` (R19), in-file recap (R25), decision-log current (R31).
## ▶ SESSION-4 CHECKPOINT (2026-07-12 later) — RESUME FROM HERE (fresh session)
## ▶ SESSION-5 CHECKPOINT (2026-07-13) — RESUME FROM HERE (fresh session)
**Stage-2 isolation build — parser DONE + single-cut PROVEN; full isolate-all hit the gcc-2.7.2 scoping wall. Effort: Max. Drew's decision (2026-07-13, Fable5 window extended to 7/19 so no time pressure): BUILD declaration-completion + apply isolation LAZILY per-core (not upfront-×134 — avoids the ~7,200-file cost; same tool, isolate only cores that crack). Tools committed this session.**
Built this session (baseline reverted + intact — ov_SC01_077 rebuilds `d19c9580`; only the 2 new tools are untracked, NOT yet committed):
1. **`tools/overlay_src_split.py`** — overlay-`.c`-aware partition (header = includes + Phase-17 canonical-sig layer; per-address items = preamble + body; robust def/decl/K&R/`DEFINE_func`/`SETTER`/`RETCONST` classification). **VALIDATED fleet-wide: 404/404 overlay `.c`, 341,902 items — round-trip exact / 0 unresolved / 0 non-monotonic.** This is solid, reusable, commit-worthy.
2. **`tools/jr_isolate_all.py`** — multi-cut resegment (config split at every jr boundary, source repartition + INCLUDE_ASM path repoint, banked-jr carve repoint via config + `overlays.mk --order`, -O0-object skip, ambient file-scope decl carry). **SINGLE-cut isolation BYTE-IDENTICAL** (func_8013FFD8 in `main` → clean `make build` = `d19c9580`, R22). Full 54-jr isolation on the dense `_after` object hit the wall.
**THE WALL (byte-verified, decision-log 2026-07-13):** mechanical TU-splitting breaks gcc-2.7.2's **block-scope-extern TU-persistence** — `func_801734BC` uses `D_80126B3E`, declared `extern s16` ONLY inside `DEFINE_func` macros in `engine_core.h`; the original `_after.c` compiles because an earlier macro's block-scope `extern` persists to file scope for the whole TU. Splitting separates the core from that macro → `undeclared`. (7 distinct C-scoping edge cases surfaced + fixed incrementally; this last one is the deep one — col-0 ambient-carry can't reach macro-injected decls.)
**THE FIX TO BUILD (Drew-approved, fresh session):** **declaration-completion** — build a global symbol→type map from `engine_core.h` macro `extern`s (the `DEFINE_func`/`SETTER`/`RETCONST` bodies) + all overlay col-0 decls; then for each isolated region emit a file-scope `extern <type> <sym>;` for every `D_`/`func_` symbol the region USES, EXCLUDING the type-shadowed set (symbols with inconsistent decls across the codebase, e.g. `D_80115118` = `unsigned short` vs struct `S115118` — kept block-scope in bodies). This makes every region self-contained regardless of where the original declared the symbol (col-0, block-scope body, or macro). Est. ~40–60 LOC on the proven `overlay_src_split.py`; the whole-binary byte-gate (G3/P9/R22) arbitrates. Wire it into `jr_isolate_all._render_region` REPLACING the col-0-only ambient carry (which can't reach macro-injected decls).
**NEXT (fresh session):** (1) build declaration-completion (above); (2) re-prove full isolate-all byte-identical on ov_SC01_077; (3) switch isolation to LAZY per-core — integrate a single-core isolate into the bank flow (`jtbl_family_bank`), isolating a heavy jr core only when banking it (fewer files than upfront-×134); (4) then the heavy-jr-core crack waves — Fable5 (window now to 7/19) → §8 carve + template ×134 → R22. Committed tools this session: `tools/overlay_src_split.py`, `tools/jr_isolate_all.py`. Ghidra MCP NOT needed (matching uses cached asm). **R27 boundary: prompt Drew to re-confirm effort/Fable5 before the crack waves.**
---
## ▶ SESSION-4 CHECKPOINT (2026-07-12 later) — superseded by SESSION-5 above
**Stage 1 in progress — multi-jtbl `--order` BUILT + proven cross-subseg at fleet scale.** Effort: Max (this session, Tier-1 tooling build).
@@ -118,6 +134,7 @@ On approval → `/model opus` + `/effort xHigh` (Tasks 0–4; ALL Fable5 via `Ag
## Log
- **2026-07-13 (session 5, Stage-2 isolation build — parser DONE, single-cut PROVEN, full isolate-all hit the gcc-2.7.2 scoping wall; Max):** Drew re-approved Stage 2 + set `/effort max`; steer = scalable isolate-ALL-jr-per-sibling upfront resegment, then Fable5 on the heavy cores. Built + fleet-validated **`tools/overlay_src_split.py`** (overlay-`.c`-aware partition; **404/404 overlay `.c`, 341,902 items, round-trip exact / 0 unresolved / 0 non-monotonic** — fixed 5 parser edge cases: definition-vs-declaration by brace/`;` scan, K&R defs, comment/`#`-directive peeling, `def_name` from the signature not preamble, `SETTER`/`RETCONST` macro anchors, trailing-footer preservation). Built **`tools/jr_isolate_all.py`** (multi-cut resegment: config split at jr boundaries, source repartition + INCLUDE_ASM path repoint, banked-jr carve repoint, -O0 skip, ambient file-scope decl carry). **SINGLE-cut isolation BYTE-IDENTICAL** (func_8013FFD8 in `main`, clean `make build` `d19c9580`, R22) — Task 2 proven. **Full 54-jr isolation FAILED on the dense `_after` object** after fixing 7 C-scoping edge cases in sequence, on the deep one (byte-verified): **gcc-2.7.2 block-scope-extern TU-persistence** — `func_801734BC` uses `D_80126B3E` declared `extern s16` only inside `engine_core.h` `DEFINE_func` macros; splitting the TU separates the core from the earlier macro expansion that declares it → `undeclared`. Root: mechanical TU-splitting rebuilds the decl ENVIRONMENT (semantics), not just text — col-0 ambient-carry can't reach macro-injected decls. **Candidate fix (not built): declaration-completion** from a global `engine_core.h`+overlay symbol→type map (decision-log 2026-07-13, R31). **Baseline reverted + intact; 2 tools untracked, NOT committed** — surfaced the fork to Drew (P5a) before sinking more time. NEXT: Drew's steer (build declaration-completion vs pivot Stage-2), then commit the tools.
- **2026-07-12 (session 4, multi-jtbl `--order` + family-1 — the Stage-1 de-risk build; Max):** Recon proved the Stage-1 core risk is precise: `jtbl_family_bank.revert()` restores each sibling to committed = has func_8012ACE0's carve, so banking ANY 2nd jr family makes the overlay multi-jtbl. Built `ld_interleave.py --order` (address-ordered N-piece sandwich; legacy `--front/--tail` byte-untouched → main + 133 single-carve siblings unaffected) + rewrote `jtbl_carve.py` additive/regenerate-from-config (parses tail data-region + existing carves, splits the containing data piece for the new jtbl, re-emits `--order`; same-subseg → fails loud). Fixed `jtbl_family_bank.bank()` to `make extract` BEFORE the carve (asm must match the reverted config; the old error-string retry was fragile — 3 early siblings carve-failed). **Family-1 `func_801734BC` (34-ins PURE jr, ov_SC01_077_after) matched in ov077 (shared-tail switch idiom `case N: t=-N; break; default: goto after;`) + banked 133/133 siblings — CROSS-subseg multi-jtbl (func_8012ACE0 `_a` + func_801734BC `_after`), the primary Stage-1 goal. R22 clean-fleet 136/136 byte-identical (~52s).** Wrote `tools/jr_isolate.py` (same-subseg non-contiguous case — split a fn into its own subseg via `split_src_region`, `jtbl_carve` re-derives the carve subseg; ascending-order discipline avoids carve-renames) — not yet integrated/committed. §8a finding: the still-INCLUDE_ASM carve build fails (migrated rodata not emitted through INCLUDE_ASM) — OFF the banking critical path (bank swaps C before building). Committed (Drew pushes — R6). NEXT: prove isolation on a higher fn → integrate → pivot to Stage 2.
- **2026-07-12 (session 3, R22 parallelization — Drew asked "why so slow, profile+optimize"):** Profiled the clean-fleet R22 (`make clean` + extract-all + check-all). **Phase split: extract-all ~6m11s (the BIGGER phase, 136 serial `splat split` runs) + check-all ~2m58s (136 serial builds; most overlays ~1.3s, ov077 slow — maspsx on its 15K-line `.c`) = ~9 min.** Both phases were serial on a 32-core box (the old Makefile comment called cross-binary `-j` "racy"). **Verified the only shared write is the 4 generated `include/*.inc` macros at extract time (identical content); everything else is per-binary-disjoint and `include/` is read-only during a build → parallel is safe.** Added `JOBS ?= 16` + **`make extract-all`** (seed `main` serial for the macros, then parallel-extract the rest) + **parallel `make check-all`** (`xargs -P$(JOBS)`), correctness-gated. **Result: `make clean && make extract-all && make check-all` = ~50s, 136/136 byte-identical — a ~10.5× speedup (9m→~50s).** Committed the Makefile change (own commit). This compounds across the endgame (R22 runs every commit).
- **2026-07-12 (session 3, §8 ×134 AUTOMATION — func_8012ACE0 banked fleet-wide):** Built the jr-function ×134 harvest pipeline and proved it end-to-end. `tools/jtbl_carve.py` = per-overlay §8 carve generator (finding: the same fn is at the same vram across siblings but its jtbl is at a DIFFERENT address in each — ov_SC01_000 jtbl_801A331C / ov_SC02_000 jtbl_801E587C — so the carve is recomputed per sibling; rewrites the config data-tail subsegs + sets `<ov>_JTBL_INTERLEAVE`). `tools/jtbl_family_bank.py` = the sibling sweep driver (per sibling: revert→carve→extract→remap_hseq+reconcile→whole-binary gate; idempotent, revert-on-fail). **Propagation-cap fix:** `family_remap.extract_unit` now carries single-line `typedef`s (jr-function bodies define local `typedef struct{} Foo_<addr>;` that must template with the body, else the sibling sees `Foo undeclared` — additive, byte-gate-protected). **func_8012ACE0 family: 133/133 siblings BANKED, 0 failures; R22 clean-fleet 136/136 byte-identical.** Metrics: distinct-code 39.1% (50,698 unique fns), instr 63.0%. **Opportunity map (`has_mid_jr` families):** 237 total (5,805 members / 5.68M ins) — 46 small mid/tiny (771 members, mechanical, same pipeline) + 191 substantial = the Fable5 cores (Task 7, paused). Committed. **NEXT: (a) R22 profiling/parallelization (Drew asked — check-all builds 136 binaries serially on a 32-core box); (b) the other 45 small jr families.**
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""Phase-26 §8b: isolate every jr (switch) function in an overlay into its OWN code
subseg — the one-shot multi-cut resegment that unblocks the Stage-2 heavy-jr-core
harvest (each isolated jr carves its jtbl independently, so banking any core is a
clean fill with no same-subseg collision; cookbook §8/§8b, the whale `_o0b` precedent
generalized).
For each -O2 code object that contains jr-functions, the object is cut right BEFORE
each jr vram: [gap0][jr1 + trailing non-jr][jr2 + ...] ... . The leading gap keeps
the object's name; each jr-led region becomes `<ov>_jr_<addr>`. Source is repartitioned
(overlay_src_split, H5) and INCLUDE_ASM stub paths repointed to the new subseg. The 2
already-banked jr (their real-C is PRESERVED) have their `.rodata` carve repointed to
their new `_jr_<addr>` subseg (config piece + overlays.mk --order) — no un-banking, no
metric churn. -O0 objects (`*_o0`, `*_o0b`) are skipped (their new subsegs would lose
the Makefile -O0 flag; the heavy Stage-2 cores are all -O2).
Byte-neutral by construction: the split only reorganizes .text into more objects placed
contiguously in address order (the existing -O0 multi-object precedent), and the carve
bytes are unchanged (only the owning object's NAME changes). `make build` (SHA1) is the
sole arbiter (G3/P9/R22).
jr_isolate_all.py <ov> [--only func_X,func_Y] [--dry-run]
"""
import argparse
import glob
import json
import os
import re
import subprocess
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import overlay_src_split as oss
REPO = oss.REPO
O0_SUFFIX = ("_o0", "_o0b")
def sh(cmd):
return subprocess.run(cmd, shell=True, cwd=REPO, capture_output=True, text=True)
def oss_vram(ov):
txt = open(os.path.join(REPO, f"config/splat.{ov}.yaml")).read()
m = re.search(r"vram:\s*(0x[0-9A-Fa-f]+)", txt)
if not m:
sys.exit(f"jr_isolate_all: no vram in config for {ov}")
return int(m.group(1), 16)
def code_objects(cfg_lines):
"""[(line_idx, indent, off, name)] for every `- [off, c, name]` code piece, in order."""
objs = []
for i, ln in enumerate(cfg_lines):
m = re.match(r'^(\s*)- \[(0x[0-9A-Fa-f]+),\s*c,\s*(\w+)\]', ln)
if m:
objs.append((i, m.group(1), int(m.group(2), 16), m.group(3)))
return objs
def rodata_carves(cfg_lines):
"""[(line_idx, off, subseg)] for every `.rodata` carve piece."""
out = []
for i, ln in enumerate(cfg_lines):
m = re.match(r'^\s*- \[(0x[0-9A-Fa-f]+),\s*\.rodata,\s*(\w+)\]', ln)
if m:
out.append((i, int(m.group(1), 16), m.group(2)))
return out
def jr_inventory(ov):
"""Return (all_jr:{vram:src_kind}, banked:{vram:func_name}). src_kind in
{'asm','banked'}. jr = INCLUDE_ASM funcs whose .s references a jtbl_ + the
already-banked jr (real-C `def`/`define` items whose name is recorded in a
.run/banked_func_*.json — the exemplar is NOT in its own sibling list, so we
confirm presence by parsing the source, not by the sibling roster)."""
asm_jr = {}
for p in glob.glob(os.path.join(REPO, f"asm/{ov}/nonmatchings/*/*.s")):
if re.search(r'jtbl_[0-9A-Fa-f]{8}', open(p).read()):
fn = os.path.basename(p)[:-2]
if re.fullmatch(r'func_[0-9A-Fa-f]{8}', fn):
asm_jr[int(fn[5:], 16)] = fn
# candidate banked-jr names (global roster) -> confirm each is a real-C def here
cand = set()
for bj in glob.glob(os.path.join(REPO, ".run/banked_func_*.json")):
cand.add(os.path.basename(bj)[len("banked_"):-len(".json")])
realc = {} # addr -> name for def/define items
syms = oss.load_ov_syms(ov)
for cf in glob.glob(os.path.join(REPO, f"src/{ov}/*.c")):
_, items = oss.parse_overlay_c(open(cf).read(), syms)
for addr, name, kind, _ in items:
if kind in ("def", "define") and name and addr is not None:
realc[addr] = name
banked = {a: nm for a, nm in realc.items() if nm in cand}
alljr = dict(asm_jr)
alljr.update({a: "banked" for a in banked}) # marker; name in `banked`
return alljr, banked
def plan(ov, only=None):
"""Compute the resegment plan without touching disk. Returns a dict."""
base = oss_vram(ov)
cfg_lines = open(os.path.join(REPO, f"config/splat.{ov}.yaml")).read().splitlines()
objs = code_objects(cfg_lines)
obj_ranges = [] # (start_vram, end_vram, name, line_idx, indent)
for k, (li, ind, off, nm) in enumerate(objs):
end = objs[k + 1][2] if k + 1 < len(objs) else None
obj_ranges.append((base + off, (base + end) if end is not None else None, nm, li, ind))
def obj_of(vram):
for s, e, nm, li, ind in obj_ranges:
if s <= vram and (e is None or vram < e):
return nm
return None
alljr, banked = jr_inventory(ov)
if only:
only_addrs = {int(x[5:], 16) for x in only if re.fullmatch(r'func_[0-9A-Fa-f]{8}', x)}
alljr = {a: v for a, v in alljr.items() if a in only_addrs}
banked = {a: nm for a, nm in banked.items() if a in only_addrs}
# group jr by their -O2 object (skip -O0 objects + objects with no jr)
skipped_o0 = []
per_obj = {}
for a in sorted(alljr):
nm = obj_of(a)
if nm is None:
continue
if nm.endswith(O0_SUFFIX):
skipped_o0.append(a)
continue
per_obj.setdefault(nm, []).append(a)
# banked jr -> its object (for carve repoint)
banked_obj = {banked[a]: obj_of(a) for a in banked}
return {
"base": base, "cfg_lines": cfg_lines, "obj_ranges": obj_ranges,
"per_obj": per_obj, "banked": banked, "banked_obj": banked_obj,
"skipped_o0": skipped_o0,
}
def subseg_name(ov, vram):
return f"{ov}_jr_{vram:08X}" # uppercase hex, matching the func_XXXXXXXX convention
def build_new_config(ov, p):
"""Return (new_cfg_lines, new_files:{path:content}, carve_renames:{old_sub:new_sub})."""
base = p["base"]
cfg_lines = list(p["cfg_lines"])
syms = oss.load_ov_syms(ov)
# 1) source repartition + the config code-region replacement (per object, bottom-up so
# line indices stay valid).
new_files = {}
replacements = [] # (line_idx, [new config lines])
carve_renames = {} # old code-subseg name -> banked jr's new _jr_ subseg
banked_by_obj = {}
for fn, obj in p["banked_obj"].items():
banked_by_obj.setdefault(obj, []).append(fn)
for s, e, nm, li, ind in p["obj_ranges"]:
if nm not in p["per_obj"]:
continue
cuts = p["per_obj"][nm] # jr vrams in this object
srcpath = os.path.join(REPO, f"src/{ov}/{nm}.c")
header, regions = _partition(srcpath, cuts, syms)
# region 0 (lo=None) keeps the object name; each jr-led region -> _jr_<lo>. Regions are
# processed in address order, accumulating this object's file-scope decls as `ambient` so
# each region carries the decl context it had in the original single object.
cfg_block = []
ambient = []
for (lo, hi, items) in regions:
sub = nm if lo is None else subseg_name(ov, lo)
off = (s if lo is None else lo) - base
cfg_block.append(f"{ind}- [{hex(off)}, c, {sub}]")
body = _render_region(header, items, old_sub=nm, new_sub=sub, ambient=ambient)
new_files[os.path.join(REPO, f"src/{ov}/{sub}.c")] = body
ambient = ambient + _file_scope_decls(items) # context for later regions
# a banked jr leading this region -> its carve must repoint to `sub`
if lo is not None:
for fn in banked_by_obj.get(nm, []):
if int(fn[5:], 16) == lo:
carve_renames[nm] = sub
replacements.append((li, cfg_block))
# apply config code-region replacements bottom-up
for li, block in sorted(replacements, reverse=True):
cfg_lines[li:li + 1] = block
# 2) repoint the .rodata carve pieces to the banked jr's new subseg
for i, ln in enumerate(cfg_lines):
m = re.match(r'^(\s*- \[0x[0-9A-Fa-f]+,\s*\.rodata,\s*)(\w+)(\].*)$', ln)
if m and m.group(2) in carve_renames:
cfg_lines[i] = m.group(1) + carve_renames[m.group(2)] + m.group(3)
return cfg_lines, new_files, carve_renames
def _partition(srcpath, cuts, syms):
"""overlay_src_split.partition but taking a preloaded syms dict."""
header, items = oss.parse_overlay_c(open(srcpath).read(), syms)
if any(it[0] is None and it[2] == "tail" for it in items):
sys.exit(f"jr_isolate_all: unaddressable content in {srcpath}")
footer = [it for it in items if it[2] == "footer"]
addressed = [it for it in items if it[0] is not None]
cuts = sorted(set(cuts))
bounds = [None] + cuts + [None]
regions = []
for lo, hi in zip(bounds[:-1], bounds[1:]):
sel = sorted((it for it in addressed
if (lo is None or it[0] >= lo) and (hi is None or it[0] < hi)),
key=lambda it: it[0])
regions.append((lo, hi, sel))
if footer:
lo, hi, sel = regions[-1]
regions[-1] = (lo, hi, sel + footer)
return header, regions
# a hoistable declaration line: an `extern` decl, or a func/data prototype ending in `;`
# with no `{` body. These are legal to REPEAT in C (unlike typedef/struct/enum), so we hoist a
# deduped copy to the region top — every symbol is then declared before any body uses it (a cut can
# strand a use above its in-region decl; the source redundantly re-declares externs per fn-group).
_HOIST_RE = re.compile(
r'^\s*(?:extern\b.*;'
r'|[A-Za-z_][\w\*\s]*\b(?:func_[0-9A-Fa-f]{8}|D_[0-9A-Fa-f]{8})\b[^{]*;)\s*(?:/\*.*\*/)?\s*$')
_SYM_RE = re.compile(r'\b((?:D_|func_)[0-9A-Fa-f]{8})\b')
# a decl whose base type is a BUILTIN / include-provided type is safe to hoist to the region
# top; one that names a FILE-LOCAL type (`Vec8`, `struct BigCopy`, ...) must stay where the type
# is defined (hoisting it above the typedef is a parse error), so it is NOT carried as ambient.
_SAFE_TYPE = re.compile(
r'^\s*(?:extern\s+)?(?:const\s+)?(?:(?:un)?signed\s+)?'
r'(?:void|char|short|int|long|float|double'
r'|[su](?:8|16|32|64)|M2C_UNK|MNC_UNK)\b')
def _file_scope_decls(items):
"""[(line, [syms])] for each COL-0 (file-scope) extern/proto decl with a builtin base type,
in item order (file-local-typed decls are left in place — see _SAFE_TYPE)."""
out = []
for _, _, _, text in items:
for line in text.split("\n"):
if not line or line[0].isspace(): # col-0 only (block-scope stays put)
continue
if "{" in line or "}" in line:
continue
if _HOIST_RE.match(line) and _SAFE_TYPE.match(line):
out.append((line.rstrip(), _SYM_RE.findall(line)))
return out
def _render_region(header, items, old_sub, new_sub, ambient):
"""Region .c = header + AMBIENT file-scope decls (from earlier regions of this object, in
original order, deduped by symbol) + the region's items unchanged. Ambient preserves the
exact decl visibility each function had in the original single object (a cut otherwise
strands a use above the decl that lived in an earlier region) — byte-neutral (decls emit
nothing; order preserved). Prepending is always safe: a symbol that has a file-scope decl
cannot also carry a *different*-typed block-scope shadow (the original would not compile),
so a same-typed block-scope redeclaration below is compatible."""
if new_sub != old_sub:
items = [(a, n, k, oss.rewrite_asm_subseg(t, old_sub, new_sub)) for a, n, k, t in items]
lines, seen = [], set()
for line, syms in ambient:
key = tuple(sorted(syms)) if syms else (line,)
if key in seen:
continue
seen.add(key)
lines.append(line)
parts = [header]
if lines:
parts.append("/* Phase-26 §8b jr_isolate_all.py: ambient file-scope decls carried from "
"earlier code regions of this object (original order, shadow-excluded => "
"byte-neutral). */\n" + "\n".join(lines))
parts.extend(t for _, _, _, t in items)
return "\n".join(parts) + "\n"
def repoint_overlays_mk(carve_renames, dry):
mk = os.path.join(REPO, "config/overlays.mk")
txt = open(mk).read()
changed = []
for old_sub, new_sub in carve_renames.items():
pat = rf'(--order[^#\n]*?){re.escape(old_sub)}\.o'
if re.search(pat, txt):
txt = re.sub(pat, lambda m: m.group(1) + new_sub + ".o", txt, count=1)
changed.append(f"{old_sub}.o -> {new_sub}.o")
if not dry:
open(mk, "w").write(txt)
return changed
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("ov")
ap.add_argument("--only", help="comma-separated func_XXXX to isolate (default: all -O2 jr)")
ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args()
only = set(a.only.split(",")) if a.only else None
p = plan(a.ov, only)
n_jr = sum(len(v) for v in p["per_obj"].values())
print(f"jr_isolate_all {a.ov}: {n_jr} jr in {len(p['per_obj'])} -O2 objects "
f"{ {k: len(v) for k, v in p['per_obj'].items()} }")
if p["skipped_o0"]:
print(f" SKIPPED {len(p['skipped_o0'])} jr in -O0 objects: {[hex(x) for x in p['skipped_o0']]}")
if not p["per_obj"]:
print(" nothing to isolate.")
return
cfg_lines, new_files, carve_renames = build_new_config(a.ov, p)
mk_changes = repoint_overlays_mk(carve_renames, dry=True)
print(f" -> {len(new_files)} region .c files; carve repoints: {carve_renames or '(none)'}")
for c in mk_changes:
print(f" overlays.mk --order: {c}")
if a.dry_run:
print(" [dry-run] no files written.")
return
# write config, source region files, overlays.mk
cfg_path = os.path.join(REPO, f"config/splat.{a.ov}.yaml")
open(cfg_path, "w").write("\n".join(cfg_lines) + "\n")
# remove the original per-object .c files that were replaced (region 0 rewrites them;
# extra regions are new — but a stale original with the OLD single-object content would
# shadow nothing since we overwrite region 0 to the same path). Write all region files:
for path, content in new_files.items():
open(path, "w").write(content)
repoint_overlays_mk(carve_renames, dry=False)
print(f" wrote config + {len(new_files)} region files + overlays.mk. Run `make extract "
f"BINARY={a.ov} && make build BINARY={a.ov}` to byte-gate (R22).")
if __name__ == "__main__":
main()
+400
View File
@@ -0,0 +1,400 @@
#!/usr/bin/env python3
"""Overlay-`.c`-aware source partition (Phase-26 §8b — jr isolation).
The overlay `src/<ov>/*.c` files carry a far richer top-level structure than the
library-region `split_src_region.py` was built for, so its "header = leading
#includes; every following item resolves to one address" model chokes (~922
unresolved). This module parses an overlay `.c` into (header, [Item]) correctly:
header = the leading `#include`s + the banner-delimited Phase-17 canonical-sig
extern layer, preserved verbatim (a file-scope decl layer every split
piece needs).
Item = a leading PREAMBLE (blank lines, `// @class`/comment annotations, and
*declarations* — `extern` protos, `typedef`/`struct`/`union`/`enum`,
`#define` — anything that is not itself an addressed anchor) attached
to the following ADDRESSED ANCHOR, which is one of:
- a function DEFINITION (`<type> func_XXXX(...) { ... }`)
- an `INCLUDE_ASM("asm/...", func_XXXX);` stub
- a `DEFINE_func_XXXXXXXX()` dedup macro (shared engine-core body)
- a `#ifdef NON_MATCHING ... #endif` block
The anchor's address is its `func_XXXX` / `DEFINE_func_XXXX` hex, else a
`config/symbols.<ov>.txt` lookup.
The definition-vs-declaration discriminator is a forward brace/semicolon scan (NOT
a regex on one line): a *definition* has a `{` body not terminated by `;`; a
*declaration* ends at a top-level `;` before any `{`, or is a braced type ending
`};`. This is what distinguishes `void func_X(void) { ... }` (anchor) from
`s32 func_X(...); /* extern */` (preamble — the `extern` keyword is omitted, the
line ends in a trailing comment, so an "ends-with-;" test misfires).
Primary consumer: `jr_isolate_all.py` (multi-cut resegment), via `partition()`.
The whole-binary SHA1 byte-gate remains the sole correctness arbiter (G3/P9).
"""
import os
import re
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CANON_OPEN = re.compile(r'/\*\s*=+\s*Phase-\d+ canonical-sig layer')
CANON_CLOSE = re.compile(r'end canonical-sig layer')
INCLUDE_ASM = re.compile(r'^\s*INCLUDE_ASM\("[^"]*",\s*(\w+)\)')
DEFINE_FUNC = re.compile(r'^DEFINE_func_([0-9A-Fa-f]{8})\s*\(')
# a dedup macro whose address is its FIRST arg: SETTER(func_X, ..), RETCONST(func_X, ..)
MACRO_ARG_ANCHOR = re.compile(r'^[A-Za-z_]\w*\(\s*(func_[0-9A-Fa-f]{8})\b')
NONMATCH = re.compile(r'^#if(?:def)?\s+.*NON_MATCHING')
def load_syms(path):
s = {}
if not os.path.exists(path):
return s
for ln in open(path):
m = re.match(r"(\w+)\s*=\s*0x([0-9A-Fa-f]+)", ln)
if m:
s[m.group(1)] = int(m.group(2), 16)
return s
def addr_of(name, syms):
if not name:
return None
m = re.match(r'func_([0-9A-Fa-f]{8})$', name)
if m:
return int(m.group(1), 16)
return syms.get(name)
def item_func_name(text):
"""The addressed name for an INCLUDE_ASM / NON_MATCHING item (name is in the
INCLUDE_ASM macro)."""
m = re.search(r'INCLUDE_ASM\("[^"]*",\s*(\w+)\)', text)
return m.group(1) if m else None
def def_name(construct_lines):
"""The defined function's name = the identifier immediately before the first
params `(` in the (comment/string-stripped) definition signature. Robust to a
leading banner comment and to multi-line prototypes in a shared preamble (those
never reach here — scan_construct hands us the definition construct only)."""
code = []
in_block = False
for line in construct_lines:
c = line
if in_block:
if "*/" in c:
c = c.split("*/", 1)[1]
in_block = False
else:
continue
c = re.sub(r'/\*.*?\*/', '', c)
if "/*" in c:
c = c.split("/*", 1)[0]
in_block = True
c = re.sub(r'//.*$', '', c)
code.append(c)
if "(" in "".join(code):
break
joined = "".join(code)
m = re.search(r'([A-Za-z_]\w*)\s*\(', joined)
return m.group(1) if m else None
def _strip(line, in_block):
"""Blank out // and /* */ comments (block state carried) + string/char literals,
for brace/paren/semicolon token counting. Returns (code, in_block)."""
c = line
if in_block:
if "*/" in c:
c = c.split("*/", 1)[1]
in_block = False
else:
return "", True
c = re.sub(r'/\*.*?\*/', '', c)
if "/*" in c:
c = c.split("/*", 1)[0]
in_block = True
c = re.sub(r'//.*$', '', c)
c = re.sub(r'"(?:\\.|[^"\\])*"', '""', c)
c = re.sub(r"'(?:\\.|[^'\\])*'", "''", c)
return c, in_block
_DECL_KW = ("extern", "typedef", "struct", "union", "enum")
def split_header(lines):
"""Return the number of leading lines that form the header (includes + the
banner-delimited canonical-sig layer, if present)."""
n = len(lines)
i = 0
while i < n and (lines[i].startswith("#include") or lines[i].strip() == ""):
i += 1
if i < n and CANON_OPEN.search(lines[i]):
while i < n and not CANON_CLOSE.search(lines[i]):
i += 1
if i < n:
i += 1 # consume the close-marker line
return i
def scan_construct(lines, i):
"""From top-level line `i`, return (end_exclusive, is_definition).
Handles, brace/paren/comment/string aware:
- ANSI function def `<type> name(<typed args>) { ... }` -> definition
- K&R function def `<type> name(a, b) <type a; type b;> {` -> definition
- prototype `<type> name(...);` -> declaration
- extern / data decl `extern ...;` `<type> x;` -> declaration
- braced type `typedef struct { ... } T;` `struct S {...};` -> decl
A function header is `name(...)` closed at depth 0; a `;` immediately after its
`)` is a prototype, a `;` after intervening K&R param decls is not the end, and a
`{` after the header opens the body. Declarations starting with a decl keyword
(extern/typedef/struct/union/enum) never become definitions (guards fn-ptr
typedefs and struct-returning shapes absent from this codebase)."""
n = len(lines)
first = lines[i].lstrip()
force_decl = any(first == k or first.startswith(k + " ") or first.startswith(k + "\t")
for k in _DECL_KW)
paren = brace = 0
seen_header = False # closed a depth-0 (...) that is a function header
body_open = False
since_header = [] # code chars since the header ) (empty => prototype at ;)
in_block = False
j = i
while j < n:
code, in_block = _strip(lines[j], in_block)
for ch in code:
if ch == '(':
paren += 1
elif ch == ')':
if paren > 0:
paren -= 1
if paren == 0 and brace == 0 and not seen_header and not force_decl:
seen_header = True
since_header = []
continue
elif ch == '{':
brace += 1
if brace == 1:
body_open = True
elif ch == '}':
if brace > 0:
brace -= 1
if brace == 0 and body_open:
if seen_header and not force_decl:
return j + 1, True # function body closed
body_open = False # braced type -> await terminating ;
elif ch == ';' and paren == 0 and brace == 0:
if seen_header and not body_open and not force_decl:
if "".join(since_header).strip() == "":
return j + 1, False # prototype: ) ;
# else: a K&R param decl -> keep scanning for the body {
else:
return j + 1, False # plain / braced declaration end
if seen_header and not body_open:
since_header.append(ch)
j += 1
return n, (seen_header and not force_decl) or body_open
def parse_overlay_c(src, syms):
"""Return (header, items) where items = [(addr, name, kind, text)] in file order.
kind in {asm, define, def, nonmatch, tail}. `tail` = trailing content with no
following anchor (addr None) — normally absent in a well-formed file."""
lines = src.split("\n")
hdr_end = split_header(lines)
header = "\n".join(lines[:hdr_end])
n = len(lines)
items = []
i = hdr_end
pre_start = i
while i < n:
raw = lines[i]
s = raw.strip()
# ---- peel non-anchor prefix lines into the accumulating preamble ----
if s == "":
i += 1
continue
if s.startswith("//"):
i += 1
continue
if s.startswith("/*"): # block comment (1+ lines)
while i < n and "*/" not in lines[i]:
i += 1
i = min(i + 1, n)
continue
if s.startswith("#") and not NONMATCH.match(raw): # #define/#undef/#include...
while i < n and lines[i].rstrip().endswith("\\"):
i += 1
i += 1
continue
# ---- now `raw` is a real construct: an anchor or a def/decl ----
m = INCLUDE_ASM.match(raw)
if m:
items.append((addr_of(m.group(1), syms), m.group(1), "asm",
"\n".join(lines[pre_start:i + 1])))
i += 1
pre_start = i
continue
m = DEFINE_FUNC.match(raw)
if m:
items.append((int(m.group(1), 16), "func_" + m.group(1), "define",
"\n".join(lines[pre_start:i + 1])))
i += 1
pre_start = i
continue
m = MACRO_ARG_ANCHOR.match(raw)
if m: # SETTER/RETCONST(func_X, ...) dedup macro (1 line)
items.append((int(m.group(1)[5:], 16), m.group(1), "define",
"\n".join(lines[pre_start:i + 1])))
i += 1
pre_start = i
continue
if NONMATCH.match(raw):
j = i
while j < n and not lines[j].startswith("#endif"):
j += 1
j = min(j + 1, n)
name = item_func_name("\n".join(lines[i:j]))
items.append((addr_of(name, syms), name, "nonmatch",
"\n".join(lines[pre_start:j])))
i = j
pre_start = i
continue
# a definition-or-declaration; classify by forward scan
j, is_def = scan_construct(lines, i)
if is_def:
name = def_name(lines[i:j])
items.append((addr_of(name, syms), name, "def",
"\n".join(lines[pre_start:j])))
i = j
pre_start = i
else:
i = j # declaration => stays in the preamble
if pre_start < n:
# preserve the exact trailing chunk (incl. blank lines) so round-trip stays
# exact; a comment/blank-only chunk is a harmless footer, anything else is a
# genuine parser gap (kind=tail -> partition() fails loud).
trailing = "\n".join(lines[pre_start:])
kind = "footer" if _comment_blank_only(trailing) else "tail"
items.append((None, None, kind, trailing))
return header, items
def _comment_blank_only(text):
"""True if `text` is only // / /* */ comments and whitespace."""
stripped = re.sub(r'/\*.*?\*/', '', text, flags=re.S)
stripped = re.sub(r'//.*', '', stripped)
return stripped.strip() == ""
def load_ov_syms(ov):
"""Merge every symbol file the overlay's splat config actually links
(config's symbol_addrs_path) — us + resident + per-overlay."""
cfg = os.path.join(REPO, f"config/splat.{ov}.yaml")
syms = {}
in_list = False
for ln in open(cfg):
if re.match(r'\s*symbol_addrs_path:', ln):
in_list = True
continue
if in_list:
m = re.match(r'\s*-\s*(\S+)', ln)
if m and m.group(1).endswith(".txt"):
syms.update(load_syms(os.path.join(REPO, m.group(1))))
else:
break
if not syms: # fallback if the key wasn't found
syms = load_syms(os.path.join(REPO, "config/symbols.us.txt"))
return syms
REAL_KINDS = ("def", "define", "nonmatch") # items splat will NOT regenerate
def rewrite_asm_subseg(text, old_sub, new_sub):
"""Repoint an INCLUDE_ASM stub's path component to the new subseg name."""
return text.replace(f'/nonmatchings/{old_sub}"', f'/nonmatchings/{new_sub}"')
def partition(srcpath, cuts, syms_path, verbose=True):
"""Split `srcpath` at the sorted vram `cuts` (each = the low bound of a new
region). Returns [(lo, hi, [item...]), ...] region descriptors WITHOUT writing
(the caller assigns subseg names + output paths). lo/hi are vram bounds; the
first region is [start, cuts[0]) and the last is [cuts[-1], +inf)."""
header, items = parse_overlay_c(open(srcpath).read(), load_syms(syms_path))
tail = [it for it in items if it[0] is None and it[2] == "tail"]
if tail:
sys.exit(f"partition: {srcpath} has unaddressable trailing content:\n"
f"{tail[0][3][:160]}")
footer = [it for it in items if it[2] == "footer"]
addressed = [it for it in items if it[0] is not None]
cuts = sorted(set(cuts))
bounds = [None] + cuts + [None] # (start,cut0),(cut0,cut1),...,(cutN,end)
regions = []
for lo, hi in zip(bounds[:-1], bounds[1:]):
sel = sorted((it for it in addressed
if (lo is None or it[0] >= lo) and (hi is None or it[0] < hi)),
key=lambda it: it[0])
regions.append((lo, hi, sel))
if footer: # documentation footer -> stays with the last region
lo, hi, sel = regions[-1]
regions[-1] = (lo, hi, sel + footer)
if verbose:
for lo, hi, sel in regions:
rc = sum(1 for it in sel if it[2] in REAL_KINDS)
print(f" region [{'.' if lo is None else hex(lo)},"
f"{'.' if hi is None else hex(hi)}): {len(sel)} items ({rc} real-C)")
return header, regions
# --------------------------------------------------------------------------- CLI
def selftest(srcpath, syms):
"""Round-trip + anchor-order + kind census — proves the parser on a real file."""
src = open(srcpath).read()
header, items = parse_overlay_c(src, syms)
rebuilt = header + "\n" + "\n".join(it[3] for it in items)
# exact round-trip (allow a single trailing-newline difference)
ok = rebuilt == src or rebuilt + "\n" == src or rebuilt == src + "\n"
from collections import Counter
kinds = Counter(it[2] for it in items)
addrs = [it[0] for it in items if it[0] is not None]
unres = [it for it in items if it[0] is None and it[2] != "footer"]
mono = sum(1 for a, b in zip(addrs, addrs[1:]) if b < a)
print(f"{os.path.basename(srcpath)}: {len(items)} items {dict(kinds)}")
print(f" round-trip exact: {ok}")
print(f" addressed: {len(addrs)} unresolved(non-footer): {len(unres)} "
f"non-monotonic transitions: {mono}")
if unres:
for it in unres[:5]:
print(f" UNRESOLVED [{it[2]}]: {it[3].strip()[:90]}")
if not ok:
# locate first divergence for debugging
a, b = src.split("\n"), rebuilt.split("\n")
for idx in range(min(len(a), len(b))):
if a[idx] != b[idx]:
print(f" first diff at line {idx+1}:\n orig: {a[idx][:80]!r}\n"
f" got : {b[idx][:80]!r}")
break
print(f" line counts: orig={len(a)} rebuilt={len(b)}")
return ok and not unres and mono == 0
def _ov_from_path(p):
m = re.search(r'src/(ov_\w+?)/', p)
return m.group(1) if m else None
def main():
args = sys.argv[1:]
if len(args) == 2 and args[0] == "selftest":
ov = _ov_from_path(args[1])
syms = load_ov_syms(ov) if ov else load_syms(os.path.join(REPO, "config/symbols.us.txt"))
ok = selftest(args[1], syms)
sys.exit(0 if ok else 1)
sys.exit(__doc__)
if __name__ == "__main__":
main()