dep: Provide semantics for setting the value of a non-leaf dep.
authorMark Wooding <mdw@distorted.org.uk>
Mon, 9 Jun 2008 22:50:35 +0000 (23:50 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Mon, 9 Jun 2008 23:58:59 +0000 (00:58 +0100)
The behaviour is that we recompute the rest of the system as usual, but
retain dependencies on the deps that the value-function examines.

We do more: it's now possible for cycles to occur in the dependency
graph, as long as a value is explicitly set somewhere in the cycle,
which effectively breaks it.  Cycles of purely computational deps are
still considered to be meaningless, and therefore forbidden.

Finally, we note that DELAY-RECOMPUTING-DEPS can be used to set
explicitly the values of a number of deps simultaneously, even if their
value functions would otherwise forbid it.

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: