operon debug is the interactive debugger for Operon programs. It runs on
the interpreter lane (the VM lane is the performance lane; the debugger
forces the tree-walker) and stops AFTER each statement completes, reporting
the statement's own source line.
Status: implemented (W08r, 2026-10-01; W008 polish, 2026-10-02). Phases: the
human REPL, the NDJSON machine protocol, the Debug Adapter Protocol adapter,
and the VS Code extension. W008 polish added conditional breakpoints,
stopOnEntry, variable assignment (set/setVariable), and real call-site
lines for outer stack frames — on all three surfaces at once.
operon debug f.op --break 12 # start a session, break at line 12
operon debug f.op --break 3 --break 7 # multiple initial breakpoints
At a stop the prompt names the line you stopped at:
(dbg) line 12 >
Commands:
| command | meaning |
|---|---|
c / continue |
resume to the next stop |
s / step |
step-INTO: break after the next statement at any depth |
n / next |
step-OVER: break after the next statement at this depth (calls run to completion without trapping) |
fin / finish |
step-OUT: run until the current frame returns |
until N |
one-shot continue-to-line: stop when line N's statement completes |
b N |
add a breakpoint at line N (fires immediately, even mid-session) |
b N if COND |
add a CONDITIONAL breakpoint: fires only when COND evaluates truthy in the current frame (a condition that cannot parse or raises never fires — no surprise stops) |
b del N |
delete the breakpoint at line N |
b list |
list active breakpoints (conditions rendered) |
set NAME EXPR |
rebind NAME in the current frame (same reach as assignment; const names are refused — frozen is frozen — and unknown names are refused, never silently created) |
bt |
the call chain with each frame's current line: the innermost frame is at the stop line, an outer frame is parked at the call site of the frame one level deeper |
vars |
dump the frame chain's variables (innermost first, 8 frames) |
p EXPR |
evaluate EXPR in the current frame (bare literals included — p 99 works via the let-wrapper fallback) |
q / quit |
end the session, exit code 0 |
EOF on stdin resumes to completion: piped sessions never wedge. This is a
load-bearing contract, enforced by scripts/debug_e2e.sh on every gate run.
The trap matches the statement's OWN line (extracted from its line-bearing
expression nodes; the parser wraps line-silent statements — pure
literals/idents — in a transparent position marker). A breakpoint inside a
called gene never re-fires on the caller's call statement the moment the
call returns, and a stop after let a = work(10) reports line 8, not the
last line inside work.
operon debug f.op --protocol=json replaces the human REPL with a
line-delimited JSON protocol — one JSON object per line on stdout, one
request per line on stdin. Program print output is captured and drained to
stderr ([out] ... lines) so stdout stays pure protocol.
Events (stdout): {"event":"stopped","reason":"breakpoint"|"step"|"until"|"entry","line":N,"depth":D}
Requests (stdin), replies {"id":N,"ok":true,...} / {"id":N,"ok":false,"error":"..."}:
{"id":1,"cmd":"continue"} # resume
{"id":2,"cmd":"next"} {"id":2,"cmd":"stepIn"} {"id":2,"cmd":"stepOut"}
{"id":3,"cmd":"until","args":{"line":9}}
{"id":4,"cmd":"breakpoints","args":{"add":[9],"remove":[3]}}
{"id":4,"cmd":"breakpoints","args":{"add":[{"line":4,"condition":"y > 100"}],"remove":[]}}
{"id":5,"cmd":"set","args":{"name":"x","expr":"777"}} # live rebind, assignment reach
{"id":6,"cmd":"stack"} # frames with real lines, innermost first
{"id":7,"cmd":"vars"} # scopes with rendered variable maps
{"id":8,"cmd":"eval","args":{"expr":"z * 100"}}
{"id":9,"cmd":"status"} # line + depth
{"id":10,"cmd":"quit"}
scripts/debug_protocol_e2e.py is a complete reference client.
operon dap f.op speaks the Debug Adapter Protocol on stdio
(Content-Length base protocol) — the same protocol VS Code, Visual Studio,
Neovim, emacs and JetBrains debug clients speak natively:
- lifecycle:
initialize→initializedevent →launch→setBreakpoints→configurationDone→ run →stoppedevents →stackTrace/scopes/variables/evaluate/setVariable→terminated/exited - stepping verbs:
continue,next(step-over),stepIn,stepOut— mapped onto the same depth-aware machinery as the REPL - program output arrives as
outputevents (categorystdout); the adapter's stdout is protocol-only evaluateruns in the stopped frame, same semantics as the REPL'sp- W008 polish: the launch arguments accept
stopOnEntry(one-shot stopped event, reasonentry, at the program's first statement);setBreakpointsaccepts per-breakpointconditionfields (evaluated in the stopped frame like the REPL'sb N if COND);setVariablerebinds live frame state with the language's own assignment reach (consts stay frozen);stackTracereports real call-site lines for outer frames
scripts/dap_e2e.py is a complete reference client.
The repository ships editors/vscode/ — a VS Code extension contributing
the operon debug type. It launches operon dap <program> as an embedded
Debug Adapter (no extra server process). Install: copy/symlink the folder
into your extensions directory (or package it with vsce), open a .op
file, add a launch configuration:
{
"type": "operon",
"request": "launch",
"name": "Debug Operon file",
"program": "${file}",
"operonPath": "operon",
"args": [],
"cell": ""
}operonPath defaults to operon on PATH; cell grants capabilities for
programs that read/write files.
| gate | what it pins |
|---|---|
scripts/debug_e2e.sh |
the REPL contract (breaks fire, vars/p/s/c, next/finish/until, bp management, conditional bps, set, bt lines, EOF resumes) |
scripts/debug_protocol_e2e.py |
the NDJSON contract (purity, stop reasons, all verbs, conditional bps, set, stack lines, print rerouting) |
scripts/dap_e2e.py |
the DAP contract (lifecycle, capabilities, stopOnEntry, conditional bps, setVariable, stacks/scopes/vars/eval, output events) |
- variables are rendered strings (no child/struct expansion references)
setVariable/settarget named frame bindings (no element assignment inside lists/maps)- no logPoints or hitConditions
- the CLI
--breakflag stays numeric-only; conditions arrive through the live surfaces (REPL/NDJSON/DAP)