Commit | Line | Data |
---|---|---|
abdf50aa MW |
1 | ;;; -*-lisp-*- |
2 | ;;; | |
3 | ;;; Various handy utilities | |
4 | ;;; | |
5 | ;;; (c) 2009 Straylight/Edgeware | |
6 | ;;; | |
7 | ||
8 | ;;;----- Licensing notice --------------------------------------------------- | |
9 | ;;; | |
10 | ;;; This file is part of the Simple Object Definition system. | |
11 | ;;; | |
12 | ;;; SOD is free software; you can redistribute it and/or modify | |
13 | ;;; it under the terms of the GNU General Public License as published by | |
14 | ;;; the Free Software Foundation; either version 2 of the License, or | |
15 | ;;; (at your option) any later version. | |
16 | ;;; | |
17 | ;;; SOD is distributed in the hope that it will be useful, | |
18 | ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
19 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
20 | ;;; GNU General Public License for more details. | |
21 | ;;; | |
22 | ;;; You should have received a copy of the GNU General Public License | |
23 | ;;; along with SOD; if not, write to the Free Software Foundation, | |
24 | ;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
25 | ||
26 | (cl:in-package #:sod) | |
27 | ||
28 | ;;;-------------------------------------------------------------------------- | |
29 | ;;; List utilities. | |
30 | ||
1f1d88f5 MW |
31 | (defun mappend (function list &rest more-lists) |
32 | "Like a nondestructive MAPCAN. | |
33 | ||
34 | Map FUNCTION over the the corresponding elements of LIST and MORE-LISTS, | |
35 | and return the result of appending all of the resulting lists." | |
36 | (reduce #'append (apply #'mapcar function list more-lists) :from-end t)) | |
37 | ||
abdf50aa MW |
38 | (define-condition inconsistent-merge-error (error) |
39 | ((candidates :initarg :candidates | |
40 | :reader merge-error-candidates)) | |
41 | (:documentation | |
42 | "Reports an inconsistency in the arguments passed to MERGE-LISTS.") | |
43 | (:report (lambda (condition stream) | |
44 | (format stream "Merge inconsistency: failed to decide among ~A." | |
45 | (merge-error-candidates condition))))) | |
46 | ||
47 | (defun merge-lists (lists &key pick (test #'eql)) | |
48 | "Return a merge of the given LISTS. | |
49 | ||
50 | The resulting LIST contains the items of the given lists, with duplicates | |
51 | removed. The order of the resulting list is consistent with the orders of | |
52 | the input LISTS in the sense that if A precedes B in some input list then | |
53 | A will also precede B in the output list. If the lists aren't consistent | |
54 | (e.g., some list contains A followed by B, and another contains B followed | |
55 | by A) then an error of type INCONSISTENT-MERGE-ERROR is signalled. | |
56 | ||
57 | Item equality is determined by TEST. | |
58 | ||
59 | If there is an ambiguity at any point -- i.e., a choice between two or | |
60 | more possible next items to emit -- then PICK is called to arbitrate. | |
61 | PICK is called with two arguments: the list of candidate next items, and | |
62 | the current output list. It should return one of the candidate items. If | |
63 | PICK is omitted then an arbitrary choice is made. | |
64 | ||
65 | The primary use of this function is in computing class precedence lists. | |
66 | By building the input lists and selecting the PICK function appropriately, | |
67 | a variety of different CPL algorithms can be implemented." | |
68 | ||
69 | ;; In this loop, TAIL points to the last cons cell in the list. This way | |
70 | ;; we can build the list up forwards, so as not to make the PICK function | |
71 | ;; interface be weird. HEAD is a dummy cons cell inserted before the list, | |
72 | ;; which gives TAIL something to point to initially. (If we had locatives, | |
1f1d88f5 MW |
73 | ;; I'd have TAIL point to the thing holding the final NIL, but we haven't; |
74 | ;; instead, it points to the cons cell whose cdr holds the final NIL -- | |
75 | ;; which means that we need to invent a cons cell if the list is empty.) | |
abdf50aa MW |
76 | (do* ((head (cons nil nil)) |
77 | (tail head)) | |
78 | ((null lists) (cdr head)) | |
79 | ||
80 | ;; The candidate items are the ones at the front of the input lists. | |
81 | ;; Gather them up, removing duplicates. If a candidate is somewhere in | |
82 | ;; one of the other lists other than at the front then we reject it. If | |
83 | ;; we've just rejected everything, then we can make no more progress and | |
84 | ;; the input lists were inconsistent. | |
85 | (let* ((candidates (delete-duplicates (mapcar #'car lists) :test test)) | |
86 | (leasts (remove-if (lambda (item) | |
87 | (some (lambda (list) | |
88 | (member item (cdr list) :test test)) | |
89 | lists)) | |
90 | candidates)) | |
91 | (winner (cond ((null leasts) | |
92 | (error 'inconsistent-merge-error | |
93 | :candidates candidates)) | |
94 | ((null (cdr leasts)) | |
95 | (car leasts)) | |
96 | (pick | |
97 | (funcall pick leasts (cdr head))) | |
98 | (t (car leasts)))) | |
99 | (new (cons winner nil))) | |
100 | ||
101 | ;; Check that the PICK function isn't conning us. | |
102 | (assert (member winner leasts :test test)) | |
103 | ||
104 | ;; Update the output list and remove the winning item from the input | |
105 | ;; lists. We know that it must be at the front of each input list | |
106 | ;; containing it. At this point, we discard input lists entirely when | |
107 | ;; they run out of entries. The loop ends when there are no more input | |
108 | ;; lists left, i.e., when we've munched all of the input items. | |
109 | (setf (cdr tail) new | |
110 | tail new | |
111 | lists (delete nil (mapcar (lambda (list) | |
112 | (if (funcall test winner (car list)) | |
113 | (cdr list) | |
114 | list)) | |
115 | lists)))))) | |
116 | ||
117 | ;;;-------------------------------------------------------------------------- | |
118 | ;;; Strings and characters. | |
119 | ||
120 | (defun frob-case (string) | |
121 | "Twiddles the case of STRING. | |
122 | ||
123 | If all the letters in STRING are uppercase, switch them to lowercase; if | |
124 | they're all lowercase then switch them to uppercase. If there's a mix | |
125 | then leave them all alone. This is an invertible transformation." | |
126 | ||
127 | ;; Given that this operation is performed by the reader anyway, it's | |
128 | ;; surprising that there isn't a Common Lisp function to do this built | |
129 | ;; in. | |
130 | (let ((flags (reduce (lambda (state ch) | |
131 | (logior state | |
132 | (cond ((upper-case-p ch) 1) | |
133 | ((lower-case-p ch) 2) | |
134 | (t 0)))) | |
135 | string | |
136 | :initial-value 0))) | |
137 | ||
138 | ;; Now FLAGS has bit 0 set if there are any upper-case characters, and | |
139 | ;; bit 1 if there are lower-case. So if it's zero there were no letters | |
140 | ;; at all, and if it's three then there were both kinds; either way, we | |
141 | ;; leave the string unchanged. Otherwise we know how to flip the case. | |
142 | (case flags | |
143 | (1 (string-downcase string)) | |
144 | (2 (string-upcase string)) | |
145 | (t string)))) | |
146 | ||
147 | (declaim (inline whitespace-char-p)) | |
148 | (defun whitespace-char-p (char) | |
149 | "Returns whether CHAR is a whitespace character. | |
150 | ||
151 | Whitespaceness is determined relative to the compile-time readtable, which | |
152 | is probably good enough for most purposes." | |
153 | (case char | |
154 | (#.(loop for i below char-code-limit | |
155 | for ch = (code-char i) | |
156 | unless (with-input-from-string (in (string ch)) | |
157 | (peek-char t in nil)) | |
158 | collect ch) t) | |
159 | (t nil))) | |
160 | ||
161 | ;;;-------------------------------------------------------------------------- | |
1f1d88f5 MW |
162 | ;;; Symbols. |
163 | ||
164 | (defun symbolicate (&rest symbols) | |
165 | "Return a symbol named after the concatenation of the names of the SYMBOLS. | |
166 | ||
167 | The symbol is interned in the current *PACKAGE*. Trad." | |
168 | (intern (apply #'concatenate 'string (mapcar #'symbol-name symbols)))) | |
169 | ||
170 | ;;;-------------------------------------------------------------------------- | |
171 | ;;; Object printing. | |
172 | ||
173 | (defmacro maybe-print-unreadable-object | |
174 | ((object stream &rest args) &body body) | |
175 | "Print helper for usually-unreadable objects. | |
176 | ||
177 | If *PRINT-ESCAPE* is set then print OBJECT unreadably using BODY. | |
178 | Otherwise just print using BODY." | |
179 | (let ((func (gensym "PRINT"))) | |
180 | `(flet ((,func () ,@body)) | |
181 | (if *print-escape* | |
182 | (print-unreadable-object (,object ,stream ,@args) | |
183 | (,func)) | |
184 | (,func))))) | |
185 | ||
186 | ;;;-------------------------------------------------------------------------- | |
abdf50aa MW |
187 | ;;; Keyword arguments and lambda lists. |
188 | ||
189 | (eval-when (:compile-toplevel :load-toplevel :execute) | |
190 | (defun transform-otherkeys-lambda-list (bvl) | |
191 | "Process a simple lambda-list BVL which might contain &OTHER-KEYS. | |
192 | ||
193 | &OTHER-KEYS VAR, if it appears, must appear just after the &KEY arguments | |
194 | (which must also be present); &ALLOW-OTHER-KEYS must not be present. | |
195 | ||
196 | The behaviour is that | |
197 | ||
198 | * the presence of non-listed keyword arguments is permitted, as if | |
199 | &ALLOW-OTHER-KEYS had been provided, and | |
200 | ||
201 | * a list of the keyword arguments other than the ones explicitly listed | |
202 | is stored in the VAR. | |
203 | ||
204 | The return value is a replacement BVL which binds the &OTHER-KEYS variable | |
205 | as an &AUX parameter if necessary. | |
206 | ||
207 | At least for now, fancy things like destructuring lambda-lists aren't | |
208 | supported. I suspect you'll get away with a specializing lambda-list." | |
209 | ||
210 | (prog ((new-bvl nil) | |
211 | (rest-var nil) | |
212 | (keywords nil) | |
213 | (other-keys-var nil) | |
214 | (tail bvl)) | |
215 | ||
216 | find-rest | |
217 | ;; Scan forwards until we find &REST or &KEY. If we find the former, | |
218 | ;; then remember the variable name. If we find the latter first then | |
219 | ;; there can't be a &REST argument, so we should invent one. If we | |
220 | ;; find neither then there's nothing to do. | |
221 | (when (endp tail) | |
222 | (go ignore)) | |
223 | (let ((item (pop tail))) | |
224 | (push item new-bvl) | |
225 | (case item | |
226 | (&rest (when (endp tail) | |
227 | (error "Missing &REST argument name")) | |
228 | (setf rest-var (pop tail)) | |
229 | (push rest-var new-bvl)) | |
230 | (&aux (go ignore)) | |
231 | (&key (unless rest-var | |
232 | (setf rest-var (gensym "REST")) | |
233 | (setf new-bvl (nconc (list '&key rest-var '&rest) | |
234 | (cdr new-bvl)))) | |
235 | (go scan-keywords))) | |
236 | (go find-rest)) | |
237 | ||
238 | scan-keywords | |
239 | ;; Read keyword argument specs one-by-one. For each one, stash it on | |
240 | ;; the NEW-BVL list, and also parse it to extract the keyword, which | |
241 | ;; we stash in KEYWORDS. If we don't find &OTHER-KEYS then there's | |
242 | ;; nothing for us to do. | |
243 | (when (endp tail) | |
244 | (go ignore)) | |
245 | (let ((item (pop tail))) | |
246 | (push item new-bvl) | |
247 | (case item | |
248 | ((&aux &allow-other-keys) (go ignore)) | |
249 | (&other-keys (go fix-tail))) | |
250 | (let ((keyword (if (symbolp item) | |
251 | (intern (symbol-name item) :keyword) | |
252 | (let ((var (car item))) | |
253 | (if (symbolp var) | |
254 | (intern (symbol-name var) :keyword) | |
255 | (car var)))))) | |
256 | (push keyword keywords)) | |
257 | (go scan-keywords)) | |
258 | ||
259 | fix-tail | |
260 | ;; We found &OTHER-KEYS. Pick out the &OTHER-KEYS var. | |
261 | (pop new-bvl) | |
262 | (when (endp tail) | |
263 | (error "Missing &OTHER-KEYS argument name")) | |
264 | (setf other-keys-var (pop tail)) | |
265 | (push '&allow-other-keys new-bvl) | |
266 | ||
267 | ;; There should be an &AUX next. If there isn't, assume there isn't | |
268 | ;; one and provide our own. (This is safe as long as nobody else is | |
269 | ;; expecting to plumb in lambda keywords too.) | |
270 | (when (and (not (endp tail)) (eq (car tail) '&aux)) | |
271 | (pop tail)) | |
272 | (push '&aux new-bvl) | |
273 | ||
274 | ;; Add our shiny new &AUX argument. | |
275 | (let ((keys-var (gensym "KEYS")) | |
276 | (list-var (gensym "LIST"))) | |
277 | (push `(,other-keys-var (do ((,list-var nil) | |
278 | (,keys-var ,rest-var (cddr ,keys-var))) | |
279 | ((endp ,keys-var) (nreverse ,list-var)) | |
280 | (unless (member (car ,keys-var) | |
281 | ',keywords) | |
282 | (setf ,list-var | |
283 | (cons (cadr ,keys-var) | |
284 | (cons (car ,keys-var) | |
285 | ,list-var)))))) | |
286 | new-bvl)) | |
287 | ||
288 | ;; Done. | |
289 | (return (nreconc new-bvl tail)) | |
290 | ||
291 | ignore | |
292 | ;; Nothing to do. Return the unmolested lambda-list. | |
293 | (return bvl)))) | |
294 | ||
295 | (defmacro lambda-otherkeys (bvl &body body) | |
296 | "Like LAMBDA, but with a new &OTHER-KEYS lambda-list keyword." | |
297 | `(lambda ,(transform-otherkeys-lambda-list bvl) ,@body)) | |
298 | ||
299 | (defmacro defun-otherkeys (name bvl &body body) | |
300 | "Like DEFUN, but with a new &OTHER-KEYS lambda-list keyword." | |
301 | `(defun ,name ,(transform-otherkeys-lambda-list bvl) ,@body)) | |
302 | ||
303 | (defmacro defmethod-otherkeys (name &rest stuff) | |
304 | "Like DEFMETHOD, but with a new &OTHER-KEYS lambda-list keyword." | |
305 | (do ((quals nil) | |
306 | (stuff stuff (cdr stuff))) | |
307 | ((listp (car stuff)) | |
308 | `(defmethod ,name ,@(nreverse quals) | |
309 | ,(transform-otherkeys-lambda-list (car stuff)) | |
310 | ,@(cdr stuff))) | |
311 | (push (car stuff) quals))) | |
312 | ||
313 | ;;;-------------------------------------------------------------------------- | |
314 | ;;; Iteration macros. | |
315 | ||
316 | (defmacro dosequence ((var seq &key (start 0) (end nil) indexvar) &body body) | |
317 | "Macro for iterating over general sequences. | |
318 | ||
319 | Iterates over a (sub)sequence SEQ, delimited by START and END (which are | |
320 | evaluated). For each item of SEQ, BODY is invoked with VAR bound to the | |
321 | item, and INDEXVAR (if requested) bound to the item's index. (Note that | |
322 | this is different from most iteration constructs in Common Lisp, which | |
323 | work by mutating the variable.) | |
324 | ||
325 | The loop is surrounded by an anonymous BLOCK and the loop body forms an | |
326 | implicit TAGBODY, as is usual. There is no result-form, however." | |
327 | ||
328 | (let ((seqvar (gensym "SEQ")) | |
329 | (startvar (gensym "START")) | |
330 | (endvar (gensym "END")) | |
331 | (ivar (gensym "INDEX")) | |
332 | (bodyfunc (gensym "BODY"))) | |
333 | ||
334 | (flet ((loopguts (indexp listp use-endp) | |
335 | ;; Build a DO-loop to do what we want. | |
336 | (let* ((do-vars nil) | |
337 | (end-condition (if use-endp | |
338 | `(endp ,seqvar) | |
339 | `(>= ,ivar ,endvar))) | |
340 | (item (if listp | |
341 | `(car ,seqvar) | |
342 | `(aref ,seqvar ,ivar))) | |
343 | (body-call `(,bodyfunc ,item))) | |
344 | (when listp | |
345 | (push `(,seqvar (nthcdr ,startvar ,seqvar) (cdr ,seqvar)) | |
346 | do-vars)) | |
347 | (when indexp | |
348 | (push `(,ivar ,startvar (1+ ,ivar)) do-vars)) | |
349 | (when indexvar | |
350 | (setf body-call (append body-call (list ivar)))) | |
351 | `(do ,do-vars (,end-condition) ,body-call)))) | |
352 | ||
353 | `(block nil | |
354 | (flet ((,bodyfunc (,var ,@(and indexvar `(,indexvar))) | |
355 | (tagbody ,@body))) | |
356 | (let* ((,seqvar ,seq) | |
357 | (,startvar ,start)) | |
358 | (etypecase ,seqvar | |
359 | (vector | |
360 | (let ((,endvar (or ,end (length ,seqvar)))) | |
361 | ,(loopguts t nil nil))) | |
362 | (list | |
363 | (let ((,endvar ,end)) | |
364 | (if ,endvar | |
365 | ,(loopguts t t nil) | |
366 | ,(loopguts indexvar t t))))))))))) | |
367 | ||
368 | ;;;-------------------------------------------------------------------------- | |
369 | ;;; Meta-object hacking. | |
370 | ||
371 | (defgeneric copy-instance-using-class (class object &rest initargs) | |
372 | (:documentation | |
373 | "Return a copy of OBJECT. | |
374 | ||
375 | OBJECT is assumed to be an instance of CLASS. The copy returned is a | |
376 | fresh instance whose slots have the same values as OBJECT except where | |
377 | overridden by INITARGS.") | |
378 | ||
379 | (:method ((class standard-class) object &rest initargs) | |
380 | (let ((copy (apply #'allocate-instance class initargs))) | |
381 | (dolist (slot (class-slots class)) | |
382 | (if (slot-boundp-using-class class object slot) | |
383 | (setf (slot-value-using-class class copy slot) | |
384 | (slot-value-using-class class object slot)) | |
385 | (slot-makunbound-using-class class copy slot))) | |
386 | (apply #'shared-initialize copy nil initargs) | |
387 | copy))) | |
388 | ||
389 | (defun copy-instance (object &rest initargs) | |
390 | "Return a copy of OBJECT. | |
391 | ||
392 | The copy returned is a fresh instance whose slots have the same values as | |
393 | OBJECT except where overridden by INITARGS." | |
394 | (apply #'copy-instance-using-class (class-of object) object initargs)) | |
395 | ||
1f1d88f5 MW |
396 | (defmacro default-slot ((instance slot) &body value &environment env) |
397 | "If INSTANCE's SLOT is unbound, set it to VALUE. | |
398 | ||
399 | Both INSTANCE and SLOT are evaluated; VALUE is an implicit progn and only | |
400 | evaluated if it's needed." | |
401 | ||
402 | (let* ((quotep (constantp slot env)) | |
403 | (instancevar (gensym "INSTANCE")) | |
404 | (slotvar (if quotep slot (gensym "SLOT")))) | |
405 | `(let ((,instancevar ,instance) | |
406 | ,@(and (not quotep) `((,slotvar ,slot)))) | |
407 | (unless (slot-boundp ,instancevar ,slotvar) | |
408 | (setf (slot-value ,instancevar ,slotvar) | |
409 | (progn ,@value)))))) | |
410 | ||
abdf50aa | 411 | ;;;----- That's all, folks -------------------------------------------------- |