/
syntax.lisp
294 lines (242 loc) · 10.3 KB
/
syntax.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
;; For license see LICENSE
(in-package #:reasonable-utilities.syntax)
(named-readtables:in-readtable rutils-readtable)
(declaim (optimize (speed 3) (space 1) (debug 0)))
(define-condition case-failure (type-error
#+sbcl sb-kernel:case-failure)
((name :reader case-failure-name :initarg :name)
(possibilities :reader case-failure-possibilities :initarg :possibilities))
(:report
(lambda (condition stream)
(format stream "~@<~S fell through ~S expression. ~
~:_Wanted one of ~:S.~:>"
(type-error-datum condition)
(case-failure-name condition)
(case-failure-possibilities condition)))))
;; predicate case
(defun expand-predicate-case (pred keyform clauses case)
`(once-only (,keyform)
`(cond
,@(loop :for (key actions) :in ,clauses
:collect (cons (if (and (eq case 'case) (eq key 'otherwise))
t
`(funcall ,pred ,keyform ,key))
actions))
,@(ecase case
(case nil)
(ccase '((t (cerror 'case-failure))))
(ecase '((t error 'case-failure)))))))
(defmacro pcase (pred keyform &rest clauses)
"Like CASE, but uses given PRED instead of EQL to select appropriate CLAUSE.
Example usage:
CL-USER> (pcase '< 1
(0 (print \"Below zero\"))
(2 (print \"OK\"))
(otherwise (error \"Oops\")))
"
(expand-predicate-case pred keyform clauses 'case))
(defmacro pcase (pred keyform &rest clauses)
"Like CCASE, but uses given PRED instead of EQL to select appropriate CLAUSE.
Example usage:
CL-USER> (pccase '< 1
(0 (print \"Below zero\"))
(2 (print \"OK\")))
"
(expand-predicate-case pred keyform clauses 'ccase))
(defmacro pecase (pred keyform &rest clauses)
"Like ECASE, but uses given PRED instead of EQL to select appropriate CLAUSE.
Example usage:
CL-USER> (pecase '< 1
(0 (print \"Below zero\"))
(2 (print \"OK\")))
"
(expand-predicate-case pred keyform clauses 'ecase))
;; desctructuring case
(defun expand-destructuring-case (key clauses case)
(once-only (key)
`(if (typep ,key 'cons)
(,case (car ,key)
,@(mapcar (lambda (clause)
(destructuring-bind ((keys . lambda-list) &body body)
clause
`(,keys
(destructuring-bind ,lambda-list (cdr ,key)
,@body))))
clauses))
(error "Invalid key to D~S: ~S" ',case ,key))))
(defmacro dcase (keyform &body clauses)
"DCASE is a combination of CASE and DESTRUCTURING-BIND.
KEYFORM must evaluate to a CONS.
Clauses are of the form:
((CASE-KEYS . DESTRUCTURING-LAMBDA-LIST) FORM*)
The clause whose CASE-KEYS matches CAR of KEY, as if by CASE, CCASE, or ECASE,
is selected, and FORMs are then executed with CDR of KEY is destructured and
bound by the DESTRUCTURING-LAMBDA-LIST.
Example:
(defun dcase-test (x)
(dcase x
((:foo a b)
(format nil \"foo: ~S, ~S\" a b))
((:bar &key a b)
(format nil \"bar, ~S, ~S\" a b))
(((:alt1 :alt2) a)
(format nil \"alt: ~S\" a))
((t &rest rest)
(format nil \"unknown: ~S\" rest))))
(dcase-test (list :foo 1 2)) ; => \"foo: 1, 2\"
(dcase-test (list :bar :a 1 :b 2)) ; => \"bar: 1, 2\"
(dcase-test (list :alt1 1)) ; => \"alt: 1\"
(dcase-test (list :alt2 2)) ; => \"alt: 2\"
(dcase-test (list :quux 1 2 3)) ; => \"unknown: 1, 2, 3\"
(defun decase-test (x)
(dcase x
((:foo a b)
(format nil \"foo: ~S, ~S\" a b))
((:bar &key a b)
(format nil \"bar, ~S, ~S\" a b))
(((:alt1 :alt2) a)
(format nil \"alt: ~S\" a))))
(decase-test (list :foo 1 2)) ; => \"foo: 1, 2\"
(decase-test (list :bar :a 1 :b 2)) ; => \"bar: 1, 2\"
(decase-test (list :alt1 1)) ; => \"alt: 1\"
(decase-test (list :alt2 2)) ; => \"alt: 2\"
(decase-test (list :quux 1 2 3)) ; =| error
"
(expand-destructuring-case keyform clauses 'case))
(defmacro dccase (keyform &body clauses)
"DCCASE is a combination of CCASE and DESTRUCTURING-BIND.
KEYFORM must evaluate to a CONS.
Clauses are of the form:
((CASE-KEYS . DESTRUCTURING-LAMBDA-LIST) FORM*)
The clause whose CASE-KEYS matches CAR of KEY, as if by CASE, CCASE, or ECASE,
is selected, and FORMs are then executed with CDR of KEY is destructured and
bound by the DESTRUCTURING-LAMBDA-LIST.
Example:
(defun dccase-test (x)
(dcase x
((:foo a b)
(format nil \"foo: ~S, ~S\" a b))
((:bar &key a b)
(format nil \"bar, ~S, ~S\" a b))
(((:alt1 :alt2) a)
(format nil \"alt: ~S\" a))))
(decase-test (list :foo 1 2)) ; => \"foo: 1, 2\"
(decase-test (list :bar :a 1 :b 2)) ; => \"bar: 1, 2\"
(decase-test (list :alt1 1)) ; => \"alt: 1\"
(decase-test (list :alt2 2)) ; => \"alt: 2\"
(decase-test (list :quux 1 2 3)) ; =| continueable error
"
(expand-destructuring-case keyform clauses 'ccase))
(defmacro decase (keyform &body clauses)
"DECASE is a combination of ECASE and DESTRUCTURING-BIND.
KEYFORM must evaluate to a CONS.
Clauses are of the form:
((CASE-KEYS . DESTRUCTURING-LAMBDA-LIST) FORM*)
The clause whose CASE-KEYS matches CAR of KEY, as if by CASE, CCASE, or ECASE,
is selected, and FORMs are then executed with CDR of KEY is destructured and
bound by the DESTRUCTURING-LAMBDA-LIST.
Example:
(defun decase-test (x)
(dcase x
((:foo a b)
(format nil \"foo: ~S, ~S\" a b))
((:bar &key a b)
(format nil \"bar, ~S, ~S\" a b))
(((:alt1 :alt2) a)
(format nil \"alt: ~S\" a))))
(decase-test (list :foo 1 2)) ; => \"foo: 1, 2\"
(decase-test (list :bar :a 1 :b 2)) ; => \"bar: 1, 2\"
(decase-test (list :alt1 1)) ; => \"alt: 1\"
(decase-test (list :alt2 2)) ; => \"alt: 2\"
(decase-test (list :quux 1 2 3)) ; =| error
"
(expand-destructuring-case keyform clauses 'ecase))
;; switch
(defun generate-switch-body (whole object clauses test key &optional default)
(with-gensyms (value)
(setf test (extract-function-name test))
(setf key (extract-function-name key))
(when (and (consp default)
(member (first default) '(error cerror)))
(setf default `(,@default "No keys match in SWITCH. Testing against ~S with ~S."
,value ',test)))
`(let ((,value (,key ,object)))
(cond ,@(mapcar (lambda (clause)
(if (member (first clause) '(t otherwise))
(progn
(when default
(error "Multiple default clauses or illegal use of a default clause in ~S."
whole))
(setf default `(progn ,@(rest clause)))
'(()))
(destructuring-bind (key-form &body forms) clause
`((,test ,value ,key-form)
,@forms))))
clauses)
(t ,default)))))
(defmacro switch (&whole whole (object &key (test 'eql) (key 'identity))
&body clauses)
"Evaluates first matching clause, returning its values, or evaluates and
returns the values of DEFAULT if no keys match."
(generate-switch-body whole object clauses test key))
(defmacro eswitch (&whole whole (object &key (test 'eql) (key 'identity))
&body clauses)
"Like SWITCH, but signals an error if no key matches."
(generate-switch-body whole object clauses test key '(error)))
(defmacro cswitch (&whole whole (object &key (test 'eql) (key 'identity))
&body clauses)
"Like SWITCH, but signals a continuable error if no key matches."
(generate-switch-body whole object clauses test key
'(cerror "Return NIL from CSWITCH.")))
(defmacro dotable ((k v table &optional rez) &body body)
"Like DOLIST but iterates over key-value pairs (K V) in anything, that can be
viewed as a table (hash-table, alist, plist, object)."
(with-gensyms (pair)
`(block nil
(etypecase ,table
(list (if (alistp ,table)
(dolist (,pair ,table)
(ds-bind (,k . ,v) ,pair
,@body))
(error 'simple-type-error
:format-control "Can't iterate over proper list in DOTABLE: need an alist")))
(hash-table (maphash (lambda (,k ,v)
,@body)
,table))
(standard-object (dolist (,k (mapcar #'c2mop:slot-definition-name
(c2mop:class-slots
(class-of ,table))))
(let ((,v (slot-value ,table ',k)))
,@body))))
,rez)))
(defmacro multiple-value-prog2 (first-form second-form &body forms)
"Evaluates FIRST-FORM, then SECOND-FORM, and then FORMS. Yields as its value
all the value returned by SECOND-FORM."
`(progn ,first-form (multiple-value-prog1 ,second-form ,@forms)))
;; bind
(defmacro bind ((&rest bindings) &body body)
"Bind variables from BINDINGS to be active inside BODY, as if by LET*,
combined with MULTIPLE-VALUE-BIND, DESTRUCTURING-BIND and other -bind forms,
depending on the type of the first argument."
(let ((rez body))
(dolist (binding (reverse bindings) (car rez))
(setf rez `((,@(funcall #'expand-binding binding rez)))))))
(defun expand-binding (binding form)
(append (apply #'bind-dispatch binding)
form))
(defgeneric bind-dispatch (arg &rest args)
(:method ((arg symbol) &rest args)
(if (cdr args)
`(multiple-value-bind (,arg ,@(butlast args)) ,(car (last args)))
`(let ((,arg ,(car args))))))
(:method ((arg list) &rest args)
`(destructuring-bind ,arg ,args))
(:method ((arg hash-table) &rest args)
`(let (,@(let (bindings)
(dotable (k v arg (reverse bindings))
(push (list v `(gethash ,k ,(car args)))
bindings)))))))
#+:cl-ppcre
(defmethod bind-dispatch ((arg string) &rest args)
(assert (cdr args))
`(ppcre:register-groups-bind ,(car args) (,arg ,(cadr args))))