A toolkit to construct interface-aware yet standard-compliant debugger hooks.

Upstream URL



Atlas Engineer LLC


BSD 3-Clause

NDebug provides a small set of utilities to make graphical (or, rather non-REPL-resident) Common Lisp applications easier to integrate with the standard Lisp debugger (*debugger-hook*, namely) and implementation-specific debugger hooks (via trivial-custom-debugger), especially in a multi-threaded context.

1Getting started


NDebug is pretty light on dependencies:


NDebug has two API layers: CLOS API and a globals&functions API. The CLOS API is more structured and I recommend to use it in most cases, while the globals&functions API overrides the CLOS methods and allows to customize the solid base CLOS provides. globals&functions is more REPL-friendly, so you may start your debugger with this, while transitioning to the CLOS API later.


To make your application debugger-friendly, you have to specialize ui-display, ui-cleanup, query-read and query-write methods (the latter two are not required and will be replaced with *query-io* if not present). And then use the make-debugger-hook function to set the debugger.

Note that there are

  • ndebug:invoke to invoke a restart for a condition,
  • and ndebug:evaluate (only works on SBCL at the moment) to evaluate the code in the context of the condition.
  (defclass my-wrapper (ndebug:condition-wrapper)
    ((prompt-text :initform "[prompt text]"
                  :accessor prompt-text)
     (debug-window :initform nil
                   :accessor debug-window)))

  (defmethod ndebug:query-read ((wrapper my-wrapper))
    (prompt :text (prompt-text wrapper)))

  (defmethod ndebug:query-write ((wrapper my-wrapper) (string string))
    (setf (prompt-text wrapper) string))

  (defmethod ndebug:ui-display ((wrapper my-wrapper))
    (setf (debug-window wrapper)
          (make-windown :text-contents (dissect:present (ndebug:stack wrapper) nil)
                        :buttons (loop for restart in (ndebug:restarts wrapper)
                                       collect (make-button
                                                :label (restart-name restart)
                                                :action (lambda ()
                                                          (ndebug:invoke wrapper restart)))))))

  (defmethod ndebug:ui-cleanup ((wrapper my-wrapper))
    (delete-window (debug-window wrapper)))

  (ndebug:with-debugger-hook (:wrapper-class 'my-wrapper)

1.2.2Globals&functions API

With globals&function API you have a bit more flexibility in how you configure the debugger. You can let-bind or flet-bind special variables (ndebug:*query-read*, ndebug:*query-write*, ndebug:*ui-display*, ndebug:*ui-cleanup*), you can setf them, you can provide them as arguments to ndebug:make-debugger-hook or ndebug:with-debugger-hook. The possibilities are endless, although it tends to look less structured than the CLOS API.

  (defvar *prompt-text* "[prompt text]")

  (defvar *window* nil)

  (defun show-wrapper-window (wrapper)
      :text-contents (dissect:present (ndebug:stack wrapper) nil)
      :buttons (loop for restart in (ndebug:restarts wrapper)
                     collect (make-button
                              :label (restart-name restart)
                              :action (lambda ()
                                        (ndebug:invoke wrapper restart)))))))

  (let ((ndebug:*query-read* (lambda (wrapper)
                               (declare (ignore wrapper))
                               (prompt :text *prompt-text*))))
    (flet ((cleanup (wrapper)
             (declare (ignore wrapper))
             (delete-window *window*)))
      (setf ndebug:*ui-cleanup* #'cleanup))
        (:ui-display #'show-wrapper-window
         :query-write (setf *prompt-text* %string%))

1.3Mixing the two

The good thing about these API is that you can intermix those. So, you can subclass the ndebug:condition-wrapper, define some methods on it and then, if there's some corner case (like needing a custom display or custom reading function), you can always provide an additional argument to make-debugger-hook to override the initial method.


  • Stop depending on Swank for two-way-stream construction, depend on trivial-gray-streams instead.
    • The implementation is quite basic, but it seems to work.
  • (Maybe) stop depending on Lparallel and depend on Bordeaux Thread semaphores/conditions instead.
    • Semaphores it is!
  • Better names for handlers?
  • Use methods to specialize the behavior?
  • (Maybe) allow falling back to *query-io* by providing nil as both :query-write and :query-read.

Dependencies (5)

  • bordeaux-threads
  • dissect
  • lisp-unit2
  • trivial-custom-debugger
  • trivial-gray-streams

Dependents (0)

    • GitHub
    • Quicklisp