;; -*- mode: Lisp; Syntax: Common-Lisp; Package: cells; -*-
#|

    Cells -- Automatic Dataflow Managememnt

Copyright (C) 1995, 2006 by Kenneth Tilton

This library is free software; you can redistribute it and/or
modify it under the terms of the Lisp Lesser GNU Public License
 (http://opensource.franz.com/preamble.html), known as the LLGPL.

This library is distributed  WITHOUT ANY WARRANTY; without even 
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the Lisp Lesser GNU Public License for more details.

|#

(in-package :cells)

(defparameter *ide-app-hard-to-kill* t)

(defun md-slot-value (self slot-name &aux (c (md-slot-cell self slot-name)))
  (when (and (not *not-to-be*) (mdead self))
    ;#-its-alive!
    (unless *stop*
      (trc nil "md-slot-value passed dead self:" self :asked4slot slot-name :cell c)
      ;#-sbcl (inspect self)
      ;(setf *stop* t)
      ;(break "md-slot-value sees dead ~a" self)
      )
    (return-from md-slot-value (slot-value self slot-name))) ;; we can dream
  (tagbody
    retry
    (when *stop*
      (if *ide-app-hard-to-kill*
          (progn
            (princ #\.)
            (princ "stopped")
            (return-from md-slot-value))
        (restart-case
            (error "Cells is stopped due to a prior error.")
          (continue ()
            :report "Return a slot value of nil."
            (return-from md-slot-value nil))
          (reset-cells ()
            :report "Reset cells and retry getting the slot value."
            (cells-reset)
            (go retry))))))
  
  ;; (count-it :md-slot-value slot-name)
  (if c
      (cell-read c)
    (values (slot-value self slot-name) nil)))

(defun cell-read (c)
  (assert (typep c 'cell))
  (prog1
      (with-integrity ()
        (ensure-value-is-current c :c-read nil))
    (when *depender*
      (record-caller c))))
  
(defun chk (s &optional (key 'anon))
  (when (mdead s)
    (break "model ~a is dead at ~a" s key)))

(defvar *trc-ensure* nil)

(defun qci (c)
  (when c
    (cons (md-name (c-model c)) (c-slot-name c))))


(defun ensure-value-is-current (c debug-id ensurer)
  ;
  ; ensurer can be used cell propagating to callers, or an existing caller who wants to make sure
  ; dependencies are up-to-date before deciding if it itself is up-to-date
  ;
  (declare (ignorable debug-id ensurer))
  ;(count-it! :ensure.value-is-current)
  ;(trc "evic entry" (qci c))
  (wtrcx (:on? nil) ("evic>" (qci c) debug-id (qci ensurer))
    ;(count-it! :ensure.value-is-current )
    #+chill 
    (when ensurer ; (trcp c)
      (count-it! :ensure.value-is-current (c-slot-name c) (md-name (c-model c))(c-slot-name ensurer) (md-name (c-model ensurer))))
    #+chill
    (when (and *c-debug* (trcp c)
            (> *data-pulse-id* 650))
      (bgo ens-high))
    
    (trc nil ; c ;; (and *c-debug* (> *data-pulse-id* 495)(trcp c))
      "ensure.value-is-current > entry1" debug-id (qci c) :st (c-state c) :vst (c-value-state c)
      :my/the-pulse (c-pulse c) *data-pulse-id* 
      :current (c-currentp c) :valid (c-validp c))
    
    #+nahhh
    (when ensurer
      (trc (and *c-debug* (> *data-pulse-id* 495)(trcp c))
        "ensure.value-is-current > entry2" 
        :ensurer (qci ensurer)))
    
    (when *not-to-be*
      (when (c-unboundp c)
        (error 'unbound-cell :cell c :instance (c-model c) :name (c-slot-name c)))
      (return-from ensure-value-is-current
        (when (c-validp c) ;; probably accomplishes nothing
          (c-value c))))
    
    (when (and (not (symbolp (c-model c))) ;; damn, just here because of playing around with global vars and cells
            (eq :eternal-rest (md-state (c-model c))))
      (break "model ~a of cell ~a is dead" (c-model c) c))
    
    (cond
     ((c-currentp c)
      (count-it! :ensvc-is-indeed-currentp)
      (trc nil "EVIC yep: c-currentp" c)
      ) ;; used to follow c-inputp, but I am toying with letting ephemerals (inputs) fall obsolete
     ;; and then get reset here (ie, ((c-input-p c) (ephemeral-reset c))). ie, do not assume inputs are never obsolete
     ;;
     ((and (c-inputp c)
        (c-validp c) ;; a c?n (ruled-then-input) cell will not be valid at first
        (not (and (typep c 'c-dependent)
               (eq (cd-optimize c) :when-value-t)
               (null (c-value c)))))
      (trc nil "evic: cool: inputp" (qci c)))
     
     ((or (bwhen (nv (not (c-validp c)))
            (count-it! :ens-val-not-valid)
            (trc nil "not c-validp, gonna run regardless!!!!!!" c)
            nv)
        ;;
        ;; new for 2006-09-21: a cell ended up checking slots of a dead instance, which would have been
        ;; refreshed when checked, but was going to be checked last because it was the first used, useds
        ;; being simply pushed onto a list as they come up. We may need fancier handling of dead instance/cells
        ;; still being encountered by consulting the prior useds list, but checking now in same order as
        ;; accessed seems Deeply Correct (and fixed the immediate problem nicely, always a Good Sign).
        ;;
        (labels ((check-reversed (useds)
                   (when useds
                     (or (check-reversed (cdr useds))
                       (let ((used (car useds)))
                         (ensure-value-is-current used :nested c)
                         #+slow (trc nil "comparing pulses (ensurer, used, used-changed): "  c debug-id used (c-pulse-last-changed used))
                         (when (> (c-pulse-last-changed used)(c-pulse c))
                           (count-it! :ens-val-someused-newer)
                           (trc nil "used changed and newer !!!!######!!!!!! used" (qci used) :oldpulse (c-pulse used)
                             :lastchg (c-pulse-last-changed used))
                           #+shhh (when (trcp c)
                                    (describe used))
                           t))))))
          (assert (typep c 'c-dependent))
          (check-reversed (cd-useds c))))
      (trc nil "kicking off calc-set of!!!!" (c-state c) (c-validp c) (qci c) :vstate (c-value-state c)
        :stamped (c-pulse c) :current-pulse *data-pulse-id*)
      (calculate-and-set c :evic ensurer)
      (trc nil "kicked off calc-set of!!!!" (c-state c) (c-validp c) (qci c) :vstate (c-value-state c)
        :stamped (c-pulse c) :current-pulse *data-pulse-id*))
     
     ((mdead (c-value c))
      (trc nil "ensure.value-is-current> trying recalc of ~a with current but dead value ~a" c (c-value c))
      (let ((new-v (calculate-and-set c :evic-mdead ensurer)))
        (trc nil "ensure.value-is-current> GOT new value ~a to replace dead!!" new-v)
        new-v))
     
     (t (trc nil "ensure.current decided current, updating pulse" (c-slot-name c) debug-id)
       (c-pulse-update c :valid-uninfluenced)))
    
    (when (c-unboundp c)
      (error 'unbound-cell :cell c :instance (c-model c) :name (c-slot-name c)))
    
    (bwhen (v (c-value c))
      (if (mdead v)
          (progn
            #-its-alive!
            (progn
              (format t "~&on pulse ~a ensure.value still got and still not returning ~a dead value ~a" *data-pulse-id* c v)
              (inspect v))
            nil)
        v))))


(defun calculate-and-set (c dbgid dbgdata)
  (declare (ignorable dbgid dbgdata)) ;; just there for inspection of the stack during debugging
  (flet ((body ()
           (when (c-stopped)
             (princ #\.)
             (return-from calculate-and-set))

           #-its-alive!
           (bwhen (x (find c *call-stack*)) ;; circularity
             (unless nil ;; *stop*
               (let ()
                 (inspect c)
                 (trc "calculating cell:" c (cr-code c))
                 (trc "appears-in-call-stack (newest first): " (length *call-stack*))
                 (loop for caller in (copy-list *call-stack*)
                     for n below (length *call-stack*)
                     do (trc "caller> " caller #+shhh (cr-code caller))
                       when (eq caller c) do (loop-finish))))
             (setf *stop* t)  
             (c-break ;; break is problem when testing cells on some CLs
              "cell ~a midst askers (see above)" c)
             (error 'asker-midst-askers :cell c))
  
           (multiple-value-bind (raw-value propagation-code)
               (calculate-and-link c)
             
             (when (and *c-debug* (typep raw-value 'cell))
               (c-break "new value for cell ~s is itself a cell: ~s. probably nested (c? ... (c? ))"
                 c raw-value))
             
             (unless (c-optimized-away-p c)
               ; this check for optimized-away-p arose because a rule using without-c-dependency
               ; can be re-entered unnoticed since that clears *call-stack*. If re-entered, a subsequent
               ; re-exit will be of an optimized away cell, which we need not sv-assume on... a better
               ; fix might be a less cutesy way of doing without-c-dependency, and I think anyway
               ; it would be good to lose the re-entrance.
               (md-slot-value-assume c raw-value propagation-code)))))
    (if (trcp c) ;; *dbg*
        (wtrc (0 100 "calcnset" c) (body))
      (body))))

(defun calculate-and-link (c)
  (let ((*call-stack* (cons c *call-stack*))
        (*depender* c)
        (*defer-changes* t))
    (assert (typep c 'c-ruled))
    (trc nil "calculate-and-link" c)
    (cd-usage-clear-all c)
    (multiple-value-prog1
        (funcall (cr-rule c) c)
      (c-unlink-unused c))))


;-------------------------------------------------------------

(defun md-slot-makunbound (self slot-name
                            &aux (c (md-slot-cell self slot-name)))
  (unless c
    (c-break ":md-slot-makunbound > cellular slot ~a of ~a cannot be unbound unless initialized as inputp"
      slot-name self))
  
  (when (c-unboundp c)
    (return-from md-slot-makunbound nil))

  (when *within-integrity* ;; 2006-02 oops, bad name
    (c-break "md-slot-makunbound of ~a must be deffered by wrapping code in with-integrity" c))
  
  ; 
  ; Big change here for Cells III: before, only the propagation was deferred. Man that seems
  ; wrong. So now the full makunbound processing gets deferred. Less controversially,
  ; by contrast the without-c-dependency wrapped everything, and while that is harmless,
  ; it is also unnecessary and could confuse people trying to follow the logic.
  ;
  (let ((causation *causation*))
    (with-integrity (:change c)
      (let ((*causation* causation))
        ; --- cell & slot maintenance ---
        (let ((prior-value (c-value c)))
          (setf (c-value-state c) :unbound
            (c-value c) nil
            (c-state c) :awake)
          (bd-slot-makunbound self slot-name)
          ;
           ; --- data flow propagation -----------
          ;
          (without-c-dependency
              (c-propagate c prior-value t)))))))

;;; --- setf md.slot.value --------------------------------------------------------
;;;

(defun (setf md-slot-value) (new-value self slot-name
                              &aux (c (md-slot-cell self slot-name)))
  #+shhh (when *within-integrity*
    (trc "mdsetf>" self (type-of self) slot-name :new new-value))
  (when *c-debug*
    (c-setting-debug self slot-name c new-value))
  
  (unless c
    (c-break "cellular slot ~a of ~a cannot be SETFed because it is not 
mediated by a Cell with :inputp t. To achieve this, the initial value ~s -- whether 
supplied as an :initform, :default-initarg, or at make-instance time via 
an :initarg -- should be wrapped in either macro C-IN or C-INPUT. 
In brief, initialize ~0@*~a to (c-in ~2@*~s) instead of plain ~:*~s"
      slot-name self (slot-value self slot-name)))

  (cond
   ((find (c-lazy c) '(:once-asked :always t))
    (md-slot-value-assume c new-value nil))

   (*defer-changes*
    (c-break "SETF of ~a must be deferred by wrapping code in WITH-INTEGRITY" c))

   (t
    (with-integrity (:change slot-name)
      (md-slot-value-assume c new-value nil))))

  ;; new-value 
  ;; above line commented out 2006-05-01. It seems to me we want the value assumed by the slot
  ;; not the value setf'ed (on rare occasions they diverge, or at least used to for delta slots)
  ;; anyway, if they no longer diverge the question of which to return is moot
  )
                    
(defun md-slot-value-assume (c raw-value propagation-code)
  (assert c)
  (trc nil "md-slot-value-assume entry" (qci c)(c-state c))
  (without-c-dependency
      (let ((prior-state (c-value-state c))
            (prior-value (c-value c))
            (absorbed-value (c-absorb-value c raw-value)))

        (c-pulse-update c :slotv-assume)

        ; --- head off unchanged; this got moved earlier on 2006-06-10 ---
        (when (and (not (eq propagation-code :propagate))
                (find prior-state '(:valid :uncurrent))
                (c-no-news c absorbed-value prior-value))
          (setf (c-value-state c) :valid) ;; new for 2008-07-15
          (trc nil "(setf md-slot-value) > early no news" propagation-code prior-state prior-value  absorbed-value)
          (count-it :nonews)
          (return-from md-slot-value-assume absorbed-value))

        ; --- slot maintenance ---
        
        (unless (c-synaptic c) 
          (md-slot-value-store (c-model c) (c-slot-name c) absorbed-value))
        
        ; --- cell maintenance ---
        (setf
         (c-value c) absorbed-value
         (c-value-state c) :valid
         (c-state c) :awake)
        
        (case (and (typep c 'c-dependent)
                   (cd-optimize c))
          ((t) (c-optimize-away?! c)) ;;; put optimize test here to avoid needless linking
          (:when-value-t (when (c-value c)
                           (c-unlink-from-used c))))
        
        ; --- data flow propagation -----------
        (unless (eq propagation-code :no-propagate)
          (trc nil "md-slot-value-assume flagging as changed: prior state, value:" prior-state prior-value )
          (c-propagate c prior-value (cache-state-bound-p prior-state)))  ;; until 06-02-13 was (not (eq prior-state :unbound))
        (trc nil "exiting md-slot-val-assume" (c-state c) (c-value-state c))
        absorbed-value)))

(defun cache-bound-p (c)
  (cache-state-bound-p (c-value-state c)))

(defun cache-state-bound-p (value-state)
  (or (eq value-state :valid)
    (eq value-state :uncurrent)))

;---------- optimizing away cells whose dependents all turn out to be constant ----------------
;

(defun flushed? (c)
  (rassoc c (cells-flushed (c-model c))))

(defun c-optimize-away?! (c)
  #+shhh (trc nil "c-optimize-away?! entry" (c-state c) c)
  (when (and (typep c 'c-dependent)
          (null (cd-useds c))
          (cd-optimize c)
          (not (c-optimized-away-p c)) ;; c-streams (FNYI) may come this way repeatedly even if optimized away
          (c-validp c) ;; /// when would this not be the case? and who cares?
          (not (c-synaptic c)) ;; no slot to cache invariant result, so they have to stay around)
          (not (c-inputp c)) ;; yes, dependent cells can be inputp
          )
    ;; (when (trcp c) (break "go optimizing ~a" c))
    
    (when (trcp c)
      (trc "optimizing away" c (c-state c) (rassoc c (cells (c-model c)))(rassoc c (cells-flushed (c-model c))))
      )

    (count-it :c-optimized)
    
    (setf (c-state c) :optimized-away)
    
    (let ((entry (rassoc c (cells (c-model c)))))
      (unless entry
        (describe c)
        (bwhen (fe (rassoc c (cells-flushed (c-model c))))
          (trc "got in flushed thoi!" fe)))
      (c-assert entry)
      ;(trc (eq (c-slot-name c) 'cgtk::id) "c-optimize-away?! moving cell to flushed list" c)
      (setf (cells (c-model c)) (delete entry (cells (c-model c))))
      #-its-alive! (push entry (cells-flushed (c-model c)))
      )
    
    (dolist (caller (c-callers c) )
      ;
      ; example: on window shutdown with a tool-tip displayed, the tool-tip generator got
      ; kicked off and asked about the value of a dead instance. That returns nil, and
      ; there was no other dependency, so the Cell then decided to optimize itself away.
      ; of course, before that time it had a normal value on which other things depended,
      ; so we ended up here. where there used to be a break.
      ;
      (setf (cd-useds caller) (delete c (cd-useds caller)))
      ;;; (trc "nested opti" c caller)
      (c-optimize-away?! caller) ;; rare but it happens when rule says (or .cache ...)
      )))

    
