-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathndebug.lisp
257 lines (235 loc) · 11.8 KB
/
ndebug.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause
(in-package #:ndebug)
(defvar *query-write* nil
"A function/lambda to unconditionally override/alter the `query-write' call.")
(defvar *query-read* nil
"A function/lambda to unconditionally override/alter the `query-read' call.")
(defvar *ui-display* nil
"A function/lambda to unconditionally override/alter the `ui-display' call.")
(defvar *ui-cleanup* nil
"A function/lambda to unconditionally override/alter the `ui-cleanup' call.")
(defclass condition-wrapper ()
((condition-itself
:initform (error "condition-wrapper should always wrap a condition.")
:initarg :condition-itself
:accessor condition-itself
:type condition
:documentation "The condition itself.")
(restarts
:initform '()
:initarg :restarts
:accessor restarts
:type list
:documentation "A list of `dissect:restart's for the given condition.")
(chosen-restart
:initform nil
:documentation "The restart chosen in the interface and brought by `invoke'.")
(code-to-evaluate
:initform nil
:type (or list function)
:documentation "The code to evaluate in `evaluate'.
Can be either a list of a zero-argument function.")
(restart-semaphore
:initform (bt:make-semaphore)
:type bt:semaphore
:documentation "The semaphore to wait on until the restart is returned.")
(stack
:initform nil
:initarg :stack
:accessor stack
:documentation "The state of call stack at the time of the condition firing.
A list of `dissect:call' objects."))
(:documentation "The wrapper for condition.
Made so that `*debugger-hook*' can wait for the condition to be resolved based on
the `channel', wrapped alongside the condition and its restarts."))
(defgeneric query-write (wrapper string)
(:method ((wrapper condition-wrapper) (string string))
nil)
(:method :around (wrapper string)
(if *query-write*
(funcall *query-write* wrapper string)
(call-next-method)))
(:documentation "The function to call as part of custom `*query-io*' when prompting the user.
Always prefers `*query-write*' (if set) over the default method."))
(defgeneric query-read (wrapper)
(:method ((wrapper condition-wrapper))
nil)
(:method :around (wrapper)
(if *query-read*
(funcall *query-read* wrapper)
(call-next-method)))
(:documentation "The function to call as part of custom `*query-io*' when getting user input.
Always prefers `*query-read*' (if set) over the default method."))
(defgeneric ui-display (wrapper)
(:method ((wrapper condition-wrapper))
nil)
(:method :around (wrapper)
(if *ui-display*
(funcall *ui-display* wrapper)
(call-next-method)))
(:documentation "Part of custom debugger, called when showing the condition to the user.
Always prefers `*ui-display*' (if set) over the default method."))
(defgeneric ui-cleanup (wrapper)
(:method ((wrapper condition-wrapper)))
(:method :around (wrapper)
(if *ui-cleanup*
(funcall *ui-cleanup* wrapper)
(call-next-method)))
(:documentation "Part of custom debugger, called once the debugger is done.
Always prefers `*ui-cleanup*' (if set) over the default method."))
(declaim (ftype (function (&key (:wrapper-class t)
(:ui-display (or null (function (condition-wrapper))))
(:ui-cleanup (or null (function (condition-wrapper))))
(:query-read (or null (function (condition-wrapper) string)))
(:query-write (or null (function (condition-wrapper string))))))
make-debugger-hook))
(defun make-debugger-hook (&key (wrapper-class 'condition-wrapper)
(ui-display *ui-display*) (ui-cleanup *ui-cleanup*)
(query-read *query-read*) (query-write *query-write*))
"Construct a `*debugger-hook*'-compatible function with multi-threading and UI interaction.
WRAPPER-CLASS is a class designator for the class to wrap the
condition in. Defaults to `condition-wrapper'. WRAPPER-CLASS
designated class must inherit from `condition-wrapper'.
UI-DISPLAY is a function to invoke when showing the debugger
window/prompt/query. Is called with a condition wrapper to
display. Overrides a `ui-display' method (if present), defined for the
WRAPPER-CLASS.
UI-CLEANUP is a function to invoke after the debugging is done and the
interface is in need of cleaning up (like removing debug windows or
flushing the shell.) Accepts a condition wrapper to clean up
after. Overrides a `ui-cleanup' method (if present), defined for the
WRAPPER-CLASS.
QUERY-READ is a function to invoke when querying the user, like
opening a an input window or waiting for shell input. Must return an
inputted string. The only argument is the condition wrapper for a
related condition. Overrides a `query-read' method (if present),
defined for the WRAPPER-CLASS.
QUERY-WRITE is a unary function to invoke when showing the user the
prompting text, like when opening a dialogue window or writing to the
shell. Can refer to the outside state to interface with the
QUERY-READ. The arguments are:
- Condition wrapper for the current condition.
- The string to show to the user.
Overrides a `query-write' method (if present), defined for the
WRAPPER-CLASS.
QUERY-READ and QUERY-WRITE should both be present (in which case
prompting happens in the custom interface), or both absent (in which
case the default `*query-io*' is used.)"
(lambda (condition hook)
(let* ((restarts (dissect:restarts))
(wrapper (make-instance wrapper-class
:condition-itself condition
:restarts restarts
:stack (dissect:stack)))
(*query-io* (if (or (and (ignore-errors (find-method #'query-read nil (list wrapper-class)))
(ignore-errors (find-method #'query-write nil (list wrapper-class 'string))))
(and query-read query-write))
(make-debugger-stream
(lambda ()
(let* ((*query-read* query-read)
(*debugger-hook* nil)
(result (query-read wrapper)))
(if (uiop:string-suffix-p result #\newline)
result
(uiop:strcat result #\newline))))
(lambda (string)
(let ((*query-write* query-write)
(*debugger-hook* nil))
(query-write wrapper string))))
*query-io*)))
(when (or (ignore-errors (find-method #'ui-display nil (list wrapper-class)))
ui-display)
(let ((*ui-display* ui-display)
(*debugger-hook* nil))
(ui-display wrapper)))
(unwind-protect
;; FIXME: Waits indefinitely. Should it?
(let ((restart (loop for got-something = (bt:wait-on-semaphore (slot-value wrapper 'restart-semaphore))
for code = (slot-value wrapper 'code-to-evaluate)
for restart = (slot-value wrapper 'chosen-restart)
when code
do (let ((*debugger-hook* hook))
(typecase code
(list (eval code))
(function (funcall code))))
and do (setf (slot-value wrapper 'code-to-evaluate) nil)
else when restart
do (return restart)))
(*debugger-hook* hook))
(invoke-restart-interactively
(etypecase restart
(dissect:restart (dissect:object restart))
(restart restart)
(symbol (find-restart restart))
(function restart))))
(when (or (ignore-errors (find-method #'ui-cleanup nil (list wrapper-class)))
ui-cleanup)
(let ((*ui-cleanup* ui-cleanup)
(*debugger-hook* nil))
(ui-cleanup wrapper)))))))
(defgeneric invoke (wrapper restart)
(:method ((wrapper condition-wrapper) (restart symbol))
(setf (slot-value wrapper 'chosen-restart) restart)
(bt:signal-semaphore (slot-value wrapper 'restart-semaphore)))
(:method ((wrapper condition-wrapper) (restart dissect:restart))
(setf (slot-value wrapper 'chosen-restart) restart)
(bt:signal-semaphore (slot-value wrapper 'restart-semaphore)))
(:method ((wrapper condition-wrapper) (restart restart))
(setf (slot-value wrapper 'chosen-restart) restart)
(bt:signal-semaphore (slot-value wrapper 'restart-semaphore)))
(:method ((wrapper condition-wrapper) (restart function))
(setf (slot-value wrapper 'chosen-restart) restart)
(bt:signal-semaphore (slot-value wrapper 'restart-semaphore)))
(:documentation "Invoke the RESTART in the initial debugger hook of the WRAPPER.
The RESTART should be one of the `restarts' of the WRAPPER. Otherwise
the behavior is implementation-dependent, but never exactly pretty."))
(defgeneric evaluate (wrapper code)
(:method ((wrapper condition-wrapper) (code list))
(setf (slot-value wrapper 'code-to-evaluate) code)
(bt:signal-semaphore (slot-value wrapper 'restart-semaphore)))
(:method ((wrapper condition-wrapper) (code function))
(setf (slot-value wrapper 'code-to-evaluate) code)
(bt:signal-semaphore (slot-value wrapper 'restart-semaphore)))
(:documentation "Evaluate the CODE in the debugger WRAPPER context.
CODE can be
- A quoted list of Lisp code, in which case it will be avaluated.
- A function object, in which case if will be called in the context of the debugger."))
(defmacro with-debugger-hook ((&key wrapper-class query-read query-write ui-display ui-cleanup)
&body body)
"Execute the BODY with the newly-created (as per `make-debugger-hook') debugger hook.
The ARGS are `make-debugger-hook' arguments passed to it with the
following rules:
- If the argument form starts with a `lambda' or `function' (which
sharp-quote expands to), pass it to `make-debugger-hook' as-is.
- If not, then wrap it in a lambda with a special variable
%WRAPPER% (and %STRING% in case of :QUERY-WRITE) accessible to the
argument form.
Example:"
(declare (ignorable wrapper-class query-read query-write ui-display ui-cleanup))
(flet ((wrap-lambda-maybe (form)
(list (if (member (first form) '(lambda function))
form
`(lambda (,(alexandria:symbolicate "%WRAPPER%"))
(declare (ignorable ,(alexandria:symbolicate "%WRAPPER%")))
,form)))))
`(trivial-custom-debugger:with-debugger
((make-debugger-hook
,@(when wrapper-class
(list :wrapper-class wrapper-class))
,@(when query-read
(cons :query-read (wrap-lambda-maybe query-read)))
,@(when query-write
(list :query-write
(if (member (first query-write) '(lambda function))
query-write
`(lambda (,(alexandria:symbolicate "%WRAPPER%")
,(alexandria:symbolicate "%STRING%"))
(declare (ignorable ,(alexandria:symbolicate "%WRAPPER%")
,(alexandria:symbolicate "%STRING%")))
,query-write))))
,@(when ui-display
(cons :ui-display (wrap-lambda-maybe ui-display)))
,@(when ui-cleanup
(cons :ui-cleanup (wrap-lambda-maybe ui-cleanup)))))
,@body)))