A dynamically typed language like Scheme does not restrict the type of values bound or assigned to variables to be constant throughout the run-time of a program. This provides a lot of flexibility and makes it easy to get code up and running quickly, but can make maintenance of larger code bases more difficult as the implicit assignment of types to variables done by the programmer has to be "recovered" when the code is inspected or debugged again. Statically typed languages enforce distinct types for all variables, optionally providing type-inference to compute types without requiring the user to specify explicit type declarations in many cases.
If the compiler has some knowledge of the types of local or global variables then it can help in catching type-related errors like passing a value of the wrong type to a user-defined or built-in procedure. Type-information also can be used to generate more efficient code by omitting unnecessary type-checks.
CHICKEN provides an intra-procedural flow-analysis pass and two compiler options for using type-information in this manner:
-specialize will replace certain generic library procedure calls with faster type-specific operations.
-strict-types makes type-analysis more optimistic and gives more opportunities for specialization, but may result in unsafe code if type-declarations are violated.
Note that the interpreter will always ignore type-declarations and will not perform any flow-analysis of interpreted code.
Type information for all core library units is available by default. User-defined global variables can be declared to have a type using the (declare (type ...)) or : syntax.
- (: IDENTIFIER TYPE)syntax
Declares that the global variable IDENTIFIER is of the given type.
- (the TYPE EXPRESSION)syntax
Equivalent to EXPRESSION, but declares that the result will be of the given type. Note that this form always declares the type of a single result, the can not be used to declare types for multiple result values. TYPE should be a subtype of the type inferred for EXPRESSION, the compiler will issue a warning if this should not be the case.
- (assume ((VARIABLE TYPE) ...) BODY ...)syntax
Declares that at the start of execution of BODY .., the variables will be of the given types. This is equivalent to
(let ((VARIABLE (the TYPE VARIABLE)) ...) BODY ...)
- (define-type NAME TYPE)syntax
Defines a type-abbreviation NAME that can be used in place of TYPE. Type-abbreviations defined inside a module are not visible outside of that module.
|deprecated||any use of this variable will generate a warning|
|(deprecated NAME)||generate a warning and advise alternative NAME|
|(or VALUETYPE ...)||"union" or "sum" type|
|(not VALUETYPE)||non-matching type (*)|
|(struct STRUCTURENAME)||record structure of given kind|
|(procedure [NAME] (VALUETYPE ... [#!optional VALUETYPE ...] [#!rest [VALUETYPE]]) . RESULTS)||procedure type, optionally with name|
|(VALUETYPE ... [#!optional VALUETYPE ...] [#!rest [VALUETYPE]] -> . RESULTS)||alternative procedure type syntax|
|(VALUETYPE ... [#!optional VALUETYPE ...] [#!rest [VALUETYPE]] --> . RESULTS)||procedure type that is declared not to modify locally held state|
|(VALUETYPE -> VALUETYPE : VALUETYPE)||predicate procedure type|
|(forall (TYPEVAR ...) VALUETYPE)||polymorphic type|
|TYPEVAR||VARIABLE or (VARIABLE TYPE)|
|boolean||true or false|
|list||null or pair|
|number||fixnum or float|
|pointer-vector||vector or native pointers|
|input-port output-port||input- or output-port|
|(pair TYPE1 TYPE2)||pair with given component types|
|(list-of TYPE)||proper list with given element type|
|(list TYPE1 ...)||proper list with given length and element types|
|(vector-of TYPE)||vector with given element types|
|(vector TYPE1 ...)||vector with given length and element types|
|*||any number of unspecific results|
|(RESULTTYPE ...)||specific number of results with given types|
|undefined||a single undefined result|
|noreturn||procedure does not return normally|
(*) Note: no type-variables are bound inside (not TYPE).
Note that type-variables in forall types may be given "constraint" types, i.e.
(: sort (forall (e (s (or (vector-of e) (list-of e)))) (s (e e -> *) -> s)))
declares that sort is a procedure of two arguments, the first being a vector or list of an undetermined element type e and the second being a procedure that takes two arguments of the element type. The result of sort is of the same type as the first argument.
Some types are internally represented as structure types, but you can also use these names directly in type-specifications - TYPE corresponds to (struct TYPE) in this case:
|u8vector||SRFI-4 byte vector|
|s8vector||SRFI-4 byte vector|
|u16vector||SRFI-4 byte vector|
|s16vector||SRFI-4 byte vector|
|u32vector||SRFI-4 byte vector|
|s32vector||SRFI-4 byte vector|
|f32vector||SRFI-4 byte vector|
|f64vector||SRFI-4 byte vector|
|queue||see "data-structures" unit|
|time||SRFI-18 "time" object|
|lock||lock object from "posix" unit|
|mmap||memory mapped file|
|condition||object representing exception|
|tcp-listener||listener object from "tcp" unit|
Additionally, some aliases are allowed:
|immediate||(or eof null fixnum char boolean)|
|port||(or input-port output-port)|
For portability the aliases &optional and &rest are allowed in procedure type declarations as an alternative to #!optional and #!rest, respectively.
Procedure-types of the form (DOM -> RNG : TYPE) specify that the declared procedure will be a predicate, i.e. it accepts a single argument of type DOM, returns a result of type RNG (usually a boolean) and returns a true value if the argument is of type TYPE and false otherwise.
Procedure types are assumed to be not referentially transparent and are assumed to possibly modify locally held state. Using the (... --> ...) syntax, you can declare a procedure to not modify local state, i.e. not causing any side-effects on local variables or data contain in local variables. This gives more opportunities for optimization but may not be violated or the results are undefined.
Type information of declared toplevel variables can be used in client code that refers to the definitions in a compiled file. The following compiler options allow saving type-declarations to a file and consulting the type declarations retained in this manner:
-emit-type-file FILENAME writes the type-information for all declared definitions in an internal format to FILENAME.
-types FILENAME loads and registers the type-information in FILENAME which should be a file generated though a previous use of -emit-type-file.
If library code is used with require-extension or (declare (unit ...)) and a .types file of the same name exists in the extension repository path, then it is automatically consulted. This allows code using these libraries to take advantage of type-information for library definitions.
Note that procedure-definitions in dynamically loaded code that was compiled with -strict-types will not check the types of their arguments which will result in unsafe code. Invoking such procedures with incorrectly typed arguments will result in undefined program behaviour.
If argument types are known, then calls to known library procedures are replaced with non-checking variants (if available). Additionally, procedure checks can be omitted in cases where the value in operator position of a procedure call is known to be a procedure. Performance results will vary greatly depending on the nature of the compiled code. In general, specialization will not make code that is compiled in unsafe mode any faster: compilation in unsafe mode will omit most type checks anyway. But specialization can often improve the performance of code compiled in safe (default) mode.
Specializations can also be defined by the user:
- (define-specialization (NAME ARGUMENT ...) [RESULTS] BODY)syntax
Declares that calls to the globally defined procedure NAME with arguments matching the types given by ARGUMENTs should be replaced by BODY (a single expression). Each ARGUMENT should be an identifier naming a formal parameter, or a list of the form (IDENTIFIER TYPE). In the former case, this argument specializes on the * type. If given, RESULTS (which follows the syntax given above under "Type Syntax") adjusts the result types from those previously declared for NAME.
NAME must have a declared type (for example by using :). If it doesn't, the specialization is ignored.
User-defined specializations are always local to the compilation unit in which they occur and cannot be exported. When encountered in the interpreter, define-specialization does nothing and returns an unspecified result.
When multiple specializations may apply to a given call, they are prioritized by the order in which they were defined, with earlier specializations taking precedence over later ones.
There is currently no way of ensuring specializations take place. You can use the -debug o compiler options to see the total number of specializations performed on a particular named function call during compilation.
- (compiler-typecase EXP (TYPE BODY ...) ... [(else BODY ...)])syntax
Evaluates EXP and executes the first clause which names a type that matches the type inferred during flow analysis as the result of EXP. The result of EXP is ignored and should be a single value. If a compiler-typecase form occurs in evaluated code, or if it occurs in compiled code but specialization is not enabled, then it must have an else clause which specifies the default code to be executed after EXP. If no else clause is given and no TYPE matches, then a compile-time error is signalled.
Assignments make flow-analysis much harder and remove opportunities for optimization. Generally you should avoid using a lot of mutations of both local variables and data held in local variables. It may even make your code do unexpected things when these mutations violate type-declarations.
Note that using threads which modify local state makes all type-analysis pointless.