-
Notifications
You must be signed in to change notification settings - Fork 3
/
check.lisp
488 lines (437 loc) · 18.2 KB
/
check.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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
;;; Copyright 2020 Google LLC
;;;
;;; Use of this source code is governed by an MIT-style
;;; license that can be found in the LICENSE file or at
;;; https://opensource.org/licenses/MIT.
;;; Variations on CHECK assertion.
;;;
;;; EXPECT - an assertion that only warns instead of signaling an error.
;;; CHECK - an assertion that does not insert the CONTINUE restart code.
;;; DCHECK - a debug mode version of CHECK that returns (VALUES).
;;;
;;; cllint: disable=invalid-assert
;;; cllint: disable=warn-not-log
(defpackage #:ace.core.check.condition
(:export
#:alternate-truth-form
#:*on-missed-expectation*
#:check-file
#:check-line
#:failed
#:failed*
#:missed
#:missed*))
(defpackage #:ace.core.check
(:use #:common-lisp #:ace.core
#:ace.core.check.condition)
(:import-from #:ace.core.macro
ace.core.macro:strcat
ace.core.macro:current-file-namestring
ace.core.macro:line-and-column-numbers
ace.core.macro:without-code-deletion-notes)
;; TODO(czak): Extract the printer generator out of here.
;; Generating a form of a function/macro call for pseudo-types
;; should be a different aspect from this stuff.
(:import-from #:ace.core.type
ace.core.type:variable-information
ace.core.type:remove-null
ace.core.type:info-format
ace.core.type:function-information
ace.core.type:function-form-argument-types)
(:export
#:check
#:dcheck
#:expect))
(in-package #:ace.core.check)
;;;
;;; Define the conditions system.
;;;
(declaim (type (or null function) *failure-hook*))
(defvar *on-missed-expectation* nil
"Functions called from the missed EXPECT handler.")
(define-condition check (condition)
((file
:type (or null string) :reader check-file :initarg :file :initform nil)
(line
:type (or null integer) :reader check-line :initarg :line :initform nil)
(package
:type (or null package)
:reader check-package
:initarg :package
:initform nil)
(format-control
:type (or string function)
:reader check-format-control
:initarg :format-control
:initform (warn "No :format-control string specified."))
(format-arguments
:type list
:reader check-format-arguments
:initarg :format-arguments
:initform nil))
(:report print-check))
(defgeneric fill-values (check values)
(:documentation
"Returns the CHECK condition, with additional format arguments substituted.
If VALUES list is empty, the original CHECK condition is returned.")
(:method (check (values null)) check))
(defmethod fill-values ((check check) (values list))
(if (and (every #'null values)
(= (ash (length values) 1)
(1- (length (check-format-arguments check)))))
check ;; Nothing changes then.
(with-slots (format-control format-arguments file line package) check
(make-condition
(type-of check)
:file file :line line :package package
:format-control format-control
:format-arguments
(list* (first format-arguments)
(loop :for (form) :on (rest format-arguments) :by #'cddr
:for value :in values
:nconc `(,form ,value)))))))
(defun print-check (check stream)
"Print the CHECK condition to STREAM possibly including file and line."
(declare (type check check))
(with-slots (format-control format-arguments file line package) check
(let ((*package* (or package (find-package :common-lisp-user)))
(*print-circle* t)
#+sbcl
(sb-ext:*suppress-print-errors* t))
(write-string
(with-output-to-string (stream)
(declare (stream stream))
(pprint-logical-block (stream format-arguments)
(declare (stream stream))
(cond (file
(format stream "[~A~@[:~D~]] " (pathname-name file) line))
(package
(format stream "[:~(~A~)] " (package-name package))))
(handler-case
(apply #'format stream format-control format-arguments)
(error (e)
(format stream "<could not print ~A condition: ~A>"
(type-of check)
(or (ignore-errors (princ-to-string e))
(type-of e)))))))
stream))))
;;; CHECK
(define-condition failed (error check) ()
(:documentation "A type of ERROR used for failed checks."))
(declaim (ftype (function (failed) nil) failed))
(defun failed (check)
"Handles a CHECK failure."
(error check))
(declaim (ftype (function (failed &rest t) nil) failed*))
(defun failed* (check &rest args)
"Handles a CHECK failure. ARGS are used to fill values for CHECK."
(error (fill-values check args)))
(defgeneric check (result &optional datum &rest arguments)
(:documentation
"Returns RESULT if non-NIL. Otherwise signals error.
Note: this is not a pure generic-function. It may be compiler-optimized.
DATUM can be a symbol or string. In that case the ARGUMENTS
are used together with DATUM to create the condition.
If DATUM is a string, it is used as a format control string.
If DATUM is a symbol, it is used as the first argument to MAKE-CONDITION."))
(defmethod check (result &optional datum &rest arguments)
(declare (ignore datum arguments))
result)
(defmethod check ((result null) &optional datum &rest arguments)
(declare (ignore result))
(error
(apply #'coerce-to-condition 'failed nil nil nil datum arguments)))
(define-compiler-macro check (&whole whole &environment env &rest ignore)
(declare (ignore ignore))
(%make-check-form whole env))
;;; EXPECT
(define-condition missed (warning check) ()
(:documentation "A type of WARNING used for missed EXPECT conditions."))
(declaim (ftype (function (missed) (values null &optional)) missed))
(defun missed (missed)
"Handles a MISSED expectation."
(warn missed)
(when *on-missed-expectation*
(funcall (the function *on-missed-expectation*) missed))
nil)
(declaim (ftype (function (missed &rest t) (values null &optional)) missed*))
(defun missed* (missed &rest args)
"Handles a MISSED expectation. ARGS are used to fill the values for MISSED."
(missed (fill-values missed args)))
(defgeneric expect (result &optional datum &rest arguments)
(:documentation
"Returns RESULT if non-NIL. Otherwise signals a warning.
Note: this is not a pure generic-function. It may be compiler-optimized.
DATUM can be a symbol or string. In that case the ARGUMENTS
are used together with DATUM to create the condition.
If DATUM is a string, it is used as a format control string.
If DATUM is a symbol, it is used as the first argument to MAKE-CONDITION."))
(defmethod expect (result &optional datum &rest arguments)
(declare (ignore datum arguments))
result)
(defmethod expect ((result null) &optional datum &rest arguments)
(declare (ignore result))
(missed
(apply #'coerce-to-condition 'missed nil nil nil datum arguments)))
(define-compiler-macro expect (&whole whole &environment env &rest ignore)
(declare (ignore ignore))
(%make-check-form whole env))
;;; DCHECK macro
(defmacro dcheck (test-form &optional datum &rest arguments)
"Specialized form of ASSERT and CHECK.
Compared to CHECK, this code is only inserted in unoptimized code.
DCHECK also does not return any VALUES.
DCHECK will signal an ASSERTION-ERROR in case the TEST-FORM evaluates to NIL.
The use case for DCHECK is in non-interactive production code for tests
that are very expensive and should only run in a debug build.
Parameters:
TEST-FORM - is evaluated and a NIL value will trigger an error.
DATUM - a specification of the error to be thrown in case the assertion fails.
ARGUMENTS - are the arguments to the DATUM specification.
See: CL:ASSERT, CHECK, EXPECT"
(declare (ignorable test-form datum arguments))
(if (find :opt *features* :test 'eq)
`(values)
`(without-code-deletion-notes
(check ,test-form ,@(when datum `(,datum)) ,@arguments)
(values))))
;;; CHECK helpers
;; Needs to be inline for the compiler to derive further actions.
(defgeneric alternate-truth-form (from)
(:documentation
"Augment the FROM used a check predicate.
This is useful for injecting code into every check and used in tests."))
(defmethod alternate-truth-form (form) form)
;; TODO(czak): The cached-formatter should be put in ace.core.macro.
(defvar *formatter-cache* (make-hash-table :test #'equal)
"Saves cached formatters.")
(defmacro cached-formatter (fctrl)
"Returns a FORMATTER for the FORMAT control string FCTRL."
(declare (string fctrl))
`(or (gethash ,fctrl *formatter-cache*)
(setf (gethash ,fctrl *formatter-cache*)
(formatter ,fctrl))))
(defun %make-check-form (whole env)
;; Generate a simple check form that used DATUM and ARGUMENT to
;; provide a warning or an error message to the user.
;;
;; Example forms generated:
;;
;; (or (eq foo :foo)
;; (error (coerce-to-condition 'failed "foo.lisp" 42 "CHECK failed!")))
;;
;; (let ((#:bar-23 bar))
;; (or (x:= #:bar-23 10)
;; (missed (%make-condition
;; 'missed "bar.lisp" 172 "Expected that ~S" '(x:= bar 10))))
;;
;; Parameters
;; WHOLE - whole check form.
;; ENV - lexical environment.
(let* ((action
(ecase (first whole)
((check) 'failed)
((expect) 'missed)))
(action*
(ecase (first whole)
((check) 'failed*)
((expect) 'missed*)))
(test-form (second whole))
(file (current-file-namestring))
(line (line-and-column-numbers whole))
(prefix (ecase action
((failed) "Failed check~:_ ~S")
((missed) "Expected~:_ ~S")))
(op (and (consp test-form) (first test-form))))
(cond
((third whole) ; datum
`(or ,(alternate-truth-form test-form)
(,action
(coerce-to-condition
',action ,file ,line ,*package* ,@(cddr whole)))))
((and (member op '(null not))
(consp (cadr test-form))
(eq (function-information (caadr test-form) env) :function))
;; Construct a message using the NOT form.
;; Also extract the bindings for the inside form if any.
(multiple-value-bind (%inside-form inside-bindings inside-formatters)
(%get-formatters (second test-form) env)
(multiple-value-bind (%test-form not-bindings not-formatters)
(%get-formatters `(,op ,%inside-form) env)
`(let ,inside-bindings
(let ,not-bindings
(or ,(alternate-truth-form %test-form)
(,(if (or inside-bindings not-bindings) action* action)
(load-time-value
(make-condition
',action
,@(when file `(:file ,file))
,@(when line `(:line ,line))
:package ,*package*
:format-control
(cached-formatter
,(%make-format-control
prefix (append inside-formatters not-formatters)))
:format-arguments
'(,test-form
,@(loop :for (%var form) :in inside-bindings
:append `(,form nil))
,@(and not-bindings
`(,(second test-form) nil)))))
,@(mapcar #'car inside-bindings)
,@(and not-bindings `(,(caar not-bindings))))))))))
((eq op 'and)
(multiple-value-bind (%test-form bindings)
(%get-formatters test-form env)
(declare (list bindings))
(let* ((%args (gensym* :args))
(%test-args
(loop
:for arg :in (rest test-form)
:for %arg :in (rest %test-form)
:for %index
= (unless (eq %arg arg)
(position %arg bindings :key #'car :test #'eq))
:collect
(if %index
`(or (setf (nth ,%index ,%args) ,arg)
(setf (cdr (nthcdr ,%index ,%args)) nil))
arg)))
(fctrl (strcat prefix "~@[~:_ with~@{~_ ~S = ~S~^,~}~].")))
`(let ((,%args (make-list ,(length bindings))))
(declare (dynamic-extent ,%args))
(or ,(alternate-truth-form `(,op ,@%test-args))
(apply
#',action*
(load-time-value
(make-condition
',action
,@(when file `(:file ,file))
,@(when line `(:line ,line))
:package ,*package*
:format-control
(cached-formatter ,fctrl)
:format-arguments
'(,test-form ,@(loop :for (%var form) :in bindings
:append `(,form nil)))))
,%args))))))
((and op (eq (function-information op env) :function))
;; Construct a message using TEST-FORM.
(multiple-value-bind (%test-form bindings formatters)
(%get-formatters test-form env)
`(let ,bindings
(or ,(alternate-truth-form %test-form)
(,(if bindings action* action)
(load-time-value
(make-condition
',action
,@(when file `(:file ,file))
,@(when line `(:line ,line))
:package ,*package*
:format-control
(cached-formatter ,(%make-format-control prefix formatters))
:format-arguments
'(,test-form ,@(loop :for (%var form) :in bindings
:append `(,form nil)))))
,@(mapcar #'car bindings))))))
(t ; atom or special form.
`(or ,(alternate-truth-form test-form)
(,action
(load-time-value
(make-condition
',action
,@(when file `(:file ,file))
,@(when line `(:line ,line))
:package ,*package*
:format-control (cached-formatter ,(strcat prefix "."))
:format-arguments '(,test-form)))))))))
(defun coerce-to-condition (default file line package datum &rest arguments)
"Coerces the DATUM specification and its ARGUMENTS to a condition.
If DATUM is a string, it is used as a format control for the ARGUMENTS.
If DATUM is a symbol, it is used with ARGUMENTS when calling MAKE-CONDITION.
FILE and LINE are passed to the constructor of the DEFAULT condition.
Parameters:
DEFAULT - the default type of the condition.
FILE, LINE - the location from where the condition is signaled.
PACKAGE - the package in which the condition was defined (for printing).
DATUM - a symbol, string, function, or condition - specifies the condition.
ARGUMENTS - arguments for the DATUM.
"
(etypecase datum
(null
(make-condition
default :format-control
(cond ((subtypep default 'failed) "Failed CHECK.")
((subtypep default 'missed) "Missed EXPECT condition."))
:file file :line line :package package))
(symbol (apply #'make-condition datum arguments))
((or string function)
(make-condition
default :format-control datum
:format-arguments arguments
:file file :line line :package package))
(condition datum)))
(defun freep (form env)
"True if the FORM is side-effect free in the lexical environment ENV."
(or (constantp form env)
(typep form '(cons (member function lambda)) env)))
;; TODO(czak): Extract the printer generator out of here.
;; Generating a print-form for a function/macro call with pseudo-types
;; is a different aspect and can be moved out.
(defun %get-formatters (form env)
"Returns a list of format control directives for the FORM and
the corresponding sub-forms.
Parameters:
FORM - the form for which we derive the format directives.
ENV - the lexical environment.
"
(let (arg-types alt-form bindings formatters)
(push (first form) alt-form)
(flet ((add-binding (arg formatter &aux %prev)
(cond ((and
(symbolp arg)
(not (eq (variable-information arg env) :symbol-macro))
(setf %prev (car (find arg bindings :key #'second))))
;; Deduplicate variables from the display.
(push %prev alt-form))
(t
(let ((%var (gensym* arg)))
(push formatter formatters)
(push `(,%var ,arg) bindings)
(push %var alt-form))))))
(multiple-value-bind (type local) (function-information (first form) env)
(cond
((and (eq type :function) (not local)
;; Use debug info to derive argument types.
(setf arg-types (function-form-argument-types form)))
(loop
:for arg :in (rest form)
:for type :in arg-types
:unless (freep arg env) :do
(let* ((type-info
(and (typep type '(and symbol (not boolean)))
(get type 'ace.core.type:info)))
(formatter
(and type-info (info-format type-info))))
(add-binding
arg (etypecase formatter
(null "~S")
(string formatter)
(symbol (format nil "~~/~S/" formatter)))))
:else :do
(push arg alt-form)))
(t
(loop
:for arg :in (rest form)
:unless (freep arg env) :do
(add-binding arg "~S")
:else :do
(push arg alt-form))))))
(values (nreverse alt-form) (nreverse bindings) (nreverse formatters))))
(defun %make-format-control (prefix formatters)
"Makes a format control string for a failure condition.
Parameters:
PREFIX - contains the prefix.
FORMATTERS - a list of FORMAT control directives for each binding.
"
(format nil "~A~@[~~:_ with~{~~_ ~~S = ~A~^,~}~]." prefix formatters))