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.
Getting started
Installation
NDebug is pretty light on dependencies:
Usage
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.
CLOS API
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:invoketo 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))
Globals&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) (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)))
Mixing 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.
To-Dos
- [X] Stop depending on Swank for two-way-stream construction, depend on
trivial-gray-streamsinstead.- 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 providingnilas both:query-writeand:query-read.