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.
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)
(obviously-erroring-operation))
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)
(setf
*window*
(make-window
: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))
(ndebug:with-debugger-hook
(:ui-display #'show-wrapper-window
:query-write (setf *prompt-text* %string%))
(obviously-erroring-operation)))
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.
- [X] 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.
- [X] (Maybe) stop depending on Lparallel and depend on Bordeaux Thread semaphores/conditions instead.
- Semaphores it is!
- [ ] Better names for handlers?
- [X] Use methods to specialize the behavior?
- [ ] (Maybe) allow falling back to
*query-io*
by providingnil
as both:query-write
and:query-read
.