Skip to content

Latest commit

 

History

History
99 lines (77 loc) · 5.09 KB

README.org

File metadata and controls

99 lines (77 loc) · 5.09 KB

NDebug

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: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))

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-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 providing nil as both :query-write and :query-read.