git.fiddlerwoaroof.com
Raw Blame History
;;; 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 <filename>)
;;; (binary-filename <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 <boolean>)
;;;   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 <boolean>)
;;;   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))))