ToC DocOverview CGDoc RelNotes FAQ Index PermutedIndex
Allegro CL version 11.0


1.0 Implementation introduction

The Common Lisp standard is deliberately vague on many of the specifics of an implementation. The authors of that book were aware that implementation details are dependent on the nature of the hardware and the operating system, as well as the differing priorities of the implementors and the different user communities. This document details some of the specifics of the implementation of and extensions in Allegro CL.

2.0 Data types and array types

Allegro CL contains all of the required Common Lisp data types. Fixnums are signed 30-bit quantities (29 bits of value, one sign bit) on 32-bit machines and signed 61-bit quantities (60 bits of value, one sign bit) on 64 bit machines. There are two distinct floating-point types on all platforms (32 bit and 64 bit floats). Short-float and single-float are equivalent and are 32 bit floats. Double-float and long-float are equivalent and are 64 bit floats.

The distinct array data types are shown in the following list (in the case of the simple arrays, we use suspension points, ..., to indicate that there may be any number of dimensions). When you specify an element-type to make-array, you will get an array whose element type is upgraded-array-element-type applied to the value specified. Arrays of type t are general arrays and arrays of any other type are called specialized arrays. After the list, we give some examples of the types of arrays created with particular values for element-type.

The new short-array type is not mentioned in this list. See Arrays and short arrays for information on short-arrays and also on maximum array sizes.

Allegro CL allows most types of arrays to be allocated in static space (where they are never moved or even looked at by the garbage collector). See cl:make-array for information on creating such arrays. Only (as noted) arrays of type t cannot be allocated in static space (because such arrays usually contain pointers to other Lisp objects whicxh must be looked at and updated by the garbage collector when the objects pointed to are relocated).

  (array t)  ;; cannot have :allocation :static, 
             ;; :malloc, or :static-reclaimable as noted above
  (array bit)
  (array (unsigned-byte 4))
  (array (unsigned-byte 8))
  (array (unsigned-byte 16))
  (array (unsigned-byte 32))
  (array (unsigned-byte 64)) [64-bit Lisps only]
  (array character)
  (array single-float)
  (array double-float)
  (array fixnum)
  (array (complex single-float))
  (array (complex double-float))
  (array (signed-byte 8))
  (array (signed-byte 16))
  (array (signed-byte 32))
  (array (signed-byte 64)) [64-bit Lisps only]
  (array nil) 
  (simple-array t (* ...))
  (simple-array bit (* ...))
  (simple-array (unsigned-byte 4) (* ...))
  (simple-array (unsigned-byte 8) (* ...))
  (simple-array (unsigned-byte 16) (* ...))
  (simple-array (unsigned-byte 32) (* ...))
  (simple-array (unsigned-byte 64) (* ...)) [64-bit Lisps only]
  (simple-array character (* ...))
  (simple-array single-float (* ...))
  (simple-array double-float (* ...))
  (simple-array fixnum (* ...))
  (simple-array (signed-byte 8) (* ...))
  (simple-array (signed-byte 16) (* ...))
  (simple-array (signed-byte 32) (* ...))
  (simple-array (signed-byte 64) (* ...)) [64-bit Lisps only]
  (simple-array nil (* ...))

Now let us look at some examples. When we specify (unsigned-byte 3) as the value of element-type, we get an array of type (unsigned-byte 4):

cl-user(2): (setq fn-arr 
                  (make-array 5 :element-type '(unsigned-byte 3) 
                                :initial-element 0))
#(0 0 0 0 0)
cl-user(3): (array-element-type fn-arr)
(unsigned-byte 4)
cl-user(4): (upgraded-array-element-type '(unsigned-byte 3))
(unsigned-byte 4)

Note that upgraded-array-element-type applied to (unsigned-byte 3) returns (unsigned-byte 4). Note too that we have specified 0 as the value of the initial-value. If we had not, the initial value would be nil, which is not of type (unsigned-byte 4).

Here is what is returned by upgraded-array-element-type for some other common types:

cl-user(7): (upgraded-array-element-type 'single-float)
cl-user(8): (upgraded-array-element-type 'double-float)
cl-user(9): (upgraded-array-element-type 'float)
cl-user(10): (upgraded-array-element-type 'integer)
cl-user(11): (upgraded-array-element-type 'character)
cl-user(12): (upgraded-array-element-type '(signed-byte 6))
(signed-byte 8)
cl-user(13): (upgraded-array-element-type '(unsigned-byte 100))

Note that specifying float and integer both result in arrays of type t, not in specialized arrays. Specifying signed or unsigned bytes of particular sizes results in that size or bigger, or possibly t.

It is good programming practice to use upgraded-array-element-type to determine exactly what sort of array you will get.

Stack allocating vectors

Certain types of vectors can be stack allocated, thus saving space in applications. See Stack consing, avoiding consing using apply, and stack allocation in compiling.html for details.

3.0 Arrays and short arrays

Arrays are stored internally as vectors. The underlying vector associated with an array is accessible with the macro with-underlying-simple-vector.

Release 7.0 contains a new array implementation with larger array size limits: now most-positive-fixnum. In earlier releases, the limit in 32-bit Lisps was (expt 2 24), a value 32 times smaller. In 64-bit images, the limit was (expt 2 56), 16 times smaller than the new most-positive-fixnum limit.

Because the structure of arrays had to change in order to implement this change, and because there exists the possibility that users have done some coding which assumes a particular arrangement for arrays (such as is the case for the lisp.h file for compiling C code to recognize lisp structure), we have retained the older array types with their smaller limits, and have renamed them to be short arrays.

The make-array function now accepts the additional :short keyword argument. :short defaults to nil and when nil, a (long) array is produced, and when specified true, a short array (that used in earlier releases) is produced, with these exceptions:

The next section discusses these and other anomalies.

The new functions short-vector and short-string create short vectors and short strings, analogously to the standard functions vector and string.

3.1 Array short-ness

Most of the array set is symmetrical with respect to short-ness; i.e. a call to make-array for most element types will either produce a simple-array or short-simple-array of the specified element-type, based on the :short argument, (and, for specifications which normally create non-simple arrays, these results will be either arrays or short-arrays of the specified element-type) with the following exceptions:

1. The fixnum element-type has been added to (long) arrays only, if
specified as short array, the element-type is upgraded to
`(unsigned-byte 32)` in 32-bit images and 
`(unsigned-byte 64)` in 64-bit images. For example:

CL-USER(1): (featurep :64bit)
CL-USER(2): (type-of (make-array 10 :element-type 'fixnum))
CL-USER(3): (type-of (make-array 10 :element-type 'fixnum :short t))

;; and

CL-USER(9): (featurep :64bit)
CL-USER(10): (type-of (make-array 10 :element-type 'fixnum))
CL-USER(11): (type-of (make-array 10 :element-type 'fixnum :short t))
  1. An element-type specification of nil always results in a (long) array:

    CL-USER(4): (type-of (make-array 0 :element-type nil))
    CL-USER(5): (type-of (make-array 0 :element-type nil :short t))
  2. An element-type of excl::foreign always results in a short array:

    CL-USER(6): (type-of (make-array 5 :element-type 'excl::foreign))
    CL-USER(7): (type-of (make-array 5 :element-type 'excl::foreign :short t))

In all other cases, the "short-ness" of arrays depends only on the :short argument to make-array:

CL-USER(8): (type-of (make-array 5 :element-type 'double-float))
CL-USER(9): (type-of (make-array 5 :element-type 'double-float :short nil))
CL-USER(10): (type-of (make-array 5 :element-type 'double-float :short t))

3.2 Relationship of arrays to array-like structures

At a low level, and below the level most programmers will ever need to know, some other CL objects retain the same basic structure (and thus the allocation limitations) as short arrays, though these can certainly be reviewed and addressed as necessary in the future.

They are:

These objects should never be arguments to svref, even if they had been punned on simple-vectors in unsafe code (punned means declared to be simple-vectors even when they are not). If such punning is still needed for these objects, use ssvref.

3.3 Short-arrays in the type hierarchy

Short-arrays are not Common Lisp standard types. Some of the relationships between short-arrays and normal (long) arrays are intuitive, but some are not. For example, a short-vector of element-type character is arrayp, and is short-array-p, but is not stringp, though it is short-string-p (this is because only (array character (*)) is stringp. And a short-simple-vector (i.e. of (short-simple-array t (*)) type) is short-simple-vector-p, but is not simple-vector-p, because only (simple-array t (*)) is a simple-vector.

Most other relationships between short array types are consistent and type-of, typep, and subtypep know about them.

The list of short array types, classes, and utility functions follows. The symbols naming them are the standard Common Lisp symbol names with short- prepended. All are in the excl package.

All short array types are subtypes of array, but not subtypes of any other Common Lisp array type. Their type hierarchy is the same as the corresponding Common Lisp array type hierarchy.

The various predicates also correspond to their standard Common Lisp counterparts. arrayp and (where appropriate) vectorp return true when applied to short arrays, but no other Common Lisp array predicate returns true when applied to a short array.

There are the following two types. Each is defined by a deftype form, the source is shown.

aref and other accessors

aref and its setf works on short arrays and normal arrays. But the specialized accessors sbit, schar, and svref and their setf's only work on normal arrays (it is an error to pass a short array to them). The following three specialized short array accessors work, in the same way as their Common Lisp counterparts, on short arrays.

In optimized code, care must be taken to match the kind of array with its accessor; svref will open-code to a single instruction access that assumes a normal (long) vector of type t. If the vector is instead a short vector, the access might be to a nonexistent slot beyond the allocation of the short-simple-vector. In the other direction, ssvref will open-code to a single instruction access that assumes a short array. An inverse-ssvref of the zeroth "slot" of a normal (long) array will overwrite the length word, and will result in eventual GC corruption.

An aref in optimized code will generate the correct code if the declaration is missing or matches the kind of array that will actually be accessed. If it is unknown whether the array being accessed will be short or normal (long), then a declaration of dual-simple-array or dual-simple-vector will generate the right code (but code which is still much faster than an out-of-line call to aref or its inverse).

It is strongly recommended, in the face of all of these dangers, that use of short-arrays is kept at a minimum. The space-savings of short-arrays over normal arrays is on average one word per array (depending on the parity of the size; odd-sized short-arrays will save 2 words, and even-sized short-arrays will not save any memory) so the desirability of short arrays is very small when compared to the risks.

3.3.1 String comparisons with short strings

Because short strings are not true strings (i.e. are not stringp), short strings are not permitted as arguments to string comparison functions such as string= and string-lessp. An error will be signaled if you pass a short string as an argument to a string comparison function.

If you need to compare short strings with each other or with regular strings, you can use equalp for equality tests. You must write your own functions for greater than or less than tests, such as the following:

(defun my-string-lessp (s1 s2)
  (let ((l1 (length s1)) (l2 (length s2)) minl)
    (setq minl (min l1 l2))
    (dotimes (i minl)
      (if (char-lessp (aref s1 i) (aref s2 i)) 
        (return-from my-string-lessp i)))
    (cond ((>= l1 l2) (return-from my-string-lessp nil))
          (t (return-from my-string-lessp l1)))))

4.0 Characters

X3J13, the ANSI subcommittee chartered to propose a specification for the forthcoming ANSI Common Lisp, has voted to make several changes to Common Lisp's treatment of characters. The intent of these changes is to clean up ideas that are felt not to have worked out in pre-ANSI Common Lisp as well as to allow for Common Lisp to be extensible to international languages. Unfortunately, some of these changes affect backward compatibility and storage efficiency. The result is that Franz Inc. has had to make some user-visible changes that may affect code which explicitly makes arrays or vectors of type character.

X3J13 has removed discussion of bit and font attributes of characters from the Common Lisp language. The string-char type specifier has also been removed from the language by X3J13. Finally, strings are now equivalent to (vector character) for creation purposes. X3J13 allows characters to be attributed with bit/font features as described in CLtL, but in an implementation-dependent way.

ANSI compatible Allegro CL continues to support font/bit attributes of characters. For example, the reader and printer acts on such characters in the pre-ANSI CL way (e.g., #\control-a is #\a with the control bit set, #3\meta-b is #\b with font 3 and the meta bit set). What's more, functions operating on bits and fonts from pre-ANSI CL (e.g., string-char-p, char-bits, char-font, make-char) are available in the cltl1 package, though the use of that package is deprecated.

Because Franz Inc. wants to achieve as much backward compatibility as possible with code using pre-ANSI font/bit attributed characters, and because Franz Inc. also wants to represent strings at least as efficiently as they have been in pre-ANSI versions of Allegro CL, difficulties arise in representing attributed characters in strings (which are now vectors of characters instead of vectors of string-chars). What ANSI-compatible Allegro CL does is to specify that it is an error to store attributed characters in a string. What in fact happens if one tries to do so is that the attributes are stripped. Thus an attributed character that has been stored in an array and extracted is no longer attributed and no longer EQL to its previous value.

Although this behavior violates the spirit of how elements are stored in arrays, this behavior was chosen by Franz Inc. because (a) pre-ANSI CL code using fonts/bits will not have been storing attributed characters into strings since it has always been an error to do so, and (b) representing strings as arrays that can hold attributed characters would have made strings less efficient and incompatible with existing foreign function code that uses strings.

In other words, portable ANSI CL code should not notice this compromise and pre-ANSI CL code should mostly be able to run as before with very little source change. The one area where portable pre-ANSI CL may run into problems is in places where the character type specifier is explicitly specified in calls to make-array, or to sequence functions that create a vector. (Such sequence functions include coerce, map, concatenate, etc.) These places in pre-ANSI CL where the character type specifier is used should most likely be changed to specify the t type specifier. In pre-ANSI versions of Allegro CL (array character) was equivalent to (array t).

5.0 Autoloading

Allegro CL has the ability to autoload certain files and modules. In order to keep the size of the system down by excluding parts not always needed, some of Allegro CL is not included in the system when it is built. These parts must be loaded in when they are required. This section describes how that code is loaded in.

Autoloads are triggered by referencing certain objects associated with an unloaded module. Typically, calling a function triggers an autoload, but autoloads can also be triggered by referencing a package or a class associated with an unloaded module. Note that only certain objects associated with a module trigger autoloads. If you reference unloaded functionality that does not trigger an autoload, the functionality may seem to be undefined.

An autoload is an automated form of load. When an autoload occurs, a message is printed unless *load-verbose* is nil, in which case the autoload is done silently. The autoload message is sent to the stream specified by the variable *system-messages*.

5.1 Where the autoloaded files are located

All the fasl files which have the potential to be autoloaded are part of the Allegro CL library. All the files are collected into a single file called the bundle file. Its filename is files and its type depends on the version of Allegro CL, but is always some variant of [letter]bu, for example files.bu and files.ebu. The bundle file is located in the Allegro directory. It contains a set of fasl files which can be loaded individually (the whole file is not loaded when a part is). The function bundle-pathname returns the pathname of the bundle file.

5.2 Common Lisp symbols

Code for some Common Lisp functions and macros (notably trace, inspect, and step) are contained in modules separate from the default binary. (The modules are called :trace, :inspect, and :step.) Whenever any Common Lisp function or macro is called, the necessary module will be loaded automatically. Note that using auxiliary features provided as extensions (such as referring to the variable *trace-print-length*) will not cause the module to be loaded. Even though the modules can be automatically loaded, we recommend explicitly loading those that you need with a call to require, as described below.

5.3 Major extensions

The code for major extensions, such as the foreign function interface or multiprocessing, also is loaded when needed instead of being in the default Lisp binary. Again, calls to some functions will cause the correct module to be loaded, but we recommend loading the module before using the facility, using require, as described next.

5.4 How to load modules

While most modules will be loaded automatically when an important function or macro defined in the module is called, you have to load modules explicitly to use some of the less central functionality. Some users also prefer to explicitly load modules in order to save waiting when the module is actually needed.

To load a module with require, simply enter the form:

(require :module-name)

It is useful to put this form at the beginning of any source file containing code which uses symbols in the module. It is not an error to call require when the module is already loaded.

6.0 Extensions to Common Lisp operators

This section describes extensions to Common Lisp operators.

An extension is additional functionality beyond what is specified in the ANSI spec. This section describes extensions to a number of CL functions. Usually, these extensions use an additional (non-standard) argument. Portable programs should conditionalize (with, for example, the feature :allegro) any use of that argument so that it is only used when run in Allegro CL.

An implementation detail either clarifies some part of the spec that is intentionally or unintentionally under specified. The spec usually says that details are left to the implementation when it intentionally under specifies. Unintentional under specification is more subtle: the spec simply says nothing about what should be done in a particular situation. (So for example, should a defpackage call which defines an existing package completely redefine the package according to the new description or should it add features to the package without removing existing features -- see cl:defpackage and cl:in-package for details on this issue.) A number of subsections discuss such details of various Common Lisp operators (and some variables).

6.1 cl:apropos implementation

function, package: common-lisp

Arguments: string &optional package external-only (case-insensitive t)

apropos in Allegro CL accepts two optional arguments in addition to the single standard optional argument. The second optional argument is external-only. If a package designator is specified as the value of the first (standard) optional argument, only symbols external in that package will be considered as candidates for output. If package is specified nil (some value must be given if external-only is to be specified), the external-only is ignored. external-only defaults to nil.

CL-USER(1): (apropos :defun nil t)
DEFUN               [macro] (name varlist &rest body)
COMP::PA-DEFUN-PROTO-1 [function] (xform)
COMP::QC-DEFUN-IN-RUNTIME [function] (node target cc)
COMP::COMPILE-P-DEFUN [function] (form)
EXCL::DEFUN-LIKE    [function] (xp list &rest args)
EXCL::RECORD-SOURCE-FILE-DEFUN [function] (fspec &optional icsp)
DEFUN-PROTO         [macro] (name varlist &rest body)
FF::DEFUN-FOREIGN-CALLABLE-1 [function] (name arglist body)
FF:DEFUN-FOREIGN-CALLABLE [macro] (name arglist &rest body)
FF:DEFUN-C-CALLABLE [macro] (&whole form &rest args)
:DEFUN              value: :defun
CL-USER(2): (apropos :defun (find-package :excl) t)
DEFUN-PROTO         [macro] (name varlist &rest body)
CL-USER(3): (apropos :defun (find-package :excl) nil)
EXCL::DEFUN-LIKE    [function] (xp list &rest args)
EXCL::RECORD-SOURCE-FILE-DEFUN [function] (fspec &optional icsp)
DEFUN-PROTO         [macro] (name varlist &rest body)

The third optional argument is case-insensitive. If true (which is the default), comparisons between string and symbol names are done in a case-insensitive fashion. Thus, in an ANSI (case-insensitive, symbols are named with uppercase strings) image,

(apropos "car" (find-package :common-lisp) nil nil)
  PRINTS nothing (as no symbols in the CL package have "car" in
         their names)
(apropos "car" (find-package :common-lisp))


And in a modern image (case-senstive, symbols are named with lowercase strings),

(apropos "CaR" (find-package :common-lisp) nil nil)
  PRINTS nothing (as no symbols in the CL package have "CaR" in
         their names)
(apropos "CaR" (find-package :common-lisp))


apropos binds *print-alternate-package-name* to true

When printing output, apropos binds *print-alternate-package-name* to true, so package nicknames are always used when they exist. For example, the alternate name of the foreign-functions package is "ff", and we have:

cg-user(5): *print-alternate-package-name*
cg-user(6): (format t "~S~%" 'ff:def-foreign-call)
cg-user(7): (apropos :def-foreign-call)
ff:def-foreign-call [macro] (ff::name-and-options ff::args &key ff::call-direct ...)
:def-foreign-call   value: :def-foreign-call

6.2 cl:apropos-list implementation

function, package: common-lisp

Arguments: string &optional package external-only case-insensitive

Like apropos, apropos-list accepts two additional optional arguments, external-only and case-insensitive. If external-only is true and a package designator is specified for the standard optional argument package, only external symbols in that package are included in the result. If case-insensitive is true (the default is nil), comparisons between string and symbol names are done in a case-insensitive fashion.

6.3 cl:defpackage implementation

macro, package: common-lisp

Arguments: defined-package-name &rest options

The specification of defpackage is silent on whether, when there are two defpackage forms for the same package, the second should augment the first or the second should replace the first.

Consider, for example, the following two defpackage forms:

(defpackage :newpack (:use :excl :cl))
(defpackage :newpack (:use :net.uri))

What is the package-use-list after the second defpackage form returns: a list of three packages (excl, common-lisp, and net.uri) or a list of a single package (net.uri)? Allegro CL augments the package specification rather than replacing it, as illustrated by the following transcript:

cl-user(1): (defpackage :newpack (:use :excl :cl))
#<The newpack package>
cl-user(2): (package-use-list (find-package :newpack))
(#<The excl package> #<The common-lisp package>)
cl-user(3): (defpackage :newpack (:use :net.uri))
#<The newpack package>
cl-user(4):  (package-use-list (find-package :newpack))
(#<The net.uri package> #<The excl package> #<The common-lisp package>)

The :implementation-packages option

Allegro CL support an additional option :implementation-packages. This option helps control whether a warning or error is signaled when a definition is made using a symbol from the package being defined by defpackage. See Implementation packages in packages.html for details and examples.

The :alternate-name option

If specified, the value must either be the package name or one of its nicknames. The alternate name is used when *print-alternate-package-name* is true. The alternate name of a package is returned by package-alternate-name. If no alternate name is specified at package creation time, a default alternate name is used, as described in the description of package-alternate-name.

The :local-nicknames option

Allegro CL support an additional option :local-nicknames. This option takes lists of the form (nickname package) as arguments. nickname serves as a nickname for package while the package being defined by defpackage is the value of *package*. See Package-local Nicknames in packages.html for details and examples.

Treatment of string designator arguments named by symbols

If you use a symbol (other than a keyword) to specify a value which is eventually converted into a string (such as the package name foo in (defpackage foo)), then the macroexpansion of the defpackage form will reference the uninterned symbol named foo, not foo internal in some package. This makes little difference to the Lisp which processes, either evaluating or compiling, the defpackage form -- foo will end up being interned in the current package when the form is read -- but does make a difference to Lisp images which simply read the fasl (compiled Lisp) file which contains the defpackage form.

This means that you can use symbols for names in defpackage forms in your application files, compile those files, and when you later use the compiled files to build your application, it will not have package name spaces cluttered by these symbols. Using a symbol has the advantage that it finesses the case-mode issue. (defpackage foo) creates the "FOO" package in an ANSI Lisp and the "foo" package in a modern Lisp (see case.html).

The :flat option

Allegro CL supports true hierarchical packages (see Hierarchical Packages in packages.html). Therefore, it will by default treat package names containing dots (.) as hierarchical package names. Hoever, if this is not what is desired, add (:flat t) to the defpackage specification. With (:flat t), package names and nicknames will not be treated as hierarical.
make-package (see cl:make-package implementation) has a similar :flat keyword argument. rename-package does not accept a :flat option: rename-package cannnot change the flat or hierarchical state of any package it renames.

6.4 cl:defstruct implementation

function, package: common-lisp

Arguments: name-and-options [documentation] &rest slot-descriptions

slot-description::= slot-name | (slot-name [slot-initform [[slot-option]]])

slot-option::= :type slot-type | :read-only slot-read-only-p | :type-unsafe type-unsafe-p

Starting in the 11.0 release, defstruct accessors are by default more safe than in prior versions. To be clear, we'll use the term type-safe. In versions of Allegro CL up through 10.1 the functional accessors always check that the underlying type of the object is a struct, and it checks the index of the access to ensure that it is in range, but it does not check for the object's actual type: (defstruct foo ...) will define an object of type foo when (make-foo ...) is called, and (defstruct bar ...) defines a bar type for objects created with (make-bar ...). But a bar object should not be an argument to a foo accessor, and vice versa. This is the sense of type-safety we're using here. Type-safety also applies to included structs; if (defstruct bas ...) includes the foo type, then `(make-bas ...) returns an object that is not only type bas, but also type foo. So a foo accessor can safely access a foo object or a bas object, but not a bar object. Finally, type-safety applies in compiled situations; a defstruct accessor is generally compiled down to a single instruction access (which of course is extremely type-unsafe), and in 10.1 the only way to achieve some safety is to either ensure that verify-non-generic-switch returns true (normally when speed is less than 3 or safety is 2 or 3) or else declare the accessor notinline.

Starting in 11.0, defstruct readers for structs that define types are type-safe by default, i.e. they will call error when an object (even a struct object) is passed to it that is not of its own type or one of its subtypes (i.e. other structs that include the accessor's struct type). These new accessors do still compile down to single instructions when compile-macroexpansions-for-safety-switch is nil (when safety is less than 3) and when verify-non-generic-switch is nil (when speed is 3 and safety is 0 or 1), so no speed is lost when the fast compilation is desired.

Compatibility issues

The two situations where users are likely to notice a (possibly unwanted) difference with these new accessors are:

  1. when the defstruct accessor is called in interpreted code: Such calls trigger the type-safe aspect of the accessors.

  2. when the accessor is invoked by funcall through a variable: Note that a lexically-visible funcall is not an issue; if (defstruct foo a b) is defined and then a call is compiled like (funcall 'foo-a x) then the compiler will transform that form into (foo-a x) so it can be further optimized into a single instruction. The problem case is when a variable is used as the function or function-name to funcall:

(defun call-accessor (acc x)
  (funcall acc x))

If this is the case the accessor will behave in a safe way and call out the error, possibly unexpectedly.

Mitigations for compatibility

Some lisp applications take advantage of the simplicity of structures' layouts and consistency, and may access a bar object with a foo accessor even though foo and bar have no common ancestry.

In order to support this non-conforming functionality, a new defstruct slot option is provided: :type-unsafe. When it is specified, its value determines whether the accessor that is installed for the accessor's name is safe or unsafe. If it is unspecified, then the accessor installed depends on the value of *expand-defstruct-accessors-unsafely* at the time of the defstruct form's macroexpansion. The value defaults to nil, and should normally remain nil, which means that any unspecified slots will install type-safe accessors. When the value is true, then unsafe accessors will be installed for unspecified slots, and all slots (specified or unspecified) will be considered specified (in case the struct type is later included by another defstruct form).

The best way to allow for known aliasing of differing struct objects is to use the new :type-unsafe slot-option in those slots which are known to be in the right place for an access. If this level of precision is not practical in a large app, the *expand-defstruct-accessors-unsafely* global can be lambda-bound to t when compiling those files for which type safety is not desired. Note that usage of the :type-unsafe option overrides the variable, so even if the majority of the app is compiled unsafely, it is possible to guarantee type safety by specifying :type-unsafe nil for the slot for which safety is desired.

Note that using a type-unsafe accessor, even when the layout of the struct is known and equivalent, does not protect against mismatches if the compiler has assumed something about the slot's type:

 (defstruct foo (a 0 :type fixnum :type-unsafe t))
 (defstruct bar (a 0.0 :type single-float :type-unsafe t))

Trying to cross-access one of these instances may be dangerous, even if foo-a and bar-a are declared to be notinline, because the compiler might reach in and assume it knows the type of the slot's value and compile the call with that assumption in mind.

For source compatibility, a 10.1 patch also adds the :type-unsafe defstruct slot option but non-functionally; it does nothing (because defstruct accessors in 10.1 are not type-safe and cannot be made type-safe). The defstruct macro allows the option, however, so that it can be used in code intended for both 10.1 and 11.0 (and later) Lisps.

See *expand-defstruct-accessors-unsafely* for an example of the contrast between type-safe and type-unsafe accessors.

6.5 cl:directory implementation

function, package: common-lisp

Arguments: path &key (directories-are-files t) (follow-symbolic-links t)

The directory function has some keyword arguments added to it to assist in recursive walks down a directory tree. (Note that even though the new argument is not specified, Common Lisp: the Language says the following about directory: `It is anticipated that an implementation may need to provide additional parameters to control the directory search. Therefore directory is specified to take additional keyword arguments so that implementations may experiment with extensions, even though no particular keywords are specified here.')

Returns a list of pathnames matching path, which may be a pathname, string, symbol or stream. Returns nil if there is no match.

If the keyword argument directories-are-files is specified true (the default), this function will return directories as files (that is pathnames with name and/or type components true). If the argument is nil, directories are returned as directories (pathnames with name and type components nil). In the latter case it is possible to walk down a directory tree recursively using directory.

The elements of the list returned by directory are in the same order as returned by the associated system function (e.g. readir() on UNIX).

If directory is given wildcards, for example "*/*.cl", it will ignore files which are symbolic links that point to other directories. This prevents directory recursing into these symbolically named directories. For example, (directory "*/*.cl") will, in the face of a foo symlink to a directory, not descend into foo. However, when follow-symbolic-links is non-nil (the default), directory recurses into directories pointed to by symlinks when the appropriate "**" (that is, :wild-inferiors) directory component is used. (This issue affects UNIX and UNIX like platforms only; following symbolic links is not supported on the Windows implementation.)

Wildcard handling

directory uses pathname-match-p, which, when presented with wildcards in path (when path is a string), converts the pathname into Allegro CL regular expressions, according to the rules given next. (See regexp.html for information on regular expression handling.)

Handling of non-directory components:

    . turned into \.
    * turned into .*
    ? turned into .
    ^ prepended onto beginning
    $ appended onto end

Handling of directory components:

    . turned into \.
    * turned into .[^/]* (or .[^\\]* on windows)
    ** matches any number of directory levels
    ? turned into .
    ^ prepended onto beginning
    $ appended onto end

6.6 cl:disassemble implementation

function, package: common-lisp

Arguments: name-or-compiled-function &key absolute references-only recurse print-header profile start end

The standard disassemble does not have any keyword arguments. The keyword arguments are extensions which are likely not supported in implementations of Common Lisp other than Allegro CL.

In standard CL, name-or-compiled-function should be a function-object, a lambda expression, or a symbol with a function definition. Allegro CL also accepts function names which are lists as well (see Function specs (fspecs) for a discussion of function names which are lists).

name-or-compiled-function can also be a string. A string is interpreted as naming a foreign (C or Fortran) function. The string must match the name identified by applying nm (or similar system function) to the current symbol table. This is often the result of applying convert-to-lang to the routine name, but there are exceptions -- e.g. Lisp internal routines typically do not have a prepended underscore. name-or-compiled-function can also be a codevector. These are extensions to Common Lisp.

The :absolute keyword argument

If the value of the absolute keyword argument is nil (the default), then relative pc addresses are given, starting at 0. If the value of absolute is true, addresses are given as absolute addresses. Note that these addresses are consistent within a single disassembly, but any gc activity may have moved the code vector by the time the disassembly is done.

The :recurse keyword argument

The recurse keyword argument, if true, causes internal functions to be disassembled after the specified function. It defaults to t if the name-or-compiled-function represents a function and if references-only is nil, and neither start, or end is specified. Otherwise it defaults to nil.

The :references-only keyword argument

If the references-only keyword argument is specified true (its default value is nil) then no disassembly is printed. Instead, a list is returned of all references the function identified by the required argument makes (from either the function object or the global table) to any Lisp object. When references-only is non-nil, recurse defaults to nil.

The :print-header keyword argument

If the print-header argument is given and non-nil, then information other than the pure code in the function (including the header) is printed before and after the code is disassembled. If nil, then no extra info is printed. The print-header argument defaults to true if neither start nor end have been specified, and defaults to nil if either start or end are specified.

The :profile keyword argument

If the profile argument is given and is either the name of a named profile (see prof:save-named-profile or a profile object (see prof:find-named-profile, then profile data are looked up for the function being disassembled. If such data are found, then the instructions in the function's code vector are annotated with the profiler hits for the specified profile. The default value for profile is nil, which causes no profile data to be searched for and/or annotated. The current profile can be specified using a profile argument of :current.

If there are hits which aren't subsumed by the function, and if neither the start nor end keywords are given, then these hits are listed after the function has been disassembled - first any symbol-trampoline hits are shown in a similar style as for the function itself, with the first part (as a caller) and the second part (as a callee) are divided and hits are annotated. Then, if there are any hits which simply don't match any address which the profiler recognizes, then these are listed at the end.

If the profile argument is specified and represents a profile, and either the start or end argument are specified, then no extra hits are displayed other than those which are subsumed by the function.

disassemble with the profile argument specified replaces the function prof:disassemble-profile, whose use is now deprecated. That function did just call cl:disassemble but did not provide all the options that cl:disassemble itself offers in Allegro CL.

The :start and :end keyword arguments

The start and end keyword arguments act in the spirit of the start and end keyword argument to sequence functions, but the output of disassemble is not a sequence so the arguments differ from those. Both values, if specified, should be non-negative integers indicating the pc-offset where printing of disassembled code should start and stop. The absolute argument is ignored: start and end work with respect to a start of 0 regardless of what the absolute address is. When start or end or both are specified, recurse defaults to nil.


Here is an example:

cl-user(1): (defun foo (x y)
              (+ (sqrt (* 2 y)) (log x)))
cl-user(2): (compile 'foo)
cl-user(3): (disassemble 'foo)
;; disassembly of #<Function foo>
;; formals: x y
;; constant vector:
0: sqrt
1: log

;; code start: #x40e922c4:
   0: 55          pushl ebp
   1: 8b ec       movl  ebp,esp
   3: 83 ec 30    subl  esp,$48
   6: 89 75 fc    movl  [ebp-4],esi
   9: 89 5d e4    movl  [ebp-28],ebx
  12: 39 a3 be 00 cmpl  [ebx+190],esp   ; "thread: stacklim"
      00 00 
  18: 76 02       jbe   22
  20: cd 65       int   $101            ; sys::trap-stack-ovfl
  22: 83 f9 02    cmpl  ecx,$2
  25: 74 02       jz    29
  27: cd 61       int   $97             ; sys::trap-argerr
  29: 89 45 dc    movl  [ebp-36],eax    ; x
  32: 80 7f cb 00 cmpb  [edi-53],$0     ; sys::c_interrupt-pending
  36: 74 02       jz    40
  38: cd 64       int   $100            ; sys::trap-signal-hit
  40: 8b 9f af fd movl  ebx,[edi-593]   ; excl::*_2op
      ff ff 
  46: b8 08 00 00 movl  eax,$8          ; 2
  51: ff 57 27    call  *[edi+39]       ; sys::tramp-two
  54: 8b 5e 12    movl  ebx,[esi+18]    ; sqrt
  57: b1 01       movb  cl,$1
  59: ff d7       call  *edi
  61: 89 45 d8    movl  [ebp-40],eax    ; excl::local-1
  64: 8b 45 dc    movl  eax,[ebp-36]    ; x
  67: 8b 5e 16    movl  ebx,[esi+22]    ; log
  70: b1 01       movb  cl,$1
  72: ff d7       call  *edi
  74: 8b d8       movl  ebx,eax
  76: 0b 5d d8    orl   ebx,[ebp-40]    ; excl::local-1
  79: f6 c3 03    testb bl,$3
  82: 75 0f       jnz   99
  84: 8b d8       movl  ebx,eax
  86: 03 5d d8    addl  ebx,[ebp-40]    ; excl::local-1
  89: 70 08       jo    99
  91: 8b c3       movl  eax,ebx
  93: f8          clc
  94: c9          leave
  95: 8b 75 fc    movl  esi,[ebp-4]
  98: c3          ret
  99: 8b d0       movl  edx,eax
 101: 8b 45 d8    movl  eax,[ebp-40]    ; excl::local-1
 104: 8b 5f 8f    movl  ebx,[edi-113]   ; excl::+_2op
 107: ff 57 27    call  *[edi+39]       ; sys::tramp-two
 110: eb ee       jmp   94

;; Note the start is pc-offset = 3 even though 5 was specified
;; since that instruction includes location 5:

cl-user(4): (disassemble 'foo :start 5 :end 34)
;; disassembly of #<Function foo>
;; formals: x y
;; constant vector:
0: sqrt
1: log

;; code start: #x40ed6464:
   3: 83 ec 30    subl  esp,$48
   6: 89 75 fc    movl  [ebp-4],esi
   9: 89 5d e4    movl  [ebp-28],ebx
  12: 39 a3 be 00 cmpl  [ebx+190],esp   ; "thread: stacklim"
      00 00 
  18: 76 02       jbe   22
  20: cd 65       int   $101            ; sys::trap-stack-ovfl
  22: 83 f9 02    cmpl  ecx,$2
  25: 74 02       jz    29
  27: cd 61       int   $97             ; sys::trap-argerr
  29: 89 45 dc    movl  [ebp-36],eax    ; x
  32: 80 7f cb 00 cmpb  [edi-53],$0     ; sys::c_interrupt-pending

;; When :absolute is true, start and end still use offsets with
;; respect to 0:

cl-user(5): (disassemble 'foo :start 5 :end 34 :absolute t)
;; disassembly of #<Function foo>
;; formals: x y
;; constant vector:
0: sqrt
1: log
40ed6467: 83 ec 30 subl esp,$48
40ed646a: 89 75 fc movl [ebp-4],esi
40ed646d: 89 5d e4 movl [ebp-28],ebx
40ed6470: 39 a3 be 00 cmpl  [ebx+190],esp   ; "thread: stacklim"
          00 00 
40ed6476: 76 02   jbe   0x40ed647a
40ed6478: cd 65   int   $101            ; sys::trap-stack-ovfl
40ed647a: 83 f9 02 cmpl ecx,$2
40ed647d: 74 02   jz    0x40ed6481
40ed647f: cd 61   int   $97             ; sys::trap-argerr
40ed6481: 89 45 dc movl [ebp-36],eax    ; x
40ed6484: 80 7f cb 00 cmpb  [edi-53],$0     ; sys::c_interrupt-pending

There are other keyword arguments to disassemble but they are not for programmer use.

6.7 cl:ensure-directories-exist implementation

function, package: common-lisp

Arguments: path &key verbose mode

If verbose is specified true, it prints the fact that a directory is created when one is. The default value for the mode argument is #o777. The value should be a non-negative integer less than or equal to #o777. Any directory created will be created with that mode.

6.8 cl:file-length

function, package: common-lisp

Arguments: stream

Allegro CL allows stream to be a pathname or a namestring as well as a stream open to a file (ANSI CL specifies only a stream open to a file). For a pathname or a namestring argument, the file-length function returns the size (number of octets, that is 8-bit bytes) of the associated file.

However, a namestring is passed to the relevant system call unchanged. If you use shell abbreviations, like '~' to indicate your home directory it will likely fail because the OS system call may not know what is intended by '~' (which is a shell, not an OS, convention). Applying pathname to the namestring will perform the necessary conversions before making the system call.

We also do not signal an error when the argument to file-length is a string stream or a buffer stream (instead of just a stream open to a file). See the discussion of file-length in Conformance with the ANSI specification for further details.

6.9 cl:file-write-date implementation

function, package: common-lisp

Arguments: pathspec

There are two implementation details for file-write-date:

  1. A setf method has been provided for file-write-date. It sets the mtime (the modification time on UNIX) of the file. On Windows, the comparable value is set.

  2. If the file specified by the pathspec argument does not exist, nil is returned (rather than an error being signaled).

The fact that nil is returned when the argument file does not exist is arguably an ANSI non-compliance. The Spec says: "An error of type file-error is signaled if the file system cannot perform the requested operation". But it also says: "returns nil if such a time cannot be determined". Returning nil in this situation is longstanding behavior in Allegro CL and is being maintained.

6.10 cl:format implementation

function, package: common-lisp

Arguments: destination control-string &rest args

Provides the functionality as specified by format. In addition, when destination is not nil (i.e. when writing to an actual stream), format utilizes with-staged-output if destination is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.11 cl:function-lambda-expression implementation

function, package: common-lisp

Arguments: function

function-lambda-expression returns three values: the defining lambda expression, if available (nil is returned if it is unavailable); information on whether the function was or was not defined in the null lexical environment; and information on the function's name. Here we discuss when the first returned value might be non-nil:

6.12 cl:in-package implementation

macro, package: common-lisp

Arguments: package-name

The Common Lisp macro in-package changes the value of *package* to the package designated by package-name. If package-name is a symbol, the macroexpansion of the in-package form converts that symbol reference to an uninterned symbol of that name. See the discussion under the heading Treatment of string designator arguments named by symbols in the description of defpackage for why this is a useful feature.

6.13 cl:interactive-stream-p implementation

function, package: common-lisp

Arguments: stream

The Common Lisp function interactive-stream-p returns true if its argument is an interactive stream, which is a stream "on which it makes sense to perform interactive querying". Allegro CL extends this function so that it is setf'able.

When (setf (interactive-stream-p stream) t) is evaluated, not only does (interactive-stream-p stream) return true, but also any writing that is done is encapsulated into blocks of output that are forced out by a call to force-output at the end of the call. This makes the stream seem like it is unbuffered, yet without sacrificing as much performance as a raw unbuffered stream would require, since the actual output takes place only at the end of each group of write operations.

6.14 cl:intern implementation

function, package: common-lisp

Arguments: string &optional packages

Allegro CL may allow a symbol as the first (string) argument to intern. Standard Common Lisp requires that the first argument be a string, but specifies no consequences if it is not. Allegro CL controls the behavior with the variable *intern-allows-symbol*, which, if true, causes intern to also accept a symbol as its first argument. If *intern-allows-symbol* is nil, passing a symbol as the first argument signals an error.

6.15 cl:lisp-implementation-version implementation

function, package: common-lisp


In Allegro CL, lisp-implementation-version returns two value. The first is a string which is of the form

"[Version number] [[Platform]] ([Date and time of build])"

For example

"8.1 [64-bit Linux (AMD64)] (Feb 7, 2007 14:55)"

The second return value is a list of strings (there may be only one) that identify the Allegro shared library build version. For example:


NNN is an integer which is increased with each new build. (The library is often updated between releases because of patches.) So, a call looks like:

cl-user(2): (lisp-implementation-version)
"8.1 [64-bit Linux (AMD64)] (Feb 7, 2007 14:55)"

These examples are for illustration only. The values you see will be different. This information may be helpful when trying to identify a potential problem, allowing users at different sites to be sure they are running the same versions of all parts of Allegro CL.

6.16 cl:load implementation

function, package: common-lisp

Arguments: filespec &key verbose print if-does-not-exist external-format searchlist system-library script foreign

We have added several new arguments to load. These are described in Using the load function in loading.html for the general implementation and in Load foreign code with cl:load in foreign-functions.html (for loading foreign code).

6.17 cl:loop and the for-as-in-sequence subclause for looping over sequences

The loop macro, loop, is extended to support for-as-in-sequence subclauses, which is in addition to the standard for-as-in-list and for-as-across (for looping over vectors).

In the ANS Section Iteration Control, descriptions are provided for several iteration controls over object types which are suited for iteration. Two of these are elements of lists (The for-as-in-list subclause) and vectors (The for-as-across subclause). But there is no single iterator which will work on either lists or vectors.

Allegro CL has introduced a new for-as-in-sequence clause, which allows iteration over either lists or simple, general vectors. It allows for implementational switches from lists to such vectors and vice versa, and it does so with as little run-time expense as possible (the restriction to simple vectors allows much faster performance than would be possible if any type of vector was allowed). It combines common aspects of the for-as-in-list and for-as-across subclauses.

The template is simplified compared to templates for for-as-in-list and for-as-across and vectors appearing in the clause have certain restrictions:

Examples using for-as-in-sequence

(defun foo (x)
  (loop for y in-sequence x collect (1+ y)))
(foo '(1 2 3)) => (2 3 4)
(foo #(1 2 3)) => (2 3 4)

The for-as-in-sequence only iterates over the top level of a list or vector. Elements of the list or vector which are themselves lists of vectors are treated as simple data, but destructuring works in the for-as-in-sequence subclause but only for elements which are lists:

CL-USER(2): (loop for (x y) in-sequence '((1 2) (3 4)) collect (list x y))
((1 2) (3 4))
CL-USER(3: (loop for (x y) in-sequence #((1 2) (3 4)) collect (list x y))
((1 2) (3 4))

;; But this does not work 

CL-USER(4): (loop for (x y) in-sequence '(#(1 2) #(3 4)) collect (list x y))
Error: Attempt to take the car of #(1 2) which is not listp.
  [condition type: TYPE-ERROR]

6.18 cl:macroexpand implementation

function, package: common-lisp

Arguments: form &optional environment special-operator-stop

A second (non-standard) optional argument, special-operator-stop, has been added to allow controlling behavior for specific kinds of environments.

If the environment passed in is a :compiler environment (as opposed to a :compilation, :interpreter, :evaluation, or :macros-only) then compiler-macros will be expanded by both macroexpand and macroexpand-1. This is to simulate what happens when the compiler does its macro expansions. Note that this behavior is an extension to the ANSI Spec, which states that compiler-macros are not expanded by macroexpand/macroexpand-1. Note also that any portable code-walker which expects to receive an ansi-compliant environment must condition the environment by using sys:ensure-portable-walking-environment on the argument.

If a :compiler environment is passed in to macroexpand/macroexpand-1 and special-operator-stop is true, then a special-form (a form whose car is a special-operator) will not be macroexpanded, as is otherwise usually the situation. This feature is provided to allow a specialized code-walker (not necessarily a portable one) to see what special forms the compiler sees. If the walker then knows how to interpret the syntax of the special-operator, it can do so in an implementation-dependent way; otherwise, it can always do the macroexpansion again with the special-operator-stop set to nil, in order to get a full macroexpansion through the special form.


cl-user(1): (setq env (sys:make-compilation-unit-environment))
#<Augmentable compiler environment @ #x40c916aa>
cl-user(2): (macroexpand '(case num
                            (1 (foo "one"))
                            (2 (foo "two"))
                            (3 (foo "three"))
                            (otherwise (foo "unknown")))
(let ()
  (cond ((eql '1 num) (foo "one"))
        ((eql '2 num) (foo "two"))
        ((eql '3 num) (foo "three"))
        (t (foo "unknown"))))
cl-user(3): (macroexpand '(case num
                            (1 (foo "one"))
                            (2 (foo "two"))
                            (3 (foo "three"))
                            (otherwise (foo "unknown")))
(let ()
  (cond ((eql '1 num) (foo "one"))
        ((eql '2 num) (foo "two"))
        ((eql '3 num) (foo "three"))
        (t (foo "unknown"))))
cl-user(4): (macroexpand '(case num
                            (1 (foo "one"))
                            (2 (foo "two"))
                            (3 (foo "three"))
                            (otherwise (foo "unknown")))
                            env t)
(excl::simple-case num (1 (foo "one")) (2 (foo "two")) (3 (foo "three"))
                   (otherwise (foo "unknown")))
cl-user(5): (macroexpand-1 * env t)
(excl::simple-case num (1 (foo "one")) (2 (foo "two")) (3 (foo "three"))
                   (otherwise (foo "unknown")))
cl-user(6): (macroexpand-1 * env)
(let ()
  (cond ((eql '1 num) (foo "one"))
        ((eql '2 num) (foo "two"))
        ((eql '3 num) (foo "three"))
        (t (foo "unknown"))))

6.19 cl:macroexpand-1 implementation

function, package: common-lisp

Arguments: form &optional environment special-operator-stop

The effect of the second (non-standard) optional argument to macroexpand-1 is the same as described in the description of macroexpand in Allegro CL. See that description and the associated examples for further details.

6.20 cl:make-array implementation

function, package: common-lisp

Arguments: dims &key allocation element-type weak short [and other standard CL keyword args not listed here]

allocation is discussed first and then weak. short is discussed briefly after the discussion of weak and in detail in Arrays and short arrays.

allocation: make-array, a standard Common Lisp function, has been extended to accept the allocation keyword argument. The value of this argument must be one of the following keywords (the default is :new, which produces the behavior of earlier releases).

allocation is not a standard Common Lisp argument to make-array so programmers may wish to conditionalize it with #+allegro to preserve code portability.

Having created a static array, you may wish to free it. To do this, first pass the array to the function lispval-other-to-address, which will return an address (an integer). That address can be passed to aclfree. Note: if you reference the array after it has been freed, you will get garbage values. If you set a value in the array after it has been freed, you may cause Lisp to fail.

weak: make-array, a standard Common Lisp function, has been extended to accept the weak keyword argument. weak is not a standard Common Lisp argument to make-array so programmers may wish to conditionalize it with #+allegro to preserve code portability. weak may be true (meaning create a weak array) or nil (meaning create a standard array). The default is nil.

A Lisp object becomes garbage when nothing points to or references it. The way the garbage collector works is it finds and identifies live objects (often then moving them somewhere). Whatever is left is garbage. Weak arrays allow pointers to objects which will not, however, keep them alive. If one of these pointers exists, the garbage collector will see the item and (depending on the circumstances), either keep it alive or abandon it.

If you specify weak true, you cannot specify the non-standard allocation argument or the standard displaced-to argument. The only values accepted for the standard element-type argument are those for which no specialized array type for that element-type is defined (i.e. upgraded-array-element-type applied to element-type should return t, which in essence means you should not specify element-type).

short: Allegro CL supports two fundamental kinds of arrays: standard and short. Short arrays (equivalent to the array type in releases prior to 7.0) have a smaller maximum size than standard arrays. See Arrays and short arrays for details. When :short t is specified, a short array is produced. Otherwise a standard array is produced.

See Weak arrays and hashtables in gc.html for more information on weak arrays.

6.21 cl:make-hash-table implementation

function, package: common-lisp

Arguments: &key test size rehash-size rehash-threshold hash-function values weak-keys implementation

In this section we give details of the Allegro CL implementation of make-hash-table (the link is to make-hash-table page in the ANS). The arguments rehash-size and rehash-threshold are as described there and they are not further discussed here. For other arguments, use the links in the argument list to jump to the specific argument description.

Hash tables with standard tests (eq, eql, equal, and equalp) have been optimized in Allegro CL to make putting values into and getting values from a hash table fast. eq hashtables are the fastest, followed closely by eql, and then equal and equalp.

The size argument to make-hash-table

The maximum size of a hash table is one less than the value of array-dimension-limit. In safe code, if the value specified by the size is greater than or equal to array-dimension-limit, then array-dimension-limit minus 1 will be used instead and a warning will be signaled.

Extensions to make-hash-table

Allegro CL has also extended make-hash-table in several ways:

  1. to accept the (non-standard) hash-function keyword argument,

  2. to allow test functions other than the standard four,

  3. to allow for weak hashtables, and

  4. to allow for valueless hashtables.

Also, as of Allegro CL 11.0, hash-tables have been completely refactored to

  1. scale properly in SMP Lisps. No locks are used, yet no known race conditions exist in this implementation. Any gethash operations done on a single hash-table with 8 threads in an 8-or- more core computer should get close to an 8x advantage in speed, to the extent that the gethashes do not affect each other serially.

  2. be extensible. A new macro, def-hash-table-implementation, allows the definition of new implementation strategies by the user, allowing standard hash-table operations to be performed on each new implementation.

  3. merge the smp and gmp versions of the hash-table implementation. Now, all hashing is performed in the same way regardless of the smp nature of the lisp.

The :hash-function keyword argument

The hash-function keyword argument allows further specialization when standard functionality is inefficient (usually because of excessive collisions caused by bunching of the hash codes of the data). Code that uses the hash-function argument is not portable Common Lisp, of course.

If specified, the value from hash-function must be a symbol naming a function of one argument in the global environment which reproducibly returns an integer in the correct range when applied to any Lisp object intended to be used as a hash key. (The value must be a symbol, not a function object.)

The correct range is between 0 and (1- (expt 2 24)) (inclusive) in 32-bit Lisps and between 0 and (1- (expt 2 32)) (inclusive) in 64-bit Lisps. Reproducibly here means the function will return the same value on equivalent objects whenever it is called. The consequences of returning a value outside the correct range are undefined (and so may result in an incorrect answer or cause an error or program failure).

hash-function defaults to sxhash except when test is one of the four standard tests (eq, eql, equal, equalp) when hash-function defaults to an internal function optimized for that test. (For equal and equalp, the hash-function is an internal version of sxhash.)

Note about hash-code generation:

A good hash-function will not only conform to the requrements of the hash-function argument, but will make efforts to distribute the hash-codes evenly across the objects which will be used as keys.

To aid in hash-code generation, an unexported function is provided which is otherwise undocumented: excl::hash-table-stats, which accepts as its only argument a hash-table, and which prints various statistics about that hash-table, including a histogram at the end of how hard it is to access each key (i.e. how far the key resides from its hash-code position). A long histogram is not good; the shortest histogram has only one entry at distance 0 (with all of the keys at that distance) which means that the current distribution of keys is perfect and the access is linear. A long histogram means that the hash code generation has poor distribution, likely either not as random as expected, or else bunched up close to zero.

The :test keyword argument

The value of test must be a symbol naming a function of two arguments in the global environment. This function will be passed two keys, and should return t if the keys are equivalent and nil if the keys are not equivalent. The standard values for test are eq, eql, equal, and equalp (or, for these four functions only, the associated function objects #'eq etc.) but any test function can be specified. (But note (1) that symbol is reserved for internal use so test should not be specified 'symbol in application or user code; and (2) the value must be a symbol naming a function, not a function object; the four standard function objects listed just above are accepted as values but no other function objects.) If hash-function is specified, it is the programmer's responsibility to ensure the test function and the hash function work together correctly and consistently.

The :weak-keys keyword argument

weak-keys defaults to nil, which specifies the default behavior. When weak-keys is specified as t, the keys of the resulting hash table are treated specially by the garbage-collector: when a key in such a hash table has no more references to it, the entire entry is removed from the hash table, and the hash-table-count is decremented. This entry removal will occur regardless of whether :values :weak is specified (which by itself will never affect the hash-table-count, but only the value of an entry). See gc.html for information on weak objects.

If weak-keys is given the value :tenurable, then the key vector (the part of the weak-key hash-table that is normally kept in newspace) is allowed to be tenured. Any other true value for weak-keys causes the key vector to be forced to stay in newspace (but it is best to use t as this allows other non-nil values which have special meaning to be added later). The :tenurable option allows the amount of data copied between newspace halves to remain smaller than if the key vector were forced to remain in newspace. This difference can be large if the hash-table is large. Allegro CL now uses this option internally. If a tenurable weak-keys hash-table must be rehashed due to growth, the new key vector is allocated in newspace, but is still allowed to be tenured. (This means the vector is not created with :allocation :old described in make-array.)

The downside of tenuring the weak-key vector is that references to the values will remain until a global garbage collection examines the weak-key vector. An untenured weak-key vector is examined whenever there is a scavenge. Global gc's are typically rare, but scavenges occur regularly. A decision to use the :tenurable option should take this into consideration.

The :implementation keyword argument

The implementation option allows various different hash-table implementations to be used to vary the behaviors of hash-tables according to the requirements of the application. If the implementation option is not provided, the hash-table implementation that is used is :acl.

If the implementation argument is given, its value must be one of the implementations shown as the result of calling list-hash-table-implementations. User-defined implementations can be defined with def-hash-table-implementation.

See Creating user-defined hash-table implementations for more information about the new design and extensibility of hash-tables.

Pre-defined implementations

The :acl hash-table implementation

The :acl implementation is a full hash-table implementation across all possible hash-function and test combinations. Hash-tables made with :acl either explicitly or implicitly specified as the implementation will be printed as before, with either the test specification only for the standard four hash-table types, or with the hash-function/test identifying the hash-table with a user-supplied hash-function. A hash-table built with any user-defined implementation will be printed to include that implementation name as well as the hash-function and test.

The :unsafe-clrhash implementation

The :unsafe-clrhash hash-table implementation is provided for speed when it is known that a hash-table will only ever be used by a single thread. It is not smp-safe.

In contrast, the version of clrhash that is provided with the :acl implementation is completely smp-safe, but it accomplishes that feat by allocating a new empty hash-table instance and then storing that instance into the hash-table. This turns potential race conditions into data races (see Race conditions vs data races below), so that any operations on the same hash-table by other threads may appear to occur either before or after the clrhash, but the integrity of the hash-table stays intact. The problem with this safety is that it conses heavily, even though it is much faster than the locking version of clrhash from prior versions.

The :unsafe-clrhash implementation provides a version of clrhash that is extremely fast but is not safe for use by multiple threads on the same hash-table. If you have an application which allocates hash-tables, clears them, and reuses them, all on the same thread, you might consider allocating such a hash-table with the :unsafe-clrhash implementation. Note that the safety of this hash-table against accidental use by other threads cannot be guaranteed. Some attempts are made to provide such safety: when the clrhash operation is started, the hash-table is quickly reconfigured to cause any other operations on it to cause an error. However, any operations that had already started on the hash-table before the clrhash operation may find an inconsistent hash-table state. However, this situation should never happen if you are following the rule to only ever access such a hash-table from the same thread.

:unsafe-clrhash example:

This example shows the contrast between a regular (i.e. :acl implementation) hash-table and a :unsafe-clrhash hash-table. Note that the unsafe version is much faster than the safe version, but also (and possibly more importantly) that the unsafe version does no consing at all. If your goal is to reduce garbage generation to reduce garbage-collection times, then the unsafe version may be a win, if the one-thread-per-hash-table rule can be followed:

cl-user(1): (setq foo (make-hash-table))
#<eql hash-table with 0 entries @ #x30883d472>
cl-user(2): (setq bar (make-hash-table :implementation :unsafe-clrhash))
#<unsafe-clrhash eq-hash-fcn/eql hash-table with 0 entries @
cl-user(3): (dotimes (i 10000) (setf (gethash i foo) (* 100 i)))
cl-user(4): (dotimes (i 10000) (setf (gethash i bar) (* 100 i)))
cl-user(5): (time (clrhash foo))
; cpu time (non-gc) 0.000131 sec user, 0.000039 sec system
; cpu time (gc)     0.009352 sec user, 0.000436 sec system
; cpu time (total)  0.009483 sec user, 0.000475 sec system
; real time  0.009971 sec (99.87%)
; space allocation:
;  0 cons cells, 490,272 other bytes, 0 static bytes
; Page Faults: major: 0 (gc: 7), minor: 22 (gc: 7)
#<eql hash-table with 0 entries @ #x300d8e2e2>
cl-user(6): (time (clrhash bar))
; cpu time (non-gc) 0.000071 sec user, 0.000006 sec system
; cpu time (gc)     0.000000 sec user, 0.000000 sec system
; cpu time (total)  0.000071 sec user, 0.000006 sec system
; real time  0.000079 sec (97.47%)
; space allocation:
;  0 cons cells, 0 other bytes, 0 static bytes
; Page Faults: major: 0 (gc: 0), minor: 0 (gc: 0)
#<unsafe-clrhash eq-hash-fcn/eql hash-table with 0 entries @

The :values keyword argument

values can be t (the default), :weak, or nil.

:values t or :values unspecified

When values is t, the hash table will contain both a key and a value for each entry (that is, it will be a normal hash table). As said above, t is the default value for values.

:values :weak

When :values :weak is specified, then the hash table will hold a value only as long as it is referenced non-weakly by some other object. If no other objects reference the value, it becomes nil and a gethash on the key will return nil for the value (the value is collected by the gc).

:values :weak example

;;  We create a :values :weak hashtable:
cl-user(26): (setq ht (make-hash-table :values :weak))
#<eql hash-table with weak values, 0 entries @ #x48aef52>
;;  We create an object to store aa a value:
cl-user(27): (setq a (list 1 2 3))
(1 2 3)
;;  We store the list as the value of the key 100:
cl-user(28): (setf (gethash 100 ht) a)
(1 2 3)
;;  And the list is returned when we ask for it:
cl-user(29): (gethash 100 ht)
(1 2 3)
;;  We break the link from the symbol A to the list:
cl-user(30): (setq a nil)
;;  We break the links from variables like *, **, etc. to the list
;;  (this works here but be aware that links may exist that you are
;;  unaware of, and it make take longer for those links to disappear).
cl-user(31): t
cl-user(32): t
cl-user(33): t
cl-user(34): (gc)
cl-user(35): (gc)
;;  Now when we get the value associated with 100, it is NIL
cl-user(36): (gethash 100 ht)
;;  Note, second value is T as 100 still has a value.  But the
;;  value is now NIL, not the list which was the original value.

:values nil

When :values nil is specified, a sans values hash table is created, and only keys are stored. gethash returns the key as its first return if the key is in the table, and t as the second value in that case. As usual, gethash returns nil and nil if the key is not in the table. You can use setf and gethash to store a key. You must specify a value but that value is ignored. You can also use the function puthash-key to store a key in the table.

On sans-value hash tables, maphash will call its argument function with the key as both arguments (as the key argument and as the value argument), as there is no value to pass.

One use of :values nil (sans-value) hash tables is to identify a set of objects, such as those objects which have a particular property, in a space efficient way. Suppose, for example, you have many instances (millions of them) of a particular class, and only 20 are XYZ-positive. You could have an xyz-positive instance slot in the class, but that could use megabytes of space. A sans-value hash table with the 20 objects as keys uses just a few hundred bytes. That table could be the value of a class slot of the class and a method that looked to the user like an ordinary reader could test whether an instance was in the hash table or not, while a writer could add an instance to the hash table.

Sans-value hash tables are also a good way to store conses. If you have a bunch of conses you will need many times, place each as you first create it as a key into a sans-value hash table with the appropriate test function (say equal). Then, if you need that cons, create one and test it using puthash-key or gethash, and always using the return value (unless nil in the case of gethash) and discarding the test value. Only one permanent copy of the cons will then be stored no matter how many you create. (See the second example below.)

:values nil example

;; We create a sans-value hashtable:
cl-user(49): (setq svht (make-hash-table :values nil))
#<eql hash-table (sans values) with 0 entries @ #x4a94bb2>
;;  We store as keys all CL symboles with more than 3 e's in
;;  the symbol name. Note we use puthash-key to store the key.
;;  We do not need a value because being in the hashtable indicates
;;  the key has the desired property (more that 3 e's).
cl-user(50): (do-external-symbols (s (find-package :cl))
           (if (> (count #\e (symbol-name s) :test 'char-equal) 3)
           (puthash-key s svht)))
;;  There are 39 such symbols:  #x4a94bb2
cl-user(51): svht
#<eql hash-table (sans values) with 39 entries @ #x4a94bb2>
;;  We use MAPHASH to print out the 39 symbols. Note the value
;;  passed to the MAPHASH argument function is the key (that
;;  is, the key is passed as both the K and the V arguments).
;;  We have added line breaks for clarity in some cases
cl-user(52): (maphash #'(lambda (k v)
              (format t "~S, value is ~S~%" k v))
integer-decode-float, value is integer-decode-float
  value is least-negative-normalized-single-float
set-difference, value is set-difference
  value is update-instance-for-different-class
  value is double-float-negative-epsilon
make-sequence, value is make-sequence
make-instances-obsolete, value is make-instances-obsolete
stream-element-type, value is stream-element-type
  value is least-negative-normalized-double-float
delete-package, value is delete-package
  value is least-negative-normalized-long-float
delete-file, value is delete-file
array-element-type, value is array-element-type
upgraded-array-element-type, value is upgraded-array-element-type
encode-universal-time, value is encode-universal-time
  value is least-negative-normalized-short-float
  value is internal-time-units-per-second
type-error-expected-type, value is type-error-expected-type
ensure-generic-function, value is ensure-generic-function
delete-duplicates, value is delete-duplicates
define-setf-expander, value is define-setf-expander
  value is least-negative-single-float
read-sequence, value is read-sequence
get-decoded-time, value is get-decoded-time
  value is concatenated-stream-streams
invoke-restart-interactively, value is invoke-restart-interactively
read-preserving-whitespace, value is read-preserving-whitespace
get-internal-real-time, value is get-internal-real-time
  value is least-positive-normalized-double-float
decode-universal-time, value is decode-universal-time
*compile-file-truename*, value is *compile-file-truename*
least-negative-double-float, value is least-negative-double-float
nset-difference, value is nset-difference
ensure-directories-exist, value is ensure-directories-exist
make-concatenated-stream, value is make-concatenated-stream
  value is update-instance-for-redefined-class
write-sequence, value is write-sequence
  value is least-positive-normalized-single-float
  value is single-float-negative-epsilon
;;  GETHASH works as usual but returns the KEY as if it 
;;  were the value:
cl-user(53): (gethash 'write-sequence svht)
;;  You can use SETF of GETHASH instead of PUTHASH-KEY. Note
;;  the value specified (10 in this case) is discarded:
cl-user(54): (setf (gethash nil svht) 10)
;;  GETHASH returns the key as the value. The value specified
;;  just above (10) is not stored so is not available:
cl-user(55): (gethash nil svht)
;;  NIL is returned because the KEY is NIL. When the key is NIL,
;;  you must look at the second return value to see if NIL is 
;;  in the hash table.

;;  In the second example, we create a EQUAL sans-value hash table
;;  and store some conses in it. We create a new cons and use
;;  PUTHASH-KEY to store it if necessary.  PUTHASH-KEY returns
;;  the stored cons if there, or the cons if just stored.  
cl-user(59): (setq cons-storer-ht (make-hash-table :test 'equal :values nil))
#<equal hash-table (sans values) with 0 entries @ #x4b78e3a%gt;
;;  We put some conses in the hash table:
cl-user(60): (puthash-key (list 'baltimore 'md) cons-storer-ht)
(baltimore md)
cl-user(61): (puthash-key (list 'boston 'ma) cons-storer-ht)
(boston ma)
cl-user(62): (puthash-key (list 'berkeley 'ca) cons-storer-ht)
(berkeley ca)
cl-user(63): (puthash-key (list 'reno 'nv) cons-storer-ht)
(reno nv)
;;  Here is a cons. We put it in the hashtable if necessary.
;;  PUTHASH-KEY returns the one there if present:
cl-user(64): (setq a (list 'boston 'ma))
(boston ma)
cl-user(65): (puthash-key a cons-storer-ht)
(boston ma)
;;  Note the new one is not the one returned:
cl-user(66): (eql a *)
;;  So we break the link to the new one, and use the stored
;;  one so only one copy is live in the image:
cl-user(67): (setq a **)
(boston ma)

Race conditions vs data races

Note about races: In a blog by Prof John Regehr of University of Utah ( a distinction is made between two terms: "race condition" and "data race".

A data race is something that a program must accept as a possibility, where two threads might update an object in a simultaneous manner, and the winner of the race is non-deterministic. So for example, if one thread performs a puthash [i.e a (setf gethash)] on a hash-table and a second thread performs a gethash on the same hash-table with the same key value, the new value for the key will eventually show up in the hash-table, but it is not known whether the gethash will return the new value or the old value. This is known as a data race.

A race condition is a situation where the integrity of the program becomes suspect. Hopefully there are no race conditions in this implementation. For example, if two threads increment a usage-count of the same hash-table simultaneoiusly without protections, each thread might get a wrong value, and that might lead to leaving the hash-table with an incorrect count. But if the update uses an atomic conditional setf and the losing thread loops back to try again, the race condition is alleviated.

Note that Regehr states that "In practice there is considerable overlap: many race conditions are due to data races, and many data races lead to race conditions. On the other hand, we can have race conditions without data races and data races without race conditions."

This new implementation of hash-tables attempts to take advantage of data race situations in dealing with hash-table operations, in order to remove all locks from hash-table operations. Locks can still be used at a higher level to guarantee ordering, but for example if a puthash is about to add a new key to a hash-table, and a gethash happens to be looking for that same key, it may not find it yet because the puthash has only just reserved the key but not placed the value, or perhaps it has reserved the key and placed the value, but not stored the actual key yet. The gethash thus does not find the key it looks for, and returns its default. It's as if the gethash has executed before the puthash has done any work, even though the puthash may have done almost all of its work.

The removal of locks is largely done by collecting several components of a hash-table into a separate instance object and conceptually treating these components as immutable for many operations, only updating a hash-table with a new instance atomically after all of the work is done on that new instance. This tends to be more consey, but it also removes the need for locks. Note that this rule is followed by clrhash and the rehashing operation, but not by puthash and remhash - these operations perform their interlocks by arranging that races between them and other functionalities are data races only.

Some ways we turn race conditions into data-races:

  1. When the default implementation of remhash finds a key to delete, it replaces the key with a deleted key. But it does not then null out the value slot, but instead allows the gc to perform the removal of the value. This avoids a race condition where a gethash isn't allowed to complete with a consistent entry that has been removed, by turning it into a data race where the gethash appears to have completed before the remhash started, even if the remhash might have completed before the gethash was finished.

  2. When the default implementation performs a clrhash operation, it does not delete specific key/value pairs from the hash-table, where locking would be necessary to avoid a race condition; instead, a completely new hash-table instance is allocated, which would then be empty, and then that instance is atomically stored into the hash-table's instance slot. This turns a potential race condition, where a gethash in another thread would see an inconsistent key/value pair, into a data race, where the gethash that is started after the clrhash has been started will appear to have been started and completed before the clrhash was started.

6.22 cl:make-package implementation

function, package: common-lisp

Arguments: package-name &key use implementation-packages (internal-symbols 10) (external-symbols 10) alternate-name flat local-nicknames

We have added some additional keyword arguments to make-package. The implementation-packages keyword argument is an Allegro CL extension described fully in the section Implementation packages in packages.html. Its value should be a list.

The local-nicknames keyword argument is an Allegro CL extension described fully in the section Package-local Nicknames in packages.html.

The default for the use argument is implementation-dependent. The default in Allegro CL is a list containing one element, the common-lisp package.

The flat argument. Allegro CL supports true hierarchical packages (see Hierarchical Packages in packages.html). It will treat nicknames containing dots as relative nicknames and ignore everything up to the final dot. However, if this is not what is desired, you can ensure that nicknames are not considered relative by specifying the :flat keyword argument as true (it defaults to nil). (Doing so also makes the package name itself flat, but there are few user-visible consequences to that.) defpackage has a similar :flat option, as described in defpackage and in-package. rename-package does not accept a :flat option: rename-package cannot change the flat or hierarchical state of any package it renames. See also the discussion of the defpackage implementation in defpackage and in-package. defpackage also supports a :flat option. Note that if the variable *regard-package-names-as-flat* is true, a warning will be printed whenever a package is defined with make-package or defpackage without specifying a :flat option (even if the value of the option is nil).. See *regard-package-names-as-flat* for details.

The alternate-name keyword argument: if specified, the value must either be the package name or one of its nicknames. The alternate name is used when *print-alternate-package-name* is true. The alternate name of a package is returned by package-alternate-name. If no alternate name is specified at package creation time, a default alternate name is used, as described in the description of package-alternate-name.

6.23 cl:namestring implementation

function, package: common-lisp

Arguments: pathname &key syntax

namestring takes a pathname designator and returns the full namestring of the pathname. Allegro CL adds an additional keyword argument: syntax. The value of syntax can be nil or :unix. The behavior of the syntax argument is different on Unix and Unix-like platforms and on Windows.

On Unix and Unix-like platforms

The syntax argument is ignored.

On Windows

If syntax is :unix, any backward slashes in the pathname are converted to forward slashes. If syntax is nil (the default), no slashes are converted and namestring behaves normally.

Thus, on Windows only,

(namestring "\\ftp\\pub\\patches\\8.0\\ftp.001" :syntax :unix)
  RETURNS "/ftp/pub/patches/8.0/ftp.001"


(namestring "\\ftp\\pub\\patches\\8.0\\ftp.001")
  RETURNS "\\ftp\\pub\\patches\\8.0\\ftp.001"

The argument was added to assist ftp functions called from Windows. Functions like map-over-ftp-directory called on Windows generates pathnames of the files in an ftp directory, but these generated pathnames use Windows syntax (with backward slashes delimiting directories). In order for these pathnames to be used in calls to other ftp functions, such as ftp-stream-file-mod-time, they must be first converted to Unix syntax. Users writing their own mapping functions for ftp directories may find this added feature of namestring useful. The ftp client module is described in ftp.html.

6.24 cl:open implementation

function, package: common-lisp

Arguments: file &key direction element-type if-exists if-does-not-exist class follow-symlinks external-format &allow-other-keys

The specification of this Common Lisp function allows a great deal of latitude to the implementation since interfacing with file systems is hard to specify generally. Here we discuss the if-exists, class, and (briefly) the if-does-not-exist keyword arguments. For a discussion of the external-format keyword argument, see Streams in iacl.html.

The if-exists argument is looked at only if the direction argument is specified as :io or :output. In that case the following values are allowed for if-exists and have the effect described.

The if-does-not-exist keyword argument also accepts the value :always-append when a file is opened for output. This value causes the file to be created and opened using O_APPEND. See the description of the :always-append value for if-exists described just above for details of the effect of specifying :always-append.

The class keyword argument

The open function has been further extended to take a class keyword argument. open passes this argument to make-instance when it creates the stream, and as with make-instance, the argument may be a stream class object or a symbol naming such a class. If the class argument is not supplied or is nil, open selects one of the following built-in classes according to the direction and element-type arguments:


These classes all contain file-stream and are variously mixed with


Although the file-stream subclasses returned by open are all instantiable, at present they require hidden initialization (for element-type upgrading, buffer allocation, etc.) and therefore they should only be created using open. It is fine to further specialize them, but you are required to create instances of your specializations of these stream classes using the :class keyword argument to open rather than by calling make-instance yourself.

Missspelled keyword arguments

open is also modified with &allow-other-keys and &rest to pass all keyword arguments as initialization arguments to make-instance. This has the unfortunate side effect of removing error checking for misspelled keyword arguments.

See streams.html, particularly the discussion of using open to create streams in Implementation of Common Lisp Functions for simple-streams.

The follow-symlinks keyword argument

When called with :direction :probe, open essentially works like probe-file and checks to see whether the file named by file exists and returns its truename if it does. The value of the follow-symlinks keyword argument, which is ignored unless direction is :probe, is passed as the value of that argument to truename in order to get the pathname to return. If file evaluates to a pathname that references a symbolic link, the symbolic link is returned if follow-symlinks is nil, the canonical name of the file if follow-symlinks is true, the default. See the description of the Allegro CL implementation of truename.

6.25 cl:pprint implementation

function, package: common-lisp

Arguments: object &optional output-stream

Provides the functionality as specified by pprint. In addition, pprint utilizes with-staged-output if output-stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.26 cl:pprint-fill implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p

Provides the functionality as specified by pprint-fill. In addition, pprint-fill utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.27 cl:pprint-linear implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p

Provides the functionality as specified by pprint-linear. In addition, pprint-linear utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.28 cl:pprint-tabular implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p tabsize

Provides the functionality as specified by pprint-tabular. In addition, pprint-tabular utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.29 cl:print-unreadable-object implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p tabsize

Provides the functionality as specified by print-unreadable-object. In addition, print-unreadable-object utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.30 cl:probe-file implementation

function, package: cl

Arguments: filespec &key (follow-symlinks t)

probe-file checks to see whether the file named by filespec exists and returns its truename if it does. The value of the follow-symlinks keyword argument is passed as the value of that argument to truename in order to get the pathname to return. If filespec evaluates to a pathname that references a symbolic link, the symbolic link is returned if follow-symlinks is nil, the canonical name of the file if follow-symlinks is true, the default. See the description of the Allegro CL implementation of truename.

6.31 cl:room implementation

function, package: common-lisp

Arguments: &optional x

room is implemented to provide information about memory management in Allegro CL. When x is unspecified or has the value :default, room displays the basic Lisp heap location, its addresses, and its totals, as well as the Aclmalloc heap location and number of root pages (see the (room) example). When x is nil, the bare minimum Lisp heap totals only (see the (room nil) example). When x is t, much more data is given, as seen by this example.

See Getting information on memory management using cl:room for more information on room.


Example of (room) or (room :default) output

Here is an example of room output with no argument or with a :default argument. See Getting information on memory management using cl:room for explanations of each part of the display.

cl-user(1): (room)
area area  address(bytes)        cons         other bytes
  #  type                   16 bytes each
                             (free:used)      (free:used)
     Top #x10089a0000
     New #x10088e0000(786432)   -----            -----
     New #x1008820000(786432)   14:1315      404800:264496
   1 CVO #x1002820000(100663296)    0:0       93009232:7604720
   0 Old #x1000020000(41943040)  471:182949  25415824:13557424
     OTot(Old Areas)           471:182949  118425056:21162144
Root pages: 122
  Lisp heap:    #x1000000000  pos: #x10089a0000 resrve: #x1008d20000
Aclmalloc heap: #x6000000000  pos: #x6000028000 resrve: #x6000100000


Example of (room nil) output

Here is an example of room output with an argument of nil. See Getting information on memory management using cl:room for explanations of each part of the display.

cl-user(1): (room nil)
              cons         other bytes
         16 bytes each
          (free:used)      (free:used)
     New     10:1319      404800:264496
     Old    471:182949  118425056:21162144

6.32 cl:sleep implementation

function, package: common-lisp

Arguments: seconds

sleep is modified when multiprocessing is enabled to cause only one process (i.e. lisp thread) to sleep rather than the whole Lisp process.

See process-sleep for more information.

6.33 cl:sort implementation

function, package: common-lisp

Arguments: sequence predicate &key key (strategy *simple-vector-sort-strategy*)

The Common Lisp sort function returns a sequence, possibly new, possibly the argument sequence modified, with all the elements of sequence ordered so that if element1 precedes element2 in the result sequence, then (predicate (apply key (list element1)) (apply key (list element2))) returns true.

When sequence is a vector

When sequence is a vector, the merge sort algorithm is used. This requires use of a temporary scratch vector of the same size as sequence. Where this scratch vector is created depends on the value of the strategy keyword argument, whose value defaults to the value of *simple-vector-sort-strategy*. If specified, strategy can have three allowable values: :stack, :alloc, and a vector. These values have the following effects:

It is not necessary to supply a value for the strategy keyword argument. The right thing will be done (that is the result will be a sorted vector of the same element type as sequence) regardless of its value or the value of *simple-vector-sort-strategy*.

When strategy is a vector, a simple vector is usually better

Unless the argument sequence is a specialized vector with element type a small byte like (unsigned-byte 8), a simple vector is usually a better choice for strategy if its value is a vector. Consider if sequence has element-type single-float. If strategy is also a single-float vector, then every time a value is extracted from the startegy vector, it must be boxed (converted to a single-float object). (Values extracted from sequence also must be boxed but that happens in any case.) Further, when strategy is a simple vector, references to strategy use the faster svref rather than aref. Finally the copy of values from sequence to strategy also involves boxing even when both are specialized arrays because the sort code cannot be compiled to handle such special cases when element types are not known in advance.

6.34 cl:time implementation

Allegro CL implements the time macro so that code in the body is compiled if necessary (and the compiler is present). The macro prints timing information and then the return valkue of the body:

cl-user(2): (defun foo (n)
          (let ((lis nil))
        (dotimes (i 100000)
          (push (* n i) lis))
cl-user(3): (compile 'foo)
;; This example run on an SMP Lisp so includes a 'cpu time (thread)'
;; line. That line will not appear in non-SMP Lisps.
cl-user(4): (time (foo 120034))
; cpu time (non-gc) 0.003999 sec user, 0.001000 sec system
; cpu time (gc)     0.032995 sec user, 0.001000 sec system
; cpu time (total)  0.036994 sec user, 0.002000 sec system
; cpu time (thread) 0.003999 sec user, 0.000000 sec system  ;; SMP Lisps only
; real time  0.038979 sec (100.0%)
; space allocation:
;  99,580 cons cells, 0 other bytes, 0 static bytes
; Page Faults: major: 0 (gc: 267), minor: 388 (gc: 267)
(12003279966 12003159932 12003039898 12002919864 12002799830 12002679796
 12002559762 12002439728 12002319694 12002199660 ...)

The information reported is:

The garbage collector when doing global gc's can use multiple cores even in a non-SMP Lisp. As a result, total CPU time may be more than clock time, as in the following actual example:

; cpu time (non-gc) 230.788942 sec (00:03:50.788942) user, 0.135657 sec system
; cpu time (gc)     189.803826 sec (00:03:09.803826) user, 0.173186 sec system
; cpu time (total)  420.592768 sec (00:07:00.592768) user, 0.308843 sec system
; real time  289.965705 sec (00:04:49.965705) (145.2%)

In this example, four cores were used for global gcs, with approximately 50-60 msecs used by each core. The real time was thus non-gc-time plus max-core-gc-time for a total of 290 msecs.

6.35 cl:truename implementation

function, package: cl

Arguments: pathname &key (follow-symlinks t)

As specified by section of the ANS, truename must follow symbolic links. Allegro CL adds the follow-symlinks keyword argument to control this behavior. truename follows symbolic links if the follow-symlinks keyword arguments is true (the default). It returns the symbolic link pathname if follow-symlinks is specified nil.

Note that when pathname evaluates to a pathname that references a symbolic link, (delete-file (truename pathname)) will delete the actual file while (delete-file (truename pathname :follow-symlinks nil)) will delete the symbolic link.

6.36 cl:write implementation

function, package: common-lisp

Arguments: object &key stream [lots of print variable values] alternate-package-name

The Common Lisp function write has keyword arguments for the various standard *print-[attribute]* variables, with the argument name being the attribute. So the keyword argument circle sets the value of *print-circle* during the write, and defaults to the current value of *print-circle*. The complete list of arguments is shown below.

Because Allegro CL has added a *print-alternate-package-name* printer variable, which specifies whether the package-name or its alternate name should be used in printing when a package is printed (usually as a package qualifier), an alternate-package-name keyword argument has been added to write. See also package-alternate-name.

cg-user(9): (package-alternate-name (find-package :foreign-functions))
cg-user(10): (write 'ff:def-foreign-call :alternate-package-name nil)
cg-user(11): (write 'ff:def-foreign-call :alternate-package-name t))

cl-user(239): (pprint (arglist 'write))

(excl::object &key stream ((:array *print-array*)) ((:base *print-base*))
 ((:case *print-case*)) ((:circle *print-circle*)) ((:escape *print-escape*))
 ((:gensym *print-gensym*)) ((:length *print-length*)) ((:level *print-level*))
 ((:lines *print-lines*)) ((:miser-width *print-miser-width*))
 ((:pprint-dispatch *print-pprint-dispatch*)) ((:pretty *print-pretty*))
 ((:radix *print-radix*)) ((:readably *print-readably*))
 ((:right-margin *print-right-margin*)) 
 ((alternate-package-name excl:*print-alternate-package-name*))

In addition, write utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.37 cl:write-line implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p tabsize

Provides the functionality as specified by write-line. In addition, write-line utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.38 cl:write-sequence implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p tabsize

Provides the functionality as specified by write-sequence. In addition, write-sequence utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.39 cl:write-string implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p tabsize

Provides the functionality as specified by write-string. In addition, write-string utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

6.40 cl:write-vector implementation

function, package: common-lisp

Arguments: stream object &optional colon-p at-sign-p tabsize

Provides the functionality as specified by write-vector. In addition, write-vector utilizes with-staged-output if stream is marked as synchronized output-stream-p.

See also Stream thread safety in smp.html for more information on the use of a single stream by multiple threads.

7.0 Miscellaneous implementation details

This section describes implementation details for Common Lisp concepts.

7.1 A comment about with-open-file and timing hazards

with-open-file tries to guarantee that the file stream opened for the evaluation of its body is closed, thus avoiding open but unused files. (Such open files can cause an error if the limit of allowable open files set by the operating system is reached.)

But note that there is a hazard between the time Lisp calls out to the operating system to open a file and the time Lisp sets the stream variable to the newly opened file stream. Between those events, an interruption that causes a non-local exit may leave the file open, but Lisp, lacking any handle on the newly opened stream object, cannot in fact close it.

The risk is small, but can be exacerbated by the following:

7.2 cl:delete, cl:delete-if, cl:delete-if-not, cl:delete-duplicates: multiprocessing issues

The functions delete, delete-if, delete-if, and delete-duplicates have traditionally tried to shorten simple-vectors in-place, so that copies need not be made when items are deleted from these vectors. But to truly be SMP-safe these functions must act more like their remove counterparts, due to the difficulty in synchonizing the use of the original object efficiently. However, this means that legacy code which assumed that the simple-vector was modified in-place might break.

Decisions can be made at various levels as to whether "in-place" modification will be done or whether copying will be done instead. This is controlled by the new variable *delete-in-place*, and also by a new in-place keyword argument to delete, delete-if, and delete-if-not. (That argument is not portable and so should be conditionalized in portable code.) delete-duplicates always follows the mandate of *delete-in-place* and has no new argument.

Defaults have been set so that non-SMP Lisps will still perform the in-place modification, and SMP Lisps will do the copying. Programmers should only specify in-place deleting on SMP if they can guarantee that the vector is not being (and can not be) traversed simultaneously on multiple threads. The problem can (for example) arise when one thread changes the last elements of a vector without noticing another thread has shortened the vector. The change can then modify (illegally) the header of an entirely different Lisp object with the result that the Lisp heap becomes corrupted.

Deleting elements from lists

The new arguments to certain deletion functions and the new *delete-in-place* variable do not affect the behavior of these functions on list arguments. However, (and this has always been true in a multiprocessing Lisp) although failures are less likely when deleting elements from a list compared to deleting elements from a vector (it is easy, as noted in bold above, to modfy an illegal location when a vector is shortened, but not when deleting elements from a list), there are ways that deletion or modification in one thread and accessing in another can cause unspecified (and unexpected) behavior with lists.

Use the returned sequence, not the argument sequence

Correct code should use the return value of the delete functions.

Problem can happen in non-SMP Lisps as well

It is much less likely, but the same problem described in this section can occur in a non-SMP Lisp. Good coding practice says do not use in-place modification in any multiporcessing Lisp (SMP or not) where multiple threads can traverse the sequence.

7.3 Reader macros and cl:*features*

#+(version>= ...)/#-(version>= ... )

Reader macros

We have extended the #+ and #- reader macros to accept (version>= N [ M]) as an argument. It is interpreted to mean that the form following will only be read if the version (also called release) of Allegro CL is greater than or equal to N.M. The N must be supplied. The M is optional. Both must be integers. With #+, version>= signifies read the next form only if the version is greater than or equal to N.M. With #-, it means read the next form only is the version is less than N.M. For example, because of an X3J13 change, the element type for an array of characters is character starting in release 4.1 and string-char in earlier releases. To have code work in all Allegro CL releases, do the following:

(make-array 3 
            :element-type #+(version>= 4 1) 'character
                          #-(version>= 4 1) 'string-char)

Warning: while most Common Lisp implementations (including Allegro CL prior to version 4.1) ignore (version>=...), it is possible that an implementation would signal an error upon encountering it. As a workaround for truly portable code, use:

#+(and allegro-version>= (version>=...))

Because :allegro-version>= is (presumably) only on the features list of Allegro CL 4.1 and later, this will fail in all versions without version>= having to have a definition.


Variable, package: common-lisp

This standard Common Lisp variable can be used with the #+ and #- reader macros to conditionalize code for different Lisp implementations and releases. The exact value is different in every version of Allegro CL. Here are some useful values which may or may not be in your version. Please check the value of *features* in your version to see exactly what is there. The function featurep can be used to test whether a feature is present or not.

7.3.1 Features present or missing from *features* in Allegro CL

This is a partial list features and their meaning:

7.3.2 The issue of nested conditionals in Allegro CL

Assume :allegro is on the *features* list and that :foo is not. Consider the following two forms and their evaluations:

;; CASE 1
(list #+allegro :allegro #-allegro #+foo :foo #-foo :default)

  Versions of Allegro CL prior to 8.0 return (:allegro :default)
  Allegro CL 8.0 and many other implementations return (:allegro)

;; CASE 2
(list #+allegro :allegro #-allegro #+foo :foo)

  Versions of Allegro CL prior to 8.0 return (:allegro)
  Allegro CL 8.0 and many other implementation signal an error

We will explain these disparate behaviors below, but first we recommend that conditions be nested using not, or, and and within the #+ or #- test expressions (the expression which follows the #+ or #-) as that is always unambiguous in any Lisp. Thus the first conditional below implements the old Allegro CL behavior and the second implement the current behavior:

(list #+allegro :allegro #+(and (not allegro) foo) :foo #-foo :default)
(list #+allegro :allegro #+(and (not allegro) foo) :foo
                         #-(or allegro foo) :default)

Using nesting within the test expression for Case 2 should make clear what is desired when :allegro holds and :foo does not -- presumably:

(list #+allegro :allegro #+(and (not allegro) foo) :foo)

In older Allegro CL implementations, when a conditional fails (like #-allegro fails), a conditional in the associated form (the one that will be ignored) is not further considered. Thus that conditional and its associated form are taken to be the form to be ignored. So in the first example, #-allegro #+foo :foo is considered to be a (failing) conditional and its associated form. It is ignored and the reader then encounters #-foo :default. The #-foo conditional succeeds so the subsequent form -- :default -- is evaluated.

Other Lisp implementations resolve the conditionals following a conditional as part of determining what the form associated with a conditional is. Allegro CL has been changed to match that behavior. As a result, conditionals following a conditional (i.e. nested conditionals) are considered and resolved as part of determining the form that follows a conditional, the form that should be ignored (when the original conditional fails) or evaluated (when it succeeds). So in #-allegro #+foo :foo #-foo :default the inner conditionals #+foo :foo #-foo :default are resolved to :default. Thus #-allegro #+foo :foo #-foo :default resolves to #-allegro :default which is then ignored.

In the second example, #-allegro #+foo :foo resolves to #-allegro which signals an error because no form follows the #-allegro conditional, and that is erroneous code. (The conditional doing the nesting within the test expression, show above, does not error.)

We believe (although we do not present our analysis here) that the ANSI standard is ambiguous on the handling of these cases and so both the older Allegro CL behavior and the newer behavior are within standard. However, since the #+/#- conditonals are designed to allow for using the same code in various implementations of Common Lisp, we believe it is most important that all implementation do the same thing. Since other implementations of Common Lisp resolve inner conditionals to produce the form that outer conditionals apply to, Allegro CL has been changed (starting in release 8.0) to do that as well. Again, we recommend doing the nesting in the test expressions rather than nesting #+/#-'s.

The change in the handling of nested #+/#-'s is a non-backward-compatible change in Allegro CL 8.0, and a rather obscure one which may cause difficult to diagnose errors in user code which has heretofore worked correctly. To mitigate this, in Allegro CL 8.0 (and later), a warning is signaled when nested conditionals are detected. This warning remarks on the behavior change. The warning is suppressed when the variable *warn-on-nested-reader-conditionals* is set to nil (its initial value is t). Users who want to revert to the old behavior (not resolving inner conditionals before applying outer) can do so by setting the variable *sharp-plus-de-facto-standard-compatible* to nil (its initial value is also t). We do recommend that user change their code to conform to the new behavior where that is possible rather than reverting to the old behavior.

7.4 cl:random and cl:make-random-state

Allegro CL uses the Mersenne-Twister algorithm, MT179937. MT179937 is described in detail in the paper "Mersenne Twister: A 623-dimensionally equidistributed uniform pseudorandom number generator" by Makoto Matsumoto (Keio University/Max-Planck-Institut fuer Mathematik) and Takuji Nishimura (Keio University), which appeared in the issue 1/1998 of the ACM Transactions on Modeling and Computer Simulation.

The required argument to random must be a positive real (integer or float). In the Allegro CL implementation, there are internal functions that are called based on the type of the required argument. If the compiler trusts declarations (see trust-declarations-switch) and the type of the required argument is known at compile time (because its type is declared or it is a constant), the call to random will be transformed into a call to the appropiate internal function in most cases. If the type is not known at compile time or declarations are not trusted, random is called directly and dispatches to the correct internal function after determining the type of the required argument.

Initial values returned by random

Because random may be called by any Lisp function at any time, there can be no guarantee that the sequence of numbers seen by your calls to random will be the same each time you invoke Lisp even if your actions are seemingly identical. However, absent specific action on your part, often the values returned by random are the same from invocation to invocation. If either repeatability or ensuring different runs are different are important to you, you should manage random by specifying the optional random-state argument with random-state objects you have created and stored (see make-random-state). Printed versions of random-state objects are readable so values can be stored in text files.

random and multiple processes

When a new process is created, the value of *random-state* may be bound as part of the initial bindings for the process (see make-process and process-run-function). *random-state* is one of the variables included in the suggested list of bindings which is the value of *required-top-level-bindings*, but that list is used for processes you create only if you specify that it be used. The binding is to a copy of an existing random-state object. This means that if that list is used, different processes may start with copies of the same random-state object or with random-states that produce similar (i.e. slighly displaced) random number sequences. This may or may not be what is required for your application. As we suggest with managing random numbers in general, we suggest that if the nature of random sequences is important to your application, you manage the random sequences for processes that you create by creating your own random-state objects (with make-random-state) and using them in the processes you create.


function, package: common-lisp

Arguments: number &optional state

random is a standard Common Lisp function which returns a pseudo-random number in an implementation-dependent way.

Returns a pseudo-random number uniformly distributed between 0 and (- number 1) if number is an integer and between 0 (inclusive) and number (exclusive) if number is real but not an integer. number must be real and positive. state should be a random-state object. If supplied, it will be made the state while the returned value is calculated.


function, package: common-lisp

Arguments: &optional state seed

make-random-state is a standard Common Lisp function that returns a random-state object. In Allegro CL, it is enhanced with an additional (now deprecated) optional argument, seed, and with more allowable values for the state argument.

Here are the allowable values for state (the first three are standard ANSI Common Lisp, the last two are Allegro CL extensions):

When state is :entropy, a value to create a seed will be taken from CryptGenRandom on Windows, and /dev/urandom for non-Windows based machines. /dev/random is not used by the system for any value but you can use it, collecting values read into an integer and passing the integer as the state argument. Allegro CL does not use /dev/random because it can block for an indeterminant amount of time.

The seed argument could be used in earlier releases to allow users to specify their own seed value. That role is now provided by allowing an integer value for state. Use of seed is deprecated. Using it will work but will also signal a warning. seed should only be specified when state is t.

7.5 Functionality for quickly writing and reading floats

Often you wish to write floating point numbers to a file which is read later, perhaps in the same Lisp invocation but more likely in a different one. The simple way to do this, writing the decimal representation of the floats, suffers from being very inefficient and somewhat inexact. (It is expensive to convert the internal binary representation to decimal for writing, and then the decimal representation back to binary when the values are read.)

Allegro CL provides functions that write and read the binary representation of floats (rather than the decimal representation), thus saving the time and loss of accuracy associated with conversion to and from decimal format. The functions are single-float-to-shorts, double-float-to-shorts, shorts-to-single-float, and shorts-to-double-float. Note that machine binary representations are used by most languages, and so, just as Allegro CL can read the files produced by writing the integers returned by single-float-to-shorts and convert them back to floating point numbers, programs easily written in other languages can do so as well.

7.6 cl:provide and cl:require

An additional version optional argument has been added to provide and require. It allows specifying a minimal version acceptable for loading a module.


function, package: common-lisp

Arguments: module-name &optional version

The non-standard version argument, if specified, should be a positive real number (a float or an integer). This value is checked when the module specified by module-name is loaded. If the require form also specified a version, it is compared (numerically) with the version in the provide form. If the require version is less than the provide version, a continuable error is signaled. If either form does not have a version specified, there will be no error.


function, package: common-lisp

Arguments: module-name &optional pathname min-version

The non-standard min-version argument, if specified, should be a positive real number (a float or an integer). This value is compared with the version specified in the provide form in the module. An error will be signaled if the provide form has a version less than the value of min-version. If the provide form has no version specified (or there is no provide), no error will be signaled.

7.7 cl:simple-condition-format-arguments and cl:simple-condition-format-control

The generic functions simple-condition-format-control and simple-condition-format-arguments take condition arguments and return the values of the respective slots. Allegro CL extends the condition system to define and sometimes bind the format-control and format-arguments slots in all conditions. That is, the slots always exist but are only sometimes bound. The slot names are internal in the excl package, and so are excl::format-control and excl::format-arguments.

Because the slots are not always bound (except for actual simple-conditions), code should check that there are bound before trying to access them, with tests like:

(slot-boundp instance 'excl::format-control)
(slot-boundp instance 'excl::format-arguments)

Because these slots need not exist in Lisps other than Allegro CL (again except for simple-conditions) code which tries to access them should be conditionalized for Allegro CL.

Many conditions in Allegro CL which are not simple conditions bind these slots (including, for example, undefined-function and and unbound-variable). We do not give a list, however, because it will likely go out of date. Users who wish to make use of the slot value should, again, test whether they are bound before accessing them.

7.8 What user-homedir-pathname does on Windows

user-homedir-pathname is a Common Lisp function that "determines the pathname that corresponds to the user's home directory on host." host is an optional argument.

In Allegro CL, the host argument is ignored in all cases. Allegro CL simply polls the Operating system in which it is running asking for the current user's home directory. On UNIX, this concept is well defined. On Windows, the notion of a home directory is more murky. Here is what Allegro CL does on Windows.

  1. If the environment variables HOMEDRIVE and HOMEPATH are set and the string made from concatenating them together names a valid pathname, it is returned.

  2. Otherwise, if there is a value for the HOME environment variable, and it names a valid pathname, that pathname is returned.

  3. Otherwise #P"C\" is returned.

If in step 1 or 2, an invalid pathname is constructed (typically a pathname naming a non-existent directory), a warning is signaled and the invalid pathname is not returned.

On Windows, you can change what user-homedir-pathname returns in a running Lisp by setting the values of the HOMEDRIVE and HOMEPATH environment variables to be strings which when concatenated name the desired existing directory, for example (here we leave HOMEDRIVE unchanged):

(setf (sys:getenv "HOMEPATH") "\\mydir\\")

Only the Lisp process see these new values. You are not setting them for all processes or permanently. See also username-to-home-directory.

7.9 The standard readtable is read-only, affect on with-standard-io-syntax and modifying the readtable in init files and with -e

The standard readtable, which is the initial value of *readtable* cannot be modified. You can, of course, copy it and modify the copy as desired.

The restriction on modifying the standard readtable can affect user code in the following cases:

Making changes to *readtable* in your file or with the -e argument

Suppose you want to make the { character into a reader macro which extracts a specified element from a list (this is the example in the ANS description of set-dispatch-macro-character), and you put the following form into your file or with the -e command-line argument (see Initialization and the and [.] files and Command line arguments in startup.html):

 (set-dispatch-macro-character #\# #\{        ;dispatch on #{
    #'(lambda(s c n)
        (let ((list (read s nil (values) t)))  ;list is object after #n{
          (when (consp list)                   ;return nth element of list
            (unless (and n (> 0 n (length list))) (setq n 0))
            (setq list (nth n list)))

Since the optional readtable argument is not specified, it defaults to *readtable*, but while the init file is being processed, the value of *readtable* is the initial readtable and that is read-only, so that form will signal an error. What you must do is create your own readtable and modify it:

(defvar *my-rt* (copy-readtable nil))
(setq *readtable* *my-rt*)

Now the form will work, modifying the reatable which is the value of *my-rt* and (for now) the value of *readtable*.

The same forms work when evaluated with the -e command-line argument (see Command line arguments in startup.html).

But when the listener starts up, it will bind *readtable* to a value specified in the association list which is the value of *cl-default-special-bindings* (see Setting global variables in initialization files in startup.html). The entry for *readtable* is initially (*readtable* copy-readtable nil), which means the listener will see a copy of the default readtable, not your modified one. (In earlier releases, you modified the default readtable so the changes were propagated.)

You modify *required-top-level-bindings* with a form like the following which you put below the forms above in your init file or as a later form passed with -e:

(setf (third (assoc '*readtable* *required-top-level-bindings*)) '*my-rt*)

7.10 Validity of value of end arguments to sequence functions not checked

If you specify an improper value as the value of the end keyword argument to sequence functions (or the end1 or end2 arguments), no checking is done and you may get an error or an incorrect result. In this example, you get an incorrect result (the third returned value should also be nil because the effective end of array is 0, but end2 is specified as 3 so that value is used):

cl-user(3): (let ((*print-array* t)
                   (array (make-array 0 :fill-pointer 0
                                        :adjustable t)))
               (vector-push-extend :a array)
               (vector-push-extend :b array)
               (vector-push-extend :c array)
               (vector-push-extend :d array)
               (setf (fill-pointer array) 0)
               (values array
                       (search '(:b :c) array)
                       (search '(:b :c) array :end2 3)))

This lack of checking is permitted by the ANS. Actually checking every case would burden legal code to protect against erroneous code. Users should check themselves if this is an issue.

7.11 Speed and pretty printing

While investigating ways to speed up Allegro CL, developers at Franz determined that pretty printing was a significant user of compute cycles, and that turning pretty printing off produced significant speedup of code that did output. This conclusion is not particularly suprising, of course. It takes work to produce pretty output. The question is, what to do about it. Turning off pretty printing sounds easier than it is.

Allegro CL starts with *print-pretty* set to t and further, the value in *cl-default-special-bindings* is (essentially) t as well. So simply setting *print-pretty* to nil will not work because the true value will tend to return unexpectedly (in new processes, for example).

Further, user code may depend on the initial value of *print-pretty* being t, so the initial value could not be changed.

However, we can make suggestions to users so that they can achieve the speedups when desired.

In a development image

There are three steps.

(1) change the value of *print-pretty* in *cl-default-special-bindings* to nil by evaluating

(setq *print-pretty* nil)
(tpl:setq-default *print-pretty* nil)

(See setq-default.)

(2) avoid using format strings that are pretty-printing by nature (such as ~< ... ~:>).

(3) Set the value of *pprint-gravity* to nil. Code in Allegro CL that used to bind *print-pretty* to t now bind it to *pprint-gravity*. That variable is not set on the *cl-default-special-bindings* list.

In custom images

There is a module pprint.fasl. When loaded into an image, it sets *print-pretty* and *pprint-gravity* to t. This module is loaded automatically when an image is built with a standard top-level. However, when an image is built with a minimal top-level (as described in Minimal top levels in building-images.html, the pprint module is not loaded.

So when building an image, you can include them for a development image (above), putting them in, say,, or you can build the image with a minimal top-level.

Note that the guts of pretty-printing are in the pprint module. Whenever a format statement, or a print statement with *print-pretty* set to t, is executed, the pprint module is required, so that the machinery is present to do the pretty-printing. So you have to be careful to avoid such cases. If you need pretty printing, you should use the strategy presented above rather than a minimal top-level strategy.

Starting in release 6.2, the new variable *print-circle-gravity* acts with respect to *print-circle* as *pprint-gravity* does with *print-pretty*: no Allegro CL code sets the value of *print-circle*. Instead, Allegro CL code binds it where necessary to the value of *print-circle-gravity*, and *print-circle-gravity* is only set in two places: initially to nil and in the :pprint module to t.

Further notes

Please check the Allegro CL FAQ from time to time to see if there is new information on this issue.

7.12 class-precedence-list: when is it available?

The class-precedence-list is calculated by mop:finalize-inheritance but it is not installed into the class until close to the end of finalization, as mop:class-precedece-list signals a program error when the class is not finalized. But the class-precedence-list is available much earlier and can be accessed with

(slot-value class 'mop:class-precedence-list)

after it is actually calculated (but before the operator mop:class-precedece-list can access it).

7.13 Floating-point infinities and NaNs, and floating-point underflow and overflow

The IEEE floating-point standard calls for infinities and NaNs (Not-a-Number) to be represented and used. So division of a non-zero finite float by zero produces an infinity, while a division of zero by zero produces a NaN.

the Common Lisp standard does not call for these special floats (as they are often called), but does allow for implementation of IEEE. Further, compiled code which dispatches directly to an IEEE floating-point processor may get back a special float result which it may just return, particularly if it is compiled at high speed and low safety. So consider the following from Allegro CL:

cl-user(115): (defun foo-err (sf)
                (declare (single-float sf))
                (declare (optimize (speed 1) (safety 1)))
                (/ 1.0 sf))
cl-user(116): (compile *)
cl-user(117): (foo-err 0.0)
Error: Attempt to divide 1.0 by zero.
  [condition type: division-by-zero]
[1] cl-user(118): :reset
cl-user(119): (defun foo-inf (sf)
                (declare (single-float sf))
                (declare (optimize (speed 3) (safety 1)))
                (/ 1.0 sf))
cl-user(120): (compile *)
cl-user(121): (foo-inf 0.0)

In the safe code, we get an error. In the fast code, we (silently) get infinity. This is not unexpected. Fast code is supposed to be fast and it achieves speed by discarding checks. The fp processor on the machine where this was run is an IEEE processor and it does not set the error flag for division by zero. Instead, it retuns the legal IEEE float infinity.

There are predicate function that return true when an object is a floating-point infinity or a NaN. exceptional-floating-point-number-p returns true when passed an floating-point infinity or NaN. nanp returns true when passed a NaN and infinityp returns true when passed an infinity.

Because underflows and overflows often signal an error, code like (expt most-positive-single-float 2) will now error rather than returning an infitity. Users who want infinite value and who do something like:

(defvar +single-positive-infinity+ (expt most-positive-single-float 2))

should instead do

(defvar +single-positive-infinity+ excl:*infinity-single*)

The six special-float constants are:

Arithmetic operations with special floats are legal. Generally, the result of an operation with a special float is a special float, the exception being dividing by infinity which produces zero. The following table shows the result of operations. Note we do not cover all cases nor most floating-point coercion cases. An operation with a double and a single results in a double.

Operation Arg1 Arg2 result
Any *nan-single* Any single *nan-single*
Any *nan-single* Any double *nan-double*
Any *nan-double* Any *nan-double*
+ *infinity-single* finite single-float *infinity-single*
+ *infinity-single* finite double-float *infinity-double*
+ *infinity-double* Any finite *infinity-double*
+ *infinity-single* *negative-infinity-single* *nan-single*
- Any finite single-float *infinity-single* *negative-infinity-single*
* *infinity-single* Any positive single-float *infinity-single*
* *infinity-single* Any negative single-float *negative-infinity-single*
* *infinity-single* 0.0S0 *nan-single*
/ *infinity-single* *infinity-single* *nan-single*
/ *infinity-single* any positive finite single-float *infinity-single*
/ any finite single-float *infinity-single* 0.0S0
/ 0.0S0 *infinity-single* 0.0S0
/ *infinity-single* 0.0S0 *infinity-single*
/ Any positive single-float 0.0S0 *infinity-single*
/ 0.0S0 0.0S0 *nan-single*

Handling underflow (and overflow) errors

Actually, we just discuss underflow errors. The same method will work with overflow errors. Before handling the error, you must decide what you want to do: for an underflow, do you want to return zero? Or (if sorking in single-floats), switch to doubles and try again? Or return least-positive-single-float (assuming positives)? See also the function read-tiny-float which handles the underflow error when reading a single float from a string, giving you the option of returning zero or the least-positive/least-negative float of the appropriate format.

Once you have decided that, code like the following will handle the error:

;; This read-from-string form will signal an underflow error:
cl-user(1): (read-from-string "4.9e-324")
Error: expt operation on (2.0 -723) resulted in floating point underflow.
   [condition type: floating-point-underflow]

;; This condition code will trap the error and return 0.0:

(catch 'trap-error
 (handler-bind ((floating-point-underflow
         #'(lambda (c)
             (declare (ignore c))
             (throw 'trap-error 0.0))))
   (read-from-string "4.9e-324")))

That condition code can be adapted to other underflows and also overflows (changing the condition type to floating-point-overflow).

7.14 The :nat and :unsigned-nat types

Because the natural word size on 32-but machine is 32-bits and on 64-bit machines is 64-bit, we have defined new types nat and unsigned-nat which is a 32-bit integer on 32-bit machines and a 64-bit integer on 64-bit machines. One can use nat and unsigned-nat in places where int and unsigned-int (and sometimes, long and unsigned-long) normally go. This allows sources for both 32-bit and 64-bit lisps to be the same.

Functions that accept nat and unsigned-nat as values for arguments include memref and memref-int and stack-cushion and set-stack-cushion. See also ftype.html (particularly The Syntax for Foreign Types) and lisp.h.

7.15 The #A reader macro

The #A reader macro is a standard Common Lisp reader macro, document here is the ANS. Allegro CL extends #A as we describe next. The usage template is:

  #{n}A{t}data      ;; The braces -- {} -- indicate the value is optional
                    ;; data must be a sequence.

The standard CL usage template is #nAdata. Allegro CL makes the n parameter optional (it defaults to 1). The new optional parameter t can be used to specify the element-type and can be one of u4, u8, u16, u32, u64, s8, s16, s32, s64, f32, f64, c32, c64, or fix. s indicates signed-byte, u unsigned-byte, where the integer represents the number of bits per byte. f represents float, c represents complex float, and the 32 and 64 represent the widths of each individual float component. Finally, fix represents fixnum. So, for example, #Au8(1 2 3 4) reads in an (unsigned-byte 8), 1-dimensional array (that is, a vector) with 4 elements, 1, 2, 3, and 4. Or #Af32(1.0 2.0) reads in a single-float vector with two elements 1.0 and 2.0. Finally, #2afix((1 2 3) (4 5 6)) results in a (simple-array fixmum (2 3)).

The reader will always read the new format correctly, but the printer will only use the new format when the variable *print-simple-array-specialized* is true. The new format is, of course, not portable to other Common Lisp implementations. When that variable is nil, the standard #nAdata format is printed.

When n is 0, it is assumed that t is not provided, so #0Au8123 produces a 0-dimensional array of type t (not type (unsigned-byte 8)).

7.16 Allegro CL print variables can follow the CL print variable value

Allegro CL has a number of printer variables which control the length and level of particular kinds of printing. Thus, tpl:*print-length* controls the print-length used when printing return values at the top-level. cl:*print-length* controls the print-length used by the various print functions. This example illustrates the difference. Recall print returns its argument after printing it:

(setq *print-length* nil)
(setq tpl:*print-length* 5)

cl-user(16): (print '(1 2 3 4 5 6 7 8 9 10))

(1 2 3 4 5 6 7 8 9 10) 
(1 2 3 4 5 ...)

The Allegro CL print variables can have the same values as the CL ones (an integer or nil). They may also have the value :follow. That value means use the value of the corresponding CL variable. So if we make :follow the value of tpl:*print-length*, we get this behavior:

cl-user(9): (setq *print-length* 5)
cl-user(10): (setq tpl:*print-length* :follow)
cl-user(11): (print '(1 2 3 4 5 6 7 8 9 10))

(1 2 3 4 5 ...) 
(1 2 3 4 5 ...)
cl-user(12): (setq *print-length* nil)
cl-user(13): (print '(1 2 3 4 5 6 7 8 9 10))

(1 2 3 4 5 6 7 8 9 10) 
(1 2 3 4 5 6 7 8 9 10)

This facility should be used with care, especially with the tracing print variables (*trace-print-level* etc.), since it eliminates the protection those variables provide by default when huge or circular objects are encountered during tracing, or contrarily, it may prevent trace from providing useful output. A traced function may be called in a dynamic environment where some surrounding code has for its own legitimate purposes bound some of these variables to extreme values.

The Allegro CL print variables include:

The last four are related to *print-long-string-length* which controls the printing or string with many (hundreds or thousands of) characters. Seeing the entire string, especially in contexts unrelated to its specific contents such as backtraces, is rarely useful. See the description of *print-long-string-length* for more information.

7.17 64 bit Allegro CL Implementations

Most platforms support 32-bit and 64-bit Lisps. (See preinstallation information in installation.html for a list of supported platforms and Lisps.)

For the most part there is no compatibility issue, especially when dealing with Lisp. The exceptions are:

Again, most pure-lisp behavior will be completely portable between 32-bit and 64-bit lisps, and that at most a user is likely only to see wider values while inspecting objects, or larger addresses in room displays.

For operations that must deal with specific sizes but do not use the def-foreign-type interface, the natural type (in Lisp) and the nat type (in C) provides a simple method to allow compatibility between 32-bit and 64-bit lisp code and foreign modules.

8.0 Allegro CL and the ANSI CL standard

Allegro CL is an implementation of Common Lisp as specified by the ANSI X3J13 committee. The standard of conformance has been accepted by ANSI as final. ANSI is the American National Standards Institute, and the X3J13 committee prepared the ANSI standard for Common Lisp.

Common Lisp was originally specified in Common Lisp: the Language, 1st edition (CLtL-1). That standard is now out of date. Common Lisp: the Language, 2nd edition (CLtL-2) describes an early version of the ANSI standard. It is still used but please understand that the final ANSI standard has diverged in a number of ways from CLtL-2, so CLtL-2 is no longer definitive.

8.1 Compatibility with pre-ANSI CLtL-1 in Allegro CL

Use of the cltl-1 module is deprecated.

Loading the cltl-1 module may affect ANSI compliance. Note compiler-let is available and exported from the excl package.

The several symbols removed from the language by X3J13 but preserved by Allegro CL for backward compatibility are exported from the cltl1 package. These generally retain their CLtL-1 definitions. We list the symbols exported from the cltl1 package at the end of this section. Note that the definitions are in the :cltl1 module.

Two symbols exported from the flavors package conflict with symbols now exported from the common-lisp package as part of CLOS: defmethod and make-instance. This means that no package can use the flavors package without shadowing these two symbols. See the code at the beginning of flavors.html.

The following symbols in the cltl1 package have been deleted from standard Common Lisp by X3J13. They (for the most part) maintain their CLtL-1 functionality. You may use the cltl1 package to get backward compatibility but we recommend that you write all new code so that you do not use these symbols and that you modify all existing code as soon as practical.

Note that special-form-p is in the cltl1 package. That symbol was previously in the common-lisp package but has been replaced in that package with the symbol special-operator-p.

These symbols were in the cltl1 package since release 4.0 and are still there:

8.2 Other package changes and compile-time-too behavior

X3J13 made a number of improvements to the package system in order to facilitate portability and to regularize the handling of top-level forms in a file. The function in-package was changed to a macro, and its various keyword arguments were deleted. The macro expansion of in-package is defined to have effect at compile, load, and eval times, but no longer creates a package if it does not exist, nor modifies any existing package. These functionalities are subsumed by the new defpackage macro, along with that of the several other package-manipulating functions. The package name argument to in-package is no longer evaluated. Execution of an in-package form referencing an unknown package or containing optional arguments signals a continuable error.

The variable *cltl1-in-package-compatibility-p* makes in-package work as it did in CLtL-1 Common Lisp. Users porting code from Allegro CL for Windows 3.0.x (which used CLtL-1 semantics in this regard) may find this variable useful. We do recommend modifying the code in the long run, however.

By compile-time-too behavior, we refer to the effect of certain top-level forms in a file being compiled. In CLtL-1, top-level forms which were calling the functions listed below were treated as if they were wrapped in an

(eval-when (compile)) 

form. That behavior has been changed in the new standard and you must wrap such forms in appropriate eval-when forms if they are to have effect while a file is being compiled. The affected functions are:


The variable *cltl1-compile-file-toplevel-compatibility-p* can be used to get CLtL-1 compile-time-too behavior when compiling files. Users porting code from Allegro CL for Windows 3.0.x (which used CLtL-1 semantics in this regard) may find this variable useful. We do recommend modifying the code in the long run, however.

8.3 The function data type

X3J13 tightened the definition of the function data type, primarily so generic functions could discriminate on functional arguments. It was necessary that the type represented by the function datatype and functionp predicate be disjoint from all other datatypes. Therefore, in Allegro CL since version 4.2 the only objects that are type function are those returned by the function special form, or by the compile function given a first argument of nil, or by coerce of a lambda expression to type function, or functions loaded from a compiled file. X3J13 specifies that the funcall and apply functions will continue to accept a symbol for the first argument, but a symbol is no longer functionp, nor are lists beginning with lambda, sometimes called lambda expressions. For backward compatibility the funcall and apply functions in Allegro CL will still accept a lambda expression, as is permitted by X3J13, but as required by X3J13 lambda expressions no longer satisfy functionp nor (typep function).

8.4 CLOS and MOP

Previous versions of Allegro CL have used Portable Common Loops (PCL) as a substitute for the Common Lisp Object System (CLOS) which was adopted by X3J13 as a standard part of Common Lisp. The last several versions of PCL worked in most ways the same as CLOS and provided most of the required features. (Some unavoidable divergences of PCL from CLOS derived from the dependence of CLOS on certain other incompatible language changes.)

Since CLOS replaces PCL completely, there has been no attempt to port any version of PCL to Allegro CL since prior to release 4.3. Doing such a port would be difficult, and would not benefit from the significant speed advantages of the native CLOS implementation in Allegro CL. User code that depends on various details of PCL (especially internals) may have temporary difficulties, but in any case such code will someday need to be brought into conformance with CLOS. In addition to full conformance with CLOS, of course, the other advantage of the native CLOS implementation is its greatly enhanced runtime performance.

CLOS is documented in chapter 28 of CLtL-2. MOP is documented in the book The Art of MetaObject Protocol.

It is possible to trace, disassemble, and compile CLOS methods by name. Here is an example of tracing.

USER(14): (defmethod my-function ((x integer)) (cons x :integer))
  #<clos:standard-method my-function ...>
  USER(15): (my-function 1)
  (1 . :integer)
  USER(16): (trace ((method my-function (integer))))
  ((method my-function (integer)))
  USER(17): (my-function 1)
  0: ((method my-function (integer)) 1)
  0: returned (1 . :integer)
  (1 . :integer)
  USER(18): (untrace (method my-function (integer)))
  ((method my-function (integer)))
  USER(19): (my-function 1)
  (1 . :integer)

Here is how to trace setf, :before, and :after methods (the names and argument types will likely be different in your case, of course):

(trace ((method (setf slot-1) (t baz))))
  (trace ((method foo :before (integer))))
  (trace ((method foo :after (integer))))

The extra set of parentheses is required to avoid confusion with specifying trace options (they are specified with a list whose car is the function to be traced and whose cdr is a possibly empty list of options). Note that the extra set of parentheses is not used with untrace:

(untrace (method (setf slot-1) (t baz)))
  (untrace (method foo :before (integer)))
  (untrace (method foo :after (integer)))

A generic function itself can be traced exactly like any other function.

8.5 CLOS and MOP conformance

We list known non-conformances with CLOS and MOP. The basic format is to list the object that is unimplemented or only partially implemented with a brief description of the non-conformance. Unqualified symbols are part of CLOS and are exported from the common-lisp package. Symbols qualified with clos: are part of MOP (they are exported from the clos package).

8.6 CLOS slot manipulators

Starting with the 11.0 release an incompatible change brings Allegro CL more fully into conformance with the Meta Object Protocol (MOP) with respect to the several slot manipulation functions and their underlying generic-functions. The change affects slot-makunbound and how it calls slot-makunbound-using-class.


"The Art of the Metaobject Protocol" (c 1991, Kiczales et al) (or AMOP) describes certain slot manipulator functions and their underlying MOP generic-functions:

slot-value -> slot-value-using-class
(setf slot-value) -> (setf slot-value-using-class)
slot-exists-p -> slot-exists-p-using-class
slot-boundp -> slot-boundp-using-class
slot-makunbound -> slot-makunbound-using-class

There are actually two versions of each of these sets of function/gf pairs that are described in the AMOP: the first is described in part I of the book and in Appendix D, where the closette implementation is described, and the second is in chapters 5 and 6 and in Appendix E, where differences are described. In particular, closette describes the last argument that each of the *-using-class generic-functions receive being the name of the slot, but in the real MOP specification, that last argument is an effective-slot-definition object.

The change:

Before Allegro CL 8.1, all of these functions called their respective gfs with slot-names as the last argument - as was specified by the closette implementation. In 8.1, an incompatible change was made to slot-value, (setf slot-value), and slot-boundp so that their gfs were called in a way that conforms to the MOP spec. But slot-makunbound was not converted. Starting in 11.0, however, slot-makunbound calls slot-makunbound-using-class with a third argument that is an effective-slot-definition instead of a name, which brings it into conformance with the MOP spec. Note that slot-exists-p-using-class is not specified in the real MOP, and so slot-exists-p still calls slot-exists-p-using-class with a symbol as its last argument.

This is an incompatible change, however, and if any program specializes any slot-makunbound-using-class methods on their own metaclasses, the applications will fail due to the unexpected change. We've provided several ways to work around this, starting with the least disruptive to user source code:

  1. Set the variable *conforming-slot-makunbound-style* to nil. This variable will revert the behavior of Allegro CL back to its non-conforming pre-11.0 behavior. Any specializations of slot-makunbound-using-class that Allegro CL or any of its sub-products makes have been modified according to #3, below, so the flipping of this switch will not affect any Allegro CL code. However, be advised that if there are any other applications which specialize on s-m-u-c that are or have been converted to only a conformant style, per #2 below, then setting this switch will cause that code to fail. This fix will not work if a user later sets *conforming-slot-makunbound-style* to nil, thus counteracting the effect of setting it true.

  2. Make the change to conformance. Note that this fix will not work if a user sets *conforming-slot-makunbound-style* to nil. There are two ways to do this:

  3. Make a change that allows for both conformant and nonconformant calls. This is the most general, and will thus be recommended if your code is a library to be used by other Allegro CL customers. Using the same code from acache code, above, the recommended solution is:

    (defmethod mop:slot-makunbound-using-class ((class persistent-class) instance slot)
      (declare (ignorable instance))
      (if* (symbolp slot)
         then ;; Old style: non-MOP-compliant:
              (let ((slot-name slot))
                (dolist (esd (mop:class-slots class) (call-next-method))
                  (if* (eq slot-name (mop:slot-definition-name esd))
                     then (if* (eq (excl::slotd-allocation esd) :persistent)
                             then (setf (mop:slot-value-using-class class
                                 (return instance)
                    else (return (call-next-method))))))
        else ;; Do it right, if we fix the bug in slot-makunbound:
             (if* (eq (excl::slotd-allocation slot) :persistent)
                then (setf (mop:slot-value-using-class class
                     (return instance)
                else (return (call-next-method)))))

Note; The variable *conforming-slot-makunbound-style* is considered temporary and may be removed in future versions of Allegro CL. For that reason, option 1 above should only be chosen short-term, and options 2 and/or 3 should be chosen as soon as is practical. Once a release of a version of Allegro CL is done which removes this variable (and thus the option to control it) only the conformant (option #2) and the conformant/compatible (option #3) options will work. At that time the recommended option to settle on will be #2.

8.7 CLOS optimization

Calls to make-instance where the class-name is a quoted constant and each of the keywords is a constant are transformed by the compiler into calls to constructor functions. A constructor function is a piece of code that is equivalent to the make-instance call except that it is significantly (10 to 100 times) faster.

The optimization is automatic when the call to make-instance is formed in a particular way. In order for an optimized constructor function to be used certain restrictions apply:

  1. The set of keywords must be valid for the call.
  2. The conditions for optimization are as follows:

The calls to make-instance are replaced by calls to the constructor regardless of whether an optimized constructor can be used. The first time the constructor function is called, the restrictions are tested and if they do not apply, an optimized constructor is generated. When the restrictions are not obeyed the constructor calls make-instance. Redefining a class or one of its superclasses or adding/removing a method to one of the generic functions mentioned above causes the constructor function to be recomputed.

9.0 Defclass optimizations: fixed-index slots and defclass embellishers

Object-oriented programming and specifically CLOS (the Common Lisp Object System) provide a powerful programming tool but it comes at a cost: the more complex the hierarchy of classes and the provision of methods, the longer it takes for the system to determine exactly what should be done in a specific method call. When a method is called, the class of every required argument must be determined and all methods associated with the class pattern of the arguments must be found.

If this was done from scratch for each method call, CLOS would run too slowly to be useful. Of course, that is not what is done. Effective method (EM) determination is done so that previously calculated methods are used when appropriate. See Appendix: Effective-method selection in Allegro CL CLOS and Associated Optimizations for a detailed discussion of effective method determination.

The more information that the compiler has on effective methods when emitting compiled code, the faster that code will be. Note that changes in classes can change the effective methods and so all relevant compiled code (or, to be safe, all compiled code) must be recompiled when such changes are made.

In the next two subsections, we describe two particularly effective optimizations: (1) fixed index slots (see Optimizing slot-value calls with fixed indices) and (2) defclass embellisher classes (see Metaclasses for embellishing class definitions).

As described in Optimizing slot-value calls with fixed indices, slot values for an instance of a class are stored in a vector. If the compiler knows what index in that vector holds a particular slot value, then getting the slot value is reduced to a simple vector access. Recent enhancements to Allegro CL allow programmers to specify the slot vector index for a slot.

Because class definitions are actually processed at run time rather than at compile time, it is complicated to add code to be executed when a class is actually created. Recent enhancements to Allegro CL allow programmers to specify an embellisher metaclass to a class definition and this metaclass will cause extra code to be executed when the class is created. This process is described in Metaclasses for embellishing class definitions. An embellisher metaclass for specifying fixed indices for class slots is provided and described, and tools for adding additional embellisher metaclasses are described.

9.1 Optimizing slot-value calls with fixed indices

The slot values of a class instance are stored in a vector associated with the instance. When asked for (slot-value instance slot-name), the system, at its least optimized, finds from the class definition the index in the vector used by slot-name and then accesses the value. Finding that index is what takes the time in the slot-value access. Once the index is known, the value comes from a simple vector access.

If one knew the index, slot-value would thus be very fast. The problem is that the index can change if classes are redefined, as other slots may be added and they must go somewhere in the value vector.

Allegro CL does permit specifying the index for a slot value. The class option excl:fixed-index when specified with an integer value in a defclass definition will ensure that a slot will always be at that location in the slot values vector for instances of the class. The system will then be able to access the value without having to first determine its location in the slot values vector.

There are a couple of problems with specifying fixed indices. One is that some indices may end up not being used. See the first example below. This is a problem only because it wastes space: every instance will have an unused vector element. The second problem is that indices may already be used by superclasses, as in the second example. This is clearly more serious and is detected only when an instance is created, not when the classes are defined. Let us look at some examples:


(defclass foo ()
  ((a :initarg :a fixed-index 2 :accessor foo-a)
   (b :initarg :b fixed-index 3 :accessor foo-b)
   (c :initarg :c :accessor foo-c)))
(defvar *foo-inst* (make-instance 'foo :a 1 :b 2 :c 3))

;; *VEC* is the slot values vector for *FOO-INST*.
;; The value of the 'A slot is index 2.
(defvar *vec* (excl:std-instance-slots *foo-inst*))

;; The slot value vector of *foo-inst* has four elements for three
;; values:
;;  0 - value if slot c
;;  1 - unused
;;  2 - value of slot a
;;  3 - value of slot b

;; Let's time some accesses. We define test functions in a file:

;; file
(in-package :user)

(defun p1 () (dotimes (i 10000000)
        (foo-a  *foo-inst*)))

(defun p2 () (dotimes (i 10000000)
        (slot-value *foo-inst* 'a)))

(defun p3 () (dotimes (i 10000000)
           (svref *vec* 2)))
;; end

;; We compile with speed 3:
Compiler optimize setting is
     (declaim (optimize (safety 1) (space 1) (speed 3) (debug 0)
               (compilation-speed 0)))
cl-user(39): :cl
;;; Compiling file
;;; Writing fasl file sv.fasl
;;; Fasl write complete
; Fast loading sv.fasl

;; And we run timings:
cl-user(41): (time (p1))  ;; USING THE FOO-A ACCESSOR:
; cpu time (non-gc) 0.127957 sec user, 0.000000 sec system
; cpu time (gc)     0.000000 sec user, 0.000000 sec system
; cpu time (total)  0.127957 sec user, 0.000000 sec system
; real time  0.127954 sec (100.0%)
; space allocation:
;  0 cons cells, 0 other bytes, 0 static bytes
; Page Faults: major: 0 (gc: 0), minor: 88 (gc: 0)
cl-user(42): (time (p2)) ;; USING SLOT-VALUE:
; cpu time (non-gc) 0.274489 sec user, 0.000000 sec system
; cpu time (gc)     0.000000 sec user, 0.000000 sec system
; cpu time (total)  0.274489 sec user, 0.000000 sec system
; real time  0.274485 sec (100.0%)
; space allocation:
;  0 cons cells, 0 other bytes, 0 static bytes
; Page Faults: major: 0 (gc: 0), minor: 0 (gc: 0)
cl-user(43): (time (p3)) ;; USING SVREF ON THE SLOT VALUE VECTOR:
; cpu time (non-gc) 0.005561 sec user, 0.000994 sec system
; cpu time (gc)     0.000000 sec user, 0.000000 sec system
; cpu time (total)  0.005561 sec user, 0.000994 sec system
; real time  0.006555 sec (100.0%)
; space allocation:
;  0 cons cells, 0 other bytes, 0 static bytes
; Page Faults: major: 0 (gc: 0), minor: 0 (gc: 0)

;; So accessing using the value vector is about 20 times faster
;; that using the FOO-A accessor and 40+ times faster than using
;; SLOT-VALUE. (While there will always be speedups, you may see
;; different values depending on other factors.)

;; EXAMPLE 2: using the same fixed index twice:

;; As above, we define the class FOO with two slots having fixed indices:

(defclass foo ()
  ((a :initarg :a fixed-index 2 :accessor foo-a)
   (b :initarg :b fixed-index 3 :accessor foo-b)
   (c :initarg :c :accessor foo-c)))

;; And we define a subclass of FOO which also uses fixed-index 2:
(defclass bar (foo)
  ((x :initarg :x fixed-index 2 :accessor bar-z)

;; No error is signaled when class BAR is defined. However,
;; when we try to create an instance of BAR:
cl-user(24): (setq *bi* (make-instance 'bar :x 5))
Error: one or more pairs of fixed slots conflict:
       ((#<aclmop:standard-effective-slot-definition a @ #x10001299a22> . 2)
        (#<aclmop:standard-effective-slot-definition x @ #x10001299bd2> . 2)
        (#<aclmop:standard-effective-slot-definition b @ #x10001299ab2> . 3))
  [condition type: program-error]

Here are some of utility operators associated with the fixed-index feature:

A complete listing of utility function is in Appendix: Operators associated with effective methods.

Here is how defclass has been modified to allow for specification of fixed indices for class slots:


macro, package: common-lisp

Arguments: name superclasses (slot-specifiers [excl:fixed-index non-negative-fixnum]) class-options

defclass is a standard Common Lisp defining macro (follow the link for the ANS definition). Allegro CL has added a slot option excl:fixed-index to slot-specifiers. When specified with a non-negative fixnum as a value, then the slot value of an instance of the defined class will be at that index of the slot-value vector.

All class instances have a vector of slot values. Unless the slot is specified to be fixed index slot, changes to the associated class hierarchy can change the index associated with a particular slot. But if the slot is specified excl:fixed-index n, its value will be at (svref slot-value-vector n) regardless of changes to classes in the class hierarchy.

No checking is done at class definition time to ensure that the specified index is actually available (that is that a superclass has not already reserved the specified index for a different slot). If that does happen, and error will be signaled when an instance of the class is created.

Specifying a fixed index location for slots is a useful tool which can provide for significant speedups when executing many slot-value accesses. However, for any complex class hierarchy, the programmer is burdened with a good deal of bookkeeping, keeping track of which indices are available and which have been used. Further it would be useful if calls to a slot accessor could be automatically transformed to (svref slot-value-vector index) for fixed-index slots. In the next section, Metaclasses for embellishing class definitions, we describe defclass embellishers which will do precisely what is desired: assign a fixed index to every slot and define compiler macros for accessor functions. Also described are tools for writing additional class embellishers.

Using a class embellisher to automatically specify fixed indices

See Metaclasses for embellishing class definitions for a complete description of class embellishers. In this brief note, we describe the use of the fixed-index-class metaclass. When specified in a defclass form, it adds code to be executed when the class is created that adds fixed indices to slots and defines compiler macros for accessors. Here is an example:

(defclass foo ()
  ((a :initarg :a :accessor foo-a)
   (b :initarg :b :accessor foo-b)
   (c :initarg :c :accessor foo-c))
  (:metaclass excl:fixed-index-class))

;; If we macroexpand that form, we get:

cl-user(56): (pprint (macroexpand '(defclass foo ()
  ((a :initarg :a :accessor foo-a)
   (b :initarg :b :accessor foo-b)
   (c :initarg :c :accessor foo-c))
  (:metaclass excl:fixed-index-class))))

(progn nil
       (eval-when (compile)
         (excl::check-lock-definitions-compile-time 'foo :type 'defclass
           (find-class 'foo nil)))
       (record-source-file 'foo :type :type)
         (excl::ensure-class-1 'foo :direct-superclasses 'nil :direct-slots
                               (list (list ':name 'a ':initargs '(:a) ':readers
                                           '(foo-a) ':writers '((setf foo-a))
                                           'fixed-index '0)
                                     (list ':name 'b ':initargs '(:b) ':readers
                                           '(foo-b) ':writers '((setf foo-b))
                                           'fixed-index '1)
                                     (list ':name 'c ':initargs '(:c) ':readers
                                           '(foo-c) ':writers '((setf foo-c))
                                           'fixed-index '2))
                               :metaclass 'fixed-index-class)
         (define-compiler-macro foo-a (excl::instance)
           (excl::bq-list `slot-value-using-class-name
                          (excl::bq-list `quote 'foo) excl::instance
                          (excl::bq-list `quote 'a)))
         (define-compiler-macro foo-b (excl::instance)
           (excl::bq-list `slot-value-using-class-name
                          (excl::bq-list `quote 'foo) excl::instance
                          (excl::bq-list `quote 'b)))
         (define-compiler-macro foo-c (excl::instance)
           (excl::bq-list `slot-value-using-class-name
                          (excl::bq-list `quote 'foo) excl::instance
                          (excl::bq-list `quote 'c)))
         (find-class 'foo))

As can be seen, each slot has been defined with a fixed index, and each accessor has an associated compiler macro which transforms it to a call to slot-value-using-class-name. (Interpreting compiler macros is not easy, but that is what these are doing.)

See Metaclasses for embellishing class definitions, the fixed-index-class, and fixed-index-filling-class documentation for more information on this embellisher class.

9.2 Metaclasses for embellishing class definitions

The defclass macro expands to a set of forms enclosed in a progn form. These forms define the class at any of the eval-when times (compile-time, load-time, or eval-time). The basic forms tend to include the definition for the class, slot definitions, and other embellishments like package-lock checking and source-file recording forms.

A typical defclass form will expand like this (we use the excl:fixed-index slot option described in Optimizing slot-value calls with fixed indices above):

cl-user(3): (pprint (macroexpand '(defclass foo () 
                                     ((a :initarg :a :reader foo-a
                                       excl:fixed-index 1)))))

(progn nil
       (eval-when (compile)
         (excl::check-lock-definitions-compile-time 'foo :type 'defclass
           (find-class 'foo nil)))
       (record-source-file 'foo :type :type)
       (excl::ensure-class-1 'foo :direct-superclasses 'nil :direct-slots
                             (list (list ':name 'a ':initargs '(:a) ':readers
                                         '(foo-a) 'fixed-index '1))))

Included is a call to excl::ensure-class-1, an internal function which, among other things, calls mop:ensure-class.

The actions taken to define a class happen at run-time, when ensure-class is actually called (and not at macroexpand time). So there is no easy way to embellish the defclass macroexpansion with extra forms that might give the compiler clues as to how to build the class and its instances quickly and with efficient operation. In particular, such extra forms might depend on class and slot characteristics that won't be known until ensure-class has done its work.

Here are a couple of examples of what might be desired:

  1. Use of the fixed-index option (see Optimizing slot-value calls with fixed indices) in a defclass definition, as we do in the example above, can provide significant speedups of slot accesses, but it is required to use slot-value-using-class-name forms rather than calls to accessors (like foo-a, in the example above). It is possible to write a compiler-macro for foo-a which will automatically compile into a slot-value-using-class-name call, but it would be nice if those compiler-macros were written automatically.

  2. Slot definitions have a type option, but it doesn't tie to the accessors in the defclass definition, and so is not automatic. It would be nice if a series of (declaim (ftype (function () <type>))...) forms were automatically added for compilation after the call to excl::ensure-class-1, so that compilation of accessors would automatically get the type information inferred from the :type options in the defclass form.

A final wish on such a mechanism is that it be terse, only requiring a one (or few) line change to get all of this extra information to the compiler.

WARNING: Because changes to macroexpansions affect how the compiler compiles the expanded forms (if the compiler is involved), there is a danger of binding types, indexes, and offsets earlier than is usually the case in CLOS/MOP mechanisms. This could lead to unexpected and incorrect operations if changes are made to classes, methods, or their relationships with each other after compilation of code representing such accesses. The embellishment mechanism described below is intended only to be used when no new uses will be added in later. Failure to heed this warning may result in otherwise-correct code resulting in incorrect behavior.

A mechanism for automatically embellishing the form which defclass expands to is available in Allegro CL. It is called a defclass-expansion-embellisher or just a defclass-embellisher.

It consists of a hierarchy of metaclasses which originate from the defclass-embellisher-class, and these can be combined and used as the :metaclass option in a defclass form and which, when used, will cause one or more of the above desired embellishments to be added to the macroexpanded defclass form.

So given a defclass form like this:

(defclass foo ()
  ((a :initarg :a :accessor foo-a)
   (b :initarg :b :accessor foo-b)
   (c :initarg :c :accessor foo-c))
  (:metaclass <embellisher-class>))

the intention is to cause the macroexpanded form to look like this:

   <package lock checks>
   (excl::ensure-class-1 ...)
   <extra embellishments>

What the embellishments are will be determined by what class is used as the metaclass. This documentation describes three such metaclasses, one of which will be provided, and the other two are documented but require programming on the part of the user.

The provided metaclasses are fixed-index-class and fixed-index-filling-class, which are class metabjects that automatically embellishes the expanded form in two ways:

  1. All slots will be notated with the fixed-index option, if they aren't already. (The difference between the metaclasses related to what indices are used.) The index values will be incremented automatically, unless overridden by already used indices or explicit excl:fixed-index specifications.

  2. All accessors will have extra forms generated that define compiler-macros, which will use slot-value-using-class-name to ensure that the slot access can compile down to a simple vector access.

So for example, a class defined this way:

(defclass foo ()
  ((a :initarg :a :accessor foo-a)
   (b :initarg :b :accessor foo-b)
   (c :initarg :c :accessor foo-c))
  (:metaclass excl:fixed-index-class))

will in fact behave as if it had looked like this

 (defclass foo ()
   ((a :initarg :a excl:fixed-index 0 :accessor foo-a)
    (b :initarg :b excl:fixed-index 1 :accessor foo-b)
    (c :initarg :c excl:fixed-index 2 :accessor foo-c)))
  (define-compiler-macro foo-a (x)
     `(excl:slot-value-using-class-name 'foo ,x 'a))
  (define-compiler-macro (setf foo-a) (v x)
     `(setf (excl:slot-value-using-class-name 'foo ,x 'a) ,v)
  ;; and additional compiler macro forms deleted here to save space

Warning about compiler macros for the find-index-[filling-]class embellishers

The compiler macros defined for slot accessors when the fixed-index-class or fixed-index-filling-class metaclasses are specified cause, under the right compiler settings, forms like (foo-a <foo-inst>) to be transformed to (svref slot-vector slot-fixed-index). This can very significantly speed up slot accesses but part of the speedup results from discarding the usual method processes: if the compiler macro kicks in, :around, :before, and :after methods on the slot accessors are ignored. See the descriptions of fixed-index-class and fixed-index-filling-class for examples and further discussion (including a way to get full generic function semantics combined with much of the speed improvement).

Additional embellisher classes

Here are two additional classes which are not defined in Allegro CL but for which code is provided which can be adapted if desired by the user:

  1. typed-slot-class: A class metaobject which can cause embellishment of the expanded defclass form by adding (declaim (ftype (function () <slot-type>) ...)) forms after the call to excl::defclass-1.

  2. combo-class: A class metaobject that demonstrates that these defclass-embelishers can be combined.

Example implementation:

(defclass typed-slot-class (defclass-embellisher-class) ())

(defmethod mop:ensure-class-using-class ((class typed-slot-class) name
                       &rest args
                       &key environment given-env-only
  ;; this is very similar to the one for excl::clos-class
  (declare (ignore name))
  (multiple-value-bind (meta initargs)
      (excl::ensure-class-values class args environment given-env-only)
    (if* (not (eq (class-of class) meta))
       then (error "can't change the metaclass of ~s" class))
    (apply #'reinitialize-instance class initargs)

(defclass typed-slot-direct-slot-definition (mop:standard-direct-slot-definition)

(defclass typed-slot-effective-slot-definition (mop:standard-effective-slot-definition)

(defmethod mop:direct-slot-definition-class ((class typed-slot-class) &rest initargs)
  (declare (ignore initargs))
  (load-time-value (find-class 'typed-slot-direct-slot-definition)))

(defmethod mop:effective-slot-definition-class ((class typed-slot-class) &rest initargs)
  (declare (ignore initargs))
  (load-time-value (find-class 'typed-slot-effective-slot-definition)))

(defmethod normalize-direct-slots ((metaclass typed-slot-class) direct-supers name direct-slots)
  (let ((dslots (do-typed-direct-slots direct-supers name direct-slots)))
    (call-next-method metaclass direct-supers name dslots)))

(defun do-typed-direct-slots (direct-supers name direct-slots)
  (declare (ignore name direct-supers))
  (let (accessors 
    (loop for dslot in direct-slots
    as new-slot = (copy-list dslot)
    do (setq slot-type nil
         accessors nil)
       (loop for pair on (cdr dslot) by #'cddr
           as key = (car pair)
           as value = (cadr pair)
           do (if* (eq key :type)
             then (setq slot-type value)
           elseif (or (eq key :reader) (eq key :accessor))
             then (push value accessors)))
       (when (and slot-type accessors)
          `(declaim (ftype (function () ,slot-type) ,@accessors))))
    collect new-slot)))

(mop:finalize-inheritance (find-class 'typed-slot-class))

;; Here is a transcript of code using the defined embellisher class:

cl-user(66): (defclass bar ()
               ((a :initarg :a :reader bar-a :type fixnum)
                (b :initarg :b :reader bar-b :type list)
                (c :initarg :c :reader bar-c :type symbol))
               (:metaclass typed-slot-class))

cl-user(67): (make-instance 'bar :a 10 :b (list 10 20) :c 'hi)

cl-user(68): (describe 'bar-a)
bar-a is a new symbol.
  It is unbound.
  It is internal in the common-lisp-user package.
  Its function binding is #<standard-generic-function bar-a>
    which function takes arguments (bar)
  Its property list has these indicator/value pairs:
system::.ftype.             (function nil
  Its source-file location is: top-level
cl-user(69): (describe 'bar-b)
bar-b is a new symbol.
  It is unbound.
  It is internal in the common-lisp-user package.
  Its function binding is #<standard-generic-function bar-b>
    which function takes arguments (bar)
  Its property list has these indicator/value pairs:
system::.ftype.             (function nil list)
  Its source-file location is: top-level
cl-user(70): (describe 'bar-c)
bar-c is a new symbol.
  It is unbound.
  It is internal in the common-lisp-user package.
  Its function binding is #<standard-generic-function bar-c>
    which function takes arguments (bar)
  Its property list has these indicator/value pairs:
system::.ftype.             (function nil symbol)
  Its source-file location is: top-level

;; Each of the reader gfs is described with an ftype peoperty
;; that is consistent with the type option in the class definition.


(defclass combo-class (fixed-index-class typed-slot-class) ())

(mop:finalize-inheritance (find-class 'combo-class))

;; Run this code to see the COMBO-CLASS effect

(defclass bas ()
  ((a :initarg :a :accessor bas-a :type fixnum)
   (b :initarg :b :accessor bas-b :type list)
   (c :initarg :c :accessor bas-c :type symbol))
  (:metaclass combo-class))

(setq bas-inst (make-instance 'bas :a 10 :b (list 10 20) :c 'hi))

 (defun grab-bas-slots (x)
   (declare (optimize speed))
   (list (bas-a x) (bas-b x) (bas-c x))))

(grab-bas-slots bas-inst)

;; Look at the disassembly and descriptions to see
;; that both embellisher classes have been applied.
(disassemble 'grab-bas-slots)

(describe 'bas-a)
(describe 'bas-b)
(describe 'bas-c)


Here are the embellisher classes and associated operators:

10.0 Function specs (fspecs)

Function specs name and possibly locate functions not named or located by symbols. Generally, a function spec is used and presented in the same way as a symbol, although there are sometimes restrictions. Some such functions like (setf foo) can be located based on the name, via (fdefinition '(setf foo)) or #'(setf foo), while others are intended only to identify the function in a meaningful way, as in (:top-level-form "" 235). The extent of the action that can be performed on a function spec depends on its function spec handler, see def-function-spec-handler.

A function spec (fspec) is a list that denotes a place to store a function. Function specs are useful for functions that don't otherwise have obvious names. ANSI CL defines Function Names as either symbols or the lists formed by (setf [symbol]) [to denote a writer function to pair with the reader function named by [symbol] which may or may not itself be defined]. Allegro CL extends the Function Names concept by defining function specs, and allows the user to create new kinds of function specs. Some pre-defined function spec names in Allegro CL are :discriminator, :effective-method, method, flet, labels, :internal, :top-level-form, etc.

Function specs are normally kept in an internal form, which allows many of the cons cells in various fspecs to be shared. They are converted to the normal external format usually only when printing, or at other times when parsing the internal form is too complex. Handlers of fspecs must be aware of these internal formats and may use the following functions to access their components: fspec-first, fspec-second, fspec-third.

Each of these functions will work on either an internal or external fspec, and will for an external fspec return the first, second, or third element, respectively (i.e. just like first, second, and third). If the fspec is in internal form, the proper corresponding element is still returned, but without the overhead of first converting to an external fspec.

Users can define their own function specs with def-function-spec-handler. The function function-name-p returns true when passed a valid function spec defined with def-function-spec-handler. fboundp can be used to determine whether a valid function spec defined with def-function-spec-handler actually names an operator.

10.1 Supported function specs

The following function specs are supported in Allegro CL. Note that some might only be recognized as function-specs when their appropriate modules are loaded. In all descriptions, the Available line describes when the function-spec is valid. The Elements line names the fspec-first element of the fspec, as well as fspec-second and, if applicable, fspec-third and elements beyond three.


Available: When compiler is present

Elements: (:anonymous-lambda index)

Names a compiled function that has not been given any other name. The index is an integer that is incremented for every new anonymous function created. This function-spec is not intended for defining or manipulation; it is recognized as a function name, but if for example the evaluation of (compile nil (lambda (x) (1- x))) returns #<Function (:ANONYMOUS-LAMBDA 2) @ #x406eae5a> then (fdefinition '(:ANONYMOUS-LAMBDA 2)) will still result in an undefined function error. These names are not stored and not retrievable. They are also not recommended - instead, when compiling an anonymous lambda, it is easier to work with when given a name:

CL-USER(10): (compile 'foobar (lambda (x) (1- x)))
CL-USER(11): #'foobar
#<Function FOOBAR>


Available: always

Elements: (:discriminator type)

Names any of the various discriminator functions, which memoize and select effective methods for particular calls. The type is a list whose car is the name of the kind of discriminator, and whose rest will have other parameterizations based on the discriminator kind. The various kinds of discriminators are listed below with their parameterizations and a short description. The parameterizations themselves are:

Here are the actual discriminators:

(:one-index-reader class-slot-p)
(:one-index-writer class-slot-p)

For slot accessors, if the index is the same for every call (even though the specialized classes might be different) then the index, which is remembered by the discriminator, can simply be used to grab the slot value out of the instance based on the single index encountered. If the discriminator ever encounters a specialization whose slot is at a different index, the discriminator is replaced with the checking version.

(:one-class-reader class-slot-p)
(:one-class-writer class-slot-p)

In this case, the accessor generic function has only been called with one class of argument. There is no cache vector, the wrapper of the one class, and the slot index are stored directly as closure variables of the discriminating function. This case can convert to either of the next kind.

(:two-class-reader class-slot-p)
(:two-class-writer class-slot-p)

Like above, but two classes. This is common enough to do specially. There is no cache vector. The two classes are stored as separate closure variables, as is the (common) slot index.

(:three-class-reader class-slot-p)
(:three-class-writer class-slot-p)
(:four-class-reader class-slot-p)
(:four-class-writer class-slot-p)

New: These are both like two-class accessors, except with three or four closure variables, respectively, to store the encountered classes.


This is the most general case. In this case, the accessor generic function has seen more than one class of argument and more than one slot index. A cache vector stores the wrappers and corresponding slot indexes.

(:few-class-reader class-slot-p)
(:few-class-writer class-slot-p)

This discriminator is obsolete and will never be seen. It was replaced with the one-through-four-class readers, described above.

(:checking metatypes applyp)

This discrimator's specializers can be numerous, as long as they all result in the same method being called. That method is stored once, and all sets of specializers, one set per unique call, and if the specialiers match, the stored method is used. If a differnt method is required, the discriminator is replaced with the equivalent caching version.

(:caching metatypes applyp)

This discriminator's specializers determine what method is to be called; if the set matches a set stored into the cache, the method becomes immediately available. If the specializers haven't been seen yet, the method is calculated and stored into the cache beore being called.


Available: always

Elements: (:effective-method num-req restp kwdcheck aroundp next-method-p)

Names a simple effective method function object, which is a closure over the applicable methods for a particular call to a generic function.

Note that kwdcheck implies restp, and aroundp implies next-method-p so the valid combinations are

    restp   kwdcheck    aroundp     next-method-p
    -----   --------    -------     -------------
    nil     nil     nil     nil
    nil     nil     nil     t
    nil     nil     t       t
    t       nil     nil     nil
    t       nil     nil     t
    t       nil     t       t
    t       t       nil     nil
    t       t       nil     t
    t       t       t       t


Available: always

Elements: (:internal outer-name index)

Provides a way of numbering compiled internal functions which serve the functions they are lodged within. The index is numbered in the order these functions are encountered within the compiler, so there is a loose top-to-bottom organization (e.g. #'(internal foo 0) is likely to textually preceed #'(:internal foo 1), etc). The outer-name is the name of the function which houses the internal function. It can itself be any fspec, including an :internal fspec, so :interal fspecs can be nested. :internal fspecs can't be used in defun, but can be found by fdefinition, as long as the outer-name can also be found by fdefinition.


Available: always

Elements: (:property symbol indicator)

Creates a convenient way to define functions which reside on property lists. The function object becomes the value of the indicator property on the symbol's plist. Example: typing

(defun (:property foo bar) (x)
  (list x))

at the toplevel will result in the symbol 'foo having a 'bar property which is a function (and that functions name is '(:property foo bar)).


Available: always

Elements: (:top-level-form filename file-character-position)

Provides naming for random forms at the top level in a fasl file which refers back to its original source file. The file element is a string specifying the filename, and the position is the character count of the opening parenthesis of the form. The file-character-position is determined assuming the first character in the file has character position 0. See file-character-position.


  1. Since progn forms are compiled as if separate forms, but they are all in fact part of the same form, there can be many function objects with the same :top-level-form fspec. Also, since a defmacro can expand into a progn form, a macro form might also result in numerous funtions with the same :top-level-form fspec, as well as other more standard fspecs such as those created by defun forms embedded in the macroexpansion.

  2. The position of :top-level-form fspecs must be adjusted on Windows, or in any situation where "CRLF" processing or any other ligature processing or stream encapsulation would cause the number of actual characters the lisp sees to be altered from the number of characters actually in the file. Usually, if the editor doesn't support counting the CR/LF line endings as two characters, the position in a Windows file can be "fixed" or at least mitigated by adding the current line number to the position. If that results in more lines being traversed, those lines should be added as well.


Available: When the Runtime Alalyzer is loaded (see runtime-analyzer.html), and when a profile exists that has been created with the :interpret-closures option to with-profiling or start-profiler.

Elements: (prof:closure index)

When :interpret-closures is true during the creation of a profile, this function spec allows the user to see closure calls that otherwise are unrelated as separate hits in the profiling run. The flat-profile and the call-graph will both show closures as (prof:closure index) rather than by trying to interpret the function name of the template, which is sometimes not helpful. See Closures in runtime analysis output in runtime-analyzer.html.


Elements: (compiler-macro function-spec)

Allows easy access to compiler macros as fspecs, rather than using the compiler-macro-function accessor. Function-spec is the same as the quoted function-spec required as an argument to compiler-macro-function.


(define-compiler-macro (setf foo) (x y) (setf x y))

will define a compiler-macro for (setf foo), and subsequently evaluating any of #'(compiler-macro (setf foo)) or (fdefinition '(compiler-macro (setf foo))) or (compiler-macro-function '(setf foo)) will yield the compiler-macro-function just created.


Available: always

Elements: (flet outer-name inner-name)

Provides a way to directly name compiled internal flet functions, which are of course always lodged within other functions. The inner-name is the function-name element of the flet form, which serves lexically within the outer funcion as the identifier. The outer-name is the name of the function which houses the flet function. It can itself be any fspec. This fspec can't be used in defun, but can be used in fdefinition (as long as the flet and its outer-name function are compiled).


Available: always

Elements: (labels outer-name inner-name)

Like flet, only a labels function is described.


Available: always

Elements: (method name [qualifiers] specializers)

Allows a CLOS method to be named directly. Name is the name of the generic-function of the method. There can be multiple qualifiers which determine and are determined by the method combination type. Specializers is a list that corresponds to the names of class specializatoins for each argument in the method; if the argument is specialized, the specializer is the name of the class; if the argument is not specialized, the specializer is T. The method fspec cannot be used in defun, but can be used in fdefinition.


Available: always

Elements: (setf reader)

This is the only standard fspec. It defines the name of the setf version of the (possibly non-existent) reader function. This fspec can be used in defun or fdefinition.

11.0 Some low-level functionality

11.1 Windows: GetWinMainArgs2

The C function GetWinMainArgs2 in the Allegro CL dll (which has a name like acly7xxx.dll, where the y, if present, is a letter and the x's are digits) is used by the IDE to retrieve Windows handle information known by the ACL runtime system. This information may also be useful for applications written in Lisp. The information returned by this function should be used carefully and non-destructively as the ACL runtime system (i.e. the low-level routines in Allegro CL, unrelated to Allegro Runtime) depends on these handles to exist and behave in predictable ways.

In order to use this function you must use the foreign function interface to create a Lisp function to call the C function:

(ff:def-foreign-call (GetWinMainArgs2 "GetWinMainArgs2") 
         ((vec (:array :int)) (count :int)) :returning :void) 

Next create a vector of five raw integers for the function to fill in:

(setq myvec (make-array 5 :element-type '(unsigned-byte 32))) 

Now call the function with the vector followed by the number of elements in the vector

(GetWinMainArgs2 myvec (length myvec)) 

The vector now contains the following information

index value 0: The Windows instance handle of the lisp process

index value 1: The previous Windows instance handle (which is always zero).

index value 2: unused

index value 3: The Windows handle of the console window (if there is one).

*index value 4: The Windows handle of the splash window. Normally the splash window is gone by the time the application starts up, but the +B command-line argument to mlisp.exe can cause the splash window to stay up longer. If this value is non-zero then the application is permitted to call the Windows function DestroyWindow() on it to make the splash window disappear. If this value is zero then the splash window is already gone.

12.0 Conformance with the ANSI specification

The following list describes the (mostly minor or obscure) known non-conformances of Allegro CL with the ANSI spec.

  1. file-length is documented to signal an error in safe code when the stream is not a stream associated with a file. Allegro CL extends file-length via simple-streams (see streams.html)to include a value for string streams, which is the length of the string buffer - the same is true for buffer streams, where the file-length is the length (in octets) of the buffer. We feel this is preferrable because file-length and file-position are closely associated. If a stream can have a file-position, it can likely also have a length. The description of file-position was more careful to account for unanticipated situations at the time of the writing of the spec, and so we believe that the file-length description is too harsh.

  2. file-write-date returns nil when given a non-existent file as an argument (rather than signaling an error), which is arguably a non-conformance. See the discussion at cl:file-write-date. There is no plan to change this behavior.

  3. declared-fixnums-remain-fixnums-switch, if true, allows code to be generated by the compiler which will produce the incorrect value when certain fixnums (those whose sum is a bignum) are added. Allowing incorrect values from fixnum addition under any circumstances is out of compliance with the ANSI specification. This compiler switch may be set to nil (thus removing any non-compliance) by evaluating (setf comp:declared-fixnums-remain-fixnums-switch nil).

  4. coerce of a sequence, as well as concatenate, map, make-sequence, and merge should signal an error if the new type specifier specifies a different length. Allegro CL presently ignores any length specifier on the new type, never signaling an error even in safe code.

  5. eval and compile are not permitted to copy and/or collapse "like" constants. The compile-file/load cycle is permitted to copy and/or collapse constants. Presently, constants in a file compilation are only collapsed within a single function. Constants are never copied in the evaluator, but Allegro CL's compile violates the no collapsing requirement.

  6. A lexical function or macro definition should prevent setf from using a global setf method. Allegro CL still uses the global definition. (Prior to the introduction of setf functions there was no way a correct program could demonstrate the problem.)

  7. multiple-value-setq of a variable with a symbol-macrolet definition operates on the expansion. Allegro CL's interpreter handles this correctly, but the compiler does not.

  8. Non-local exits from the cleanup forms of an unwind-protect that is in the process of unwinding another non-local exit. When a non-local exit (throw, return-from, or go) is being performed, cleanup forms of an intervening unwind-protect form may not transfer to any exit point between itself and the original target exit. Unfortunately, the ANSI spec leaves ambiguous certain details about transferring to exit points outside the original target. Allegro CL currently allows a non-local exit to be usurped by a cleanup form executing another transfer to an intervening exit point. Depending on the ANSI ambiguity, this is either a nonconformance or an extension upon which portable user code should not depend.

  9. The value of macroexpand-hook is coerced to type function before being called, and therefore may be a symbol, function, or lambda expression. Allegro CL has always permitted these but macros and symbol macros expanded directly by the compiler (and not indirectly by other macros) don't go through macroexpand-1 and consequently don't invoke *macroexpand-hook*. While the specification is somewhat ambiguous, this should probably be considered a bug.

  10. with-accessors, and with-slots should allow declarations, but in Allegro CL, do not.

  11. The scoping distinctions between pervasive and nonpervasive declarations has been removed. The scope of a declaration always contains the form body along with any "stepper" or "result" forms, but not in general "initialization" forms, e.g. in let binding clauses. If the declaration concerns a binding established by the form, then the declaration applies to the entire scope of the binding. The declaration may therefore include initialization forms in subsequent binding clauses of, for example, let*. Allegro CL does not yet implement this change, retaining CLtL-1 semantics. If made, it will be an incompatible change, although it is unlikely to affect significant amounts of user code.

  12. The floating point contagion rules for comparison operations. When float and rational numbers are to be compared, the float is converted to rational as if by the function rational, and then an exact comparison is performed. Allegro CL was changed in release 7.0 to do this. In earlier releases, the rational was coreced to be a float.

  13. Branch cuts for various mathematical functions to specify behavior at floating minus zero. It is difficult on most Allegro CL ports to generate minus zero, although the quantity can certainly be constructed by a foreign function. In any case, Allegro CL's branch cut behavior does not conform around minus zero.

  14. On an echo-stream, a character should be echoed only the first time it is returned by read-char, and never by peek-char. When a character is returned to the stream with unread-char, it is not unechoed, nor will it be reechoed when it is reread. Allegro CL's new stream implementation does not yet conform. An echo stream always transmits these function calls naively to its component streams.

  15. Allegro CL does not yet support the fourth positional argument to the ~D format directive and a fifth positional argument to the ~R format directive to specify the comma interval.

  16. The ~E format directive always prints a sign for the exponent portion of the number, whereas prin1 prints an exponent sign only if the exponent is negative. In Allegro CL prin1 always prints an exponent sign.

  17. Allegro CL ignores the :newest version specifier of a pathname and always forces it to be nil.

  18. In releases before 7.0, Allegro CL did not implement pathname case, and the :case keyword argument to the make-pathname, pathname-host, pathname-device, pathname-directory, pathname-name, and pathname-type was accepted but ignored. Now :case is no longer ignored. See *pathname-customary-case*.

  19. The definition of compile should permit a compiled function as the optional second argument but does not in Allegro CL.

  20. In Allegro CL, deftype forms are implicitly evaluated at compile time, making the result of the type definition available at compile time, changing the compiling lisp image inappropriately.

  21. Allegro CL does not support documentation for setf functions.

  22. Though calls to the step macro may be compiled in Allegro CL, compiled code will not correctly expand in the correct lexical environment.

  23. For macro-like defining forms such as defmacro, macrolet, define-setf-method, deftype, and define-compiler-macro, the lambda-list default initializer code should run outside the implicit named block established around the definition body but Allegro CL evaluates initializers inside the block.

  24. Allegro CL uses slot name variables as lambda list variables even though an automatically-generated defstruct keyword constructor function should not use and bind as its lambda list variables slot name symbols.

  25. An &environment variable that appears in a macro lambda list should be bound first before any of the other variables (making the binding visible to macroexpansions occurring in initialization forms elsewhere in the lambda list) but Allegro CL binds all variables in normal left-to-right order.

  26. With regard to lambda list congruence between the :arguments keyword argument to define-method-combination and the actual lambda list of a generic function to which the method combination applies, Allegro CL essentially appends an ignored &rest argument to the lambda list, preventing errors from being signaled, but not correctly handling optional/key argument bindings and default value forms.

  27. Allegro CL prints slot names in the keyword package though this behavior is deprecated.

  28. Allegro CL does not permit the type indicator to be omitted for declarations of the form (declare (type foo x)) for any symbol naming a type (Allegro CL does permit it for many predefined types). Thus (declare (foo x)) is not valid in Allegro CL.

  29. Allegro CL does not handle nested dribble's correctly, presently forgeting about the previous dribble, without even closing it.

  30. In Allegro CL, calling a &key function with :allow-other-keys nil signals an error, and it should not.

  31. In Allegro CL, call-next-method with changed arguments does not check that the set of applicable methods has not changed, though it should in safe (safety=3) code.

  32. In Allegro CL compiler macros are not invoked using macroexpand-hook and instead are called using funcall directly. Further, in Allegro CL compiler macros cannot be invoked on forms such as (funcall #'name ...). define-compiler-macro should be responsible for transparently accommodating the argument destructuring for this case but does not in Allegro CL.

  33. With regard to the documentation function, there should be a documentation type compiler-macro; and documentation for a defstruct should be able to be accessed as a class object, just as for defclass, but does not in Allegro CL.

  34. In symbol-macrolet, a type declaration on a symbol-macrolet variable should be equivalent to wrapping every reference to the variable in an appropriate the clause. The expansion should not literally include the the form, but the effect of the type declaration may be effectuated by the compiler by some other means. Allegro CL does not conform, and the expansion of a symbol-macrolet variable with a type declaration includes a the form.

  35. A symbol-macrolet definition of a variable may not shadow a global special declaration of that variable name, or for keyword symbols. symbol-macrolet should signal an error. In Allegro CL, it does not.

  36. A the form should return exactly the values returned by evaluation of its second subform. It should not be an error if more values are returned than the first subform specifies, and if fewer values are returned than the first subform specifies, the missing values are treated as nil for purposes of type checking. In Allegro CL in interpreted code, an error is signalled unless a the form agrees exactly with both the number and types of the returned values. However, in compiled code, it does not check values returned through a the form (although the type declaration may be used for code optimization) and therefore complies.

  37. Allegro signals error (and it should not) if a compiled function is given to compile. Allegro will correctly compile interpreted functions defined in a non-null lexical environment, and will additionally correctly handle references to closed-over variables. However, it improperly issues a warning when it does so.

  38. (SETF (APPLY #'FOO ...) V) should expand to the approximate equivalent (APPLY #'(SETF FOO) V ...) except that the order of evaluation of the original subforms shall be preserved. However, Allegro CL expands setf of apply according to the CLTL-1 specification.

  39. Allegro CL retains certain CLtL2 specifications of the documentation function.

  40. Allegro CL accepts nil as the second argument to do and do* (so (progn (setq i 0) (do nil nil (when (> (incf i) 10) (return)) (print i))) is accepted even though the second nil is illegal, because the form of do/do* requires an open paren, followed by a non-optional end-test-form (which can be nil, followed optionally by any number of result forms. The fact that () can't have a non-optional test-form makes it illegal.

13.0 Appendix: Effective-method selection in Allegro CL CLOS and Associated Optimizations

Object-oriented programming and specifically CLOS (the Common Lisp Object System) provide a powerful programming tool but it comes at a cost: the more complex the hierarchy of classes and the provision of methods, the longer it takes for the system to determine exactly what should be done in a specific method call. When a method is called, the class of every required argument must be determined and all methods associated with the class pattern of the arguments must be found.

If this was done from scratch for each method call, CLOS would run too slowly to be useful. Of course, that is not what is done. The first subsection below, Appendix: Effective-method selection background briefly describes how the effective method (the one actually called) is determined. The second subsection, CLOS Effective-method selection: Fixed Slot Accesses, talk about a particular optimization for slot accessors available in compiled code when you know the code will not be modified without recompilation. (Modifying the code without recompilation can lead to errors or incorrect results.)

13.1 Appendix: Effective-method selection background

When a generic function ("gf") is invoked, the actual code which is executed (i.e. which methods and in what order) depends on the classes of all of its required arguments. Code which is calculated based on this set of aguments is called the "effective-method". An effective-method ("em") may simply be a single method (if no others are applicable), or in a standard-method-combination it may be a function which invokes optional around methods, optional before methods, a primary method, and optional after methods. The process of calculating what em to funcall in service to the gf invocation is called discrimination. A discriminator is a function which aids in the calculation of what em to use in a gf invocation.

Optimization of effective method calculations

Effective-method (EM) calculation is expensive - it takes significant time to analyze the arguments and figure out which methods are applicable (and how to combine those that are applicable). The most general em calculation is to perform this work every time the generic-function is called. This calculation is avoided whenever possible, because it is extremely slow.

The next most general style of effective-method calculation is to cache the results of previous calculations. If a set of argument classes has been seen in a previous call, and the effective-method has already been calculated, then it is possible to reuse that em and simply call it.

Discriminators in CLOS

A discriminator is the glue that connects a gf and its methods. A discriminator is the piece of code that checks the classes of the generic-function call's actual arguments and selects the correct action to take. The actions taken might be based on applicable methods for the call (combinations of primary, :before, :after, and :around methods) or they might be shortcuts taken by the discriminators themselves. Thus a call to a generic-function invokes a combination of a discriminator and zero or more methods. From the MOP point of view, a discriminator is in fact a funcallable-instance-function, or fin-fun, which is installed in the generic-function as its implementation glue.

The theoretically simplest discriminator would, each time it was invoked, collect or create the set of applicable methods, select the most specific one, and call it. This would be easy to write but would be very slow (several orders of magnitude slower than a normal function call). Instead, the Lisp has generated a number of different kinds of discriminators and selects the most specific one appropriate to the call being made. In some cases the discriminator can do the work of the method (e.g in the case where the action of the method is a slot access or set).

A discriminator doesn't actually create new effective-methods, but instead uses em's which have already been created by mop:compute-effective-method. If the discriminator cannot find the correct em for the arguments, it traps out and allows mop:compute-effective-method to create a new em, which is then cached and used. During the process of calculating the em, the discriminator might be found to be inadequate for the now more-general cache of em's, and it may be swapped out for a more general discriminator.

Wrappers and Classes of Arguments

In parts of this discussion and in the actual implementation, the class of each argument is described by the class's wrapper. The class wrapper aids in the update process - whenever a class is changed, its identity is not changed but any references to it must be invalidated. Classes are usually fairly stable, and are identifiable by address, but because classes might be invalidated for any number of reasons (e.g. change-class, updates to class hierarchies, etc.) and because the mechanism for updating classes is a lazy mechanism (waiting until a class is used rather than updating the whole system for each change), discrimination is always performed on each class's wrapper rather than on the class itself. Wrappers are somewhat temporary objects which represent the class, but which also contain extra information vital to the class. This includes a list of slot names for the class, and also a wrapper-number: a random number generated when the wrapper is created which serves as both a validation flag (0 means invalid) and also as a hash code to perform fast lookups in caches. Each wrapper points to its class, and although there may be more than one wrapper that points to a particular class, that class only points back to the currently-valid wrapper, and all others will have a 0 in their wrapper-numbers and will need to be updated when accessed. For this discussion, the term wrapper and class might be used interchangably, but when wrapper is used it means that a check is performed when the wrapper/class is accessed and an update is performed if necessary.

Because what is saved in a cache is not the class object itself but its wrapper, and the wrapper contains a quick way to determine that the class has been changed (i.e. the wrapper is "invalidated") the changing of a class can occur without having to sweep the entire Lisp heap, lazily updating references to the class when it (i.e. its wrapper) is referenced. The indicator for validity of a wrapper also serves a second purpose - when the wrapper was created and is thus initially valid, the wrapper number assigned to this wrapper is a random number which allows quick hashing of the wrapper and which allows lookups in caches to be efficient, rather than just a straight-through search like looking through a list. Note that a wrapper never goes from invalid to valid - a new wrapper is always created and stored in the class object. Thus, the class always holds the current wrapper and all others that reference the class are invalid.


The most general caching discriminator simply saves the class (wrapper) of each discriminated argument and also the calculated effective-method, all grouped together to form a single cache entry. If the same set of classes of arguments is encountered at a later time, the caching descriminator can simply perform the match and funcall the em it found in the cache. If the arguments' set of class wrappers do not match any set in the cache, then a new em is calculated and a new cache entry is stored before the new em is funcalled.

A bit more specific caching mechanism is the checking discriminator. This discriminator has captured the single method to call in a closed-over variable, and as long as the wrapper of the argument is of the specified class or a subclass of it, that method will be called. If the argument set does not match that of the method (or subclasses of the arguments) then the discriminator is replaced with the most general caching discriminator.

Other caching discriminators are described in more detail below.

13.2 Appendix: Generic Function Discriminators

The kinds of discriminating functions are:

In detail:

13.3 CLOS Effective-method selection: Fixed Slot Accesses

See Optimizing slot-value calls with fixed indices for information on assigning fixed indices to slot value in an instance of a class. In this section, we give the full techincal background to that optimization.

A huge number of CLOS method calls are slot-value accesses [2]. A slot access may be in the form of a call to slot-value, which is the most general form, or the slot can be defined with gfs to use as an accessor (either reader, writer or both).

A standard instance has two major components: a slot for its class's wrapper, and a slot for its slots vector (also labelled "instance" by the inspector). The slots vector holds all of the (instance) effective-slots, but the order of those slots is only described by the class-slots list in the class-wrapper - it is a list of names of slots in the order in which they appear. If for example the class-slots list consists of (name type plist) then the instance will have a slots vector of length 3 and the name value will be stored at index 0 of the slots vector, the type will be stored at index 1, and the plist will be stored at index 2, as illustrated in Figure 1:

 instance object:    |                                              |
  ------------        \        wrapper                              |
 | wrapper    | ---------->  --------------                         |
 +------------+             |  ...         |                        |
 | instance   | -----       | hash/0       |                        |
 +------------+      |      | slots layout | --> '(name type plist) |
 | internal   |      |      | class-slots  |                        |
  ------------       |      |  class       | ---                    |
                     |      |  ...         |    |                   |
                     |       --------------     |     class         |
                     |                           --> --------       |
                     |         slots                | ...     |     |
                      ---->  --------------         | wrapper | ----
                            |   name       |        | ...     |
                            +--------------+         ---------
                            |   type       |
                            |   plist      |

    Figure 1: Example of an instance with three slots

The slot-value function is the general reader for a slot. Simplistically, it operates by accepting the instance and the slot-name as arguments, and it looks in the class-slots list of the instance's class-wrapper for the name. If it finds the name, then it notes at what index the name was seen and accesses that element in the slots vector. Slot-unbound and slot-missing protocols are not discussed here.

If a slot is defined with either :reader or :accessor options, then the name associated with that option causes a generic-function to be created (with automatic setf functions to be created as appropriate). The most simplistic operation of the reader gf is to call slot-value on the instance with a constant name. But since that name is in fact a constant and an intermediate goal of the gf call is to get the index of the slot so that the slot vector can be accessed, the majority of optimizations for slot-accesses are to try to predict or control the index of each slot. Once the index is known, the slot can be accessed trivially, without having to call slot-value, via an svref on the slots vector using the known index.

Caching slot indices for n-n-accessors: The most general slot-accessor is the n-n-accessor [4] which contains no assumptions about any of the slot accesses it has encountered; each cache line holds the wrapper of the instance argument and the index of the slot, which may be different for the same gf name described in two different classes - the gf will be the same, but will access the slot of the instances differently based on the index stored for each instance's class.

More specific accessors: Often, classes have their slots laid out in a pattern, which serendipitously causes all of the slots of the same name to reside at the same index in the slots vector, even perhaps in instances of unrelated classes. As long as instances are accessed with the same index, there are several more-specific discriminators which can handle these cached values:

Holding slot indexes to fixed locations: To keep the discriminator used by an accessor gf as specific as possible (and thus as fast as possible), with :one-index being the most specific (i.e. fastest) and :n-n or :caching being the most general (and thus the slowest), it is desirable to make constant the index locations for the same slot in various classes. As mentioned before, this might be done serendipitously, or pains can be taken to force the layout to include slots at the same location, but this might be hard to maintain because the odering might be brittle, especially when class mixins are added, which might change the order of slot assignments.

Instead, Allegro CL provides the excl:fixed-index slot option for all classes (see Optimizing slot-value calls with fixed indices). The value of the option is the zero-based index that will become the position of the slot in the slots vector. As a simple example:

(defclass foo ()
  ((a :initarg :a fixed-index 2 :accessor foo-a)
   (b :initarg :b fixed-index 3 :accessor foo-b)
   (c :initarg :c :accessor foo-c)))

will create a class which has two fixed-index slots and one floating slot. And

(defvar *foo-inst* (make-instance 'foo :a 1 :b 2 :c 3))

will create and save an instance into *foo-inst* that has three accessible slots. But note that the slot named 'a is stored at index 2 in the slots vector (because of the fixed-index specification) and the slot named 'b is stored at index 3. What about index 0 and 1? Well, there is only one slot left to fill these two slots: the slot named 'c [5]. So the slot named 'c is given the index 0 and the location at index 1 is unassigned. Normally this can't be seen - inspecting *foo-inst* normally will show just the slots that are defined. But if the :raw option to :istep is used and the slots vector is shown, it will eveal that there are actually 4 slots, with values 3, nil, 1, and 2, respectively.

Conflicts in fixed-index assignments: If two classes define slots of different names but with the same fixed-index, they cannot be both used as superclasses of another class. This is because the fixed-index specification is paramount, and placing two different slots at the same index is prohibited. An attempt to do so will result in a program-error when an make-instance is called on such a class. Care should be taken to not assign fixed-index values that might conflict with each other.

13.4 Appendix: Operators associated with effective methods

13.5 Appendix: Effective method determination and optimization: examples

Consider the following source file:

;; ------------- file start -----------------

(defclass foo ()
  ((a :initarg :a fixed-index 2 :accessor foo-a)
   (b :initarg :b fixed-index 3 :accessor foo-b)
   (c :initarg :c :accessor foo-c)))

(defclass bar (foo)
  ((d :initarg :d :accessor bar-d)))

(defclass bas (foo)
  ((e :initarg :e :accessor bas-e)))

(defclass baz (foo)
  ((f :initarg :f fixed-index 0 :accessor baz-f)))

(defclass bam (foo)
  ((g :initarg :g :accessor bam-g)))

(defclass foo1 ()
  ((a :initarg :a :accessor foo-a)
   (b :initarg :b :accessor foo-b)
   (c :initarg :c :accessor foo-c)))

(defclass not-foo ()
  ((a :initarg :a fixed-index 2 :accessor foo-a)))

(defclass xxx (foo)
  ((a excl::fixed-index 2)))

(defclass yyy (foo)
  ((x excl::fixed-index 3)))

(defvar *foo-inst* (make-instance 'foo :a 1 :b 2 :c 3))
(defvar *bar-inst* (make-instance 'bar :a 10 :b 20 :c 30 :d 40))
(defvar *bas-inst* (make-instance 'bas :a 100 :b 200 :c 300 :e 500))
(defvar *baz-inst* (make-instance 'baz :a 1000 :b 2000 :c 3000 :f 6000))
(defvar *bam-inst* (make-instance 'bam :a 10000 :b 20000 :c 30000 :g 70000))
(defvar *foo1-inst* (make-instance 'foo1 :a -1 :b -2 :c -3))
(defvar *not-foo-inst* (make-instance 'not-foo :a 'oops))

;----------------------- file end ------------------

In it there are a number of classes defined, mostly as subclasses of the base class named foo. The ``fooclass defines three slots, one of which is nameda and is accessed by #'foo-a. Note that most of the other classes inherit from foo, except for foo1, which does not, and which happens also to have a slot named a accessed by foo-a. But foo's a slot is fixed at index 2, and foo1's a slot is not (and it is likely, without any other mixins, that this slot will be placed at index 0 in the slots vector for a foo1 object). Also note that two extra classes are defined, xxx and yyy which are both subclasses of foo; xxx defines a slot named a and accessed by foo-a, and which also has the fixed-index of 2 which foo's a slot has. But yyy defines a slot named x that has a fixed index of 3, which clashes with foo's b slot which also has a fixed-index of 3.

Finally, there are a number of instances created, for demonstration purposes. Most of these instances are subclasses of 'foo, with the exception of *foo1-inst* which is not.

After compiling and loading fixed.fasl, note that an instance can easily be made of 'xxx, because its index locations are compatible with its superclass:

cl-user(4): (make-instance 'xxx :a 'hi :b 'bye :c nil)
#<xxx @ #x10003d24242>
cl-user(5): :i *
A new xxx @ #x10003d24242 = #<xxx @ #x10003d24242>
   0 Class --------> #<standard-class xxx>
   1 b ------------> symbol bye
   2 c ------------> symbol nil
   3 a ------------> symbol hi

Not so with yyy: an instance cannot be made because of fixed-index clashes:

cl-user(6): (make-instance 'yyy)
Error: one or more pairs of fixed slots conflict:
       ((#<aclmop:standard-effective-slot-definition a @ #x10003d2f942>
         . 2)
        (#<aclmop:standard-effective-slot-definition b @ #x10003d2f9d2>
         . 3)
        (#<aclmop:standard-effective-slot-definition x @ #x10003d2faf2>
         . 3))
  [condition type: program-error]

Restart actions (select using :continue):
 0: Return to Top Level (an "abort" restart).
 1: Abort entirely from this (lisp) process.
[1] cl-user(7): :res

Now, let's concentrate on looking at the caches of one accessor: #'foo-a. We'll use the gf-discriminator-cache function. Without the optional argument, nil is returned, but with the print argument the fact that foo-a has never been called is seen:

cl-user(8): (gf-discriminator-cache #'foo-a)
cl-user(9): (gf-discriminator-cache #'foo-a t)
never called

So let's call foo-a:

cl-user(10): (foo-a *foo-inst*)
cl-user(11): (gf-discriminator-cache #'foo-a t)
common index: 2
first call:    class foo
cl-user(12): (foo-a *foo-inst*)
cl-user(13): (gf-discriminator-cache #'foo-a t)
common index: 2
first call:    class foo
cl-user(14): (gf-discriminator-cache #'foo-a)
(#<standard-class foo>)

Note some things above:

  1. The discriminator is a one-class-reader.

  2. The first call to this generic function is an argument of class foo, and the discriminator is yielding a value of 2

  1. A second call to foo-a with the same argument doesn't change the cache situation.

  2. Technically, the "cache" that is returned is not really a cache, but is instead just a closed-over variable that is remembered from call to call.

  3. The value of the discriminator is really an index 2, which can then be used to grab the value at that index in the slots-vector.

Next, we call foo-a on an instance of a bar, which is a subclass of foo:

cl-user(15): (foo-a *bar-inst*)
cl-user(16): (gf-discriminator-cache #'foo-a t)
common index: 2
first call:    class foo
second call:   class bar
cl-user(17): (gf-discriminator-cache #'foo-a)
(#<standard-class foo> #<standard-class bar>)
cl-user(18): (gf-discriminator-value #'foo-a)

Note that the call with a bar has now caused a new discriminator to be stored into the generic-function. If foo-a were only called with foo instances and bar instances, the discriminator would remain as a two-class-reader.

The reference to the common index makes sense now, since accesses using two classes have resulted in slot accesses using the same index. Note however, that calling gf-discriminator-cache is returning a representation of the cache that does not include the value - instead the discriminator's value (i.e. that common index) can be seen using the gf-discriminator-value function.

If we add another class to the mix (this one is yet another subclass of foo) we see the progression to a three-class-reader:

cl-user(19): (foo-a *bas-inst*)
cl-user(20): (gf-discriminator-cache #'foo-a t)
common index: 2
first call:    class foo
second call:   class bar
third call:    class bas

and if we call with an instance of a class that is not a subclass of foo, but which has a slot accessed via foo-a at index 2, we see the same progression to four-class-reader (the class of the argument need not be of the foo hierarchy - the index being the same is what keeps it on track for one-value reader discriminators being installed):

cl-user(21): (foo-a *not-foo-inst*)
cl-user(22): (gf-discriminator-cache #'foo-a t)
common index: 2
first call:    class foo
second call:   class bar
third call:    class bas
fourth call:   class not-foo

What then happens if we call with a fifth class? Up to this point, the "cache" has really been just a closed-over set of variables that hold up to 4 wrappers for comparison. But after 4, it is inefficient to keep a linear series of classes to test, so we move to a cache-based discriminator:

cl-user(23): (foo-a *baz-inst*)
cl-user(24): (gf-discriminator-cache #'foo-a t)
1 discriminated argument
empty cache 

Note that the one-index-reader has an empty cache. It is unclear whether declining to bring over entries from prior discriminators is helpful or harmful - on one hand the wrappers are already available and have already been vetted for use. On the other hand, it is not clear how many times, a particular class will be used in a generic-function call, so starting with an empty cache will tend to act as a sort of garbage-collector for older once-called accesses. It is also fairly trivial to repopulate the cache with one more call with each of the classes:

cl-user(25): (foo-a *foo-inst*)
cl-user(26): (foo-a *bar-inst*)
cl-user(27): (foo-a *bas-inst*)
cl-user(28): (foo-a *not-foo-inst*)
cl-user(29): (foo-a *baz-inst*)
cl-user(30): (gf-discriminator-cache #'foo-a t)
1 discriminated argument
common index: 2
 ix    class
 1     bar
 3     bas
 6     foo
 9     not-foo
 15    baz

Note that the cache is now printed differently than before. It still mentions the discriminator type, which is now one-index-reader, and it also shows the common index, but it also presents the classes of the arguments in order of their appearence in the cache, and it also notes the index within the cache where the cache line has been stored. This index is important because it is no longer ordinal like the N-class-readers, but the value is hashed from the wrapper-number, which allows the desired cache line to be found with few probes into the cache.

But what happens when the "common" index is broken up by a different index? The next access, using foo1-inst, shows the story:

cl-user(31): (foo-a *foo1-inst*)
cl-user(32): (gf-discriminator-cache #'foo-a t)
1 discriminated argument
empty cache 
cl-user(33): (foo-a *foo-inst*)
cl-user(34): (foo-a *bar-inst*)
cl-user(35): (foo-a *bas-inst*)
cl-user(36): (foo-a *not-foo-inst*)
cl-user(37): (foo-a *baz-inst*)
cl-user(38): (foo-a *foo1-inst*)
cl-user(39): (gf-discriminator-cache #'foo-a t)
1 discriminated argument
 ix    class           index
 2     bar             2
 8     not-foo         2
 10    foo1            0
 18    bas             2
 22    foo             2
 30    baz             2

Note here that there is no longer a common index - instead the index is part of the cache line. In this case, most of the calls have resulted in the same index, but because the foo1 class places its a slot into a different location, it has a different index.

Finally, what happens when a method on a generic-function is not even a slot accessor? Here's what happens when we define a random method on foo-a:

cl-user(40): (defmethod foo-a ((x list)) x)
#<standard-method foo-a (list)>
cl-user(41): (foo-a '(help))
cl-user(42): (gf-discriminator-cache #'foo-a t)
1 discriminated argument
empty cache 
cl-user(43): (foo-a *foo-inst*)
cl-user(44): (foo-a *bar-inst*)
cl-user(45): (foo-a *bas-inst*)
cl-user(46): (foo-a *baz-inst*)
cl-user(47): (foo-a *not-foo-inst*)
cl-user(48): (gf-discriminator-cache #'foo-a t)
1 discriminated argument
 ix    class           effective-method
 2     bar             #<Closure (:internal excl::add-reader-method 0) @ #x10003d83b52>
 8     not-foo         #<Closure (:internal excl::add-reader-method 0) @ #x10003d844b2>
 18    bas             #<Closure (:internal excl::add-reader-method 0) @ #x10003d83e72>
 22    foo             #<Closure (:internal excl::add-reader-method 0) @ #x10003d83832>
 30    baz             #<Closure (:internal excl::add-reader-method 0) @ #x10003d84192>

Note now that the discriminator has changed to a caching discriminator, and the values of the discriminators are no longer index locations to access in the slots-vector, but are instead effective-methods which actually do the work of looking up the slots. The caching discriminator is the most general one, and is thus the slowest, and should be avoided if possible.

Another Example

Consider a hierarchy of classes on which we define methods that do no slot accesses at all:

cl-user(1): (defclass xyz () ())
#<standard-class xyz>
cl-user(2): (defclass zyx () ())
#<standard-class zyx>
cl-user(3): (defclass xyz1 (xyz) ())
#<standard-class xyz1>
cl-user(4): (defclass zyx1 (zyx) ())
#<standard-class zyx1>
cl-user(5): (defmethod do-xyz-zyx ((one xyz) (two zyx)) 'hello)
#<standard-method do-xyz-zyx (xyz zyx)>
cl-user(6): (do-xyz-zyx (make-instance 'xyz) (make-instance 'zyx))
cl-user(7): (do-xyz-zyx (make-instance 'xyz) (make-instance 'zyx))
cl-user(8): (do-xyz-zyx (make-instance 'xyz1) (make-instance 'zyx))
cl-user(9): (do-xyz-zyx (make-instance 'xyz) (make-instance 'zyx1))
cl-user(10): (do-xyz-zyx (make-instance 'xyz1) (make-instance 'zyx1))
cl-user(11): (gf-discriminator-cache #'do-xyz-zyx t)
2 discriminated arguments
common effective-method: #<Interpreted Function (method do-xyz-zyx (xyz zyx))>
 at ix 17, classes: xyz, zyx1
 at ix 21, classes: xyz, zyx
 at ix 23, classes: xyz1, zyx
 at ix 25, classes: xyz1, zyx1

Note that this discriminator's "value" is an effective-method (in this case it is the do-xyz--zyx method we just defined). And once the cache-line of the xyz zyx pair is stored into the cache, it is printed similarly to the one-index-reader, with a common effective-method, indexes in the cache, but in this case there are two arguments being discriminated, so those are listed in the entry.

If a wrench is thrown into the mix, by adding another method, the cache is emptied again, but after repopulating we see a caching discriminator that has an effective-method for each cache line:

cl-user(12): (defmethod do-xyz-zyx ((one xyz) (two integer)) 'goodbye)
#<standard-method do-xyz-zyx (xyz integer)>
cl-user(13): (do-xyz-zyx (make-instance 'xyz1) 10)
cl-user(14): (gf-discriminator-cache #'do-xyz-zyx t)
2 discriminated arguments
empty cache 
cl-user(15): (do-xyz-zyx (make-instance 'xyz) (make-instance 'zyx))
cl-user(16): (do-xyz-zyx (make-instance 'xyz1) (make-instance 'zyx))
cl-user(17): (do-xyz-zyx (make-instance 'xyz) (make-instance 'zyx1))
cl-user(18): (do-xyz-zyx (make-instance 'xyz1) (make-instance 'zyx1))
cl-user(19): (do-xyz-zyx (make-instance 'xyz1) 10)
cl-user(20): (gf-discriminator-cache #'do-xyz-zyx t)
2 discriminated arguments
for effective-method: #<Interpreted Function (method do-xyz-zyx (xyz zyx))>
 at ix 17, classes: xyz, zyx1
for effective-method: #<Interpreted Function (method do-xyz-zyx (xyz zyx))>
 at ix 21, classes: xyz1, zyx1
for effective-method: #<Interpreted Function (method do-xyz-zyx (xyz integer))>
 at ix 25, classes: xyz1, fixnum
for effective-method: #<Interpreted Function (method do-xyz-zyx (xyz zyx))>
 at ix 53, classes: xyz1, zyx

Optimization of discriminators

Using an internal function available in all Allegro CL Lisps, we can get cache dumps of every legitimate generic-function in the Lisp using the CLOS class lattice (and thus not using a heap-walker and then weeding out invalid or special-case generic-functions). The form to execute is shown below, but not most of the results, which are too large for documentation and which will vary anyway:

cl-user(1): (dolist (gf (excl::list-all-generic-functions))
              (when (mop:generic-function-name gf)
              (format t "Generic function: #'~s:~%" (mop:generic-function-name gf))
              (gf-discriminator-cache (mop:generic-function-name gf) t)))
Generic function: #'excl::short-combination-operator:
Generic function: #'net.uri:render-uri:
never called
Generic function: #'print-object:
1 discriminated argument
 ix    class            effective-method
 2     null             #<Function (method print-object (t t))>
 8     function         #<Function (method print-object (t t))>
 12    string           #<Function (method print-object (t t))>
 16    symbol           #<Function (method print-object (t t))>
 22    fixnum           #<Function (method print-object (t t))>

Listed within the above elided output are several generic functions for "fi-defsystem", a subclassing of the defsystem class lattice which is used by Franz Inc for internal usage. The listing provides clues as to how to optimize the classes defined in such a way that the discriminators can be selected which are more efficient. As an example, there is a "named-module-group" slot within a couple of classes at least which cause an n-n-reader to be used:

Generic function: #'defsystem:named-module-groups:
1 discriminated argument
 ix    class                           index
 2     system::fi-module-group         6
 22    system::fi-system               11

If the slot definitions in the fi-system and fi-module-group classes had been specified with fixed-index values of either 6 or 11, then this generic-function would have instead been a two-class-reader, a very fast discriminator.

A second example is a "target" slot:

Generic function: #'system::target:
1 discriminated argument
 ix    class                             index
 2     system::fi-module-group           13
 4     system::fi-text-module            16
 10    system::fi-quad-rs-module         16
 22    system::fi-system                 15
 24    system::fi-lisp-module            16

Note that several classes end up with the same index values (some of them have common ancestors in the class hierarchy). If the layout of the classes had been planned with a fixed-index of, say, 16 for the target slot in each class, then all of the accesses would have resolved to the same index and the discriminator would have settled on the one-class-reader. Care must be taken, of course, to ensure that none of the fixed-index values clash with each other, and it may be that there will be dead/wasted slots in some instances, but if the entire class hierarchy of a subsystem is considered at once then a good balance can be made between space efficiency and dicriminator simplicity.

Copyright (c) 2023, Franz Inc. Lafayette, CA., USA. All rights reserved.

ToC DocOverview CGDoc RelNotes FAQ Index PermutedIndex
Allegro CL version 11.0