{"op" "cider/debug-input"
"key" "<the key from the breakpoint message>"
"input" "<a command>"}
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.
The client sends one cider/init-debugger message 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 its id and session.
The client instruments code by including reader tags (#break, #dbg,
…) in the code it sends to the plain eval op. 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-input on the
init-debugger channel.
The client answers with a cider/debug-input message. Its key param
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.
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).
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. |
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 i has index 2*i and its value 2*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.
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.
{: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 new
init-debugger message replaced it. Re-initialize if you didn’t cause it.
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.
The debugger channel is currently global to the middleware instance, not
per-session: the most recent init-debugger wins and any previous channel
receives done. Send exactly one init-debugger per 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
notification warning); if the form still doesn’t fit, it reports an
error and leaves the form unevaluated.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |