From fcd83f705ed34fbf07c4834e418d50b24d7f3e51 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:30:30 -0600 Subject: [PATCH] =?UTF-8?q?fix(phase-29):=20the=20undo=20was=20EATING=20th?= =?UTF-8?q?e=20tree=20=E2=80=94=20snapshot=20src=20too;=20kill=20the=20?= =?UTF-8?q?=C2=A758=20label;=20giant=20func=5F8018F694=20banked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE DEFECT (byte-witnessed, and it is the §61c mechanism). `jr_isolate_all` repartitions a code object by writing region 0 back over the ORIGINAL src//.c — TRUNCATED to just that region — and emitting the rest as new _jr_.c files. `_jtbl_restore` undid only config/ + the new region files, so every gate-REJECTED draft left the original TU permanently truncated and its stubs gone. Nothing regenerates them (splat does not rewrite a committed overlay .c). Measured across an 11-draft re-probe: live stubs 419 -> 414 -> 406 -> 395, ending in `undefined reference to func_80191C50`. It is INVISIBLE to the gate that causes it: the incremental build keeps linking stale objects (§42b) so `make build` stays green while a CLEAN rebuild fails. That is exactly the "139/140, twice" signature that became the §61c blocker — the tree was being eaten by the undo meant to protect it. The §61c attribution (batch _jtbl_prep residue) is now a demonstrated defect, not an inference. FIXES (both negative-control-validated on a draft that fails): - _jtbl_snapshot captures every src//*.c; _jtbl_restore restores them and removes exactly the files the attempt created (derived from the snapshot's file set, not re-guessed from the _jr_* name shape, R33). Undo by SNAPSHOT-RESTORE, never an inverse transform — §61's law one level deeper. Same failing draft that previously broke the tree now leaves it byte-identical, git status clean. - classify_fail ignores `warning:` lines. The benign `conflicting types for built-in function 'memcpy'` warning was winning the match on 8 of 8 failures across four different real causes — a label identical for every input, which the cookbook had to work around by hand ("the gate label is useless here, splice individually and read real cc1 stderr"). Now reports the real error, and falls through to CC1-FAIL: rather than guessing. THE RE-PROBE THIS ENABLED (11 preserved t5wave cracks, one invocation each): - BANKED: func_8018F694 (478 ins) — one of the wave's three giants, previously recorded as part of "the gate banked ZERO". - The other 10 now carry TEN DISTINCT diagnoses: 4 data-decl conflicts (D_80193B64 x2, D_8011D030, D_80126B5C), 3 callee-decl (func_80135480 x2, func_8012F14C), 3 self-decl/own-sig (§57). ZERO jtbl-drift, ZERO local-type redefinition, ZERO codegen DIFF. - So §61a's "§8e-2 jtbl table-count drift blocks 10 of 12" does NOT survive the carve-follows-splice prep: the carve now succeeds and what is left is ordinary decl plumbing the existing ladder already handles (cast_call_sites / reconcile_tu / normalize_self_decls / fix_arity_callers) = Task 14 stages 2-3, no new tooling. - ov_SC06_018 BYTE-IDENTICAL cbbc4f44 with the giant banked; nothing committed broken at any point (tree was restored from HEAD and re-verified before these fixes) - cookbook: the `git add -u` complementary hole (an isolation's NEW region file is untracked, so a carve/isolation bank needs `git add -A src/ config/`) --- docs/matching-cookbook.md | 6 + src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c | 180 +++++++++++++++++++++- tools/harvest_verify.py | 72 ++++++--- 3 files changed, 238 insertions(+), 20 deletions(-) diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 56e8a2f09..f989d6987 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -4462,6 +4462,12 @@ diverged. **the moment it banks**. Fix: `git add -u src/` (every modified tracked file under src/), which also retires the `src/ov_*/*.c` filename glob that once omitted 4 R22-verified banks from a commit because a family's members do not all live in the same-named split. + **COMPLEMENTARY HOLE (2026-07-22): `git add -u` misses the NEW files an ISOLATION creates.** A jtbl + sweep cuts a fresh region file per sibling (`src//_jr_.c`), which is UNTRACKED — so + `-u` commits the modified TU and drops the file holding the banked body, i.e. a tree that cannot + clean-rebuild. For any carve/isolation bank use **`git add -A src/ config/`**. `jtbl_family_bank` + already refuses to sweep on an uncommitted `config/`+`src/` (its per-sibling revert restores from + HEAD), and that guard is what caught this — a fail-loud precondition doing exactly its job. 2. **The `_xform` ladder dirs accumulate.** `-cn/-cast/-rc/-uni` are reused across runs and the transform tools only write the drafts they are handed, so every stale draft from every previous run survives and is re-submitted to the byte-gate. Measured: the grinder submitted **1** draft, the gate diff --git a/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c b/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c index 98162983c..e3622d66d 100644 --- a/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c +++ b/src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c @@ -4290,7 +4290,185 @@ INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018EDB INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018F060); -INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018F694); +// @class: other +// @stuck: none — MATCH (478 ins, relocation-masked byte-identical) + +#define gte_ldv0(r0) __asm__ volatile ( \ + "lwc2 $0, 0( %0 );" \ + "lwc2 $1, 4( %0 )" \ + : \ + : "r"( r0 ) ) + +#define gte_rtps() __asm__ volatile ("nop;nop;rtps") + +#define gte_stsxy(r0) __asm__ volatile ( \ + "swc2 $14, 0( %0 )" \ + : \ + : "r"( r0 ) \ + : "memory" ) + +#define gte_stszotz(r0) __asm__ volatile ( \ + "mfc2 $12, $19;" \ + "nop;" \ + "sra $12, $12, 2;" \ + "sw $12, 0( %0 )" \ + : \ + : "r"( r0 ) \ + : "$12", "memory" ) + +#define gte_stflg(r0) __asm__ volatile ( \ + "cfc2 $12, $31;" \ + "nop;" \ + "sw $12, 0( %0 )" \ + : \ + : "r"( r0 ) \ + : "$12", "memory" ) + +typedef struct { u16 vx, vy, vz, pad; } UVEC_8018F694; +typedef struct { u32 addr : 24; u8 len; } PTag_8018F694; +typedef struct { + u32 addr : 24; + u8 len; + u8 r0, g0, b0, code; + s16 x0, y0, x1, y1; +} LineF2_8018F694; +typedef struct { + u32 addr : 24; + u8 len; + u32 code0; +} TPage_8018F694; +typedef struct { + u8 r, g, b, pad0; + u8 dr, dg, db, pad1; + s16 vx, vy, vz, tm; +} Blip_8018F694; + +void func_8018F694(void) +{ + extern void func_8004914C(void *); + extern void func_800491AC(void *); + extern void *func_80010A08(s32); + extern u8 D_800AF648; + extern u8 D_800A6610[]; + extern short D_800B9A02; + extern Blip_8018F694 D_801D57F0[]; + + UVEC_8018F694 sxy; + UVEC_8018F694 p0; + UVEC_8018F694 p1; + long flag; + long otz; + s32 i; + PTag_8018F694 *ot; + PTag_8018F694 *otp; + Blip_8018F694 *bp; + s32 t, c, d, z, x, n; + + ot = (PTag_8018F694 *)&D_800A6610[(*(u16 *)&D_800B9A02) << 14]; + func_8004914C(&D_800AF648); + func_800491AC(&D_800AF648); + bp = D_801D57F0; + +#define EMIT_8018F694() { \ + LineF2_8018F694 *pk = (LineF2_8018F694 *)func_80010A08(0x10); \ + pk->len = 3; \ + pk->code = 0x42; \ + pk->r0 = bp->r; \ + pk->g0 = bp->g; \ + pk->b0 = bp->b; \ + pk->x0 = p0.vx; \ + pk->y0 = p0.vy; \ + pk->x1 = p1.vx; \ + pk->y1 = p1.vy; \ + pk->addr = otp->addr; \ + otp->addr = (u32) pk; } + + for (i = 0; i < 32; i++, bp++) { + t = bp->tm; + __asm__ __volatile__ ("" : "=r" (t) : "0" (t)); + if (t != 0) { + gte_ldv0(&bp->vx); + gte_rtps(); + gte_stsxy((long *) &sxy); + gte_stflg(&flag); + gte_stszotz(&otz); + if ((u16) (sxy.vx + 0x200) < 0x401 && + (u16) (sxy.vy + 0x180) < 0x301 && + (u32) (otz - 0x10) < 0x1000) { + z = otz - 0x10; + c = t >> 6; + d = (t * 3) >> 8; + otp = (PTag_8018F694 *) ((z << 2) + (s32) ot); + + p0.vx = sxy.vx - c; + p0.vy = sxy.vy - c; + p1.vx = p0.vx + d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy + d; + EMIT_8018F694(); + + p0.vx = sxy.vx + c; + p1.vx = p0.vx - d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy + d; + EMIT_8018F694(); + + p0.vy = sxy.vy + c; + p1.vx = p0.vx - d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy - d; + EMIT_8018F694(); + + p0.vx = sxy.vx - c; + p1.vx = p0.vx + d; + p1.vy = p0.vy; + EMIT_8018F694(); + + p1.vx = p0.vx; + p1.vy = p0.vy - d; + EMIT_8018F694(); + + { + TPage_8018F694 *tp = (TPage_8018F694 *) func_80010A08(8); + tp->len = 1; + tp->code0 = 0xE100002A; + tp->addr = otp->addr; + otp->addr = (u32) tp; + } + + n = bp->r; + x = n - bp->dr; + n = x; + if (x < 0) { n = 0; } + bp->r = n; + n = bp->g; + x = n - bp->dg; + n = x; + if (x < 0) { n = 0; } + bp->g = n; + n = bp->b; + x = n - bp->db; + n = x; + if (x < 0) { n = 0; } + bp->b = n; + x = n | (bp->r | bp->g); + if ((u8) x == 0) { + bp->tm = 0; + } + } + } + } +} + INCLUDE_ASM("asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C", func_8018FE0C); diff --git a/tools/harvest_verify.py b/tools/harvest_verify.py index bfc777d4f..f20dd1755 100644 --- a/tools/harvest_verify.py +++ b/tools/harvest_verify.py @@ -97,13 +97,23 @@ def classify_fail(got_sha): return 'SKIP' if got_sha is not None: return 'DIFF' - m = _PLUMBING.search(_last_err) - if m: - # pull the offending line for the log (e.g. "redefinition of 's16'") - for ln in _last_err.splitlines(): - if m.re.search(ln): - return 'PLUMBING: ' + ln.strip()[:90] - return 'PLUMBING: ' + m.group(0).lower() + # §58 RED-HERRING GUARD (2026-07-22). The build log is full of BENIGN WARNINGS — chiefly + # `warning: conflicting types for built-in function 'memcpy'`, which fires from an unrelated + # TU position on essentially every overlay build. The old code searched the whole stderr and + # returned the FIRST match, so that warning won: an 11-draft re-probe returned the SAME + # 'PLUMBING: …conflicting types for built-in fu' label for 8 of 8 failures, across four + # genuinely different causes. A label that is identical for every input carries no + # information, and it is worse than none — the cookbook had to record "the gate label is + # useless here, splice individually and read real cc1 stderr" as a manual step. Classify on + # NON-warning lines only, and when nothing but warnings matched, surface the real error + # instead of guessing (R32: report the gap, do not paper over it). + lines = [ln for ln in _last_err.splitlines() if 'warning:' not in ln] + for ln in lines: + if _PLUMBING.search(ln): + return 'PLUMBING: ' + ln.strip()[:90] + errs = [ln for ln in lines if re.search(r'\berror\b|\bError \d', ln)] + if errs: + return 'CC1-FAIL: ' + errs[-1].strip()[:90] return 'CC1-FAIL' @@ -209,7 +219,22 @@ def _reload_corpus(): def _jtbl_snapshot(): - """Text of the config files a carve/isolation may rewrite (for an exact undo).""" + """Text of EVERY file a carve/isolation may rewrite — config AND the binary's sources. + + SOURCES ARE NOT OPTIONAL (byte-witnessed 2026-07-22). `jr_isolate_all` repartitions a code + object by writing region 0 back over the ORIGINAL `src//.c` (truncated to just that + region) and emitting the rest as new `_jr_.c` files. An undo that restores only config/ + and deletes the new region files therefore leaves the original TU PERMANENTLY TRUNCATED — + its stubs are gone, and nothing regenerates them (splat does not rewrite a committed + overlay .c). Measured on an 11-draft re-probe: live stubs fell 419 -> 414 -> 406 -> 395 as + successive rejected drafts each ate a TU, ending in `undefined reference to func_80191C50`. + + Worse, it is INVISIBLE to the gate that caused it: the incremental build keeps linking the + stale objects (§42b), so `make build` stays green while a CLEAN rebuild fails. That is the + mechanism behind the "139/140, twice" reading that became the §61c blocker — the tree was + being eaten by the very undo meant to protect it. + + So snapshot the whole source set, and undo by RESTORE, never by an inverse transform (§61).""" out = {} for q in (os.path.join(REPO, 'config/splat.%s.yaml' % a.binary), os.path.join(REPO, 'config/overlays.mk')): @@ -217,10 +242,15 @@ def _jtbl_snapshot(): out[q] = open(q).read() except OSError: pass + for q in glob.glob(os.path.join(REPO, 'src/%s/*.c' % a.binary)): + try: + out[q] = open(q).read() + except OSError: + pass return out -def _jtbl_restore(snap, regions_before): +def _jtbl_restore(snap): """Undo a carve/isolation EXACTLY: restore the config text and delete only the region files this attempt created. Then re-extract and re-derive the stub map. @@ -233,7 +263,11 @@ def _jtbl_restore(snap, regions_before): whole overlay for the rest of the batch (4 isolate-FAILs downstream).""" for q, txt in snap.items(): open(q, 'w').write(txt) - for rf in set(glob.glob('src/%s/%s_jr_*.c' % (a.binary, a.binary))) - regions_before: + # Remove every source file the attempt CREATED. Derived from the snapshot (which holds the + # exact pre-attempt file set), not from the `_jr_*` name shape — an isolation may emit a + # region whose name that glob does not predict, and a leftover .c is picked up by the OBJS + # glob at the next parse (R33: derive from the recorded state, do not re-guess it). + for rf in set(glob.glob(os.path.join(REPO, 'src/%s/*.c' % a.binary))) - set(snap): try: os.remove(rf) except OSError: @@ -243,10 +277,10 @@ def _jtbl_restore(snap, regions_before): def _jtbl_prep_one(fn): - """Prep ONE table-bearing draft's carve. Returns (ok, snapshot, regions_before).""" + """Prep ONE table-bearing draft's carve. Returns (ok, snapshot).""" if not _fn_has_jtbl(fn): - return True, None, None - snap, regions_before = _jtbl_snapshot(), set(glob.glob('src/%s/%s_jr_*.c' % (a.binary, a.binary))) + return True, None + snap = _jtbl_snapshot() done = [] for fn in [fn]: st = _stubs.get(fn) @@ -280,12 +314,12 @@ def _jtbl_prep_one(fn): print(' [jtbl] carve FAILED %s: %s' % (fn, last[0][:120])); continue done.append(fn) if not done: - _jtbl_restore(snap, regions_before) - return False, None, None + _jtbl_restore(snap) + return False, None _sh(['make', '--no-print-directory', 'extract', 'BINARY=%s' % a.binary]) _reload_corpus() print(' [jtbl] carved %s' % done[0]) - return True, snap, regions_before + return True, snap def _stub_line(fn): @@ -332,15 +366,15 @@ while i < len(items): # per-chunk jtbl prep (chunk==1 is the prescribed default, so this is per-function): carve the # table, and if the gate then REJECTS the draft, undo the carve — a stranded carve has no owner # and poisons every later isolation in the overlay (see _jtbl_restore). - _jsnap = _jregions = None + _jsnap = None if len(chunk) == 1: - _ok, _jsnap, _jregions = _jtbl_prep_one(chunk[0]) + _ok, _jsnap = _jtbl_prep_one(chunk[0]) if attempt(chunk): commit(chunk) print(' + chunk(%d): %s' % (len(chunk), ' '.join(chunk))) elif len(chunk) == 1: if _jsnap is not None: - _jtbl_restore(_jsnap, _jregions) # stranded-carve undo (R32 ownership stays 1:1) + _jtbl_restore(_jsnap) # stranded-carve + truncated-TU undo (R32/§61) # ATOMIC CHUNK — do NOT bisect (Phase-28 T6). The old code fell into the loop below and # re-ran attempt([fn]) on the SAME single element against the SAME baseline: a byte-identical # DUPLICATE build. classify_fail reads _last_sha/_last_err, which the failed attempt(chunk)