chippy — Project Context Dump¶
Snapshot of the running understanding of this project. Generated 2026-05-11. Treat this as a handoff document — anything not visible from
git logor the code itself should live here.
1. Project Overview¶
chippy is a Go-based TUI 6502 emulator with a Bubble Tea + Lipgloss source-level debugger. It targets ca65/cc65 toolchain output (.bin, .prg, .hex, .o via ld65) and aims to feel like an interactive debugger (gdb/lldb/nvim-dap style) for hobbyist 6502 development.
- Module:
github.com/nkane/chippy - Repo: https://github.com/nkane/chippy (public, primary branch
main) - License: MIT (
LICENSEin repo root) - Latest release: v0.0.1 (installable via Homebrew tap)
- Go version: 1.26.2 in
go.mod; CI usesstable
Vision¶
A debugger-first emulator. Run a binary from ca65, see source lines beside disassembly, set breakpoints/watchpoints with nvim-DAP-style sigils, step backwards, inspect memory, and integrate real peripherals (MMIO).
2. Architecture¶
Package layout¶
cmd/chippy/ # main binary entry point
internal/cpu/ # 6502 / 65C02 core, opcode tables, addressing, interrupts
internal/loader/ # .bin/.prg/.hex/.o loaders; invokes ld65 when needed
internal/symbols/ # cc65 .dbg parser (symbol table + source map)
internal/tui/ # Bubble Tea model, panels, breakpoints, watchpoints
internal/peripheral/ # MMIO peripherals (TextOutput @ $F001, KeyboardInput @ $F004/$F005)
example/ # ca65 sample programs + Makefile
docs/ # mascot prompts, this file
.github/workflows/ # CI + release
Core types¶
cpu.CPU— registers, flag helpers, opcode dispatch, interrupt latches- Fields:
A,X,Y,SP,P byte; PC uint16; Cycles uint64; Bus Bus; Variant Variant; Halted bool; extraCycles int; opcodes *[256]Instr; irqLine bool; nmiPending bool; nmiPrev bool cpu.Businterface —Read(addr uint16) byte; Write(addr uint16, v byte)cpu.RAM— flat 64KB backing storecpu.Instr—{ Mode AddrMode; Cycles int; PageAdd bool; Exec func(*CPU, uint16, AddrMode) }cpu.Variant—VariantNMOS|VariantCMOS65C02; selects opcode tabletui.WBus— wrapscpu.Busto capture memory access for watchpointstui.MemBP— memory breakpoint kinds (read / write / read+write)symbols.Table/symbols.SourceMap— parsed cc65.dbgdata
Opcode tables¶
Opcodes [256]Instr— NMOS, authoritative (internal/cpu/opcodes.go)OpcodesCMOS [256]Instr— initialised fromOpcodesthen overridden (internal/cpu/opcodes_cmos.go)- Illegals patched into NMOS table by
opcodes_illegal.go(runs after CMOS init due to lex file order) - CPU dispatch goes through
c.opcodes[op]so variant switching is free
Step semantics¶
Step()services interrupts at instruction boundary, THEN executes one opcode- NMI checked first (edge-triggered, always taken)
- IRQ checked second (level-triggered, only when
FlagIclear) - Servicing is 7 cycles, pushes PC+P (B clear), sets I, jumps to vector
- Servicing un-halts the CPU
- Returns total cycles including interrupt overhead + branch extras
c.Cyclesis also advanced (same total)c.extraCyclesis the side channel for branches and CMOS BCD; reset each Step
BCD differences¶
- NMOS: A and C reflect decimal arithmetic; N/V/Z reflect the parallel binary path (a real 6502 quirk)
- CMOS: N/V/Z reflect the decimal result; +1 cycle penalty
- Implementation: ADC/SBC dispatch via
c.VarianttoadcDecimalCMOS/sbcDecimalCMOS
Interrupts (PR #33, issue #10)¶
AssertIRQ()/ReleaseIRQ()— level-triggered, sets/clearsirqLineTriggerNMI()/DeassertNMI()— edge-triggered vianmiPrevrising-edge detect- Service routines push
(P | FlagU) &^ FlagB(B clear), then set FlagI, read vector ($FFFA / $FFFE) - Wakes from
Haltedso a wait-loop can be interrupted by a peripheral
Memory routing (PR #34, issue #16)¶
Bus chain: CPU → tui.WBus → cpu.MMIO → cpu.RAM
- cpu.Peripheral interface: Range() (lo, hi uint16); Read(uint16) byte; Write(uint16, byte)
- cpu.MMIO wraps an inner Bus, dispatches to registered peripherals first
- internal/peripheral.TextOutput — captures writes to $F001 into a buffer; rendered as a TUI panel
- internal/peripheral.KeyboardInput — Apple-1-style data/status register pair ($F004/$F005); TUI pushes keypresses, CPU reads & status drains
- Loader and reset-vector helpers write directly to ram, bypassing MMIO — peripherals must live at addresses no ROM will occupy
Execution trace (PR #36, issue #21; #57 issue #43)¶
cpu.Tracerinterface — optional per-instruction hook onCPU.Step(). Methods:LogStep,LogInterrupt.cpu.FileTracer— buffered file sink (64 KiB), Enable/Disable/Close/SetPath- CLI:
-trace PATH; TUI::trace PATH | :trace on | :trace off | :trace - Instruction lines: PC, opcode bytes, disasm, A/X/Y/P/SP, cumulative CYC.
- Interrupt-entry lines:
---- NMI -> $FFFA (PC=$XXXX P=PP SP=SS CYC:N)emitted at the service boundary, before the 7-cycle push/vector-load, so a reader sees where the PC jump in the next instruction originated.
3. Conventions & Workflow¶
Branch & PR flow¶
- One issue →
feat/<short-name>branch offmain gh pr createwith a body containingCloses #N- CI must go green (3-OS test matrix + lint + klaus)
gh pr merge N --squash --delete-branch- File a new GitHub issue for any work that gets deferred
Commits¶
- Conventional Commits:
feat:,fix:,docs:,ci:,test:,refactor:,chore: - These prefixes feed
.goreleaser.yml's changelog grouping
GitHub CLI¶
- Authenticated as
nkaneover SSH (key:~/.ssh/id_ed25519_github) workflowscope confirmed (can edit.github/workflows/)
Releases¶
- Cut a tag
vX.Y.Z→.github/workflows/release.ymlruns goreleaser - Binaries published to GitHub releases
- Homebrew formula at
nkane/homebrew-tapis auto-updated by goreleaser - Secret:
HOMEBREW_TAP_GITHUB_TOKEN(PAT) homebrew-coresubmission deferred until ~30 stars (issue #22)
Quality bars¶
go build ./... && go test ./...must stay green between increments- TUI must stay responsive — every Update key path returns
tea.Cmd - Old persistence files (
~/.chippy/state-<rom>.json) must keep loading
4. Progress¶
Shipped¶
- v0.0.1 released; brew install works via tap
- Release infra:
.goreleaser.yml,.github/workflows/release.yml, MITLICENSE,nkane/homebrew-tap
Closed issues¶
-
1, #2, #3, #7, #8 (cycle audit), #9 (65C02), #10 (IRQ/NMI), #11–#15¶
Merged PRs of note¶
-
23, #24, #26, #27, #28, #29 — earlier infra / features¶
-
30 — Klaus functional test harness (GPL ROM, downloaded on demand, sha256 verified)¶
-
31 — Cycle audit; introduced
extraCyclesside channel; fixes taken-branch undercount inStep()return¶ -
32 — Full 65C02 CMOS support (variant enum, table dispatch, ~30 opcodes, 3 new addr modes, JMP-IND wrap fix, CMOS BCD with +1 cycle, WDC NOP fill,
--cpuflag, ca65 demo + e2e test)¶ -
33 — IRQ/NMI with edge/level semantics¶
-
34 — MMIO peripheral abstraction (issue #16); routing bus + Apple-1-style TextOutput ($F001) and KeyboardInput ($F004/$F005)¶
-
36 — Per-instruction execution trace (issue #21):
cpu.Tracerhook onStep(),cpu.FileTracer(buffered 64K),-trace PATHCLI flag,:trace PATH|on|offTUI command¶ -
38 — CLAUDE.md "docs are part of every PR" rule: README/context/help-modal/exported docs move with code¶
-
39 — Stack panel JSR-frame annotation (issue #18): detects pushed return-address pairs via the
$20opcode atstored-2; rendersret $XXXX callee file:NN; collapses non-frame runs;Ttoggles raw view¶ -
40 — Memory editor (issue #19): byte-level
MemCursor(arrow keys, auto-scroll),eenters hex edit mode at cursor; 1–2 hex chars, Enter commits, Esc cancels; cursor persists in state file;:gotoaligns view AND moves cursor¶ -
41 — Prompt history + tab-complete (issue #20):
~/.chippy/history(cap 100, dedup, auto-save), Up/Down recall, Tab completes verbs and:bp <symbol>against the loaded.dbg, Ctrl-R reverse-incremental search (Ctrl-R again walks older). Addedsymbols.Table.NamesWithPrefix.¶ - v0.0.2 — release cut after #41. 7 features since v0.0.1; binaries + brew tap auto-updated.
-
54 — Reverse step (issue #17):
cpu.Snapshot/CPU.Snapshot/Restorecapture full regs + RAM + bookkeeping;rewindRing(cap 256, FIFO eviction, LIFO pop) records pre-step state on explicit-step paths only (free-run skipped to avoid 64 KiB/step cost);<pops one; status bar showsrwd:Ndepth.¶ -
55 — CMOS-aware disasm (issue #42):
DisasmCPU/DisasmCPUWithSymsroute through the CPU's opcode table so CMOS-only mnemonics (STZ/PHX/BRA/etc.) render correctly in the disasm panel, trace lines, and any future caller. LegacyDisasm/DisasmWithSymsretained as NMOS-default shims.¶ -
56 —
-run-on-startflag (issue #44): start the CPU running instead of paused; pair with-tracefor non-interactive capture.¶ -
57 — Trace interrupt-entry lines (issue #43):
Tracer.LogInterrupthook +FileTraceremits---- NMI -> $FFFA (PC=... P=... SP=... CYC:...)markers at the service boundary, so trace readers can spot the PC jump in the next instruction.¶ -
58 — Stack heuristic tightening (issue #45):
detectStackFramenow also rejects frames whose stored return-address or JSR target falls belowcodeMinAddr = $0200(zero-page + stack-page). Cuts most false positives without losing real frames.¶ -
68 — Help modal paging: 4 pages, space/→ next, p/← prev, any other key closes. Splits the 10-section keybinding reference so the modal fits on small terminals.¶
-
69 — DAP transport + initialize/launch/disconnect (issue #47):
internal/dappackage with Content-Length framing, request/response/event types, server dispatch loop;-dap stdio | tcp:PORTCLI flag. Launches construct CPU+RAM+MMIO fromLaunchArgumentsmatching the CLI flag shape; capabilities advertise everything #48–#53 will eventually wire (conditional bp / instruction bp / disassemble / readMemory / writeMemory etc.).¶ - v0.1.0 — release cut after #69. Minor bump signals new DAP subsystem.
- DAP step controls (issue #50):
continue/next/stepIn/stepOut/pause/threads.continuespins a background goroutine that callscpu.Stepuntil pauseRequested flips true or the CPU halts; emitsstoppedevent on exit. Single-step variants refuse while running and emitstoppedafter the synchronous step. - DAP stackTrace / scopes / variables / setVariable (issue #48):
stackTracewalks JSR frames viacpu.DetectStackFrame(moved from tui/stack.go); scopes returnsRegisters+Flags; variables emits A/X/Y/SP/PC/P/Cycles for ref=1 and N/V/U/B/D/I/Z/C for ref=2; setVariable writes registers or toggles flags. Stack-frame detection moved from tui to cpu package ascpu.DetectStackFrame/StackCodeMinAddr. - DAP breakpoints (issue #49):
setBreakpoints(source-line, resolved viasrcMap.PCToSrcreverse-lookup) andsetInstructionBreakpoints(address). Both are destructive against their respective namespace per DAP spec. Run loop checksbpHit(flattened union) at each Step and emitsstoppedwith reason=breakpoint. - DAP disassemble / readMemory / writeMemory (issue #51):
disassembleroutes throughcpu.DisasmCPU(variant-aware), reportsaddress,instructionBytes,instruction,symbol,location/line;readMemory/writeMemorybypass MMIO so peripheral side-effects don't fire on debugger pokes. Base64 envelope per spec. - DAP evaluate (issue #52):
evaluaterequest compiles + runs the same expression grammar used by:bp X if E. Expression compiler moved frominternal/tui/cond.goto a newinternal/exprpackage so DAP and TUI share semantics (expr.Compile,expr.EvalFn);tui.compileConditionis now a thin wrapper. - DAP example configs + onboarding docs (issue #53):
docs/dap.mdwalkthrough,examples/dap/launch.json(VS Code) andexamples/dap/nvim-dap.lua(nvim-dap). DAP-v1 epic complete. - v0.2.0 — release cut after #77 / DAP-v1 epic.
- DAP stepBack (issue #79, first of #78 DAP-v2 epic): wires the rewind ring into DAP. Snapshot ring promoted from
internal/tuitointernal/cpuascpu.SnapshotRingso both the TUI's<key and DAP's stepBack share storage.supportsStepBack: true. - DAP setFunctionBreakpoints (issue #82, DAP-v2): symbol-name bps via
syms.LookupName. NewbpsByNamemap joinsbpsBySrcandbpsInstinrebuildBPHit's union.supportsFunctionBreakpoints: true. - DAP loadedSources + source (issue #84, DAP-v2): editor's Loaded Scripts pane lists every file in
srcMap.Files; thesourcerequest returns joined-line content with basename fallback for clients passing absolute paths.supportsLoadedSourcesRequest: true. - DAP backward disassemble (issue #80, DAP-v2):
walkBackpromoted frominternal/tuitointernal/cpuascpu.WalkBack; DAP'sdisassemblehandler uses it for negativeinstructionOffset. Heuristic tightened to prefer earliest-start at equal sequence length, biasing toward real code boundaries. - DAP completions (issue #85, DAP-v2): debug-console autocomplete returns registers (A/X/Y/P/SP/PC), flag bits (N/V/B/D/I/Z/C), and
.dbgsymbol names matching the cursor's trailing identifier prefix.supportsCompletionsRequest: true. - DAP exception bps (issue #83, DAP-v2):
brkfilter advertised in initialize asexceptionBreakpointFilters.setExceptionBreakpointsflipsbrkOnException; run loop pauses before any$00opcode and writeslastExceptionPCfor theexceptionInforesponse.supportsExceptionInfoRequest: true. - DAP bp condition/hitCondition/logMessage (issue #81, DAP-v2): every breakpoint family (source-line, instruction, function) honors the DAP modifier triple. New
bpMetaper PC carries the compiledexpr.EvalFn, hit target + running count, and an interpolating log template.shouldFireBPis the run-loop hit handler — logMessage emits anoutputevent then continues without stopping. - DAP integration test (issue #86, DAP-v2):
internal/dap/integration_test.gounder build-tagintegration. Builds the binary, spawnschippy -dap stdio, drives initialize → launch → setInstructionBreakpoints → continue → variables → stackTrace → disconnect via an in-test JSON wire client. Newdap-integrationCI job runs it on every push. - DAP attach v1 (issue #87, DAP-v2):
Server.AttachExisting(AttachConfig)populates debuggee from an externally-built CPU/RAM/MMIO bundle without going through the loader.attachrequest now responds OK + emits stopped(entry) when a debuggee is wired. The TUI plumbing (:dap PORTcommand + shared CPU mutex) is deferred to #97. - v0.3.0 — release cut after DAP-v2 push.
- Klaus 65C02 functional test (issue #59):
internal/cpu/klaus_cmos_test.goruns against65C02_extended_opcodes_test.bin(download-on-demand + sha256-pinned). v1 skipped behindCHIPPY_KLAUS_CMOS_STRICTenv because chippy's CMOS undocumented-opcode slots aren't WDC-spec'd yet (bug #99); test infrastructure is ready for #99's fix to be validated against. - Exhaustive BCD test (issue #60):
internal/cpu/decimal_exhaustive_test.go(build tagdecimal) walks every (N1, N2, cin) through ADC and SBC in decimal mode for both variants — 524 288 cases total. Caught a real CMOS BCD bug on invalid-nibble inputs; fix applied toadcDecimalCMOS/sbcDecimalCMOS(Bruce Clark Appendix B algorithm). NewdecimalCI job runs the suite on every push. - CMOS e2e CI (issue #61): new
cmos-e2eworkflow job installs cc65, buildsexample/cmos_demo.bin, runs the existing e2e test withCHIPPY_CMOS_E2E_STRICT=1so missing fixtures fail the build instead of silently skipping. - CMOS NOP fills + interrupt D-clear (issue #99): WDC-spec NOP widths for undefined CMOS slots ($44=ZP, $54/$D4/$F4=ZPX, $DC/$FC=ABS, $5C=ABS-quirky 8-cycle). BRK / serviceIRQ / serviceNMI now clear D on CMOS variant (NMOS bug preserved). Klaus 65C02 functional test now passes end-to-end and runs unconditionally in CI.
- v0.3.1 — patch release after the CMOS correctness pass.
example/c/— cc65-based C example programs (hello, sum, fizzbuzz) with sharedchippy.cfglinker config + minimalcrt0.sruntime. Builds viamake -C example/c; runs viachippy -rom example/c/<prog>.bin. Source-map loader updated to prefer.cfiles over.sintermediates when both are recorded for the same PC, so the TUI source view (v) shows C source while stepping.- Immediate window (issue #70):
Iopens a modal REPL backed byinternal/expr. Each Enter evaluates the buffer against current CPU state, appendsexpr → resultto scrollback.↑recalls the last expression. Result formatting matches DAP's evaluate response so both surfaces report identical values. - Peripheral snapshots (issue #62):
cpu.Snapshotgrew aPeripherals map[string][]bytefield; TUI and DAP both capture TextOutput buffer + Keyboard latch state into it on every push and restore on every pop. Newperipheral.Snapshotableinterface (Snapshot/Restore); both TextOutput and KeyboardInput implement it. Reverse-step across an MMIO write/read no longer desyncs the visible peripheral state. - CoW RAM snapshots (issue #66):
cpu.Snapshot.RAM [0x10000]byteis nowPages map[byte][256]byte. RAM gained an opt-in (EnableShadow) page-level write barrier that captures pre-write images. Two-phase capture protocol — caller takes the snapshot before the step, resets the shadow, runs the step (or multi-step sweep), then claimssnap.Pages = ram.TakeShadow()and pushes. Typical 1-instr snap is ~hundreds of bytes vs 64 KiB before, so free-run now pushes on every step in both TUI tickMsg loop and DAP runLoop — reverse-step works across an unattended continue. 1000-iteration tight loop costs <1 MiB of total ring storage (validated by test). - VS Code extension (issue #88):
extension/vscode-chippy/— minimal TypeScript package that registers thechippydebug type and supplies aDebugAdapterDescriptorFactorythat spawnschippy -dap stdio.package.jsondeclares launch attributes, configuration snippets, and achippy.binaryPathsetting.npm run packageproduces an installable.vsix. - WebAssembly playground (issue #67):
cmd/chippy-wasm/builds ajs/wasmbinary that installs awindow.chippyglobal (load / step / run / state / disasm / readMem / textOutput / pushKey / setVariant).web/ships the HTML/JS shell —make -C web servebuilds + serves on :8080. Demos copy fromexample/. ld65/.o pipeline is explicitly out of scope (no shell-out in the browser); .bin / .prg / .hex parsing is inlined in the WASM main. NewwasmCI job keeps the build target green. GitHub Pages auto-deploy viapages.yml. - v0.4.0 — release cut after #62 / #66 / #88 / #67 ship.
- CPU correctness micro-audit (issue #122): WAI ($CB) and STP ($DB) were placeholder NOPs; now WAI halts until any IRQ/NMI (waking even on masked IRQ — falls through to next instruction without dispatching the handler) and STP halts permanently (new
stoppedBySTPlatch; onlyReset()clears). Regression tests cover the halt/wake matrix plus IZP $FF zero-page wrap, PHP B/U push, IRQ B-clear push, and CMOS RTI D-restore. - expr unary minus width-aware (issue #129):
-1now evaluates to$FFinstead of$FFFFFFFF; pick-smallest-power-of-two-width rule keepsA == -1matching a register holding$FF. Binary subtraction stays 32-bit modular by design. First-ever tests forinternal/expr/. - TextOutput bounded buffer (issue #128):
peripheral.TextOutputnow drops the oldest quarter when its buffer hits cap (default 64 KiB;--text-buf-capoverrides;0= unbounded). New:textsave PATHTUI command dumps the live buffer to disk. Prevents OOM on long-running programs and keeps reverse-step snapshots bounded. - DAP advertised-but-missing gaps (issue #123):
supportsBreakpointLocationsRequestwas advertised but had no handler — now wired (line-granularity lookup againstsrcMap.PCToSrc).launch.stopOnEntryandattach.stopOnEntryare now*bool; explicitfalseauto-starts the run loop / suppresses the entry stopped event.writeMemory.allowPartial=falserejects overflowing writes instead of silently truncating. - DAP input validation hardening (issue #124):
readMemoryrejects negativeCount;disassembleclamps large-negativeOffsetand rejects negativeInstructionCount;evaluaterefuses while the run loop is in flight (was racing CPU/RAM reads);stepOutdetects SP rises across the 8-bit wrap via signed-delta comparison; duplicate source-line and instruction breakpoints surface averified:false"duplicate ... — first entry kept"message instead of silently overwriting. - TUI help-modal + Tab completion polish (issue #127): help modal grew a "Prompt verbs" section listing every
:command with concrete syntax (no more "guess the modifier syntax"). Tab completion extended beyond verb-only —:trace on/off,:speed <hz>,:bp X <modifier>(once/hits/if/log), and the new:textsaveverb all complete from arg-pool. Symbol completion still works at arg-1 of address-taking verbs. - State-file format freeze (issue #112): new
StateSchemaVersion = 1written into every saved file. Loader treats absent version as v0 legacy (still decodes),== 1as current,> 1as silent ignore so an older chippy preserves a newer build's state.internal/tui/testdata/state-v1.jsonis the pinned golden;TestLoadState_GoldenV1fails when a tag or struct field changes incompatibly.docs/state-format.mddocuments the contract;CLAUDE.mdcross-references it. Pre-existing bug fixed along the way:loadMemBPswas only called on the legacy-decode path, dropping memory watchpoints from any new-shape file. - State-file content completeness (issue #125):
savedStategrewDisasmFollow,StackAnnotate,InputMode,DisasmAnchor, andImmediateHistory. The two booleans serialize as*boolso a legacy v0 file's absence doesn't clobber theNew(c, r)truedefaults. Loader gates the new fields onSchemaVersion >= 1for the same reason. Golden file extended; new tests cover legacy-defaults-preserved and round-trip-of-additions. - Release hardening (issue #130): goreleaser builds gain
-trimpath+-buildvcs=truefor reproducible / verifiable provenance; cosign keyless signing produces*.cosign.bundleper artifact (verify viacosign verify-blob --certificate-identity=https://github.com/nkane/chippy/.github/workflows/release.yml@refs/tags/<TAG> --certificate-oidc-issuer=https://token.actions.githubusercontent.com); syft emits SPDX SBOMs per archive; CI gained agovulncheckjob that runs on every push; npm Dependabot now tracks the VS Code extension's deps;SECURITY.mddocuments the reporting flow + the hardening baseline. - Docs hygiene (issue #131): README grew a "Why chippy" section with positioning vs py65 / lib6502 / visual6502; new
CONTRIBUTING.mdcovers branch flow + commit style + quality bar + the "docs are part of every PR" rule; newCHANGELOG.mdin Keep-a-Changelog format backfills v0.0.1 → v0.4.0 + an Unreleased section tracking the v1.0 epic; newdocs/editors.mdcarries the editor-integration matrix. - Perf baseline + CI regression gate (issue #113):
internal/cpu/bench_test.goships three benchmarks —BenchmarkStep_NMOS,BenchmarkStep_CMOS,BenchmarkStep_WithSnapshot. Newperfgatebuild-tag test compares measured ns/op againsttestdata/perf-baseline.jsonand fails on >15% regression. Newperf baselineCI job runs the gate on every push. Refresh procedure documented indocs/perf-baseline.md. - NO_COLOR + colorblind themes (issue #126): new
internal/tui/theme.godefines four palettes —default,mono,protan(red-green safe),tritan(blue-yellow safe).NO_COLORenv forces mono regardless of--theme.:theme NAMEruntime command; persisted in the state file's newthemefield; arg-completed by Tab. Help modal grew a Theme section. Tests cover env routing, applyTheme global swap, and round-trip persistence. - WASM playground hardening (issue #132): new boot-error banner renders the underlying WASM-load failure (e.g. file:// MIME refusal) with a hint to use
make -C web serve. CSP meta enforcesdefault-src 'self'+frame-ancestors 'none'for clickjacking + script-injection defense. Newsw.jsservice worker caches static assets for offline use. share button copies a#rom=<base64>&format=&addr=&variant=permalink (bytes stay client-side via URL fragment). Mobile-responsive: panes reflow to single column under 800 px. - VS Code extension tests + disconnect docs (issue #133):
@vscode/test-electronharness compilessrc/test/{runTest,suite/index,suite/extension.test}.ts. Smoke tests cover presence + activation + manifest-declared debug type + thechippy.binaryPathsetting. Newvscode-extCI job runs the suite underxvfb-run.package-lock.jsonis now committed sonpm ciis deterministic. Extension README documents disconnect / crash handling and the test command. - ca65 syntax highlighting (issue #117): TextMate grammar at
extension/vscode-chippy/syntaxes/ca65.tmLanguage.jsoncovers NMOS + 65C02 mnemonics, directives (.proc,.segment,.byte,.if, etc.), hex / binary / decimal literals, labels, comments, registers, operators. Files matching.s/.s65/.asm/.incare auto-tagged. Newca65.language-configuration.jsonenables;comment toggling + bracket pairing. Snippets file ships reset-vector /.proc/.ifdef/ halt-loop / Apple-1putctemplates..vsixnow ships 8 files, 7.37 KB. - CMOS 65C02 cycle audit (issue #111): new
internal/cpu/cmos_cycles_test.goexercises CMOS-only opcode cycles (BRA / INA / DEA / PHX / PLX / PHY / PLY / STZ / TRB / TSB / JMP (abs,X) / LDA (zp) / RMBx / SMBx / BBRx / BBSx), the BCD +1-cycle penalty under FlagD on ADC / SBC, the WDC NOP fills (1-byte/1-cycle defaults + the documented ZP/ZPX/ABS multi-byte slots + the quirky 8-cycle $5C), and WAI / STP. Surfaced two bugs in the CMOS opcode table: BRA was base-3 (computed as 4/5 instead of 3/4 becausebranch()adds +1 for always-taken), and the ZPX-prefixed NOP slots $54 / $D4 / $F4 were incorrectly routed throughcase 0x04and registered as 2-byte/3-cycle ZP NOPs instead of 2-byte/4-cycle ZPX NOPs. Both fixed; Klaus still green. - DAP TUI attach (issue #97): new
:dap PORTTUI command spawns the embedded DAP server in attach mode against the live CPU.AttachConfig.CPUMucarries a shared*sync.Mutexthe DAPdispatch()andrunLoop()take per iteration;Model.step()takes the same mutex when set.Model.DAPListenAddrsurfaces the TCP address;:dapreports state,:dap stopcloses the listener.Model.SrcMapretains the livesymbols.SourceMappointer so the embedded server can resolve source breakpoints. Race-detector test confirmsstep()blocks while the mutex is held. - Linux distribution beyond brew (issue #118): goreleaser
nfpms:block produces.deb,.rpm, and.apkpackages per release. Newaurs:block publisheschippy-binPKGBUILD to AUR (gated onAUR_SSH_PRIVATE_KEYsecret; skip-upload-auto so dev builds don't push). README install table covers Debian/Ubuntu (dpkg -i), Fedora/RHEL (rpm -i), Alpine (apk add), Arch (yay -S chippy-bin). - Examples expansion (issue #119): four new ca65 demos.
mul16.s(16x16 → 32-bit shift-add multiply, ZP state, ADC carry propagation),echo.s(Apple-1 I/O — poll $F005, read $F004, echo to $F001),timer_irq.s(IRQ vector + RTI handler alongside a busy main loop),guess.s(interactive number-guess state machine driven from MMIO keyboard). All build through the existing Makefile; README grouped under math / arithmetic / I/O / interrupts / CMOS categories with explicit watching tips. - chippy.dev documentation site (issue #116): new
mkdocs.ymlconfigures MkDocs-Material;docs/index.md+docs/quickstart.mdland as new pages alongside the existing reference docs. Pages workflow installsmkdocs-material, buildsdocs/to_site/, copiesweb/into_site/playground/, and uploads the combined artifact. Site root becomes the docs landing;/chippy/playground/is the WASM playground.mkdocs build --strictis part of the deploy gate. - VS Code marketplace publish prep (issue #114): release workflow gains a
vscode-extensionjob that syncsextension/vscode-chippy/package.jsonto the tag version (npm version --no-git-tag-version),npm ci && npm run compile, thenvsce publish --pat $VSCE_PAT. Skipped on prerelease tags (anything with-in the name) since the marketplace can't unpublish. Extension README documents the PAT setup.AUR_SSH_PRIVATE_KEYsecret is now also passed through so the AUR upload from #118 fires on real releases. - Trace replay (issue #64): new
internal/tracepackage parses chippy's-traceoutput back into a navigable[]Frame.cmd/chippy --trace-replay PATHopens the TUI in replay mode —sadvances a frame,<rewinds, the CPU's regs are synced from the active frame so every panel renders as if paused at that PC. Help-modal grew a "Trace replay" section. Tests cover parse-basic, step+seek, malformed/empty lines.
Open issues¶
-
22 (homebrew-core) — blocked on stars¶
- Post-DAP follow-ups: #63 VIA 6522, #64 trace replay, #65 mem-watch conditional expressions
5. Key Decisions & Rationale¶
Architecture¶
- Variant-based CPU dispatch via per-CPU table pointer — chosen over a runtime switch in every opcode so future variants (65816, etc.) only need a new table file. Tables share NMOS as a base and override.
- CMOS table init via copy-then-override, relying on Go's
init()lex file ordering (opcodes.go<opcodes_cmos.go<opcodes_illegal.go). This is a load-bearing invariant — renaming files could break the init chain. - ZPR addressing handler self-fetches operand bytes and self-advances PC;
resolve()returns(0, false)for ZPR. Simpler than encoding both zp byte and rel target throughresolve. - Disassembler is variant-aware (PR #55, issue #42). Legacy
Disasm/DisasmWithSymsstill use the NMOS table for back-compat;DisasmCPU/DisasmCPUWithSymsroute throughc.opcodesso CMOS-only mnemonics (STZ, PHX, BRA, etc.) render correctly. TUI + trace switched to the CPU-aware path.
Bug fixes worth remembering¶
- PR #31:
branch()was mutatingc.Cyclesdirectly butStep()returned onlyin.Cycles. Result: taken branches undercounted return value by 1–2. Fix:extraCycles intfield, reset eachStep, folded into the return. - Test gotcha:
r.Load(addr, prog)then laterr.Write(addr, x)clobbers the opcode. Discovered while writing the JMP (ind) wrap test — fixed by placing program at $8200 and using $8000 only as wrap-target sentinel.
Tooling¶
- CI matrix: ubuntu + macos + windows × Go
stable. Lint and Klaus jobs ubuntu-only. Coverage uploaded only from the ubuntu test job. - golangci-lint v2 syntax.
errcheckexcludes(*os.File).Close,bytes.Buffer/strings.Builderwriters, andfmt.Fprint*family. -covermode=atomicis required with-race.fail_ci_if_error: falseon Codecov so transient upload failures don't break the build.- License: MIT. GPL test ROMs (Klaus 6502_65C02_functional_tests) are NOT vendored — downloaded on demand with sha256 verification (
fa12bfc761e6f9057e4cc01a665a7b800ff01ae91f598af1e39a1201d01953fd).
UI¶
- Sigils mirror nvim-DAP:
- 🛑 plain breakpoint
- 👉 PC
- 🔶 conditional
- 💩 rejected
- 📜 logpoint
- 👁 read watch
- ✏ write watch
- 🔁 R+W watch
- Wide emoji (2 cells) in marker column → drop leading space to keep address column aligned.
6. Critical Context¶
Local commands¶
# Standard build+test
go build ./... && go test -race -count=1 ./...
# CMOS-only tests
go test -count=1 -run 'TestCMOS|TestNMOS|TestVariant' -v ./internal/cpu/...
# Coverage
go test -race -count=1 -coverprofile=coverage.out -covermode=atomic ./...
# Lint
golangci-lint run ./...
golangci-lint run --build-tags=klaus ./...
# Klaus functional test (build-tagged)
go test -tags=klaus -timeout 5m -run TestKlaus -v ./internal/cpu/...
# Build example
make -C example cmos_demo.bin
make -C example run-cmos_demo
CLI flags¶
chippy -rom <file> [-addr 0x8000] [-reset 0xADDR] [-cfg linker.cfg] [-dbg syms.dbg] [--cpu nmos|65c02]
-rom — program to load (.bin .prg .hex .o)
- -addr — load address for raw .bin (default 0x8000)
- -reset — reset vector override (0 = use file's vector or load addr)
- -cfg — ld65 linker config; required for .o files
- -dbg — cc65 .dbg symbol file (auto-detected as <rom>.dbg if omitted)
- --cpu — nmos (default) | 6502 | 65c02 | cmos | cmos65c02
Toolchain locations (macOS)¶
ca65,ld65,cc65at/opt/homebrew/bin/
References¶
- 65C02 opcodes: http://www.6502.org/tutorials/65c02opcodes.html
- 65C02 opcode matrix: http://www.oxyron.de/html/opcodesc02.html
- NMOS vs CMOS differences: http://wilsonminesco.com/NMOS-CMOSdif/
- Klaus 6502_65C02_functional_tests: https://github.com/Klaus2m5/6502_65C02_functional_tests
7. File Map (key files)¶
CPU core¶
internal/cpu/cpu.go—CPUstruct,Variantenum,New()/NewVariant(),Reset(),bindTable(), interrupt API (AssertIRQ/ReleaseIRQ/TriggerNMI), service routines, flag helpersinternal/cpu/exec.go—Step(), interrupt boundary service, addressing-mode load/store helpers, all opcode handlers (LDA/STA/ADC/SBC/branches/etc.)internal/cpu/addressing.go—AddrModeenum,resolve(); IZP/IAX/ZPR modes for CMOS; IND mode variant-branchedinternal/cpu/opcodes.go— NMOS opcode table (199 LOC)internal/cpu/opcodes_cmos.go— CMOS overrides (BRA, PHX/PHY/PLX/PLY, STZ, TRB, TSB, INA/DEA, BIT #imm, RMB/SMB/BBR/BBS, adcDecimalCMOS, sbcDecimalCMOS, cmosNOPs)internal/cpu/opcodes_illegal.go— NMOS unofficial opcodes (320 LOC)internal/cpu/disasm.go— disassembler; variant-aware viaDisasmCPU/DisasmCPUWithSyms. Legacy NMOS-fixedDisasmstill exported for callers without a CPU handy.internal/cpu/memory.go—Businterface +RAMimpl
Tests¶
internal/cpu/cpu_test.go— base helpers, LDA/ADC/etc. regression testsinternal/cpu/cycles_test.go— 4 cycle-count regression tests (PR #31)internal/cpu/cmos_test.go— 15 CMOS regression testsinternal/cpu/cmos_e2e_test.go— loadsexample/cmos_demo.bin, runs under CMOS, asserts state; self-skips when bin absentinternal/cpu/interrupts_test.go— 10 IRQ/NMI tests (PR #33)internal/cpu/klaus_test.go— build-tagged Klaus harness (PR #30); pattern reusable for BCD/decimal suites
TUI¶
internal/tui/model.go— Bubble Tea model, run loop, panel layout, key bindingsinternal/tui/wbus.go—WBuswrapscpu.Bus, captures hits for memory watchpoints, ring bufferinternal/tui/bp.go— breakpointsinternal/tui/cond.go— conditional breakpoint expressionsinternal/tui/membp.go/internal/tui/membp_test.go— memory breakpointsinternal/tui/prompt.go— command promptinternal/tui/state.go— persistence (~/.chippy/state-<rom>.json)
Other¶
cmd/chippy/main.go— CLI entry; flag parsing; bus wrap chaininternal/loader/—.bin/.prg/.hex/.oloaders (ld65 invoked for.o)internal/symbols/—.dbgparser, symbol table, source mapexample/Makefile—cmos_demotarget uses--cpu 65c02example/cmos_demo.s—.setcpu "65c02"; LDA/LDX/LDY/PHX/PHY/STZ/INC A/BRA/JMP self.gitignore— ignores*.bin/*.o/*.dbg/*.prg/*.hex/*.lst/*.map.github/workflows/ci.yml— 3-OS test matrix + lint + Codecov +klausjob (ubuntu-only).github/workflows/release.yml— goreleaser on tag push.goreleaser.yml— multi-arch binaries + brew formula publishnkane/homebrew-taprepo —Formula/chippy.rbauto-updated by goreleaser
8. Next Steps (immediate)¶
- Choose next from open issues: #17 (reverse step), #18 (stack panel), #19 (mem editor), #20 (prompt history). #22 (homebrew-core) is gated on ~30 stars.
- Deferred: CI job for the CMOS e2e test (self-skips because binary is gitignored).
- Possible: integrate Bruce Clark's BCD timing test or 6502_decimal_test as a klaus-style build-tagged suite — would also exercise the CMOS BCD path.
- User-side: mascot image generation (prompts in
docs/mascot-prompts.md).
9. Gotchas¶
- The
nkane/homebrew-tapformula update flow requires theHOMEBREW_TAP_GITHUB_TOKENsecret to remain valid — rotate if expired. - The Klaus ROM URL or sha256 changing would silently break CI's
klausjob; pin is ininternal/cpu/klaus_test.go. - CMOS table init relies on file-lexicographic Go
init()ordering. Renamingopcodes_cmos.goto come afteropcodes_illegal.gowould cause illegals to bleed into the CMOS table. Step()returns total cycles including interrupt service. Callers wanting just-the-instruction count would need separate tracking.WBusreadsc.PCafterc.PC++, so logged PC is one past the opcode for fetches. Tests assume this.