The Debugger Protocol
The interactive debugger is the most involved protocol in cider-nrepl. It
consists of two dedicated ops (cider/init-debugger and
cider/debug-input), a hook into the regular eval op, and a small
command language. This page describes the wire protocol; CIDER’s own
debugger UI is just one possible client of it.
Big Picture
-
The client sends one
cider/init-debuggermessage per connection. It never receives a normal reply; instead the server holds on to it and uses it as a channel: every debugger-initiated message (a hit breakpoint, a stacktrace, a notification) is sent as a response to this message, i.e. carrying itsidandsession. -
The client instruments code by including reader tags (
#break,#dbg, …) in the code it sends to the plainevalop. No special op is needed. -
When instrumented code hits a breakpoint, the evaluating thread blocks and the client receives a message with status
need-debug-inputon the init-debugger channel. -
The client answers with a
cider/debug-inputmessage. Itskeyparam pairs the answer to the blocked breakpoint (concurrent breakpoints on different threads are disambiguated this way). The thread wakes up, acts on the command and either resumes or breaks again.
Instrumenting Code
The following reader tags trigger instrumentation when they appear in code
sent to eval (or load-file):
| Tag | Effect |
|---|---|
|
Pause when execution reaches this form (after it’s evaluated). |
|
Instrument the whole form: pause at every "interesting" sub-form (function calls, loop iterations, etc.). |
|
Like |
|
Like |
|
Enlighten mode - no pauses; see Enlighten below. |
The op remains an ordinary eval; the value of the instrumented form is
returned as usual once execution completes. The cider/debug-instrumented-defs
op returns the vars currently believed to be instrumented, grouped by
namespace (it can contain false positives, but no false negatives).
The Breakpoint Message
When a breakpoint is hit, the init-debugger channel receives a message with
status need-debug-input and these keys:
| Key | Meaning |
|---|---|
|
Unique token for this pause; echo it back in |
|
Either a list of currently allowed command keywords, or |
|
Optional prompt string accompanying an |
|
Printed value of the form we’re paused on (truncated for display). |
|
Coordinate of the paused form within the top-level form (see below). |
|
List of |
|
Source context of the instrumented top-level form. |
|
The |
|
Present only right after an |
|
Present only for exception breakpoints: the exception’s message. |
Coordinates
coor is a vector of integers addressing a sub-form inside the top-level
form, one index per nesting level. [3 2 0] means: take the 3rd element of
the top-level form, then that element’s 2nd element, then that one’s 0th.
A few special rules:
-
In maps, the key at entry
ihas index2*iand its value2*i+1. Maps with more than eight entries are addressed in sorted-key order (so coordinates are stable across Clojure versions); maps whose keys can’t be sorted, and sets, are not instrumented at all. -
Record literals are never instrumented.
Clients typically use coor plus line/column of the top-level form to
compute where to display the "paused here" overlay.
Debug Commands
The client answers a need-debug-input message with:
{"op" "cider/debug-input"
"key" "<the key from the breakpoint message>"
"input" "<a command>"}
input is read as EDN. It’s either a bare keyword (e.g. :next) or a map
{:response :here, :coord [3 1]} when the command takes arguments. The
available commands are advertised in the breakpoint message’s input-type
(commands that need locals disappear when locals aren’t available; :quit
disappears when the eval can’t be aborted):
| Command | Effect |
|---|---|
|
Continue to the next breakpoint. |
|
Step into the function call the debugger is paused on. |
|
Skip the remaining breakpoints in the current sexp. With |
|
Run until the given position; takes |
|
Skip the remaining breakpoints for the current invocation of the instrumented form (they fire again on the next invocation). |
|
Disable all breakpoints for the rest of this evaluation. |
|
Continue, reporting each intermediate form and value along the way. |
|
Evaluate an expression in the context of the paused frame (with locals in
scope). Takes |
|
Like |
|
Render the current value with the inspector; arrives in the |
|
Prompt for an expression, evaluate and inspect its result. |
|
Inspect the map of local bindings. |
|
Sends a message with status |
|
Abort the evaluation (the eval returns the value |
Errors during :eval/:inject produce a message with status eval-error
and analyzed causes on the debugger channel; the breakpoint remains
paused and re-prompts.
Other Messages on the Debugger Channel
-
{:status :notification, :msg …, :type …}- informational messages the client should surface (e.g. a form too large to instrument with locals). -
{:status :done}- the channel was released, typically because a newinit-debuggermessage replaced it. Re-initialize if you didn’t cause it.
Enlighten
The #light reader tag (or an enlighten flag on the eval message)
enables enlighten mode: execution is not paused, but every instrumented
sub-form reports its value as it runs, as messages with status enlighten
on the debugger channel. Each carries coor and debug-value (very
aggressively truncated), plus erase-previous when the client should clear
overlays from a previous run of the same form (e.g. re-invoked functions).
Enlighten requires an initialized debugger channel, since it reuses it.
Caveats
-
The debugger channel is currently global to the middleware instance, not per-session: the most recent
init-debuggerwins and any previous channel receivesdone. Send exactly oneinit-debuggerper connection. -
Debugging is JVM-only; ClojureScript can’t block a JS runtime waiting for debug input.
-
Instrumentation can push a big function over the JVM’s 64KB method-size limit. The debugger automatically retries without local capture (with a
notificationwarning); if the form still doesn’t fit, it reports an error and leaves the form unevaluated.