%eclent; ]> Declarations &ECL; supports all kinds of declarations described in the &Steele84;. Any valid declaration will affect the &ECL; environment in some way or another, although information obtained by declarations, other than special declarations, is mainly used by the &ECL; compiler. As described in &Steele84;, Common-Lisp declarations are divided into two classes: proclamations and others. A proclamation is a global declaration given by the function proclaim, the top-level macro defvar, or the top-level macro defparameter. Once given, a proclamation remains effective during the &ECL; session unless it is shadowed by a local declaration or is canceled by another proclamation. Any other declaration is a local declaration and is given only by the special form declare. A local declaration remains in effect only within the body of the construct that surrounds the declaration. In the following nonsensical example borrowed from Chapter 9 of &Steele84;, (defun nonsense (k x z) (foo z x) (let ((j (foo k x)) (x (* k k))) (declare (inline foo) (special x z)) (foo x j z))) the inline and the special declarations both remain in effect within the surrounding let form. In this case, we say that the let form is the surrounding construct of these declarations.
the— Special Form: the value-type form The &ECL; interpreter does actually check whether the value of the form conforms to the data type specified by value-type and signals an error if the value does not. The type checking is performed by the function typep. For example,
(the fixnum (foo)) is equivalent to (let ((values (multiple-value-list (foo)))) (cond ((endp values) (error ``Too few return values.")) ((not (endp (cdr values))) (error ``Too many return values.")) ((typep (car values) 'fixnum) (car values)) (t (error ``~s is not of type fixnum." (car values))))) On the other hand, the &ECL; compiler uses the special form to obtain type information for compiled code optimization. No code for runtime type-checking is embedded in the compiled code.
Declaration Specifiers &ECL; recognizes all declaration specifiers defined in &Steele84;. The syntax of each such declaration specifier is exactly the same as defined in &Steele84;. In addition, &ECL; recognizes the object declaration specifier which is specific to &ECL;.
special— Declaration: special {variable-name}* The interpreter and the compiler of &ECL; both treat special declarations exactly as described in &Steele84;.
type— Declaration: type type {variable-name}* A type proclamation (type type var1 var2 ...) specifies that the dynamic values of the named variables are of the type type. A local type declaration specifies that the variables mentioned are bound by the surrounding construct and have values of the type type during execution of the surrounding construct. The compiler issues a warning if one of the named variables is not bound by the surrounding construct. The information given by type declarations is used by the compiler to optimize the compiled code. The behavior of the compiled code is unpredictable if a wrong type declaration is supplied. The compiler detects certain wrong type declarations at compile time.
For example, >(defun foo (x y) (declare (fixnum x) (character y)) (setq x y) ...)) foo >(compile 'foo) ; (defun foo ...) is being compiled. ;; Warning: Type mismatches between x and y. See Section 7.3 for further information on type declarations.
type— Declaration: type {variable-name}* (type var1 var2 ...) is equivalent to (type type var1 var2 ...), provided that type is one of the symbols in Table 4-1 of &Steele84;, other than function. Declaration specifications that begin with function are regarded as function declarations (see below).
function— Declaration: function function-name argument-types . return-types A function declaration is used to obtain type information for function call forms. That is, a function declaration specifies the argument and the return types of each form that calls the named function.
(defun foo () (declare (function bar (character) fixnum)) (+ (bar (atcholi1)) (bar (atcholi2)))) In this example, the function declaration specifies that the two functions atcholi1 and atcholi2 both return character objects when called within the body of foo, and that the function bar returns fixnum objects when called within the body of foo. The type information given by function declarations is used by the compiler to optimize the compiled code. The behavior of the compiled code is unpredictable if a wrong function declaration is supplied. The compiler detects certain wrong function declarations at compile time. For example, >(defun foo (x) (declare (fixnum x) (function bar (character) fixnum)) (bar x)) foo >(compile 'foo) ; (defun foo ...) is being compiled. ;; Warning: The type of the form x is not character. However, the compiler does not check the number of arguments, and thus, the following function definition will be compiled successfully without any warnings. (defun foo () (declare (function bar (character character) fixnum)) (+ (bar (atcholi1)) (bar (atcholi2) (atcholi3) (atcholi4)))) For this definition, the compiler assumes that the three functions atcholi1, atcholi2, and atcholi3 will return fixnum objects. The return type of atcholi4 is unknown at compile time. The complete syntax of a function declaration is: (function function-name ({type}* [{&optional | &rest | &key} {thing}*]) {(values {type}* ) | {type}*} ) Although &optional, &rest, and &key markers may appear in the list of argument types, only those types are recognized that appear before any such markers and the rest of the list is simply ignored. Note that functions with &optional, &rest, or &key parameters may still be declared by function declarations because of the use of function declarations mentioned above. The values construct in the specification of return types is almost useless: (function function-name argument-types (values type1 type2 …)) is equivalent to (function function-name argment-types type1 type2 …). See Section 7.3 for further information on function declarations.
ftype— Declaration: ftype function-type {function-name}* function-type must be a list whose first element is the symbol function. (ftype (function . rest) function-name-1 ... function-name-n) is equivalent to n consecutive function declarations (function function-name-1 . rest) ... (function function-name-n . rest).
notinline— Declaration: notinline {function-name}* (notinline function1 function2 ...) specifies that the compiler should not compile the named functions in-line. Calls to the named functions can be traced and an event (see Section 5.4) is pushed on the event stack when any one of the named functions is invoked.
inline— Declaration: inline {function-name}* An inline proclamation cancels currently effective notinline proclamations, and a local inline declaration locally shadows currently effective notinline declarations.
>(defun foo (x) (cons (car x) (locally (declare (inline car)) (car x)))) foo >(defun bar (x) (cons (car x) (locally (declare (inline car)) (car x)))) foo >(proclaim '(notinline car)) nil >(compile 'foo) ... >(proclaim '(inline car)) nil >(compile 'bar) ... Usually, primitive functions such as car are compiled in-line. Therefore, in this example, only the first call to car within foo is compiled not in-line. In general, the &ECL; compiler compiles functions in-line whenever possible. Thus an inline declaration (inline function1 function2 ...) is worthless if none of the named functions have previously been declared to be notinline.
ignore— Declaration: ignore {variable-name}* Usually, the compiler issues a warning if a lexical variable is never referred to. (ignore var1 ... varn) causes the compiler not to issue a warning even if the named variables are never referred to. The compiler issues a warning if one of the named variables is not bound by the surrounding construct, or if a named variable is actually referred to. ignore proclamations are simply ignored.
optimize— Declaration: optimize {(quality value) | quality}* &ECL; supports the four optimize qualities listed in the &Steele84;. speed and compilation-speed are used to set up the optimization switch of the C language compiler which is invoked to compile the C-language code generated by the &ECL; compiler (see Chapter 6). (optimize (speed n)) and (optimize (compilation-speed m)) are equivalent, where n and m are integers between 0 and 3, and m is equal to 3-n. When a &ECL; session is started, the speed quality is set to 3. That is, by default, the compiler generates the fastest code in the longest compilation time. The space quality specifies whether the code size is important or not: The compiled code is a little bit larger and faster when compiled with the space quality 0, than when compiled with the space quality 1, 2, or 3. When a &ECL; session is started, the space quality is set to 0. The safety quality determines how much runtime error checking code should be embedded in the compiled code. If the safety quality is 0, the compiled code scarcely does runtime error checking. If the safety quality is 1, then the compiled code for a function will check the number of arguments to the function at runtime. If the safety quality is 2 or 3, then the compiled code does full runtime error checking. In addition, the highest quality value 3 causes the compiler to treat all functions as if they were declared to be notinline. When a &ECL; session is started, the safety quality is set to 0.
declaration— Declaration: declaration {name}* A declaration declaration is used exactly as specified in the &Steele84;.
object— Declaration: object {variable-name}* This is the only declaration specifier that is specific to &ECL;. (object var1 ... varn) affects only variable bindings and specifies that the named variables can be allocated in the C stack (see Section 7.3). The compiler issues a warning if one of the named variables is not bound by the surrounding construct. object proclamations are simply ignored.
Significant Type Specifiers Whenever a declaration is encountered, each type specifier (if any) in the declaration is converted to one of the following type specifiers, which are collectively called the significant type specifiers. Here, the lines indicate subtype relations; the right type is a subtype of the left type. For instance, (vector t) is a subtype of (array t) and T, and (array t) itself is a subtype of T. However, (array t) and (array string-char) are disjoint types. The function subtypep is used for the conversion to significant type specifiers: If the first value of (subtypep raw-type type) is T for one of the significant type specifiers type, then the type specifier raw-type in the declaration is converted to type. If there are more than one such significant type specifiers, then the type specifier that is a subtype of other specifiers is selected. For example, type specifiers fixnum, (mod 3), and (member 0 1) are all converted to fixnum, though they are also subtypes of T. Because of this type specifier conversion, &ECL; may sometimes regard two seemingly distinct declarations as the same. For example, the following type declarations are completely equivalent, internally in &ECL;. (declare (type fixnum x))) (declare (type (mod 3) x)) (declare (type (member 0 1) x)) Type specifiers in declaration specifications passed to the &ECL; specific function proclamation are also converted to significant type specifiers. Thus, for example, >(proclaim '(function foo (fixnum) fixnum)) nil >(proclamation '(function foo ((mod 3)) (member 0 1))) t >(proclamation '(function foo (number) character)) nil The first call to proclamation returns T because both (mod 3) and (member 0 1) are converted to fixnum before the function type of foo is checked.
Treatment of Type Declarations &ECL; uses several runtime stacks. Arguments to functions, lexical and temporary variables are allocated on the C stack. Temporary values saved on the C stack may sometimes be represented as raw data instead of pointers to heap-allocated cells. Accessing such raw data on the C stack results in faster compiled code, partly because no pointer dereferencing operation is necessary, and partly because no cell is newly allocated on the heap when a new object is created. This is particularly helpful for numeric code which computes with floating point numbers. &ECL; uses a conservative garbage collector to scan the C stack and find references to live object.
Variable Allocations If a lexical variable is declared to be of fixnum, character, short-float, long-float, or their subtypes, then it is allocated on the C stack rather than on the value stack. In addition, the variable always has a raw datum as its value: 32 bit signed integer for fixnums, 8 bit character code with 24 bit padding for characters (remember that the font and bit fields of &ECL; characters are always 0), 32 bit floating point representation for short-floats, and 64 bit floating point representation for long-floats. Similarly, if a lexical variable is named in an object declaration (see Section 7.1), then it is allocated on the C stack but, in this case, the variable always has a cell pointer as its value. The user is strongly recommended to make sure that objects stored in such an object variable may never be garbage collected unexpectedly. For example, (do ((x (foo) (cdr x))) ((endp x)) (let ((y (car x))) (declare (object y)) (bar y))) this object declaration is completely safe because the value of the variable y is always a substructure of the value of x, which in turn is protected against garbage collection. Incidentally, loop variables of dolist may always be declared as object variables, since the dolist form has essentially the same control structure as the do form above. On the other hand, the result of evaluation of the following form is unpredictable, because the cons cell pointed to from the object variable z may be garbage collected before bar is called. (let ((z (cons x y))) (declare (object z)) (foo (cons x y)) (bar z)) Lexical variables that are not declared to be of fixnum, character, short-float, long-float, or their subtypes, and that are not named in object declarations are usually allocated on the value stack, but may possibly be allocated on the C stack automatically by the compiler.
Built-in Functions that Operate on Raw Data Directly Some built-in Common-Lisp functions can directly operate on raw data, if appropriate declarations are supplied. The addition function + is among such functions. (let ((x 1)) (declare (fixnum x)) ... (setq x (+ x 2)) ... ) In the compiled code for this let form, the raw fixnum datum (i.e., the 32 bit signed integer) stored in x is simply incremented by 2 and the resulting 32 bit signed integer is stored back into x. The compiler is sure that the addition for 32 bit signed integers will be performed on the call to +, because the arguments are both fixnums and the return value must be also a fixnum since the value is to be assigned to the fixnum variable. The knowledge of both the argument types and the return type is necessary for this decision: Addition of two fixnums may possibly produce a bignum and addition of two bignums may happen to produce a fixnum value. If either the argument type or the return type were not known to the compiler, the general addition function would be called to handle the general case. In the following form, for example, the compiler cannot be sure that the return value of the multiplication is a fixnum or that the arguments of the addition are fixnums. (setq x (+ (* x 3) 2)) In order to obtain the optimal code, a the special form should surround the multiplication. (setq x (+ (the fixnum (* x 3)) 2)) Built-in Common-Lisp functions that can directly operate on raw data are: arithmetic functions such as +, -, 1+, 1-, *, floor, mod, /, and expt. predicates such as eq, eql, equal, zerop, plusp, minusp, =, /=, <, <=, >, >=, char=, char/=, char<, char<=, char>, and char>=. sequence processing functions that receive or return one or more fixnum values, such as nth, nthcdr, length, and elt. array access functions such as svref, char, schar, and aref (see below). system-internal functions for array update (see below). type-specific functions such as char-code, code-char, and float. As mentioned in Section 2.5.1, array elements are represented in one of six ways depending on the type of the array. By supplying appropriate array type declarations, array access and update operations can handle raw data stored in arrays. For example, (let ((a (make-array n :element-type 'fixnum)) (sum 0)) (declare (type (array fixnum) a) (fixnum sum)) (dotimes (i n) ;;; Array initialization. (declare (fixnum i)) (setf (aref a i) i)) .... (dotimes (i n) ;;; Summing up the elements. (declare (fixnum i)) (setq sum (+ (aref a i) sum))) .... ) The setf form replaces the i-th element of the array a by the raw fixnum value of i. The aref form retrieves the raw fixnum datum stored in a. This raw datum is then added to the raw fixnum value of the fixnum variable sum, producing the raw fixnum datum to be stored in sum. Similar raw data handling is possible for arrays of types (array fixnum), (vector fixnum), (array string-char), string, (array short-float), (vector short-float), (array long-float), and (vector long-float).
Arguments/Values Passing Function proclamations (function function-name (arg-type1 arg-type2 ...) return-type) or its equivalents give the compiler the chance to generate compiled code so that arguments to the named functions and resulting values of the named functions will be passed via the C stack, thus increasing the efficiency of calls to these functions. Such arguments/values passing via the C stack is possible only if the called function is also defined in the same source file. This is because the code for the called function must have two entries: One entry for C arguments/values passing and another for ordinary Lisp arguments/values passing. (An ordinary function has only the latter entry.) When the latter entry is used, the arguments are unboxed and passed to the former entry. On return from the function, the resulting value is cast into a Lisp data type. A good example of this follows. (eval-when (compile) (proclaim '(function tak (fixnum fixnum fixnum) fixnum))) (defun tak (x y z) (declare (fixnum x y z)) (if (not (< y x)) z (tak (tak (1- x) y z) (tak (1- y) z x) (tak (1- z) x y)))) ;;; Call (tak 18 12 6). When tak is called with the arguments 18, 12, and 6, the raw fixnum data of the arguments are set to the parameters x, y, z. After that, only raw C data are used to perform the execution: No cell pointers are newly allocated nor even referenced. The built-in functions < and 1- directly operate on the raw data. Only at the return from the top-level call of tak, the resulting raw data value (which happens to be 7) is reallocated on the heap. Note that both the function proclamation and the local fixnum declaration are necessary to obtain the optimal code. The function proclamation is necessary for arguments/values passing via the C stack and the fixnum declaration is necessary to unbox the parameters into C variables.