A simple debugger that works by stopping the current thread, and launching a nREPL-enabled repl over a random port.
You can access said repl from a secondary terminal (or presumably IDE, but I haven't bothered trying).
For your convenience, the connection command is copied to the clipboard.
Once you connect, you have full access to the debugee's environment, including function arguments,
let
and dynamic bindings, top-level vars, and aliases brought in via require
.
As you exit, the stopped thread resumes execution.
I wanted a small, comprehensible debugger that I can trust to work in a variety of environments, agnostic of editor or concurrency model.
For example I may want to plant a (debugger)
statement inside my test suite, which will be run in parallel with eftest. And I want to connect to that debugger from anywhere, free from intricacies or limitations of my debugger or IDE of choice.
This of course comes at the cost of feature-richness. GUIs, step-debugging or frame exploration are awesome tools, but certainly ones which would compromise the original goals.
You can insert calls to (debugger)
anywhere in your code:
(def ^:dynamic
*doing-it?*
false)
(defn example [x]
(binding [*doing-it?* true]
(let [y (+ x 3)]
(letfn [(omg [z]
(* y 2))]
(net.vemv.nrepl-debugger/debugger) ;; We inserted a debugging statement here
3))))
Now, run (example 98)
to trigger the debugger. It will print:
Entered debugger.
Started debugging server at port 54102. Copied connection command to the clipboard.
So, go over a terminal, and paste the connection command, which is simply lein repl :connect 54102
.
You will see a plain repl in the user
namespace. Regardless, all your code is loaded, the being-debugged environment is fully-accessible via the in-context
macro:
user=> (net.vemv.nrepl-debugger/in-context [y *doing-it?*])
[101 true]
You can evaluate arbitrary forms inside in-context
.
As you finish your repl session with ControlD, execution in your app will resume, cleanly stopping the nREPL server.
debugger
ignores any passed arguments, making it apt for doto
:
(doto something debugger foo bar)
What about threading macros? The original debugger
would break the underlying ->
because of its return value.
For that scenario, debugger->
is provided:
(-> something inc debugger-> dec)
debugger->
works like debugger
, except that it accepts one argument, exposing it as the *debugger->*
dynamic variable,
and returning the argument so that ->
doesn't break.
That way, you can debug threading macros - an old problem!
There's no debugger->>
- debugger->
is enough for ->>
:
(->> something inc debugger-> dec)
- For now a single global debugging-context and nREPL server are assumed, so don't try debugging things in parallel or with reentrancy.
- JVM-only.
Probably your IDE has some sort of configurable snippet system. As an idea, you can bind the following to dbg
:
(do (require 'net.vemv.nrepl-debugger) (eval '(net.vemv.nrepl-debugger/debugger)))