diff --git a/runtime/include/parity_trace.h b/runtime/include/parity_trace.h new file mode 100644 index 00000000..cfacb1e7 --- /dev/null +++ b/runtime/include/parity_trace.h @@ -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 + +#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 */ diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 4f42f889..29f142b7 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -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 diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 797d1251..b487da7e 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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 }, diff --git a/runtime/src/fntrace.c b/runtime/src/fntrace.c index b45e8ebf..a6ec8b3f 100644 --- a/runtime/src/fntrace.c +++ b/runtime/src/fntrace.c @@ -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 #include +/* 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 diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index b6442eef..56945a16 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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 diff --git a/runtime/src/parity_trace.c b/runtime/src/parity_trace.c new file mode 100644 index 00000000..a24af17d --- /dev/null +++ b/runtime/src/parity_trace.c @@ -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 + +#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; +} diff --git a/runtime/src/traps.c b/runtime/src/traps.c index 93d12ca9..e80c551b 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -14,6 +14,12 @@ #include #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 */ } diff --git a/tools/parity_diff.py b/tools/parity_diff.py new file mode 100644 index 00000000..77131353 --- /dev/null +++ b/tools/parity_diff.py @@ -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()