;;; compile.scm -- compilation utilities ;;; ;;; author : Sandra Loosemore ;;; date : 24 Oct 1991 ;;; ;;; This file defines a makefile-like compilation system that supports ;;; a hierarchy of dependencies. ;;; The external entry points are define-compilation-unit, load-unit, and ;;; compile-and-load-unit. ;;;===================================================================== ;;; Parsing ;;;===================================================================== ;;; Establish global defaults for filenames. (define compile.source-filename source-file-type) (define compile.binary-filename binary-file-type) (define compile.binary-subdir (string-append lisp-implementation-name "/")) (define compile.delayed-loads '()) ;;; Top level units are stored in this table. ;;; This is really a slight wart on the whole scheme of things; this ;;; is done instead of storing the top-level units in variables because ;;; we were getting unintentional name collisions. (define compile.unit-table (make-table)) (define-syntax (compile.lookup-unit name) `(table-entry compile.unit-table ,name)) (define (mung-global-units names lexical-units) (map (lambda (n) (if (memq n lexical-units) n `(compile.lookup-unit ',n))) names)) ;;; Top-level compilation units are defined with define-compilation-unit. ;;; The body can consist of the following clauses: ;;; ;;; (source-filename ) ;;; (binary-filename ) ;;; Specify source and/or binary file names. For nested units, these ;;; are merged with defaults from outer units. If you don't specify ;;; an explicit binary filename, it's inherited from the source file ;;; name. ;;; (require ...) ;;; Specify compile/load dependencies. Arguments are names of other ;;; units/component files; these names have scoping like let*, so a unit ;;; can require previously listed units at the same or outer level. ;;; (unit name ....) ;;; Specifies a nested unit. This can appear multiple times. ;;; If a unit doesn't include any nested units, then it's a leaf ;;; consisting of a single source file. ;;; (load ) ;;; If supplied and false, the unit isn't loaded unless it is needed ;;; to satisfy a require clause. Used for files containing compilation ;;; support stuff. ;;; (compile ) ;;; If supplied and false, the unit isn't compiled. Only useful for ;;; leaf nodes. Typically used in combination with (load '#f) to suppress ;;; compilation of stuff only used at compile time. (define-syntax (define-compilation-unit name . clauses) `(begin (let ((unit ,(compile.process-unit-spec name clauses '#t '()))) (setf (compile.lookup-unit ',name) unit) (setf compilation-units (append compilation-units (list unit)))) ',name)) ;;; The basic approach is to turn the compilation unit definition into ;;; a big LET*, and put calls to build the actual unit object inside ;;; of this. ;;; (define (compile.process-unit-spec name clauses top-level? lexical-units) (multiple-value-bind (source-filename binary-filename require nested-units load? compile?) (compile.parse-unit-spec clauses lexical-units) `(let* ((compile.source-filename ,source-filename) (compile.binary-filename ,binary-filename) (compile.unit-require (list ,@require)) (compile.delayed-loads (append compile.delayed-loads (compile.select-delayed-loads compile.unit-require))) ,@nested-units) (make compile.unit (name ',name) (source-filename compile.source-filename) (binary-filename compile.binary-filename) (components (list ,@(map (function car) nested-units))) (require compile.unit-require) (top-level? ',top-level?) (load? ,load?) (compile? ,compile?) (delayed-loads compile.delayed-loads))))) (define (compile.parse-unit-spec clauses lexical-units) (let ((source-filename '#f) (binary-filename '#f) (require '#f) (nested-units '()) (load? ''#t) (compile? ''#t)) (dolist (c clauses) (cond ((not (pair? c)) (compile.unit-syntax-error c)) ((eq? (car c) 'source-filename) (if source-filename (compile.unit-duplicate-error c) (setf source-filename (cadr c)))) ((eq? (car c) 'binary-filename) (if binary-filename (compile.unit-duplicate-error c) (setf binary-filename (cadr c)))) ((eq? (car c) 'require) (if require (compile.unit-duplicate-error c) (setf require (mung-global-units (cdr c) lexical-units)))) ((eq? (car c) 'unit) (push (list (cadr c) (compile.process-unit-spec (cadr c) (cddr c) '#f lexical-units)) nested-units) (push (cadr c) lexical-units)) ((eq? (car c) 'load) (setf load? (cadr c))) ((eq? (car c) 'compile) (setf compile? (cadr c))) (else (compile.unit-syntax-error c)))) (values (if source-filename `(compile.merge-filenames ,source-filename compile.source-filename '#f) 'compile.source-filename) (if binary-filename `(compile.merge-filenames ,binary-filename compile.binary-filename '#f) (if source-filename '(compile.merge-filenames compile.binary-filename compile.source-filename compile.binary-subdir) 'compile.binary-filename)) (or require '()) (nreverse nested-units) load? compile?))) (predefine (error format . args)) (define (compile.unit-syntax-error c) (error "Invalid compilation unit clause ~s." c)) (define (compile.unit-duplicate-error c) (error "Duplicate compilation unit clause ~s." c)) ;;;===================================================================== ;;; Representation and utilities ;;;===================================================================== ;;; Here are constructors and accessors for unit objects. ;;; Implementationally, the compilation unit has the following slots: ;;; ;;; * The unit name. ;;; * The source file name. ;;; * The binary file name. ;;; * A list of component file/units. ;;; * A list of units/files to require. ;;; * A load timestamp. ;;; * A timestamp to keep track of the newest source file. ;;; * Flags for compile and load. (define-struct compile.unit (predicate compile.unit?) (slots (name (type symbol)) (source-filename (type string)) (binary-filename (type string)) (components (type list)) (require (type list)) (top-level? (type bool)) (load? (type bool)) (compile? (type bool)) (delayed-loads (type list)) (load-time (type (maybe integer)) (default '#f)) (source-time (type (maybe integer)) (default '#f)) (last-update (type (maybe integer)) (default 0)) )) (define (compile.newer? t1 t2) (and t1 t2 (> t1 t2))) (define (compile.select-newest t1 t2) (if (compile.newer? t1 t2) t1 t2)) (define (compile.get-source-time u) (let ((source-file (compile.unit-source-filename u))) (if (file-exists? source-file) (file-write-date source-file) '#f))) (define (compile.get-binary-time u) (let ((binary-file (compile.unit-binary-filename u))) (if (file-exists? binary-file) (file-write-date binary-file) '#f))) (define (compile.load-source u) (load (compile.unit-source-filename u)) (setf (compile.unit-load-time u) (current-date))) (define (compile.load-binary u) (load (compile.unit-binary-filename u)) (setf (compile.unit-load-time u) (current-date))) (define (compile.compile-and-load u) (let ((source-file (compile.unit-source-filename u)) (binary-file (compile.unit-binary-filename u))) (compile-file source-file binary-file) (load binary-file) (setf (compile.unit-load-time u) (current-date)))) (define (compile.do-nothing u) u) ;;;===================================================================== ;;; Runtime support for define-compilation-unit ;;;===================================================================== (define (compile.select-delayed-loads require) (let ((result '())) (dolist (r require) (if (not (compile.unit-load? r)) (push r result))) (nreverse result))) (define (compile.merge-filenames fname1 fname2 add-subdir) (let ((place1 (filename-place fname1)) (name1 (filename-name fname1)) (type1 (filename-type fname1))) (assemble-filename (if (string=? place1 "") (if add-subdir (string-append (filename-place fname2) add-subdir) fname2) place1) (if (string=? name1 "") fname2 name1) (if (string=? type1 "") fname2 type1)))) ;;;===================================================================== ;;; Load operation ;;;===================================================================== ;;; Load-unit and compile-and-load-unit are almost identical. The only ;;; difference is that load-unit will load source files as necessary, while ;;; compile-and-load-unit will compile them and load binaries instead. (define (load-unit u) (compile.update-unit-source-times u '#f (current-date)) (compile.load-unit-aux u)) (define (compile.load-unit-aux u) (with-compilation-unit () (compile.load-unit-recursive u '#f))) (define (compile-and-load-unit u) (compile.update-unit-source-times u '#f (current-date)) (compile.compile-and-load-unit-aux u)) (define (compile.compile-and-load-unit-aux u) (with-compilation-unit () (compile.load-unit-recursive u '#t))) ;;; Load a bunch of compilation units as a group. This is useful because ;;; it can prevent repeated lookups of file timestamps. Basically, the ;;; assumption is that none of the source files will change while the loading ;;; is in progress. ;;; In case of an error, store the units left to be compiled in a global ;;; variable. (define remaining-units '()) (define (load-unit-list l) (let ((timestamp (current-date))) (dolist (u l) (compile.update-unit-source-times u '#f timestamp)) (setf remaining-units l) (dolist (u l) (compile.load-unit-aux u) (pop remaining-units)))) (define (compile-and-load-unit-list l) (let ((timestamp (current-date))) (dolist (u l) (compile.update-unit-source-times u '#f timestamp)) (setf remaining-units l) (dolist (u l) (compile.compile-and-load-unit-aux u) (pop remaining-units)))) ;;; Walk the compilation unit, updating the source timestamps. (define (compile.update-unit-source-times u newest-require timestamp) (unless (eqv? timestamp (compile.unit-last-update u)) (setf (compile.unit-last-update u) timestamp) (dolist (r (compile.unit-require u)) (if (compile.unit-top-level? r) (compile.update-unit-source-times r '#f timestamp)) (setf newest-require (compile.select-newest newest-require (compile.unit-source-time r)))) (let ((components (compile.unit-components u))) (if (not (null? components)) (let ((source-time newest-require)) (dolist (c components) (compile.update-unit-source-times c newest-require timestamp) (setf source-time (compile.select-newest source-time (compile.unit-source-time c)))) (setf (compile.unit-source-time u) source-time)) (setf (compile.unit-source-time u) (compile.select-newest newest-require (compile.get-source-time u))))))) ;;; Load a compilation unit. Do this by first loading its require list, ;;; then by recursively loading each of its components, in sequence. ;;; Note that because of the way scoping of units works and the ;;; sequential nature of the load operation, only top-level ;;; units in the require list have to be loaded explicitly. (define (compile.load-unit-recursive u compile?) (let ((components (compile.unit-components u))) ;; First recursively load dependencies. ;; No need to update time stamps again here. (dolist (r (compile.unit-require u)) (if (compile.unit-top-level? r) (compile.load-unit-aux r))) (if (not (null? components)) ;; Now recursively load subunits. (dolist (c components) (unless (not (compile.unit-load? c)) (compile.load-unit-recursive c compile?))) ;; For a leaf node, load either source or binary if necessary. (let ((source-time (compile.unit-source-time u)) (binary-time (compile.get-binary-time u)) (load-time (compile.unit-load-time u))) (cond ((compile.newer? load-time source-time) ;; The module has been loaded since it was last changed, ;; but maybe we want to compile it now. (if (and compile? (compile.unit-compile? u) (compile.newer? source-time binary-time)) (begin (compile.do-delayed-loads (compile.unit-delayed-loads u) compile?) (compile.compile-and-load u)) (compile.do-nothing u))) ((compile.newer? binary-time source-time) ;; The binary is up-to-date, so load it. (compile.load-binary u)) (else ;; The binary is out-of-date, so either load source or ;; recompile the binary. (compile.do-delayed-loads (compile.unit-delayed-loads u) compile?) (if (and compile? (compile.unit-compile? u)) (compile.compile-and-load u) (compile.load-source u))) ))))) (define (compile.do-delayed-loads units compile?) (dolist (u units) (compile.load-unit-recursive u compile?))) ;;;===================================================================== ;;; Extra stuff ;;;===================================================================== ;;; Reload a unit without testing to see if any of its dependencies are ;;; out of date. (define (reload-unit-source u) (let ((components (compile.unit-components u))) (if (not (null? components)) (dolist (c components) (reload-unit-source c)) (compile.load-source u)))) (define (reload-unit-binary u) (let ((components (compile.unit-components u))) (if (not (null? components)) (dolist (c components) (reload-unit-binary c)) (compile.load-binary u)))) ;;; Find a (not necessarily top-level) compilation unit with the given ;;; name. (define (find-unit name) (compile.find-unit-aux name compilation-units)) (define (compile.find-unit-aux name units) (block find-unit-aux (dolist (u units '#f) (if (eq? name (compile.unit-name u)) (return-from find-unit-aux u) (let* ((components (compile.unit-components u)) (result (compile.find-unit-aux name components))) (if result (return-from find-unit-aux result))))))) ;;; Combine the two above: reload a compilation unit. (define-syntax (reload name) `(reload-unit-source (or (find-unit ',name) (error "Couldn't find unit named ~s." ',name))))