"""The debugger. A debugger for this machine has an unusual job: the instruction stream is not readable by construction, so inspection *is* decoding. Every view here goes through cleanup and carries its confidence, which means a low-confidence decode is visible as a fact about the program rather than hidden behind a plausible-looking mnemonic. What it offers: * breakpoints by cell index or by label; * step / step-over, with the PC held at a BREAK trap; * a trace ring of executed instructions with decode confidence and the registers each one wrote; * symbolic register inspection — for each register, the nearest named symbol and its similarity, which is the HDC equivalent of a type readout; * raw bit-plane views of registers and storage cells. """ from __future__ import annotations from dataclasses import dataclass from .isa import REG_NAMES, format_operands from .interpreter import Machine, TraceEntry @dataclass class StopReason: """Why execution stopped.""" reason: str # 'break' | 'halt' | 'budget' | 'step' | 'error' pc: int detail: str = "" def __str__(self) -> str: return "%s at pc=%d%s" % (self.reason, self.pc, (" (%s)" % self.detail) if self.detail else "") class Debugger: """Interactive control of a :class:`~hdc.interpreter.Machine`.""" def __init__(self, machine: Machine): self.m = machine self.breakpoints: set[int] = set() self.labels: dict[str, int] = {} self._trap_pc: int | None = None # ---------------------------------------------------------- breakpoints def attach_program(self, program) -> "Debugger": """Take label -> cell mapping from an assembled program.""" self.labels = dict(program.labels) self.breakpoints.clear() self._trap_pc = None return self def add_break(self, where: int | str) -> int: """Set a breakpoint at a cell index or a label name.""" pc = self._where(where) self.breakpoints.add(pc) return pc def remove_break(self, where: int | str) -> bool: pc = self._where(where) if pc in self.breakpoints: self.breakpoints.discard(pc) return True return False def _where(self, where: int | str) -> int: if isinstance(where, int): return where if where in self.labels: return self.labels[where] if where.startswith("@"): return int(where[1:]) raise KeyError("no such label: %r" % (where,)) def check(self) -> StopReason | None: """Does the current PC sit on a fresh breakpoint?""" pc = self.m.pc if pc == self._trap_pc: return None if pc in self.breakpoints: self._trap_pc = pc return StopReason("break", pc, "breakpoint") return None # -------------------------------------------------------------- running def step(self) -> TraceEntry | None: """Execute exactly one instruction, ignoring breakpoints.""" entry = self.m.step() if self.m.status == "break": self._trap_pc = self.m.pc else: self._trap_pc = None return entry def step_over(self) -> TraceEntry | None: """Step one instruction; a CALL runs to completion first.""" entry = self.m.step() if entry is not None and entry.op == "CALL": depth = len(self.m.stack) guard = 0 while len(self.m.stack) >= depth and not self.m.halted and guard < 100_000: self.m.step() guard += 1 self._trap_pc = None return entry def continue_(self, max_steps: int = 100_000) -> StopReason: """Run until the next breakpoint, HALT, fault or budget.""" m = self.m if m.halted: return StopReason("halt", m.pc, "already halted") if m.status == "ready": m.status = "running" # If we are parked on a trap, allow that cell to execute once. if m.pc == self._trap_pc: m.skip_break = True self._trap_pc = None while m.steps < max_steps: stop = self.check() if stop is not None: m.status = "break" return stop if m.halted: return StopReason("halt", m.pc) if m.pc >= m.prog_len: m.halted = True m.status = "halted" return StopReason("halt", m.pc, "past end of program") before = m.pc entry = m.step() if entry is None: return StopReason("halt", m.pc) if m.status == "error": return StopReason("error", m.pc, self.trace_tail(1)) if entry.op == "BREAK" and m.status == "break": self._trap_pc = before return StopReason("break", before, "BREAK trap") m.status = "budget" return StopReason("budget", m.pc, "step budget exhausted") # ---------------------------------------------------------------- views def trace_tail(self, n: int = 10) -> str: rows = self.m.trace[-n:] if not rows: return "" return "\n".join(e.render() for e in rows) def disasm(self, start: int | None = None, count: int = 12) -> str: """Disassemble cells, marking the PC and any breakpoints.""" m = self.m if start is None: start = max(0, m.pc - 2) rows = [] for pc in range(start, min(start + count, m.prog_len)): dec = m.decode_at(pc) marks = [] if pc == m.pc: marks.append("=>") else: marks.append(" ") if pc in self.breakpoints: marks.append("B") else: marks.append(" ") rows.append( "%s%s %04d %-8s %-26s conf=%.2f" % ("".join(marks), "", pc, dec.op, format_operands(dec), dec.confidence) ) return "\n".join(rows) if rows else "" def regs(self) -> str: """Registers: integer payload, and nearest named symbol (cleanup).""" m = self.m rows = [ "REG VALUE POP SYMBOL SIM HV(head)" , "-" * 78, ] for i in range(len(m.regs)): val = m.get_int(i) hits = m.storage.nearest_symbol(m.regs[i], k=1) sym, s = (hits[0] if hits else ("", 0.0)) if s < 0.15: # Below the noise band there is no nearest symbol worth # naming; printing the argmax anyway would imply a match. sym, s = "", 0.0 from .hypervec import bits rows.append( "%-5s %-10d %-6d %-25s %-5.2f %s" % ( REG_NAMES[i], val, m.regs[i].bit_count(), sym[:25] or "-", s, bits(m.regs[i], m.d)[:24], ) ) rows.append( "PC=%d ZF=%d STACK=%d STEPS=%d STATUS=%s" % (m.pc, int(m.flag_z), len(m.stack), m.steps, m.status) ) return "\n".join(rows) def sim_table(self, regs: list[int] | None = None) -> str: """Pairwise register similarity — the 'how HDC is my state' view.""" m = self.m idx = regs if regs is not None else list(range(len(m.regs))) head = " " + " ".join("%6s" % REG_NAMES[i] for i in idx) rows = [head] for i in idx: cells = [] for j in idx: cells.append("%6.3f" % m.get_sim(i, j)) rows.append("%5s " % REG_NAMES[i] + " ".join(cells)) return "\n".join(rows) def bits(self, reg: int) -> str: """Bit planes of one register.""" from .hypervec import bitplanes return "\n".join(bitplanes(self.m.regs[reg], self.m.d)) def cell_bits(self, name: str) -> str: """Bit planes of a storage cell.""" return "\n".join(self.m.storage.bitplanes(name)) def summary(self) -> str: m = self.m return ( "status=%-8s pc=%-4d steps=%-5d halted=%-5s zf=%d bps=%s\n%s" % ( m.status, m.pc, m.steps, m.halted, int(m.flag_z), sorted(self.breakpoints) or "[]", self.disasm(count=5), ) ) def debug_program(machine: Machine, program, breakpoints: list[int | str] = ()) -> str: """Convenience: attach, set breaks, run, and return a session transcript.""" dbg = Debugger(machine).attach_program(program) for b in breakpoints: dbg.add_break(b) lines = [dbg.summary()] stop = dbg.continue_() lines.append("stopped: %s" % stop) lines.append(dbg.regs()) lines.append("trace:\n" + dbg.trace_tail(8)) return "\n".join(lines)