* Flag retirement F1: the quiet seven Plan 20260719-01: --scanlines, --fps, --strict, --debug, --boot-delay, --trace/--trace= and --state are gone from `ewm two` — they were single-value duplicates of config keys (display.scanlines, display.fps, cpu.strict, debug.enabled, boot.delay, debug.trace, state.path), all reachable via --set or any config source. Retired flags fall into the generic usage error (owner's ruling: v0.0.1, no transition ceremony); a test sweeps the list and pins the --set spellings. Scanlines::parse went with its only caller (the flag's optional-value peek); MonitorStyle::parse follows in F2 with --color. Docs swept: README's feature blurb, STATE.md and REMOTE.md speak state.path / --set state:path now, IDEAS.md's trace item names debug.trace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Flag retirement F2: the muscle-memory trio Plan 20260719-01: --model, --color and --aux are gone from `ewm two` — machine.model, display.monitor and machine.aux are config keys, and `--config builtin:2e` is as short as `--model 2e` was. The --model alias spellings (2+, ][+, //e, iie) go with it, as does --color's optional-value peek (bare --color meant rgb) and MonitorStyle::parse, its last caller. The aux token survives internally (Options.aux, rebuilt by apply_config); only the flag is gone. Swept the consumers: main.rs's getting-started hint and subcommand line, the README feature blurbs and quick-start (builtin-first now, enforced by readme_two_examples_parse), JSON_CONFIG.md's schema inventory (sources are config + --set for everything), and the --color/--aux comment references in scr.rs, aux.rs, config.rs and two.rs. `one` keeps its --model — it has no config surface (plan non-goal). Historical notes (APPLE_IIE_ENHANCED, REWRITE, landed IDEAS items) keep their period wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Flag retirement F3: --memory Plan 20260719-01, last phase: --memory is gone from `ewm two`; memory regions come from config or overlay files (machine.memory), the one setting --set cannot reach (arrays) — apply_set's error text now says so. two's parse_memory_option went with the flag (one keeps its own copy and its --memory flag: no config surface there, plan non-goal). The print-config round trip composes the region through an overlay now, which is exactly the documented replacement. With this, `ewm two`'s surface is the plan's goal shape: the four sources (--config, --config-overlay, --set, --print-config), --serve, and the debug tooling (--wozbug, --break, hidden --screenshot). Plan marked complete with the kickoff decisions recorded as built. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * README: wrap the quick-start commands; document the three two profiles The "start a machine directly" commands wrap with backslash continuations (the readme_two_examples_parse test joins them, so they stay checked). A new section explains that bare `ewm two` boots the default machine — a ][+ with the 16K Language Card (the classic 64K build), Thunderclock, and Disk II — and documents all three machine profiles in English and JSON: the default (with its config-terms equivalent, printable via --print-config), builtin:2plus, and builtin:2e, each with its explicit `--config builtin:<name>` boot command. The later ready-made-configs paragraph folded into it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * README: brief Experimental section for WozBug, VNC, and the Mac app WozBug's walkthrough and the Native Mac app section fold into a new Experimental section at the bottom — three short bullets, each linking to its working document (notes/DEBUGGING_TOOLS.md, notes/REMOTE.md, notes/MAC_APP.md). The remote console gets its first README mention there; its --serve bullet leaves the source-kinds list, which is back to the three document sources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * README: drop the cc65 tests/ sentence The C-era assembly test programs are being removed in #284; the Testing section no longer mentions them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * README: complete Key commands section The keys table becomes a "Key commands" section and gains the missing entries: Cmd-K (the Command Palette, available on every machine) and Cmd-Shift-R (reboot, power off/on), plus a Machines column instead of per-row parentheticals. A short paragraph documents the palette's own keys (type to filter, arrows, Return, Esc), that everything else — including Ctrl-A..Z control characters — goes to the emulated machine, and drag-and-drop disk swapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
14 KiB
Debugging Tools — WozBug and Friends
A working document for building debugging support into EWM, so the next
bug hunt starts from tools instead of temporary code. In the house style
of JSON_CONFIG.md / MAC_APP.md: re-read at the start of every session,
update as phases land. Every phase passes the full gates (cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test).
Status
| Phase | Description | Size | Status |
|---|---|---|---|
| 1 | CPU breakpoints + the WozBug command core (library, used by tests) | S/M | Done |
| 2 | The --wozbug line server + --break flag + device commands |
M | Done |
| 3 | Watchpoints, runtime trace toggle, disassembly, symbol table | M | Done |
Phase 1 decisions (recorded as built)
- Breakpoint overhead is zero, as theorized: the Dormann suite
(release) runs in 0.19s both before and after the
step()breakpoint check — the empty-Vecbranch is unmeasurable, so breakpoints live in the normal build unconditionally. No debug-build split exists. - Stopped semantics: a hit (or
Cpu::stop()) makesstep()return 0 without executing;resume()clears it and skips the breakpoint at the current PC once, so resume-then-step makes progress. Burst loops must checkstopped()or they spin on the 0-cycle steps. wozbug::WozBug::execute(&mut Two, &str) -> Stringis the whole core; the only session state is the dot address.Gclears the stopped state and leaves running to the caller.- Bus-honest dumps: memory reads go through the bus, soft-switch side
effects included — authentic, documented in
?help. A side-effect-free peek is a Phase 3+ question if it ever bites. - The #253 retrofit is
ewm/tests/wozbug.rs: boot DOS,B RWTS(by symbol), CATALOG lands on the breakpoint,RnamesPC=BD00 (RWTS),DSKshows the controller, Y/A → IOB checked,S/Gresume cleanly.
Phase 2 decisions (recorded as built)
--wozbug [port](default 6502) starts the line server on 127.0.0.1;--break addr[,addr](hex or symbols —--break RWTS) arms breakpoints at boot and implies the server. Optional-value parsing peeks without consuming, so a following flag is untouched.- Threading: the server threads only move strings over channels — the
frame loop drains commands and runs
execute()on the machine's own thread, so no locking exists and an idle server costs onetry_recvper loop iteration. One client at a time; the writer uses a 200msrecv_timeoutso a silent disconnect frees it to accept the next connection. - Stop integration: the frame burst breaks on a 0-cycle step; a
rising stopped edge sends
stopped at …+ registers to the client and stderr (so a hit is visible even before a client connects — the announcement a later client misses is retold byR's[stopped]). - No stdin REPL (open question resolved): the server covers scripted
and interactive use via
nc; the tty adds a raw-mode/SDL headache for no new capability. - Verified live:
ewm two --break RWTS --set machine:slots:6:drive1=…, thennc localhost 6502→ announcement on connect,Rshows the IOB pointer in Y/A,S 3tracesSTY $48 / STA $49 / LDY #$02,Gresumes into the boot.
Phase 3 decisions (recorded as built)
- Watchpoints stop after the touching instruction (post-hoc, unlike
the pre-execution PC breakpoint):
Memoryrecords the first watched access per instruction,Cpu::stepstops and keeps the reason (watch_stop()), and the banner printswatch: write 2000 = 5A. Opcode fetches count — executing watched code is a watched read. The trace formatter's lookahead reads deliberately do not. - Overhead measured, kept in the normal build: the Dormann suite goes
0.19s → 0.21s release (~5-10% on the pure-CPU microbenchmark — the
irreducible one-branch-per-bus-access). At the app's real-time 1MHz
pacing the cost is zero, and CI moves by a couple of seconds, so
watchpoints stay always-available; the
debuggerfeature gate remains the documented fallback if that judgment ever changes. - Dialect rule:
B/W/Ltake their argument after a space or-, never glued —Bis a hex digit, andBD00must examine memory, not set a breakpoint at$D00(WAITexamines the symbol, not a watch). T/T-toggles the--tracemachinery onto stderr at runtime;Lreuses the trace disassembler via the newewm_core::fmt::disassemble(a bareLcontinues, like the Monitor's).- JSON output mode: not built — demand-driven, and no demand yet.
The motivating case (what PR #253 actually took)
Debugging the multi-controller CATALOG hang meant writing a throwaway
example binary that booted the machine, typed the command through the
keyboard latch, and every 5M cycles printed the PC plus hand-picked device
state (half_track, active_drive, drive_lit). That worked, but every
step of it should have been a tool:
- "Where is it stuck?" → sampling
two.cpu.pcby hand. Wanted: a breakpoint at$BD25, or just asking a live machine for its registers. - "What is the controller doing?" → ad-hoc
eprintln!ofDskaccessors. Wanted: aDSKcommand that dumps every controller's state. - "What code is
$BD25?" → recalling from memory that$BD00is the DOS 3.3 RWTS entry. Wanted: a small symbol table that printsBD25 (RWTS+$25). - "What's on screen?" →
text_screen()calls sprinkled around. Wanted: aTEXTcommand.
None of this needs a big debugger. It needs a small, always-available one — which is exactly the lesson of Apple's MicroBug.
Prior art: MicroBug and MacsBug (TN1136)
Apple shipped MicroBug in every Mac ROM from the Plus onward: the
debugger you got when the NMI fired and MacsBug wasn't installed. Its
entire command set was DM (dump memory), SM (set memory), G [addr],
TD (all registers), and Ax/Dx/PC/SR (display/set one register), over a
tiny expression language (hex, . = the "dot address" remembered from the
last dump, @expr indirection, +/-). Lessons worth stealing:
- Tiny is sufficient. Dump, set, registers, go, breakpoints — that covers the #253 session and most sessions like it.
- The dot address.
DMremembers where it was; a bare Return continues the dump. Browsing memory becomes two keystrokes. - Always present beats powerful. MicroBug existed for the bugs that disappear when you install MacsBug. Our analogue: features that live in the normal build (when they're free) are worth more than a fancy debug-only build nobody runs.
- From MacsBug, one idea matters more than all its power features:
domain-aware commands (
dcmds, templates, heap checks). A debugger that knows the machine — controllers, soft switches, the text screen — is worth ten generic memory dumpers.DSKis ourhz(heap zone).
And EWM has something the Mac never did: the machine's own debugger
heritage. The Apple II Monitor (CALL -151) syntax — 280.29F to dump,
300:A9 20 to deposit, 300G to go, L to disassemble — is the natural
dialect for this emulator's debugger. Hence:
WozBug
A minimal monitor, MicroBug in role, Woz Monitor in dialect. One command core with three frontends:
┌──────────────────────────────┐
tests ──────────▶│ wozbug::execute( │
│ &mut Two, &str) -> String│◀───── line server
(direct calls) └──────────────────────────────┘ (nc/telnet)
▲
│ on breakpoint / --break
the SDL loop pauses
- Library first.
wozbug::execute(&mut Two, cmd) -> Stringis a pure function over the machine. Integration tests call it directly — the #253 test's hand-rolled state assertions becomeassert!(wozbug(&mut two, "DSK").contains("S6 D1 track 17"))-style checks, and the interactive frontends are thin wrappers. - Line-oriented TCP server, not HTTP.
ewm two --wozbug [port](default: 6502, of course) accepts one connection and speaks newline-delimited commands — trivially driven by a human withnc, or by Claude withprintf 'R\nDSK\n' | nc -w1 localhost 6502. HTTP+JSON adds nothing for this use and costs a dependency; if tooling ever wants structure, a JSON output mode (R -j) or an HTTP façade can be layered on the same core later. - Pause semantics. The server thread only queues lines; the SDL loop
drains and executes them between frames (same thread as the machine, no
locking). Commands work against the running machine; a breakpoint or
an explicit
STOPcommand pauses emulation (the existing Cmd-P pause path) untilG.
Command sketch (Woz Monitor dialect where it fits)
| Command | Does | Heritage |
|---|---|---|
280.29F |
hex+ASCII dump of a range | Woz Monitor |
280 |
examine one byte; sets the dot address | Woz Monitor |
| (Return) | continue the last dump from the dot address | MicroBug DM |
300:A9 8D 20 |
deposit bytes | Woz Monitor |
R |
registers: A X Y SP PC P + decoded flags + symbol for PC |
MicroBug TD |
A=FF PC=BD00 |
set a register | MicroBug Ax |
G / 300G |
resume / go from address | both |
S / S 20 |
step 1 / n instructions, printing trace lines | MacsBug t |
B BD25 / B / B-BD25 |
set / list / clear breakpoints | — |
DSK |
every controller: slot, selected drive, half-track, motor, media | dcmd lesson |
SW |
soft-switch state (text/mixed/page2/hires, 80col, banks…) | dcmd lesson |
TEXT |
the rendered text screen | dcmd lesson |
SLOTS |
the machine's slot table | dcmd lesson |
T / T- |
toggle the CPU trace (the --trace machinery) at runtime |
— |
Parsing stays MicroBug-forgiving: hex everywhere, no $/0x required,
unknown input answers with a beep… or at least a ? (the Monitor's own
error message).
Breakpoints in the core: the cheap-theory is probably right
The owner's theory — "maybe breakpoints are simple stuff with no overhead" — is very likely correct and should be validated first thing in Phase 1:
Cpugainsbreakpoints: Vec<u16>andstep()checks!breakpoints.is_empty() && breakpoints.contains(&pc)before executing. With no breakpoints set that is one always-false, perfectly-predicted branch per instruction. Expected cost: unmeasurable. Measure it against the WOZ sweep (zz_woz_sweepis our de-facto CPU benchmark) before assuming; if it truly vanishes, breakpoints live in the normal build unconditionally and no debug-build split exists at all.- A hit sets a
stoppedreason instead of executing; the frontends decide what pausing means (tests: return fromstep_until-style loops; SDL loop: enter pause + announce on the wozbug connection / stderr). - Watchpoints (break on read/write of an address or range) sit on the
memory bus — every access, hotter than the PC check. Same empty-check
pattern, but this one might show up; measure, and if it costs, gate it
behind a
debuggercargo feature (and accept that watchpoints then need a debug build — fine; breakpoints are the always-present baseline). - Verbose memory-access logging (the "chatty debug build" idea) becomes
redundant if watchpoints exist:
W C0C0.C0CFlogging accesses to one DEVSEL range is strictly better than a firehose of every bus access.
CLI instrumentation
--break BD25[,C600]— arm breakpoints at boot and start paused-on-hit; implies--wozbugso there is somewhere to land. This alone would have turned the #253 session into a two-minute look.--wozbug [port]— the server, machine running normally.- Later, if wanted:
--log dsk,iouscoped runtime logging as the grown-up replacement for the current all-or-nothing--debug.
A small symbol table
A built-in, read-only map of the addresses this machine's software lives
at: Monitor entry points ($FF69, $FDED COUT, $FD0C RDKEY…), the DOS 3.3
RWTS/file-manager landmarks ($3D0, $9D00, $B7E8 IOB, $BD00 RWTS), ProDOS
MLI ($BF00). R and S print PC=BD25 (RWTS+$25); B RWTS works as an
address. Tiny (a static array), high leverage — in #253 this would have
named the hang site instantly.
Phases
Phase 1 (S/M) — the core, test-facing: breakpoints in Cpu (+ the
overhead measurement recorded here), the wozbug module with execute()
covering R, dump/deposit/dot-address, G/S/B, DSK/SW/TEXT/
SLOTS, the symbol table; integration tests use it (retrofit one #253
assertion as proof).
Phase 2 (M) — the interactive surface: --wozbug line server wired
into the SDL loop (queue + between-frames execution, pause/resume),
--break, breakpoint-hit announcement, README section.
Phase 3 (M) — the power-ups, each optional and demand-driven:
watchpoints (with the feature-gate decision), runtime trace toggle T,
L disassembly (ewm-core/fmt.rs already disassembles for --trace —
reuse it), JSON output mode if tooling wants it.
Open questions
- Interactive entry without the server: is stdin a useful REPL when EWM is launched from a terminal (breakpoint hit → prompt on the controlling tty)? Nice for humans; the server already covers Claude. Decide in Phase 2.
- Step-over / run-to-RTS:
Sis per-instruction; RWTS-sized hunts want "run until return". Cheap to add on top of breakpoints (break at the return address from the stack) — Phase 3 candidate. - One connection or many: one is enough; rejecting a second keeps the server trivial.
- Where symbols stop: monitor/DOS/ProDOS landmarks yes; per-program symbols no — this is MicroBug, not MacsBug.