Spy is a debugging tool for Clojure that allows you to “spy” on the values of local bindings within your functions and let blocks. It comes with an Emacs Lisp companion that provides a seamless debugging experience within Emacs.
Features:
- Effortless Spying: Use the
spymacro to automatically track the values of local bindings. - Dynamic Instrumentation: Instrument and un-instrument functions on the fly without restarting your REPL.
- Emacs Integration: A rich Emacs experience with overlays to display spied values directly in your source code.
- Minimal Setup: Works with both
deps.ednand Leiningen.
**deps.edn**
Add the following to your `deps.edn` file to include `spy` as a Git dependency.
{:deps {io.github.your-username/spy {:git/sha "..."}}}Replace `your-username` with your GitHub username and `…` with the specific commit SHA you want to use.
For local development, you can use a `:local/root` dependency. This is useful when you are working on `spy` and a project that uses it at the same time.
{:deps {spy {:local/root "/path/to/spy"}}}Replace `/path/to/spy` with the actual path to your local `spy` project.
**Leiningen**
For Leiningen-based projects, you can use JitPack to include this library.
- Add JitPack to your repositories in `project.clj`:
:repositories [["jitpack" "https://jitpack.io"]]- Add the dependency to your `project.clj`:
:dependencies [[org.clojure/clojure "1.10.1"]
[com.github.your-username/spy "the-commit-sha"]]Replace `your-username` and `the-commit-sha` with the appropriate values.
For local development with Leiningen, the `lein checkouts` feature is the standard way to work with a library and a project that uses it simultaneously. It allows Leiningen to use the local version of your library directly, so you don’t have to deploy it to a repository every time you make a change.
- Create a `checkouts` directory in the root of your project.
- Inside the `checkouts` directory, create a symbolic link to the root of the `spy` project.
cd /path/to/your/project
mkdir checkouts
cd checkouts
ln -s /path/to/spy spyNow, when you run your project, Leiningen will use the code from the `spy` directory in your `checkouts` directory instead of the version from the repository. This means any changes you make to the `spy` library will be reflected immediately in your project.
Workflows
There are two main ways to use spy: dynamic instrumentation (the recommended approach) and manual wrapping.
This is the most convenient way to use spy for quick debugging sessions. The Emacs command clojure-spy-defn-at-point (C-c q) uses spy-runtime to dynamically redefine the function at the point with spying instrumentation. You do not need to manually wrap your code with (spy/spy ...).
- Important Quirk: When you use this feature, the function in the REPL is a new, instrumented version. If you later re-evaluate the original
defnform from your source file (e.g., by using your editor’s “evaluate this form” command), the instrumentation will be lost because you are redefining the var with its original, un-instrumented code.
You can manually wrap any defn, fn, or let form with the (spy/spy ...) macro. This gives you more explicit control over what is being spied upon and when.
- Workflow: The recommended way to use this is to wrap the form using
C-c w(clojure-spy-wrap-defn), then evaluate the wrapped function to register the instrumentation. After you are done debugging, you can remove the wrapping withC-c W(clojure-spy-unwrap-defn) and re-evaluate the function.
Spying on a let block
(spy/spy
(let [x 10
y (* x 2)]
(println "x is" x "and y is" y)))
(spy/spy-val 'x) ;=> 10
(spy/spy-val 'y) ;=> 20Spying on a multi-arity function
(spy/spy
(defn my-fn
([a] (my-fn a 10))
([a b] (+ a b))))
(my-fn 5)
(spy/spy-val 'a) ;=> 5
(spy/spy-val 'b) ;=> 10
(my-fn 1 2)
(spy/spy-val 'a) ;=> 1
(spy/spy-val 'b) ;=> 2Spying on a function with destructuring
(spy/spy
(defn my-fn [{:keys [a b]}]
(+ a b)))
(my-fn {:a 1 :b 2})
(spy/spy-val 'a) ;=> 1
(spy/spy-val 'b) ;=> 2Emacs Workflow Example
- With the cursor inside the following function, press
C-c qto instrument it:
(defn my-fn [a]
(let [b (* a 2)]
(+ a b)))- Now, evaluate a call to the function:
(my-fn 10)- Move your cursor to the symbol
ainside theletblock and pressC-c o. An overlay will appear showing the value10. - Move your cursor to the symbol
band pressC-c o. An overlay will appear showing the value20. - Press
C-c Oto hide the overlays. - Press
C-c yto un-instrument the function.
The core of the library is the spy macro and a set of functions to interact with the spied values.
The spy macro takes a body of code and rewrites it to record the values of local bindings. You can wrap any let, fn, or defn form.
Retrieves the value of a spied symbol.
Resets all spy bindings.
This function is the engine for quick, dynamic instrumentation. It takes a function’s name and its source code, injects the spy logic, and redefines the function in its original namespace. This is what allows you to instrument a function without manually wrapping it in a (spy/spy ...) form.
This is the function that powers the Emacs command clojure-spy-defn-at-point (C-c q).
The spy.el file provides a powerful interface to the spy library from within Emacs. It defines a minor mode, spy-clojure-mode, which provides a set of keybindings for interacting with the spy library.
- Load the
spy.elfile in your Emacs configuration. For example:
(load-file "/path/to/spy.el")- The
spy-clojure-modeis automatically enabled when you open a Clojure file. You will see the ” Spy” indicator in your modeline. - If you want to enable it manually, you can use the
M-x spy-clojure-mode-enablecommand.
The following keybindings are available when `spy-clojure-mode` is active:
| Keybinding | Command | Description |
|---|---|---|
C-c q | clojure-spy-defn-at-point | Instrument the function at the point. |
C-c y | clojure-spy-unspy | Un-instrument the function at the point (or all with C-u). |
C-c w | clojure-spy-wrap-defn | Wrap the current function with (spy/spy ...). |
C-c W | clojure-spy-unwrap-defn | Unwrap the current function from (spy/spy ...). |
C-c o | clojure-spy-overlay-show | Show the value of the symbol at the point as an overlay. |
C-c O | clojure-spy-overlay-hide | Hide all spy overlays. |
- Improve storage of spied values: Currently, all spied values are stored in a single global atom. This means if you spy on two different functions that have a local binding with the same name, the value will be overwritten. The storage should be namespaced, perhaps on a per-function basis.
- Clarify =spy-runtime= behavior: The dynamic instrumentation is powerful, but it can be confusing that the instrumented function is a separate entity in the REPL. We need to make it clearer to the user that re-evaluating the original function will remove the instrumentation.