A debugger for PicoRuby. mruby only — on mruby/c it prints an "unsupported" message and does nothing.
This gem is the foundation for an interactive debugger. Dropping a
binding.debugger call into a script installs mruby's code_fetch_hook,
which checks each new source line against the registered breakpoints and,
when the current mode says to stop, calls back into Ruby (Debugger#on_break)
to show an interactive prompt.
Add the following line to your build configuration:
conf.gem github: 'yuuu/picoruby-debug', branch: 'main'This gem does not depend on ENV['PICORB_DEBUG'] or any other build flag —
adding it to the gem list is enough to enable binding.debugger support, on
any target (POSIX host, ESP32, etc.).
require 'debug'
a = 1
b = 2
binding.debugger # or binding.b / binding.break
c = a + b
puts cRunning this script pauses at the binding.debugger line and drops you into
an interactive prompt:
Breakpoint: /test.rb:5
(prdb)>
From there you can add more breakpoints with b <line> (matched by suffix
against the file the breakpoint applies to, so b test.rb:8 matches
/path/to/test.rb), then c to continue running until the next one is hit.
Commands at the prompt:
| Command | Description |
|---|---|
c / continue / (empty) |
Resume execution until the next breakpoint |
s / step |
Stop at the next line, stepping into calls |
n / next |
Stop at the next line in the same/shallower frame |
q / quit |
Stop the script |
bt / where |
Show the full call stack, innermost frame first (#0, #1, ...); a frame with no Ruby-level position (e.g. a C frame) is omitted; the currently selected frame (see frame/up/down below) is marked with => |
f / frame [<number>] |
Show the currently selected frame, or select frame <number> (as listed by bt) and show it |
u / up [<count>] |
Select the frame <count> (default 1) levels toward the caller (outward) |
down [<count>] |
Select the frame <count> (default 1) levels toward the callee (inward) |
b / break [<file>:]<line> |
Add a breakpoint, or list the current breakpoints (with their numbers) if no argument is given |
d / delete [<number>] |
Delete breakpoint <number> (as shown by b with no argument), or all breakpoints if no number is given |
l / list [<line>] |
Show the source around the current line of the selected frame (see frame/up/down), or around <line> if given (10 lines of context, current line marked with =>) |
p / print <expression> |
Evaluate <expression> against the selected frame's locals (see frame/up/down) and print the result (via Binding#eval); unavailable if no binding could be built for that frame |
disp / display <expression> |
Register an expression to be automatically evaluated and shown every time execution stops, or list the currently registered display expressions (with their numbers) if no argument is given |
undisp / undisplay [<number>] |
Remove display expression <number> (as shown by display with no argument), or all of them if no number is given |
w / watch <expression> |
Stop execution automatically whenever <expression>'s value changes, or list the currently registered watchpoints (with their numbers) if no argument is given |
uw / unwatch [<number>] |
Remove watchpoint <number> (as shown by watch with no argument), or all of them if no number is given |
This gem can act as a minimal
Debug Adapter Protocol
server, so an editor (e.g. VS Code) can drive it instead of (or alongside)
the (prdb) prompt. Two pieces make this up:
DapTransport(mrblib/dap_transport.rb) — the wire format only:Content-Length: <n>\r\n\r\n<n bytes of JSON>framing over a TCP socket.DapSession(mrblib/dap_session.rb) — the request/response layer on top, a second front end over the same coreDebuggeroperationsdispatch_command(the CLI) already uses. Supported requests:initialize,attach(launchis treated as an alias — this debugger has no separate "launch a process" step),setBreakpoints,configurationDone,continue/next/stepIn/stepOut,stackTrace,scopes,variables,evaluate,threads,disconnect; it emitsstoppedandterminatedevents. Unhandled requests or a bug in a handler get asuccess: falseresponse rather than aborting the session.
Wherever DapTransport.available? (i.e. picoruby-socket is in the build),
DAP is on by default on port 4711 (Debugger::DEFAULT_DAP_PORT) — just
require 'debug' and binding.debugger is enough:
require 'debug'
def add(a, b)
a + b
end
binding.debugger
puts add(1, 2)The first binding.debugger call is this debugger's only entry point, so
it's also the one point where "the script hasn't started running yet" and
"we can still talk to a client" overlap: it blocks there until a client
completes the initialize/attach/setBreakpoints/configurationDone
handshake, then reports that first stop (reason "entry") and every
subsequent one the same way over the same connection.
Use a different port with Debugger.listen_dap(port), called before the
script's first binding.debugger. Pass a falsy port
(Debugger.listen_dap(nil)/listen_dap(false)) to opt back out of DAP
entirely and fall back to the plain (prdb) prompt.
picoruby-socket is not a hard dependency of this gem — a build without
it still compiles, DapTransport.available? returns false, and the above
just runs under the normal (prdb) prompt instead regardless of the default
port. Add picoruby-socket to your build config to actually use it.
picoruby-sandboxpicoruby-editorpicoruby-io-consolepicoruby-jsonmruby-binding(mruby only)mruby-eval(mruby only)picoruby-socket(optional, soft dependency — only needed forDapTransport; see above)