[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
When a program gets large, it is often desirable to split it up into several files. One reason for this is to help keep the parts of the program organized, to make things easier to find. It's also useful to have the program broken into small pieces that are more convenient to edit and compile. It is particularly important to avoid the need to recompile all of a large program every time any piece of it changes; if the program is broken up into many files, only the files that have changes in them need to be recompiled.
The apparent drawback to splitting up a program is that more "commands" are needed to manipulate it. To load the program, you now have to load several files separately, insead of just loading one file. To compile it, you have to figure out which files need compilation, by seeing which have been edited since they were last compiled, and then you have to compile those files.
What's even more complicated is that files can have interdependencies. You might have a file called "DEFS" that contains some macro definitions (or flavor or structure definitions), and functions in other files might use those macros. This means that in order to compile any of those other files, you must first load the file "DEFS" into the Lisp environment, so that the macros will be defined and can be expanded at compile time. You'd have to remember this whenever you compile any of those files. Furthermore, if "DEFS" has changed, other files of the program might need to be recompiled because the macros might have changed and need to be re-expanded.
This chapter describes the system facility, which takes care of all these things for you. The way it works is that you define a set of files to be a system, using the defsystem special form, described below. This system definition says which files make up the system, which ones depend on the presence of others, and so on. You put this system definition into its own little file, and then all you have to do is load that file and the Lisp environment will know about your system and what files are in it. You can then use the make-system function (see (make-system-fun)) to load in all the files of the system, or recompile all the files that need compiling, and so on.
The system facility is very general and extensible. This chapter explains how to use it and how to extend it. This chapter also explains the patch facility, which lets you conveniently update a large program with incremental changes, and how to save away Lisp environments in disk partitions.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Here are a few examples.
(defsystem mysys (:compile-load ("AI: GEORGE; PROG1" "AI: GEORG2; PROG2"))) (defsystem zmail (:name "ZMail") (:pathname-default "AI: ZMAIL;") (:package zwei) (:module defs "DEFS") (:module mult "MULT" :package tv) (:module main ("TOP" "COMNDS" "MAIL" "USER" "WINDOW" "FILTER" mult "COMETH")) (:compile-load defs) (:compile-load main (:fasload defs))) (defsystem bar (:module reader-macros "RDMAC") (:module other-macros "MACROS") (:module main-program "MAIN") (:compile-load reader-macros) (:compile-load other-macros (:fasload reader-macros)) (:compile-load main-program (:fasload reader-macros other-macros))) |
The first example defines a new system called mysys, which consists of two files, both of which are to be compiled and loaded. The second example is somewhat more complicated. What all the options mean will be specified shortly, but the primary difference is that there is a file defs which must be loaded before the rest of the files (main) can be compiled. The final example has two levels of dependency. reader-macros must be compiled and loaded before other-macros can be compiled. Both reader-macros and other-macros must then be loaded before main-program can be compiled.
The defsystem options other than transformations are:
a string
a symbol
an external module component
a list of module components
a list of file names
To avoid syntactic ambiguity, this is allowed as a module component but not as a module specification.
:package
(:module prog (("AI: GEORGE; PROG" "AI: GEORG2; PROG"))) (:module foo (defs (zmail defs))) |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Transformations are of two types, simple and complex. A simple transformation is a single operation on a file, such as compiling it or loading it. A complex transformation takes the output from one transformation and performs another transformation on it, for example, loading the results of compilation.
The general format of a simple transformation is (name input dependencies condition). input is usually a module specification or another transformation whose output is used. The transformation name is to be performed on all the files in the module, or all the output files of the other transformation.
dependencies and condition are optional.
dependencies is a transformation specification, either a list (transformation-name module-names...), or a list of such lists. A module-name is either a symbol which is the name of a module in the current system, or a list (system-name module-names...). A dependency declares that all of the indicated transformations must be performed on the indicated modules before the current transformation itself can take place. Thus in the zmail example above, the defs module must have the :fasload transformation performed on it before the :compile transformation can be performed on main.
The dependency has to be a tranformation that was explicitly specified as a transformation in the system definition, not just an action that might have been performed by anything. That is, if you have a dependency (:fasload foo), it means that (fasload foo) is a tranformation of your system and you depend on that tranformation; it does not simply mean that you depend on foo's being loaded. Furthermore, it doesn't work if (:fasload foo) was an implicit piece of another tranformation. For example, the following is correct and will work:
(defsystem foo (:module foo "FOO") (:module bar "BAR") (:compile-load (foo bar))) |
(defsystem foo (:module foo "FOO") (:module bar "BAR") (:module blort "BLORT") (:compile-load (foo bar)) (:compile-load blort (:fasload foo))) |
(defsystem foo (:module foo "FOO") (:module bar "BAR") (:module blort "BLORT") (:compile-load foo) (:compile-load bar) (:compile-load blort (:fasload foo))) |
condition is a predicate which specifies when the transformation should take place. Generally it defaults according to the type of the transformation. Conditions are discussed further on (transformation-condition-discussion).
The defined simple transformations are:
:fasload
:readfile
:compile
A special simple transformation is
:do-components
The defined complex transformations are
:compile-load
:compile-load-init
As was explained above, each filename in an input specification can in fact be a list of strings for the case where the source file of a program differs from the binary file in more than just the file type. In fact, every filename is treated as if it were an infinite list of filenames with the last filename, or in the case of a single string the only filename, repeated forever at the end. Each simple transformation takes some number of input filename arguments, and some number of output filename arguments. As transformations are performed, these arguments are taken from the front of the filename list. The input arguments are actually removed and the output arguments left as input arguments to the next higher transformation. To make this clearer, consider the prog module above having the :compile-load transformation performed on it. This means that prog is given as the input to the :compile transformation and the output from this transformation is given as the input to the :fasload transformation. The :compile transformation takes one input filename argument, the name of a lisp source file, and one output filename argument, the name of the qfasl file. The :fasload transformation takes one input filename argument, the name of a qfasl file, and no output filename arguments. So, for the first and only file in the prog module, the filename argument list looks like ("AI: GEORGE; PROG" "AI: GEORG2; PROG" "AI: GEORG2; PROG" ...). The :compile transformation is given arguments of "AI: GEORGE; PROG" and "AI: GEORG2; PROG" and the filename argument list which it outputs as the input to the :fasload transformation is ("AI: GEORG2; PROG" "AI: GEORG2; PROG" ...). The :fasload transformation then is given its one argument of "AI: GEORG2; PROG".
Note that dependencies are not "transitive" nor "inherited". For example, if module a depends on macros defined in module b, and therefore needs b to be loaded in order to compile, and b has a similar dependency on c, c will not be loaded during compilation of a. Transformations with these dependencies would be written
(:compile-load a (:fasload b)) (:compile-load b (:fasload c)) |
(:compile-load a (:fasload b c)) (:compile-load b (:fasload c)) |
(:compile-load a (:fasload b c) (:fasload c)) (:compile-load b (:fasload c)) |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
(make-system 'mysys) |
(make-system 'mysys ':compile) |
make-system lists what transformations it is going to perform on what files, asks the user for confirmation, then performs the transformations. Before each transformation a message is printed listing the transformation being performed, the file it is being done to, and the package. This behavior can be altered by keywords.
These are the keywords recognized by the make-system function and what they do.
(make-system 'mysys (if compile-p ':compile ':noop)) |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
make-system keywords are defined as functions on the si:make-system-keyword property of the keyword. The functions are called with no arguments. Some of the relevant variables they can use are
The following simple example adds a new keyword to make-system called :just-warn, which means that fdefine warnings (see (fdefine-fun)) regarding functions being overwritten should be printed out, but the user should not be queried.
(si:define-make-system-special-variable inhibit-fdefine-warnings inhibit-fdefine-warnings nil) (defun (:just-warn si:make-system-keyword) () (setq inhibit-fdefine-warnings ':just-warn)) |
make-system keywords can have effect either directly when called, or by pushing a form to be evaluated onto si:*make-system-forms-to-be-evaled-after* or one of the other two similar lists. In general, the only useful thing to do is to set some special variable defined by si:define-make-system-special-variable. In addition to the ones mentioned above, user-defined transformations may have their behavior controlled by new special variables, which can be set by new keywords. If you want to get at the list of transformations to be performed, for example, the right way would be to set si:*file-transformation-function* to a new function, which then might call si:do-file-transformations with a possibly modified list. That is how the :print-only keyword works.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Options to defsystem are defined as macros on the si:defsystem-macro property of the option keyword. Such a macro can expand into an existing option or transformation, or it can have side effects and return nil. There are several variables they can use; the only one of general interest is
(si:define-simple-transformation name function default-condition input-file-types output-file-types pretty-names compile-like load-like) |
(si:define-simple-transformation :compile si:qc-file-1 si:file-newer-than-file-p ("LISP") ("QFASL")) |
pretty-names, compile-like, and load-like are optional.
pretty-names specifies how messages printed for the user should print the name of the transformation. It can be a list of the imperative ("Compile"), the present participle ("Compiling"), and the past participle ("compiled"). Note that the past participle is not capitalized, because it is not used at the beginning of a sentence. pretty-names can be just a string, which is taken to be the imperative, and the system will conjugate the participles itself. If pretty-names is omitted or nil it defaults to the name of the transformation.
compile-like and load-like say when the transformation should be performed. Compile-like transformations are performed when the :compile keyword is given to make-system. Load-like transformations are performed unless the :noload keyword is given to make-system. By default compile-like is t but load-like is nil.
Complex transformations are just defined as normal macro expansions, for example,
(defmacro (:compile-load si:defsystem-macro) (input &optional com-dep load-dep com-cond load-cond) `(:fasload (:compile ,input ,com-dep ,com-cond) ,load-dep ,load-cond)) |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
It is sometimes useful to specify a transformation upon which something else can depend, but which is not performed by default, but rather only when requested because of that dependency. The transformation nevertheless occupies a specific place in the hierarchy. The :skip defsystem macro allows specifying a transformation of this type. For example, suppose there is a special compiler for the read table which is not ordinarily loaded into the system. The compiled version should still be kept up to date, and it needs to be loaded if ever the read table needs to be recompiled.
(defsystem reader (:pathname-default "AI: LMIO;") (:package system-internals) (:module defs "RDDEFS") (:module reader "READ") (:module read-table-compiler "RTC") (:module read-table "RDTBL") (:compile-load defs) (:compile-load reader (:fasload defs)) (:skip :fasload (:compile read-table-compiler)) (:rtc-compile-load read-table (:fasload read-table-compiler))) |
.setq transformation-condition-discussion page So far nothing has been said about what can be given as a condition for a transformation except for the default functions which check for a source file being newer than the binary and so on. In general, any function which takes the same arguments as the transformation function (e.g. qc-file) and returns t if the transformation needs to be performed, can be in this place as a symbol, including for example a closure. To take an example, suppose there is a file which contains compile-flavor-methods for a system, and which should therefore be recompiled if any of the flavor method definitions change. In this case, the condition function for compiling that file should return t if either the source of that file itself or any of the files that define the flavors have changed. This is what the :compile-load-init complex transformation is for. It is defined like this:
.setq compile-load-init-transformation page (defmacro (:compile-load-init si:defsystem-macro) (input add-dep &optional com-dep load-dep &aux function) (setq function (let-closed ((*additional-dependent-modules* add-dep)) 'compile-load-init-condition)) `(:fasload (:compile ,input ,com-dep ,function) ,load-dep)) (defun compile-load-init-condition (source-file qfasl-file) (or (si:file-newer-than-file-p source-file qfasl-file) (local-declare ((special *additional-dependent-modules*)) (si:other-files-newer-than-file-p *additional-dependent-modules* qfasl-file)))) |
[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |