dep: Provide semantics for setting the value of a non-leaf dep.
[lisp] / dep.lisp
index 5903743..c2d1008 100644 (file)
--- a/dep.lisp
+++ b/dep.lisp
@@ -24,7 +24,7 @@
 (defpackage #:dep
   (:use #:common-lisp #:queue)
   (:export #:dep #:depp #:make-dep #:dep-goodp
-          :delay-recomputing-deps
+          #:delay-recomputing-deps
           #:install-dep-syntax
           #:dep-value #:dep-make-bad #:dep-bad #:dep-try
           #:dep-add-listener))
 ;;;--------------------------------------------------------------------------
 ;;; Dependencies.
 
+(defvar *generation* (list '*generation*)
+  "Generation marker, used to remember when we last updated a particular dep.
+   Essentially, if the dep's generation matches *GENERATION* then it doesn't
+   need updating again.")
+
+(defvar *evaluating-dep* nil
+  "The dep currently being evaluated.  This is bound only during the call of
+   a value-func, and is used to track the dependencies implied during the
+   function's evaluation.")
+
+(defvar *pending-deps* nil
+  "A queue of deps pending recomputation.  This is bound to a queue during
+   recomputation and restored afterwards, so it can also be used as a flag to
+   detect whether recomputation is happening.")
+
 (defstruct (dep (:predicate depp)
                (:constructor %make-dep))
   "There are two kinds of `dep', though we use the same object type for both.
   (value-predicate #'eql :type function)
   (goodp nil :type boolean)
   (state :pending :type (member :stable :pending :recomputing))
+  (generation *generation* :type list)
   (listeners nil :type list)
   (dependents nil :type list))
 
-(defvar *evaluating-dep* nil
-  "The dep currently being evaluated.  This is bound only during the call of
-   a value-func, and is used to track the dependencies implied during the
-   function's evaluation.")
-
-(defvar *pending-deps* nil
-  "A queue of deps pending recomputation.  This is bound to a queue during
-   recomputation and restored afterwards, so it can also be used as a flag to
-   detect whether recomputation is happening.")
-
 (defun kick-dep (dep)
   "Call when DEP's value (or good/bad state) has changed.  Marks the
    dependents of DEP as :pending, if they're currently :stable, and then
    clears the dependent list.  Also invokes DEP's listener functions."
+  (setf (dep-generation dep) *generation*)
   (dolist (d (dep-dependents dep))
     (when (eq (dep-state d) :stable)
       (enqueue d *pending-deps*)
 (defun recompute-dep (dep)
   "Recompute the value of DEP.  This function is careful to trap nonlocal
    transfers from the value-func."
-  (let ((winning nil))
-    (unwind-protect
-        (catch 'dep-bad
-          (setf (dep-state dep) :recomputing)
-          (when (update-dep dep (let ((*evaluating-dep* dep))
-                                  (funcall (dep-value-func dep))))
-            (kick-dep dep))
-          (setf winning t))
-      (unless winning
-       (when (update-dep dep nil nil)
-         (kick-dep dep))))))
+  (unless (eq (dep-generation dep) *generation*)
+    (let ((winning nil))
+      (unwind-protect
+          (catch 'dep-bad
+            (setf (dep-state dep) :recomputing)
+            (when (update-dep dep (let ((*evaluating-dep* dep))
+                                    (funcall (dep-value-func dep))))
+              (kick-dep dep))
+            (setf winning t))
+       (unless winning
+         (when (update-dep dep nil nil)
+           (kick-dep dep)))))))
 
 (defun recompute-deps ()
   "Recompute all the pending deps, and any others that depend on them."
   "Notifies DEP of a change in its value.  If a recomputation phase is
    currently under way, queue the dependents and leave fixing things up to
    the outer loop; otherwise start up a recomputation phase."
-  (if *pending-deps*
-      (kick-dep dep)
-      (let ((*pending-deps* (make-queue)))
-       (kick-dep dep)
-       (recompute-deps))))
+  (setf *generation* (list '*generation*))
+  (flet ((kick (dep)
+          (kick-dep dep)
+          (when (dep-value-func dep)
+            (catch 'dep-bad
+              (let ((*evaluating-dep* dep))
+                (funcall (dep-value-func dep)))))))
+    (if *pending-deps*
+       (kick dep)
+       (let ((*pending-deps* (make-queue)))
+         (kick dep)
+         (recompute-deps)))))
 
 (defun (setf dep-value) (value dep)
   "Set DEP's value to be VALUE (and mark it as being good)."
-  (when (dep-value-func dep)
-    (error "Not a leaf dep."))
-  (when (update-dep dep value)
-    (pulse-dep dep))
+  (when (update-dep dep value) (pulse-dep dep))
   value)
 
 (defun dep-make-bad (dep)
   "Mark DEP as being bad."
-  (when (dep-value-func dep)
-    (error "Not a leaf dep."))
-  (when (update-dep dep nil nil)
-    (pulse-dep dep)))
+  (when (update-dep dep nil nil) (pulse-dep dep)))
 
 (defun dep-add-listener (dep func)
   "Add a listener function FUNC to the DEP.  The FUNC is called each time the
 (defun make-dep (&rest args)
   "Create a new DEP object.  There are two basic argument forms:
 
-   (:leaf &optional OBJECT)
+   (:value &optional OBJECT)
        Return a leaf dep, whose value is OBJECT; if no OBJECT is given, the
-       dep is initially bad.
+       dep is initially bad.  The keyword :leaf is accepted as a synonym.
 
    (:function FUNCTION)
        Return a non-leaf dep whose value is computed by FUNCTION.
 
-   Additionally, if the first argument is something other than :leaf or
-   :function (ideally not a keyword, for forward compatibility), then the
+   Additionally, if the first argument is something other than :VALUE or
+   :FUNCTION (ideally not a keyword, for forward compatibility), then the
    first argument is inspected: if it's a function, then a function dep is
    retuerned (as if you'd specified :function); otherwise a leaf dep is
    returned.
 
+   Finally, it's possible to specify both :value and :function
+   simultaneously; this will set the initial values as requested, but
+   recompute them as necessary.  It is possible to establish dependency
+   cycles, but you need to suppress recomputation in order to do this
+   correctly -- see the DELAY-RECOMPUTING-DEPS macro.
+
    If no arguments are given, a bad leaf dep is returned."
 
-  (flet ((arg (&optional (default nil defaultp))
-          (cond (args (pop args))
-                (defaultp default)
-                (t (error "Not enough arguments to MAKE-DEP.")))))
+  (flet ((arg () (if args (pop args)
+                    (error "Not enough arguments to MAKE-DEP."))))
 
     ;; Sort out the arguments.
-    (multiple-value-bind (type value goodp)
-      (if (null args)
-         (values :leaf nil nil)
-         (let ((indicator (pop args)))
-           (cond ((eq indicator :leaf)
-                  (if args
-                      (values :leaf (pop args) t)
-                      (values :leaf nil nil)))
-                 ((eq indicator :function)
-                  (values :function (arg) nil))
-                 ((functionp indicator)
-                  (values :function indicator nil))
-                 (t
-                  (values :leaf indicator t)))))
-      (unless (endp args)
-       (error "Excess arguments to MAKE-DEP."))
+    (let ((value nil)
+         (valuep nil)
+         (function nil))
+      (do () ((endp args))
+       (let ((indicator (pop args)))
+         (cond ((or (eq indicator :value)
+                    (eq indicator :leaf))
+                (if args
+                    (setf value (pop args) valuep t)
+                    (setf value nil valuep t)))
+               ((eq indicator :function)
+                (setf function (arg)))
+               ((functionp indicator)
+                (setf function indicator))
+               (t
+                (setf value indicator valuep t)))))
 
       ;; Create the object appropriately.
-      (case type
-       (:function
-        (let ((dep (%make-dep :value-func value :state :pending)))
-          (if *pending-deps*
-              (enqueue dep *pending-deps*)
+      (let ((dep (%make-dep :value-func function
+                           :%value value
+                           :state (if valuep :stable :pending)
+                           :generation (if function nil *generation*)
+                           :goodp valuep)))
+       (cond ((not function) t)
+             (valuep (pulse-dep dep))
+             (*pending-deps*
+              (enqueue dep *pending-deps*))
+             (t
               (let ((*pending-deps* (make-queue)))
                 (enqueue dep *pending-deps*)
-                (recompute-deps)))
-          dep))
-       (:leaf
-        (%make-dep :%value value :goodp goodp :state :stable))))))
+                (recompute-deps))))
+         dep))))
 
 (defmacro dep-try (expr &body body)
   "Evaluate EXPR.  If it throws dep-bad then evaluate BODY instead."
   (throw 'dep-bad nil))
 
 (defun delay-recomputing-deps* (thunk)
-  "The guts of the DELAY-RECOMPUTATING-DEPS macro.  Evaluate THUNK without
+  "The guts of the DELAY-RECOMPUTING-DEPS macro.  Evaluate THUNK without
    immediately updating dependencies until THUNK finishes.  Returns the
    value(s) of THUNK."
   (if *pending-deps*
       (funcall thunk)
       (let ((*pending-deps* (make-queue)))
+       (setf *generation* (list '*generation*))
        (multiple-value-prog1
            (funcall thunk)
          (recompute-deps)))))
    recomputed as necessary); higher-level dependents won't be noticed until
    the direct dependents are recomputed.
 
-   This form is intended to be used for bulk update to leaves, for which
-   purpose it is fairly safe."
-  `(delay-recomputating-deps* #'(lambda () ,@body)))
+   It can be used to apply a number of updates simultaneously to the system.
+   This is useful for two reasons:
+
+     * Firstly, it avoids the computational overheads of propagating changes
+       repeatedly, so it can be used as a simple optimization.
+
+     * Secondly, and perhaps more interestingly, it allows the values of
+       mutually-dependent deps to be set simultaneously, even though the
+       values being set may not be compatible with the deps' value
+       functions."
+  `(delay-recomputing-deps* #'(lambda () ,@body)))
 
 (defun install-dep-syntax (&optional (readtable *readtable*))
   "Installs into the given READTABLE some syntactic shortcuts: