Debugging chippy programs from your editor¶
chippy ships with a Debug Adapter Protocol
server so any DAP-aware editor can drive the emulator. The TUI is still
the default; DAP mode is a separate process you start with the -dap
flag.
Launching the adapter¶
Four transports:
# stdio — editor spawns the binary and pipes stdin/stdout.
# Used by VS Code's default DebugAdapterExecutable.
chippy -dap stdio
# TCP — adapter listens, editor connects out.
# Used by nvim-dap's default `executable` block.
chippy -dap tcp:14785
# unix — lowest-overhead out-of-process local transport
# (nvim-dap / vscode-chippy on the same host).
chippy -dap unix:/tmp/chippy.sock
# inproc — in-process loopback self-check; the transport itself
# (dap.NewInprocServer) is the foundation for the embedded TUI-via-DAP build.
chippy -dap inproc
-dap is mutually exclusive with the TUI: when set, chippy never opens
the alt-screen and instead speaks JSON-RPC over the chosen channel.
Transport overhead¶
A stepIn round-trip on the same host (NOP sled, go test -bench, Apple M-series):
| Transport | Round-trip | Notes |
|---|---|---|
inproc |
~0.34 µs | dap.NewInprocServer — Request/Response structs pass straight to the dispatcher; nil-args requests and all responses make the trip with zero serialization. |
unix |
~30 µs | full JSON wire framing over a unix-domain socket. |
The wire transports (stdio/tcp/unix) are interchangeable — they marshal the same JSON-RPC. inproc trades wire compatibility for an ~90× faster local round-trip, which the long-term TUI-via-DAP architecture (issue #394) needs.
VS Code¶
Copy examples/dap/launch.json into your project's .vscode/launch.json
and adjust paths. The launch config maps 1:1 to the CLI flags:
{
"type": "chippy",
"request": "launch",
"name": "Run chippy",
"rom": "${workspaceFolder}/build/program.bin",
"cpuVariant": "65c02",
"dbgPath": "${workspaceFolder}/build/program.dbg",
"stopOnEntry": true
}
stopOnEntry is honored:
- absent (default): pause at the reset vector and emit a stopped(entry) event
true: same as absentfalse: skip the entry pause and auto-start the run loop after launch
Same field on attach skips/emits the entry stopped event without
spawning a new run goroutine — the host process drives execution.
TUI attach (:dap)¶
The TUI can spawn its own DAP listener so an editor can attach to the same running session:
:dap 14785 — start TCP listener on :14785
:dap 0 — auto-assign a free port
:dap — report current listener
:dap stop — close it
Once the listener is up, point your editor at localhost:<port> with
an attach launch config (chippy's attach request returns
stopped(entry) and exposes the running CPU). Both the TUI and the
editor drive the same CPU — chippy serializes access via a shared
mutex so steps from either side never race.
Only one listener is live at a time. A second :dap PORT reports
"already listening".
nvim-dap¶
Copy examples/dap/nvim-dap.lua into your config. It registers the
chippy adapter (TCP transport on 14785) and a default configuration
that launches a program from cwd/build/program.bin.
require("dap").adapters.chippy = {
type = "server",
port = 14785,
executable = { command = "chippy", args = { "-dap", "tcp:14785" } },
}
<F5> to launch, <F10> step over, <F11> step into,
<Shift-F11> step out.
Supported requests¶
| Request | Issue | Notes |
|---|---|---|
initialize |
#47 | Negotiates capabilities; reports the full subset below in one shot. |
launch |
#47 | Takes rom, loadAddr, resetVec, linkerCfg, dbgPath, cpuVariant, tracePath, stopOnEntry. |
attach |
#87 | Supported when the host process pre-populates the debuggee via Server.AttachExisting. The in-TUI listener that ties this to the :dap command is filed as a follow-up (#97). |
disconnect / terminate |
#47 | Tears down the run goroutine, closes the trace file, exits. |
continue / pause |
#50 | Continue spawns a CPU run goroutine; pause flips a signal. |
next / stepIn / stepOut |
#50 | Step-over runs to PC+3 past a JSR; step-out runs until SP rises. |
stepBack |
#79 | Pops one snapshot from the 256-entry rewind ring and restores CPU+RAM. Only pre-step paths push — continue runs aren't reversible. |
threads |
#50 | One virtual thread (id=1, name=cpu). |
stackTrace |
#48, #449 | Walks JSR frames via cpu.DetectStackFrame. Each frame adds two chippy-extension fields (additive; standard clients ignore them): chippyStackAddr ($01XX stack-page slot of the pushed return pair) and chippyCallee (symbol at the JSR target). The chippy TUI's stack panel renders from these (#449). |
scopes |
#48, #410 | Registers, Flags, and (when a .dbg is loaded) Globals. |
variables |
#48, #410 | ref=1 → A/X/Y/SP/PC/P/Cycles; ref=2 → 8 P-flag bits; ref=3 → data symbols (sized .dbg globals + data-range symbols, code labels filtered). A symbol with size>1 expands into indexed byte children via a dynamically-allocated variablesReference (paged with start/count; supportsVariablePaging). |
setVariable |
#48, #454 | Writes hex / decimal to a register, flag bit, Globals scalar (poke the byte at the symbol's address), or array child ([i] → byte at addr+i). Memory writes go through s.ram (bypass MMIO). Refused while running. |
setBreakpoints |
#49, #81 | Source-line bps resolved through the .dbg source map. Honors condition / hitCondition (integer) / logMessage (with {expr} interpolation). |
setInstructionBreakpoints |
#49, #81 | Address bps ($XX, 0xXX, decimal). Same modifier support as source bps. |
setFunctionBreakpoints |
#82, #81 | Symbol-name bps via syms.LookupName. Same modifier support. |
dataBreakpointInfo |
#453 | Resolves a hex/decimal address or symbol name to a data-breakpoint id ($XXXX); reports read/write/readWrite access types. |
setDataBreakpoints |
#453 | Memory watchpoints. The run loop's AccessWrite/AccessRead hook (chained with the #440 dirty hook) flags a watched access; the run stops with reason data breakpoint after the instruction completes. accessType defaults to write; condition/hitCondition reuse the instruction-bp meta. |
loadedSources |
#84 | Lists every file the loaded .dbg references. |
source |
#84 | Returns file contents for a previously-listed Source. Basename-matches if the client passes an absolute path. |
disassemble |
#51, #80 | Variant-aware via cpu.DisasmCPU. Negative instructionOffset resolves via cpu.WalkBack for pre-context. |
readMemory / writeMemory |
#51 | Bypasses MMIO — peripherals don't see debugger pokes. |
evaluate |
#52 | Watch / hover / debug-console expressions via internal/expr. |
completions |
#85 | Debug-console autocomplete: registers, flag bits, and loaded .dbg symbols. |
setExceptionBreakpoints |
#83 | Toggles "pause on BRK" via the brk filter. Run loop checks for $00 opcode before each cpu.Step(). |
exceptionInfo |
#83 | Describes the last-fired exception (brk with PC). |
Custom events¶
chippy-state (live state streaming, #395)¶
A server→client custom event pushed during a free-run (continue) so a
client refreshes its panels without polling variables every frame. It's
chippy-specific — standard DAP clients ignore unknown events, so this is safe
to emit on a shared channel. Throttled server-side to ≤60 Hz.
{
"type": "event",
"event": "chippy-state",
"body": {
"a": 0, "x": 0, "y": 0, "sp": 253, "p": 36, // raw bytes, not "$XX"
"pc": 32769,
"cycles": 12044,
"halted": false,
"dirtyRanges": [ // memory written since the last event
{ "start": 768, "end": 769, "data": "qg==" } // [start,end) + base64 bytes
]
}
}
dirtyRanges (#440) carries the memory written since the previous event,
coalesced into half-open [start, end) spans with the current bytes inline
(base64). During a free-run the server arms an AccessWrite hook
(cpu.SetAccessHook, #421) that stamps a dirty bitmap; each throttled
chippy-state flushes the coalesced spans and clears. The hook composes with a
host's own access hook (chained, restored on stop) and is removed when the run
ends — zero per-write cost when not running. start + len(data) is
authoritative (a span ending at $FFFF wraps end).
The chippy TUI subscribes while running: it refreshes the Registers panel and
applies the dirtyRanges deltas to its mirror RAM, so the memory and
disassembly panels stay live without a per-frame readMemory. The stopped
event still does a full-RAM reconcile as the authoritative final sync.
vscode-chippy (or any host) can subscribe for the same live update. Following
the Mesen / DAP-extension convention, the event is additive — never required
for correctness.
Host debug hooks¶
For a downstream emulator (e.g. nessy) building an NES-aware debugger on the chippy core, three opt-in extension points keep the core CPU-generic (#419):
| Hook | Use |
|---|---|
AttachConfig.CustomRequestHandler (#416) |
serve the host's own vendor/command DAP requests over the same connection. |
Server.SetHostVars(expr.HostVarResolver) (#433) |
register host identifiers (e.g. scanline, dot, frame) that conditional-breakpoint / evaluate expressions resolve at eval time — scanline == 30 works against PPU state the 6502 can't see. |
Server.SetStopPredicate(func() bool) (#433) |
a host stop condition checked once per continue-loop iteration; lets the host build NES step granularity (run-to-NMI, step-scanline, step-frame) through the server's pause/ownership model. Arm it, send continue, disarm on the stopped. |
cpu.SetAccessHook (#421, access heatmap) and RAM.Freeze (#422,
write-suppress) are the matching core-level hooks — see the API reference.
Known gaps¶
attachrequires the host process to wire a debuggee viaServer.AttachExisting. The cross-process / in-TUI flow is filed at #97.- Peripherals aren't snapshotted by reverse-step (see #62).
- The DAP server is a single session per process. To debug two ROMs
at once, run two
chippy -dapinstances on different ports.
Expression grammar (evaluate, conditional breakpoints)¶
Same compiler as the TUI:
A == $42
X > 10 && Y != 0
(A & $80) != 0
[$0042] == X
C && !Z
PC >= main
See internal/expr/expr.go for the full operator table.
Troubleshooting¶
- No output from a trace launched via DAP. Same as the TUI: pass
"tracePath"AND set"stopOnEntry": false(or sendcontinueafter launch). The CPU doesn't auto-run on launch. launchreturnslaunch requires a 'rom' argument. Your config is missing theromfield. Path can be absolute or relative to the cwd of the chippy process.- Breakpoints come back
verified: false. The .dbg source map doesn't cover the requested (file, line). ConfirmdbgPathresolves and that the file matches asymentry'sfile=field in the .dbg.