CHANGE-CLASS now works correctly on unbound slots
[sbcl.git] / src / code / late-extensions.lisp
blobffbc42e99fe5117b9d91e48a9ec5363407aa26c5
1 ;;;; various extensions (including SB-INT "internal extensions")
2 ;;;; available both in the cross-compilation host Lisp and in the
3 ;;;; target SBCL, but which can't be defined on the target until until
4 ;;;; some significant amount of machinery (e.g. error-handling) is
5 ;;;; defined
7 ;;;; This software is part of the SBCL system. See the README file for
8 ;;;; more information.
9 ;;;;
10 ;;;; This software is derived from the CMU CL system, which was
11 ;;;; written at Carnegie Mellon University and released into the
12 ;;;; public domain. The software is in the public domain and is
13 ;;;; provided with absolutely no warranty. See the COPYING and CREDITS
14 ;;;; files for more information.
16 (in-package "SB!IMPL")
18 ;;; Is X a list for which LENGTH is meaningful, i.e. a list which is
19 ;;; not improper and which is not circular?
20 (defun list-with-length-p (x)
21 (values (ignore-errors (list-length x))))
23 ;;; not used in 0.7.8, but possibly useful for defensive programming
24 ;;; in e.g. (COERCE ... 'VECTOR)
25 ;;;(defun list-length-or-die (x)
26 ;;; (or (list-length x)
27 ;;; ;; not clear how to do this best:
28 ;;; ;; * Should this be a TYPE-ERROR? Colloquially that'd make
29 ;;; ;; lots of sense, but since I'm not sure how to express
30 ;;; ;; "noncircular list" as a Lisp type expression, coding
31 ;;; ;; it seems awkward.
32 ;;; ;; * Should the ERROR object include the offending value?
33 ;;; ;; Ordinarily that's helpful, but if the user doesn't have
34 ;;; ;; his printer set up to deal with cyclicity, we might not
35 ;;; ;; be doing him a favor by printing the object here.
36 ;;; ;; -- WHN 2002-10-19
37 ;;; (error "can't calculate length of cyclic list")))
39 ;;; This is used in constructing arg lists for debugger printing,
40 ;;; and when needing to print unbound slots in PCL.
41 (defstruct (unprintable-object
42 (:constructor make-unprintable-object (string))
43 (:print-object (lambda (x s)
44 (print-unreadable-object (x s)
45 (write-string (unprintable-object-string x) s))))
46 (:copier nil))
47 string)
49 ;;; Used internally, but it would be nice to provide something
50 ;;; like this for users as well.
51 ;;;
52 ;;; FIXME / IMPORTANT: If the slot is raw, the address is correct only for
53 ;;; instances of the specified class, not its subclasses!
54 #!+sb-thread
55 (defmacro define-structure-slot-addressor (name &key structure slot)
56 (let* ((dd (find-defstruct-description structure t))
57 (slotd (when dd (find slot (dd-slots dd) :key #'dsd-name)))
58 (index (when slotd (dsd-index slotd)))
59 (raw-type (dsd-raw-type slotd)))
60 (unless index
61 (error "Slot ~S not found in ~S." slot structure))
62 `(progn
63 (declaim (inline ,name))
64 (defun ,name (instance)
65 (declare (type ,structure instance) (optimize speed))
66 (truly-the
67 word
68 (+ (get-lisp-obj-address instance)
69 (- (* ,(if (eq t raw-type)
70 (+ sb!vm:instance-slots-offset index)
71 (- (1+ (sb!kernel::dd-instance-length dd))
72 sb!vm:instance-slots-offset index
73 (1- (sb!kernel::raw-slot-words raw-type))))
74 sb!vm:n-word-bytes)
75 sb!vm:instance-pointer-lowtag)))))))
77 ;;;; ATOMIC-INCF and ATOMIC-DECF
79 (defun expand-atomic-frob (name specified-place diff env
80 &aux (place (sb!xc:macroexpand specified-place env)))
81 (declare (type (member atomic-incf atomic-decf) name))
82 (flet ((invalid-place ()
83 (error "Invalid first argument to ~S: ~S" name specified-place)))
84 (if (and (symbolp place)
85 (eq (info :variable :kind place) :global)
86 (type= (info :variable :type place) (specifier-type 'fixnum)))
87 ;; Global can't be lexically rebound.
88 (return-from expand-atomic-frob
89 `(truly-the fixnum (,(case name
90 (atomic-incf '%atomic-inc-symbol-global-value)
91 (atomic-decf '%atomic-dec-symbol-global-value))
92 ',place (the fixnum ,diff)))))
94 (unless (consp place)
95 (invalid-place))
96 (destructuring-bind (op &rest args) place
97 ;; FIXME: The lexical environment should not be disregarded.
98 ;; CL builtins can't be lexically rebound, but structure accessors can.
99 (case op
100 (aref
101 (when (cddr args)
102 (invalid-place))
103 #!+(or x86 x86-64 ppc)
104 (with-unique-names (array)
105 `(let ((,array (the (simple-array word (*)) ,(car args))))
106 (%array-atomic-incf/word
107 ,array
108 (%check-bound ,array (array-dimension ,array 0) ,(cadr args))
109 (logand #.(1- (ash 1 sb!vm:n-word-bits))
110 ,(case name
111 (atomic-incf `(the sb!vm:signed-word ,diff))
112 (atomic-decf `(- (the sb!vm:signed-word ,diff))))))))
113 #!-(or x86 x86-64 ppc)
114 (with-unique-names (array index old-value)
115 (let ((incremented-value
116 (case name
117 (atomic-incf
118 `(+ ,old-value (the sb!vm:signed-word ,diff)))
119 (atomic-decf
120 `(- ,old-value (the sb!vm:signed-word ,diff))))))
121 `(without-interrupts
122 (let* ((,array ,(car args))
123 (,index ,(cadr args))
124 (,old-value (aref ,array ,index)))
125 (setf (aref ,array ,index)
126 (logand #.(1- (ash 1 sb!vm:n-word-bits))
127 ,incremented-value))
128 ,old-value)))))
129 ((car cdr first rest)
130 (when (cdr args)
131 (invalid-place))
132 `(truly-the
133 fixnum
134 (,(case op
135 ((first car) (case name
136 (atomic-incf '%atomic-inc-car)
137 (atomic-decf '%atomic-dec-car)))
138 ((rest cdr) (case name
139 (atomic-incf '%atomic-inc-cdr)
140 (atomic-decf '%atomic-dec-cdr))))
141 ,(car args) (the fixnum ,diff))))
143 (when (cdr args)
144 (invalid-place))
145 (let ((dd (info :function :structure-accessor op)))
146 (if dd
147 (let* ((structure (dd-name dd))
148 (slotd (find op (dd-slots dd) :key #'dsd-accessor-name))
149 (index (dsd-index slotd))
150 (type (dsd-type slotd)))
151 (declare (ignorable structure index))
152 (unless (and (eq 'sb!vm:word (dsd-raw-type slotd))
153 (type= (specifier-type type) (specifier-type 'sb!vm:word)))
154 (error "~S requires a slot of type (UNSIGNED-BYTE ~S), not ~S: ~S"
155 name sb!vm:n-word-bits type place))
156 (when (dsd-read-only slotd)
157 (error "Cannot use ~S with structure accessor for a read-only slot: ~S"
158 name place))
159 #!+(or x86 x86-64 ppc)
160 `(truly-the sb!vm:word
161 (%raw-instance-atomic-incf/word
162 (the ,structure ,@args) ,index
163 (logand #.(1- (ash 1 sb!vm:n-word-bits))
164 ,(case name
165 (atomic-incf
166 `(the sb!vm:signed-word ,diff))
167 (atomic-decf
168 `(- (the sb!vm:signed-word ,diff)))))))
169 ;; No threads outside x86 and x86-64 for now, so this is easy...
170 #!-(or x86 x86-64 ppc)
171 (with-unique-names (structure old)
172 `(without-interrupts
173 (let* ((,structure ,@args)
174 (,old (,op ,structure)))
175 (setf (,op ,structure)
176 (logand
177 #.(1- (ash 1 sb!vm:n-word-bits))
178 ,(case name
179 (atomic-incf
180 `(+ ,old (the sb!vm:signed-word ,diff)))
181 (atomic-decf
182 `(- ,old (the sb!vm:signed-word ,diff))))))
183 ,old))))
184 (invalid-place))))))))
186 (def!macro atomic-incf (&environment env place &optional (diff 1))
187 #!+sb-doc
188 #.(format nil
189 "Atomically increments PLACE by DIFF, and returns the value of PLACE before
190 the increment.
192 PLACE must access one of the following:
193 - a DEFSTRUCT slot with declared type (UNSIGNED-BYTE ~D~:*)
194 or AREF of a (SIMPLE-ARRAY (UNSIGNED-BYTE ~D~:*) (*))
195 The type SB-EXT:WORD can be used for these purposes.
196 - CAR or CDR (respectively FIRST or REST) of a CONS.
197 - a variable defined using DEFGLOBAL with a proclaimed type of FIXNUM.
198 Macroexpansion is performed on PLACE before expanding ATOMIC-INCF.
200 Incrementing is done using modular arithmetic,
201 which is well-defined over two different domains:
202 - For structures and arrays, the operation accepts and produces
203 an (UNSIGNED-BYTE ~D~:*), and DIFF must be of type (SIGNED-BYTE ~D).
204 ATOMIC-INCF of #x~x by one results in #x0 being stored in PLACE.
205 - For other places, the domain is FIXNUM, and DIFF must be a FIXNUM.
206 ATOMIC-INCF of #x~x by one results in #x~x
207 being stored in PLACE.
209 DIFF defaults to 1.
211 EXPERIMENTAL: Interface subject to change."
212 sb!vm:n-word-bits most-positive-word
213 most-positive-fixnum most-negative-fixnum)
214 (expand-atomic-frob 'atomic-incf place diff env))
216 (defmacro atomic-decf (&environment env place &optional (diff 1))
217 #!+sb-doc
218 #.(format nil
219 "Atomically decrements PLACE by DIFF, and returns the value of PLACE before
220 the decrement.
222 PLACE must access one of the following:
223 - a DEFSTRUCT slot with declared type (UNSIGNED-BYTE ~D~:*)
224 or AREF of a (SIMPLE-ARRAY (UNSIGNED-BYTE ~D~:*) (*))
225 The type SB-EXT:WORD can be used for these purposes.
226 - CAR or CDR (respectively FIRST or REST) of a CONS.
227 - a variable defined using DEFGLOBAL with a proclaimed type of FIXNUM.
228 Macroexpansion is performed on PLACE before expanding ATOMIC-DECF.
230 Decrementing is done using modular arithmetic,
231 which is well-defined over two different domains:
232 - For structures and arrays, the operation accepts and produces
233 an (UNSIGNED-BYTE ~D~:*), and DIFF must be of type (SIGNED-BYTE ~D).
234 ATOMIC-DECF of #x0 by one results in #x~x being stored in PLACE.
235 - For other places, the domain is FIXNUM, and DIFF must be a FIXNUM.
236 ATOMIC-DECF of #x~x by one results in #x~x
237 being stored in PLACE.
239 DIFF defaults to 1.
241 EXPERIMENTAL: Interface subject to change."
242 sb!vm:n-word-bits most-positive-word
243 most-negative-fixnum most-positive-fixnum)
244 (expand-atomic-frob 'atomic-decf place diff env))
246 ;; Interpreter stubs for ATOMIC-INCF.
247 #!+(or x86 x86-64 ppc)
248 (defun %array-atomic-incf/word (array index diff)
249 (declare (type (simple-array word (*)) array)
250 (fixnum index)
251 (type sb!vm:signed-word diff))
252 (%array-atomic-incf/word array index diff))
254 ;; This code would be more concise if workable versions
255 ;; of +-MODFX, --MODFX were defined generically.
256 (macrolet ((modular (fun a b)
257 #!+(or x86 x86-64)
258 `(,(let ((*package* (find-package "SB!VM")))
259 (symbolicate fun "-MODFX"))
260 ,a ,b)
261 #!-(or x86 x86-64)
262 ;; algorithm of https://graphics.stanford.edu/~seander/bithacks
263 `(let ((res (logand (,fun ,a ,b)
264 (ash sb!ext:most-positive-word
265 (- sb!vm:n-fixnum-tag-bits))))
266 (m (ash 1 (1- sb!vm:n-fixnum-bits))))
267 (- (logxor res m) m))))
269 ;; Atomically frob a global variable.
270 ;; There is a quasi-bug that "can't happen" - the CAS operation will swap
271 ;; a thread-local value, however this function should only be called with
272 ;; a symbol that is known not to be thread-locally bindable.
273 ;; (We should define a CASser for SYMBOL-GLOBAL-VALUE)
274 (macrolet ((def-frob (name op)
275 `(defun ,name (symbol delta)
276 (declare (symbol symbol) (fixnum delta))
277 (loop (let ((old (truly-the
278 fixnum
279 (locally (declare (optimize (safety 0)))
280 (symbol-global-value symbol)))))
281 (when (eq (%compare-and-swap-symbol-value
282 symbol old (modular ,op old delta))
283 old)
284 (return old)))))))
285 (def-frob %atomic-inc-symbol-global-value +)
286 (def-frob %atomic-dec-symbol-global-value -))
288 ;; Atomically frob the CAR or CDR of a cons.
289 (macrolet ((def-frob (name op slot)
290 `(defun ,name (cell delta)
291 (declare (cons cell) (fixnum delta))
292 (loop (let ((old (the fixnum (,slot cell))))
293 (when (eq (cas (,slot cell) old
294 (modular ,op old delta)) old)
295 (return old)))))))
296 (def-frob %atomic-inc-car + car)
297 (def-frob %atomic-dec-car - car)
298 (def-frob %atomic-inc-cdr + cdr)
299 (def-frob %atomic-dec-cdr - cdr)))
301 (defun spin-loop-hint ()
302 #!+sb-doc
303 "Hints the processor that the current thread is spin-looping."
304 (spin-loop-hint))
306 (defun call-hooks (kind hooks &key (on-error :error))
307 (dolist (hook hooks)
308 (handler-case
309 (funcall hook)
310 (serious-condition (c)
311 (if (eq :warn on-error)
312 (warn "Problem running ~A hook ~S:~% ~A" kind hook c)
313 (with-simple-restart (continue "Skip this ~A hook." kind)
314 (error "Problem running ~A hook ~S:~% ~A" kind hook c)))))))
316 ;;;; DEFGLOBAL
318 (defmacro-mundanely defglobal (name value &optional (doc nil docp))
319 #!+sb-doc
320 "Defines NAME as a global variable that is always bound. VALUE is evaluated
321 and assigned to NAME both at compile- and load-time, but only if NAME is not
322 already bound.
324 Global variables share their values between all threads, and cannot be
325 locally bound, declared special, defined as constants, and neither bound
326 nor defined as symbol macros.
328 See also the declarations SB-EXT:GLOBAL and SB-EXT:ALWAYS-BOUND."
329 (let ((boundp (make-symbol "BOUNDP")))
330 `(progn
331 (eval-when (:compile-toplevel)
332 (let ((,boundp (boundp ',name)))
333 (%compiler-defglobal ',name :always-bound
334 (unless ,boundp ,value) (not ,boundp))))
335 (let ((,boundp (boundp ',name)))
336 (%defglobal ',name (unless ,boundp ,value) ,boundp ',doc ,docp
337 (sb!c:source-location))))))
339 (defmacro-mundanely define-load-time-global (name value &optional (doc nil docp))
340 #!+sb-doc
341 "Defines NAME as a global variable that is always bound. VALUE is evaluated
342 and assigned to NAME at load-time, but only if NAME is not already bound.
344 Attempts to read NAME at compile-time will signal an UNBOUND-VARIABLE error
345 unless it has otherwise been assigned a value.
347 See also DEFGLOBAL which assigns the VALUE at compile-time too."
348 (let ((boundp (make-symbol "BOUNDP")))
349 `(progn
350 (eval-when (:compile-toplevel)
351 (%compiler-defglobal ',name :eventually nil nil))
352 (let ((,boundp (boundp ',name)))
353 (%defglobal ',name (unless ,boundp ,value) ,boundp ',doc ,docp
354 (sb!c:source-location))))))
356 (defun %compiler-defglobal (name always-boundp value assign-it-p)
357 (sb!xc:proclaim `(global ,name))
358 (when assign-it-p
359 #-sb-xc-host
360 (set-symbol-global-value name value)
361 #+sb-xc-host
362 (set name value))
363 (sb!c::process-variable-declaration
364 name 'always-bound
365 ;; don't "weaken" the proclamation if it's in fact always bound now
366 (if (eq (info :variable :always-bound name) :always-bound)
367 :always-bound
368 always-boundp)))
370 (defun %defglobal (name value boundp doc docp source-location)
371 (%compiler-defglobal name :always-bound value (not boundp))
372 (when docp
373 (setf (fdocumentation name 'variable) doc))
374 (sb!c:with-source-location (source-location)
375 (setf (info :source-location :variable name) source-location))
376 name)
378 ;;;; WAIT-FOR -- waiting on arbitrary conditions
380 (defun %%wait-for (test stop-sec stop-usec)
381 (declare (function test))
382 (labels ((try ()
383 (declare (optimize (safety 0)))
384 (awhen (funcall test)
385 (return-from %%wait-for it)))
386 (tick (sec usec)
387 (declare (fixnum sec usec))
388 ;; TICK is microseconds
389 (+ usec (* 1000000 sec)))
390 (get-tick ()
391 (multiple-value-call #'tick
392 (decode-internal-time (get-internal-real-time)))))
393 (let* ((timeout-tick (when stop-sec (tick stop-sec stop-usec)))
394 (start (get-tick))
395 ;; Rough estimate of how long a single attempt takes.
396 (try-ticks (progn
397 (try) (try) (try)
398 (max 1 (truncate (- (get-tick) start) 3)))))
399 ;; Scale sleeping between attempts:
401 ;; Start by sleeping for as many ticks as an average attempt
402 ;; takes, then doubling for each attempt.
404 ;; Max out at 0.1 seconds, or the 2 x time of a single try,
405 ;; whichever is longer -- with a hard cap of 10 seconds.
407 ;; FIXME: Maybe the API should have a :MAX-SLEEP argument?
408 (loop with max-ticks = (max 100000 (min (* 2 try-ticks)
409 (expt 10 7)))
410 for scale of-type fixnum = 1
411 then (let ((x (logand most-positive-fixnum (* 2 scale))))
412 (if (> scale x)
413 most-positive-fixnum
415 do (try)
416 (let* ((now (get-tick))
417 (sleep-ticks (min (* try-ticks scale) max-ticks))
418 (sleep
419 (if timeout-tick
420 ;; If sleep would take us past the
421 ;; timeout, shorten it so it's just
422 ;; right.
423 (if (>= (+ now sleep-ticks) timeout-tick)
424 (- timeout-tick now)
425 sleep-ticks)
426 sleep-ticks)))
427 (declare (fixnum sleep))
428 (cond ((plusp sleep)
429 ;; microseconds to seconds and nanoseconds
430 (multiple-value-bind (sec nsec)
431 (truncate (* 1000 sleep) (expt 10 9))
432 (with-interrupts
433 (sb!unix:nanosleep sec nsec))))
435 (return-from %%wait-for nil))))))))
437 (defun %wait-for (test timeout)
438 (declare (function test))
439 (tagbody
440 :restart
441 (multiple-value-bind (to-sec to-usec stop-sec stop-usec deadlinep)
442 (decode-timeout timeout)
443 (declare (ignore to-sec to-usec))
444 (return-from %wait-for
445 (or (%%wait-for test stop-sec stop-usec)
446 (when deadlinep
447 (signal-deadline)
448 (go :restart)))))))
450 (defmacro wait-for (test-form &key timeout)
451 #!+sb-doc
452 "Wait until TEST-FORM evaluates to true, then return its primary value.
453 If TIMEOUT is provided, waits at most approximately TIMEOUT seconds before
454 returning NIL.
456 If WITH-DEADLINE has been used to provide a global deadline, signals a
457 DEADLINE-TIMEOUT if TEST-FORM doesn't evaluate to true before the
458 deadline.
460 Experimental: subject to change without prior notice."
461 `(dx-flet ((wait-for-test () (progn ,test-form)))
462 (%wait-for #'wait-for-test ,timeout)))
464 (defmacro with-progressive-timeout ((name &key seconds)
465 &body body)
466 #!+sb-doc
467 "Binds NAME as a local function for BODY. Each time #'NAME is called, it
468 returns SECONDS minus the time that has elapsed since BODY was entered, or
469 zero if more time than SECONDS has elapsed. If SECONDS is NIL, #'NAME
470 returns NIL each time."
471 (with-unique-names (deadline time-left sec)
472 `(let* ((,sec ,seconds)
473 (,deadline
474 (when ,sec
475 (+ (get-internal-real-time)
476 (round (* ,seconds internal-time-units-per-second))))))
477 (flet ((,name ()
478 (when ,deadline
479 (let ((,time-left (- ,deadline (get-internal-real-time))))
480 (if (plusp ,time-left)
481 (* (coerce ,time-left 'single-float)
482 ,(/ 1.0 internal-time-units-per-second))
483 0)))))
484 ,@body))))
486 (defmacro atomic-update (place update-fn &rest arguments &environment env)
487 #!+sb-doc
488 "Updates PLACE atomically to the value returned by calling function
489 designated by UPDATE-FN with ARGUMENTS and the previous value of PLACE.
491 PLACE may be read and UPDATE-FN evaluated and called multiple times before the
492 update succeeds: atomicity in this context means that value of place did not
493 change between the time it was read, and the time it was replaced with the
494 computed value.
496 PLACE can be any place supported by SB-EXT:COMPARE-AND-SWAP.
498 Examples:
500 ;;; Conses T to the head of FOO-LIST.
501 (defstruct foo list)
502 (defvar *foo* (make-foo))
503 (atomic-update (foo-list *foo*) #'cons t)
505 (let ((x (cons :count 0)))
506 (mapc #'sb-thread:join-thread
507 (loop repeat 1000
508 collect (sb-thread:make-thread
509 (lambda ()
510 (loop repeat 1000
511 do (atomic-update (cdr x) #'1+)
512 (sleep 0.00001))))))
513 ;; Guaranteed to be (:COUNT . 1000000) -- if you replace
514 ;; atomic update with (INCF (CDR X)) above, the result becomes
515 ;; unpredictable.
518 (multiple-value-bind (vars vals old new cas-form read-form)
519 (get-cas-expansion place env)
520 `(let* (,@(mapcar 'list vars vals)
521 (,old ,read-form))
522 (loop for ,new = (funcall ,update-fn ,@arguments ,old)
523 until (eq ,old (setf ,old ,cas-form))
524 finally (return ,new)))))
526 (defmacro atomic-push (obj place &environment env)
527 #!+sb-doc
528 "Like PUSH, but atomic. PLACE may be read multiple times before
529 the operation completes -- the write does not occur until such time
530 that no other thread modified PLACE between the read and the write.
532 Works on all CASable places."
533 (multiple-value-bind (vars vals old new cas-form read-form)
534 (get-cas-expansion place env)
535 `(let* (,@(mapcar 'list vars vals)
536 (,old ,read-form)
537 (,new (cons ,obj ,old)))
538 (loop until (eq ,old (setf ,old ,cas-form))
539 do (setf (cdr ,new) ,old)
540 finally (return ,new)))))
542 (defmacro atomic-pop (place &environment env)
543 #!+sb-doc
544 "Like POP, but atomic. PLACE may be read multiple times before
545 the operation completes -- the write does not occur until such time
546 that no other thread modified PLACE between the read and the write.
548 Works on all CASable places."
549 (multiple-value-bind (vars vals old new cas-form read-form)
550 (get-cas-expansion place env)
551 `(let* (,@(mapcar 'list vars vals))
552 (loop for ,old = ,read-form
553 for ,new = (cdr ,old)
554 until (eq ,old (setf ,old ,cas-form))
555 finally (return (car ,old))))))
557 (defun split-version-string (string)
558 (loop with subversion and start = 0
559 with end = (length string)
560 when (setf (values subversion start)
561 (parse-integer string :start start :junk-allowed t))
562 collect it
563 while (and subversion
564 (< start end)
565 (char= (char string start) #\.))
566 do (incf start)))
568 (defun version>= (x y)
569 (unless (or x y)
570 (return-from version>= t))
571 (let ((head-x (or (first x) 0))
572 (head-y (or (first y) 0)))
573 (or (> head-x head-y)
574 (and (= head-x head-y)
575 (version>= (rest x) (rest y))))))
577 (defun assert-version->= (&rest subversions)
578 #!+sb-doc
579 "Asserts that the current SBCL is of version equal to or greater than
580 the version specified in the arguments. A continuable error is signaled
581 otherwise.
583 The arguments specify a sequence of subversion numbers in big endian order.
584 They are compared lexicographically with the runtime version, and versions
585 are treated as though trailed by an unbounded number of 0s.
587 For example, (assert-version->= 1 1 4) asserts that the current SBCL is
588 version 1.1.4[.0.0...] or greater, and (assert-version->= 1) that it is
589 version 1[.0.0...] or greater."
590 (let ((version (split-version-string (lisp-implementation-version))))
591 (unless (version>= version subversions)
592 (cerror "Disregard this version requirement."
593 "SBCL ~A is too old for this program (version ~{~A~^.~} ~
594 or later is required)."
595 (lisp-implementation-version)
596 subversions))))