tooling(parity): general two-process control-flow parity trace (native producer)

End the "somewhere upstream" blindness with a reusable first-divergence
instrument (PRINCIPLES.md "find the first divergence; is the value WRONG or the
behavior MISSING"). A TCB-scoped, freeze-on-trigger control-flow ring with an
identical struct + JSON wire format on both processes, so a generic sequence-diff
(tools/parity_diff.py) locates the first logical divergence between recomp and
oracle — not a frame diff.

- parity_trace.{h,c}: armed-from-boot ring, gated on current_tcb==watched_tcb,
  latches on a configured trigger target so the pre-divergence window survives
  the wedge loop. Up to 6 watch-word snapshots/row. Self-contained TU shared by
  both binaries; frame source decoupled via parity_host_frame().
- fntrace.c: per-dispatch producer hook (cheap armed gate on the hot path).
- traps.c: YIELD control-event hook in the scheduler switch.
- main.cpp: env-armed from boot (PSX_PARITY_TRACE=1; TCB/trigger/watch tunable),
  defaults target the MMX6 cutscene wedge.
- debug_server.c: parity_dump + parity_ctl commands.
- tools/parity_diff.py: pulls both rings, sequence-diffs by (kind,pc,target).

First use (MMX6 freeze) confirmed MISSING behavior, not a wrong value: native
thread1 is permanently parked at func_8002000C resume (TCB EPC 0x800200F4),
handshake flag 0x8006D9AC never set (0 across 16384 rows), setter func_8001344C
never runs; thread1 re-resumes a dead func_8002000C frame whose saved-ra slot was
reused -> jr 0x800CD3F8 wedge. Upstream context-staleness/scheduling root.
Beetle mirror producer + the cross-process diff are the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-29 16:38:38 -07:00
parent 0f7d0f1645
commit f8aefefec1
8 changed files with 419 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
/* parity_trace.h — general two-process control-flow parity trace.
*
* Purpose: end the "somewhere upstream" blindness. A thread/TCB-scoped,
* freeze-on-trigger ring with an IDENTICAL struct + JSON wire format on BOTH
* psx-runtime (code under test) and psx-beetle (independent oracle), so a
* generic sequence-diff tool (tools/parity_diff.py) can locate the FIRST logical
* divergence between the two timelines — not a frame diff (PRINCIPLES.md "find
* the first divergence" + "is the value WRONG or is the behavior MISSING").
*
* Model (CLAUDE.md ring-buffer doctrine): armed from boot (so the pre-divergence
* window is always covered — never arm-then-time), records one row per relevant
* control event while current_tcb == watched_tcb (or all TCBs if watched==0),
* and LATCHES (freezes) the moment a configured trigger target is dispatched, so
* the lead-up to the wedge survives instead of being evicted by the wedge loop.
*
* Generality: point it at any watched TCB + freeze trigger + up to six watch-word
* addresses. The MMX6 cutscene→gameplay freeze is the first user, but nothing
* here is MMX6-specific — it is the reusable substrate for a functional-parity
* audit between recomp and oracle.
*/
#ifndef PARITY_TRACE_H
#define PARITY_TRACE_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
PARITY_KIND_DISPATCH = 0, /* psx_dispatch iteration (per-leader) */
PARITY_KIND_CHANGETHREAD, /* syscall-3 ChangeThread decision point */
PARITY_KIND_RFE, /* ReturnFromException */
PARITY_KIND_YIELD, /* scheduler cross-thread switch committed */
PARITY_KIND_SAVE, /* context saved to a TCB */
PARITY_KIND_RESTORE, /* context restored from a TCB */
PARITY_KIND_TRIGGER, /* the freeze trigger fired (final row) */
} parity_kind_t;
#define PARITY_WATCH_MAX 6
typedef struct {
uint64_t seq;
uint32_t frame;
uint32_t cycle;
uint32_t kind;
uint32_t current_tcb;
uint32_t pc;
uint32_t ra;
uint32_t sp;
uint32_t epc; /* resume PC read from watched TCB (epc slot) */
uint32_t tcb_state; /* state/status word read from watched TCB */
uint32_t target; /* dispatch / switch target */
uint32_t watch[PARITY_WATCH_MAX];
} ParityEntry;
/* Each process supplies its own RAM reader (recomp: cpu->read_word; Beetle:
* PS_CPU peek). ctx is passed back verbatim. */
typedef uint32_t (*parity_read_word_fn)(void* ctx, uint32_t addr);
/* Configure. watched_tcb==0 => record all TCBs. trigger_target==0 => no freeze.
* tcb_epc_off / tcb_state_off are watched-TCB-relative byte offsets (PSX BIOS:
* epc slot = tcb+0x88, state word = tcb+0x00). watch_addrs/watch_count: up to
* PARITY_WATCH_MAX guest word addresses snapshotted on every row. Does not arm. */
void parity_trace_config(uint32_t watched_tcb, uint32_t trigger_target,
uint32_t tcb_epc_off, uint32_t tcb_state_off,
const uint32_t* watch_addrs, int watch_count);
void parity_trace_arm(int on); /* on=1 begins recording (idempotent) */
void parity_trace_reset(void); /* clear ring + unfreeze (keeps config) */
int parity_trace_is_armed(void); /* hot-path gate: cheap one-branch check */
int parity_trace_is_frozen(void);
/* Record one event. read_word+ctx source current_tcb (via TCBH 0x108), the
* watched-TCB epc/state, and the watch words. No-op unless armed, not frozen,
* and (watched==0 || current_tcb==watched). Latches on target==trigger. */
void parity_trace_record(parity_kind_t kind, uint32_t pc, uint32_t ra,
uint32_t sp, uint32_t target,
parity_read_word_fn read_word, void* ctx);
/* Readback (matches the beetle_fntrace_get pattern): copies up to max_rows
* NEWEST entries, oldest-first, into out[]; returns the count copied. */
uint32_t parity_trace_get(ParityEntry* out, uint32_t max_rows);
uint64_t parity_trace_total(void);
const char* parity_kind_str(uint32_t kind);
#ifdef __cplusplus
}
#endif
#endif /* PARITY_TRACE_H */
+1
View File
@@ -132,6 +132,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/debug_server.c
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
${PSXRECOMP_ROOT}/runtime/src/fntrace.c
${PSXRECOMP_ROOT}/runtime/src/parity_trace.c
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
${PSXRECOMP_ROOT}/runtime/src/traps.c
${PSXRECOMP_ROOT}/runtime/src/crash_trace.c
+51
View File
@@ -2605,6 +2605,55 @@ static void handle_dirty_insn_dump_file(int id, const char *json)
* argument-passing chains, and answer "who called X with what args"
* across processes (psx-runtime port 4370, psx-beetle port 4380). */
#include "fntrace.h"
#include "parity_trace.h"
/* ---- parity_dump / parity_ctl: general two-process control-flow parity ring.
* Mirrors the IDENTICAL command on psx-beetle so tools/parity_diff.py can pull
* both timelines and align by logical sequence (PRINCIPLES.md first-divergence). */
static void handle_parity_dump(int id, const char *json)
{
int count = json_get_int(json, "count", 8192);
if (count < 1) count = 1;
if (count > 16384) count = 16384;
ParityEntry *e = (ParityEntry *)malloc(sizeof(ParityEntry) * (size_t)count);
if (!e) { send_err(id, "oom"); return; }
uint32_t got = parity_trace_get(e, (uint32_t)count);
const size_t BUF_SZ = 12 * 1024 * 1024;
char *out = (char *)malloc(BUF_SZ);
if (!out) { free(e); send_err(id, "oom"); return; }
size_t pos = 0;
pos += snprintf(out + pos, BUF_SZ - pos,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"armed\":%d,\"frozen\":%d,\"count\":%u,\"entries\":[",
id, (unsigned long long)parity_trace_total(), parity_trace_is_armed(),
parity_trace_is_frozen(), got);
for (uint32_t i = 0; i < got; i++) {
if (pos > BUF_SZ - 1024) break;
ParityEntry *r = &e[i];
pos += snprintf(out + pos, BUF_SZ - pos,
"%s{\"seq\":%llu,\"frame\":%u,\"kind\":\"%s\",\"cur_tcb\":\"0x%08X\","
"\"pc\":\"0x%08X\",\"ra\":\"0x%08X\",\"sp\":\"0x%08X\",\"epc\":\"0x%08X\","
"\"state\":\"0x%08X\",\"target\":\"0x%08X\","
"\"w\":[\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\"]}",
i ? "," : "", (unsigned long long)r->seq, r->frame, parity_kind_str(r->kind),
r->current_tcb, r->pc, r->ra, r->sp, r->epc, r->tcb_state, r->target,
r->watch[0], r->watch[1], r->watch[2], r->watch[3], r->watch[4], r->watch[5]);
}
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
debug_server_send_line(out); free(out); free(e);
}
static void handle_parity_ctl(int id, const char *json)
{
if (json_get_int(json, "reset", 0)) parity_trace_reset();
long armv = json_get_int(json, "arm", -1);
if (armv == 0 || armv == 1) parity_trace_arm((int)armv);
char buf[160];
snprintf(buf, sizeof buf,
"{\"id\":%d,\"ok\":true,\"armed\":%d,\"frozen\":%d,\"total\":%llu}\n",
id, parity_trace_is_armed(), parity_trace_is_frozen(),
(unsigned long long)parity_trace_total());
debug_server_send_line(buf);
}
static void handle_fntrace_arm(int id, const char *json)
{
@@ -10251,6 +10300,8 @@ static void handle_vk_perf(int id, const char *json)
static const CmdEntry s_commands[] = {
{ "ping", handle_ping },
{ "parity_dump", handle_parity_dump },
{ "parity_ctl", handle_parity_ctl },
{ "latency", handle_latency },
{ "vk_perf", handle_vk_perf },
{ "game_options", handle_game_options },
+17
View File
@@ -1,9 +1,18 @@
/* fntrace.c — runtime side of psx_dispatch call ring. See fntrace.h. */
#include "fntrace.h"
#include "parity_trace.h" /* general control-flow parity ring (native producer) */
#include <string.h>
#include <stdlib.h>
/* RAM reader adapter for the parity trace (cpu->read_word takes only addr). */
static uint32_t parity_rw_cb(void* ctx, uint32_t addr) {
return ((CPUState*)ctx)->read_word(addr);
}
/* Frame-source accessor for parity_trace.c (decoupled from s_frame_count type). */
uint32_t parity_host_frame(void) { extern uint64_t s_frame_count; return (uint32_t)s_frame_count; }
FntraceEntry g_fntrace_ring[FNTRACE_RING_CAP];
uint64_t g_fntrace_seq = 0;
@@ -46,6 +55,14 @@ static inline int armed_match(uint32_t target) {
}
void fntrace_record(CPUState* cpu, uint32_t target) {
/* General parity ring: one DISPATCH row per leader while current_tcb matches
* the watched thread. Gated by the cheap armed flag so disarmed runs pay only
* a single branch on this hot path. pc==target (the leader being entered). */
if (parity_trace_is_armed()) {
parity_trace_record(PARITY_KIND_DISPATCH, target, cpu->gpr[31],
cpu->gpr[29], target, parity_rw_cb, cpu);
}
/* Dispatch-level bail-source capture (Tomba 2 wild-return spin, runtime-only,
* no BIOS regen). The generated psx_dispatch_impl calls us at the top of each
* dispatch iteration. When a previous iteration bail-FLATTENED a compiled
+39
View File
@@ -7,6 +7,7 @@
#include "cpu_state.h"
#include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */
#include "parity_trace.h" /* general two-process control-flow parity ring */
#include "psx_interpreter.h"
#include "cdrom.h"
#include "fntrace.h"
@@ -2789,6 +2790,44 @@ int main(int argc, char** argv) {
psx_hle_scheduler_enabled() ? "HLE (deterministic TCB)"
: "LLE (host fibers)");
std::fflush(stdout);
/* General control-flow parity trace (parity_trace.h). Armed from boot via
* PSX_PARITY_TRACE=1 so the pre-divergence window is always covered (never
* arm-then-time). Watched TCB / freeze trigger / watch words are env-tunable;
* defaults target the MMX6 cutscene→gameplay wedge (thread1 0xA000E35C,
* trigger dispatch 0x800CD3F8, watch the handshake flag + state struct). */
if (const char* pt = std::getenv("PSX_PARITY_TRACE"); pt && pt[0] && pt[0] != '0') {
auto envhex = [](const char* k, uint32_t dflt) -> uint32_t {
const char* v = std::getenv(k);
return (v && v[0]) ? (uint32_t)std::strtoul(v, nullptr, 0) : dflt;
};
uint32_t tcb = envhex("PSX_PARITY_TCB", 0xA000E35Cu);
uint32_t trigger = envhex("PSX_PARITY_TRIGGER", 0x800CD3F8u);
uint32_t watch[PARITY_WATCH_MAX] = {
0x801FEB78u, /* thread1 stack saved-ra slot */
0x8006D9ACu, /* handshake flag (setter target) */
0x800CD3F8u, /* cutscene state struct: outer/sub */
0x800CD3FCu, /* countdown */
0x800CD404u, /* done flag region */
0x800200F4u, /* func_8002000C resume point */
};
int wc = PARITY_WATCH_MAX;
if (const char* wl = std::getenv("PSX_PARITY_WATCH"); wl && wl[0]) {
wc = 0; std::string s(wl); size_t p = 0;
while (p < s.size() && wc < PARITY_WATCH_MAX) {
size_t c = s.find(',', p);
std::string tok = s.substr(p, c == std::string::npos ? c : c - p);
if (!tok.empty()) watch[wc++] = (uint32_t)std::strtoul(tok.c_str(), nullptr, 0);
if (c == std::string::npos) break; p = c + 1;
}
}
parity_trace_config(tcb, trigger, 0x88u, 0x00u, watch, wc);
parity_trace_arm(1);
std::fprintf(stdout, "psxrecomp: parity trace ARMED tcb=0x%08X trigger=0x%08X (%d watch)\n",
tcb, trigger, wc);
std::fflush(stdout);
}
psx_scheduler_run(&cpu);
#endif
+108
View File
@@ -0,0 +1,108 @@
/* parity_trace.c — general two-process control-flow parity trace.
* See parity_trace.h. Self-contained (no runtime internals) so the SAME TU
* compiles into both psx-runtime and psx-beetle and they emit identical rows. */
#include "parity_trace.h"
#include <string.h>
#define PARITY_RING_CAP 16384u /* power of two; ~spans the cutscene window */
static ParityEntry s_ring[PARITY_RING_CAP];
static uint64_t s_seq = 0;
static uint32_t s_armed = 0;
static uint32_t s_frozen = 0;
static uint32_t s_watched_tcb = 0;
static uint32_t s_trigger = 0;
static uint32_t s_epc_off = 0x88u;
static uint32_t s_state_off = 0x00u;
static uint32_t s_watch[PARITY_WATCH_MAX];
static int s_watch_count = 0;
/* Frame stamp: each host process defines parity_host_frame() (recomp's
* s_frame_count is uint64_t, Beetle's is uint32_t — an accessor decouples the
* type/linkage so this single TU compiles into both). */
extern uint32_t parity_host_frame(void);
void parity_trace_config(uint32_t watched_tcb, uint32_t trigger_target,
uint32_t tcb_epc_off, uint32_t tcb_state_off,
const uint32_t* watch_addrs, int watch_count)
{
s_watched_tcb = watched_tcb;
s_trigger = trigger_target;
s_epc_off = tcb_epc_off;
s_state_off = tcb_state_off;
if (watch_count < 0) watch_count = 0;
if (watch_count > PARITY_WATCH_MAX) watch_count = PARITY_WATCH_MAX;
s_watch_count = watch_count;
for (int i = 0; i < PARITY_WATCH_MAX; i++)
s_watch[i] = (watch_addrs && i < watch_count) ? watch_addrs[i] : 0;
}
void parity_trace_arm(int on) { s_armed = on ? 1u : 0u; }
void parity_trace_reset(void) { s_seq = 0; s_frozen = 0; memset(s_ring, 0, sizeof(s_ring)); }
int parity_trace_is_armed(void) { return (int)s_armed; }
int parity_trace_is_frozen(void) { return (int)s_frozen; }
uint64_t parity_trace_total(void) { return s_seq; }
const char* parity_kind_str(uint32_t kind)
{
switch (kind) {
case PARITY_KIND_DISPATCH: return "dispatch";
case PARITY_KIND_CHANGETHREAD: return "changethread";
case PARITY_KIND_RFE: return "rfe";
case PARITY_KIND_YIELD: return "yield";
case PARITY_KIND_SAVE: return "save";
case PARITY_KIND_RESTORE: return "restore";
case PARITY_KIND_TRIGGER: return "trigger";
default: return "?";
}
}
void parity_trace_record(parity_kind_t kind, uint32_t pc, uint32_t ra,
uint32_t sp, uint32_t target,
parity_read_word_fn read_word, void* ctx)
{
if (!s_armed || s_frozen || !read_word) return;
/* current TCB = *(*(0x108)) — the PSX kernel TCBH chain. */
uint32_t tcbh = read_word(ctx, 0x00000108u);
uint32_t current_tcb = tcbh ? read_word(ctx, tcbh) : 0u;
if (s_watched_tcb && current_tcb != s_watched_tcb) return;
ParityEntry* e = &s_ring[s_seq & (PARITY_RING_CAP - 1u)];
e->seq = s_seq;
e->frame = parity_host_frame();
e->cycle = 0u; /* best-effort; populated by host hook if available */
e->kind = (uint32_t)kind;
e->current_tcb = current_tcb;
e->pc = pc;
e->ra = ra;
e->sp = sp;
e->target = target;
e->epc = s_watched_tcb ? read_word(ctx, s_watched_tcb + s_epc_off) : 0u;
e->tcb_state = s_watched_tcb ? read_word(ctx, s_watched_tcb + s_state_off) : 0u;
for (int i = 0; i < PARITY_WATCH_MAX; i++)
e->watch[i] = (i < s_watch_count && s_watch[i]) ? read_word(ctx, s_watch[i]) : 0u;
s_seq++;
/* Latch on the trigger so the pre-divergence window survives the wedge. The
* trigger row itself is recorded (above) before freezing. */
if (s_trigger && target == s_trigger) {
/* re-tag the just-written row as the trigger for easy diffing */
e->kind = (uint32_t)PARITY_KIND_TRIGGER;
s_frozen = 1;
}
}
uint32_t parity_trace_get(ParityEntry* out, uint32_t max_rows)
{
if (!out || max_rows == 0) return 0;
uint64_t total = s_seq;
uint32_t avail = (total < PARITY_RING_CAP) ? (uint32_t)total : PARITY_RING_CAP;
uint32_t n = (avail < max_rows) ? avail : max_rows;
uint64_t start = total - n;
for (uint32_t i = 0; i < n; i++)
out[i] = s_ring[(start + i) & (PARITY_RING_CAP - 1u)];
return n;
}
+9
View File
@@ -14,6 +14,12 @@
#include <setjmp.h>
#include "psx_fiber.h" /* cross-platform fibers (Win32 fibers / POSIX ucontext) */
#include "psx_scheduler.h" /* deterministic TCB scheduler carve-out (scaffolding) */
#include "parity_trace.h" /* general two-process control-flow parity ring */
/* RAM reader adapter for the parity trace (cpu->read_word takes only addr). */
static uint32_t traps_parity_rw(void* ctx, uint32_t addr) {
return ((CPUState*)ctx)->read_word(addr);
}
/* Forward declarations from interrupts.c */
int psx_get_in_exception(void);
@@ -623,6 +629,9 @@ static int psx_request_thread_switch(CPUState* cpu, uint32_t target_tcb)
g_sched_escape.reason = PSX_RUN_YIELD_TO_TCB;
sched_escape_ring_log(cpu, PSX_RUN_YIELD_TO_TCB, current_tcb, target_tcb, 0);
debug_server_log_thread_event(8, cpu, current_tcb, target_tcb, 0);
if (parity_trace_is_armed())
parity_trace_record(PARITY_KIND_YIELD, cpu->pc, cpu->gpr[31],
cpu->gpr[29], target_tcb, traps_parity_rw, cpu);
longjmp(g_scheduler_jmpbuf, 1); /* unwind to psx_scheduler_run; never returns */
return 1; /* unreachable */
}
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""parity_diff.py — generic two-process control-flow first-divergence finder.
Pulls the `parity_dump` ring from psx-runtime (code under test) and psx-beetle
(independent oracle), aligns the two thread-scoped control-flow streams by
LOGICAL SEQUENCE (not frame), and reports the FIRST point where they diverge —
plus the watched state-word deltas at that point. This is the instrument that
replaces "somewhere upstream" guessing (recomp-template PRINCIPLES.md: "find the
first divergence, not the final visible bug").
Both sides emit the IDENTICAL row schema (parity_trace.h), so the diff is a plain
sequence match on the (kind, pc, target) tuple. Each side is typically armed from
boot and frozen on its own trigger (native: the wedge target; beetle: the
expected-but-missing producer), so the common prefix aligns and the tail reveals
the missing/extra/reordered behavior.
Usage:
python tools/parity_diff.py [--native PORT] [--beetle PORT] [--key kind,pc,target]
[--context N] [--count N]
"""
import argparse, json, socket, sys, difflib
def query(port, cmd, **kw):
obj = {"cmd": cmd}; obj.update(kw)
s = socket.create_connection(("127.0.0.1", port), timeout=10)
try:
s.sendall((json.dumps(obj) + "\n").encode())
buf = b""
while b"\n" not in buf:
chunk = s.recv(1 << 20)
if not chunk: break
buf += chunk
return json.loads(buf.decode(errors="replace").split("\n", 1)[0])
finally:
s.close()
def rows(port, count):
r = query(port, "parity_dump", count=count)
if not r.get("ok"):
print(f"[port {port}] parity_dump failed: {r}", file=sys.stderr); sys.exit(2)
return r
def keytuple(e, fields):
return tuple(e.get(f, "") for f in fields)
def fmt(e):
w = e.get("w", [])
return (f"seq={e['seq']:>5} f={e['frame']:>5} {e['kind']:<12} "
f"cur={e['cur_tcb']} pc={e['pc']} tgt={e['target']} "
f"epc={e['epc']} st={e['state']} ra={e['ra']} sp={e['sp']} "
f"flag={w[1] if len(w)>1 else '?'} struct={w[2] if len(w)>2 else '?'}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--native", type=int, default=4490)
ap.add_argument("--beetle", type=int, default=4382)
ap.add_argument("--count", type=int, default=16384)
ap.add_argument("--key", default="kind,pc,target",
help="comma fields forming the per-row alignment key")
ap.add_argument("--context", type=int, default=12)
args = ap.parse_args()
fields = args.key.split(",")
nat = rows(args.native, args.count)
bet = rows(args.beetle, args.count)
ne, be = nat["entries"], bet["entries"]
print(f"native(:{args.native}): {len(ne)} rows total={nat['total']} "
f"armed={nat['armed']} frozen={nat['frozen']}")
print(f"beetle(:{args.beetle}): {len(be)} rows total={bet['total']} "
f"armed={bet['armed']} frozen={bet['frozen']}")
if not ne or not be:
print("\nOne side has no rows — arm both from boot and reach the window.")
return
nk = [keytuple(e, fields) for e in ne]
bk = [keytuple(e, fields) for e in be]
sm = difflib.SequenceMatcher(a=nk, b=bk, autojunk=False)
blocks = sm.get_matching_blocks()
# First divergence = end of the first matching block that does not extend
# to the end of both streams.
first = None
for blk in blocks:
end_a, end_b = blk.a + blk.size, blk.b + blk.size
if blk.size and (end_a < len(nk) or end_b < len(bk)):
first = (end_a, end_b); break
if first is None:
print("\nNo divergence in the compared window (streams match to the end).")
return
ia, ib = first
C = args.context
print(f"\n===== FIRST DIVERGENCE (native idx {ia}, beetle idx {ib}) =====")
print("--- last common rows (native) ---")
for e in ne[max(0, ia - C):ia]:
print(" ", fmt(e))
print(f"--- NATIVE diverges here (idx {ia}+) ---")
for e in ne[ia:ia + C]:
print(" N", fmt(e))
print(f"--- ORACLE/BEETLE path here (idx {ib}+) ---")
for e in be[ib:ib + C]:
print(" B", fmt(e))
if __name__ == "__main__":
main()