Ken,
thanks a lot for all the insight. First things first, using c?n for the kids works like a charm. setf on a c? cell still produces an error, suggesting to initialize the cell with c-in. Anyway, that's settled now.
As to the not-to-be issue, everything can be reproduced using test-gtk. I can share that in the cells cvs, just let me know whether it would be ok to restructure the cvs to match the old cells-gtk directory structure (I'd rather keep it the way it is in the old cells-gtk, so that I can commit it in there one day).
I did some narrowing it down and going through the back trace. So let us look at an example:
root node 1 ---- observer 1 node 1.1 ---- observer 1.1 node 2 ---- observer 2
The basic code for the observer is
(defmodel family-observer (family) ;; we'll use the "value" slot for the observed ((row :reader row :initarg :row) (:default-initargs :kids (kids-list? (bwhen (val (^value)) (mapcar #'(lambda (src) (mk-observer self src)) (kids val)))))))) :row (c? (when-bind* ((parent (upper self)) (pos (position self (kids parent)))) (let ((new-row (tree-row-create (row parent) (id parent)))) (when (tree-row-valid new-row) (tree-row-set-path new-row (row parent) pos) new-row)))))
Where mk-observer is a method specializing on both parameters, so that we can have different kinds of observers on the same type of targets.
The row is some gui object to be kept in sync.
Now we remove node1:
(with-integrity (:change 'tv-del-node) (setf (kids (upper node1)) (remove node1 (kids (upper node1)))))
Then first node2 and observer2 die, ok. Then node1 dies, and so does observer1.
The interesting part:
not-to-be :before on observer 1.1 is called -- and at this point observer 1.1 itself is already :eternal-rest, in other words, not-to-be is called on a dead object. Now not-to-be of the observer wishes to do something, so it accesses a (ruled) slot of the passed object:
(defmethod not-to-be :before ((self cells-tree-node)) (tree-row-destroy (row self))
The call to the accessor (row self) with the dead self triggers a bunch of cells calls:
Backtrace: 0: (CELLS::ENSURE-VALUE-IS-CURRENT NIL #<unused argument> #<unused argument>) 1: ((LABELS CELLS::CHECK-REVERSED) (NIL)) 2: (CELLS::ENSURE-VALUE-IS-CURRENT (NIL . <vld>)=492/OPTIMIZED-AWAY/ROW/DEAD!NODE-TREE-NODE3440] #<unused argument> #<unused argument>) 3: ((LAMBDA (CELLS::OPCODE CELLS::DEFER-INFO)) #<unused argument> #<unused argument>) 4: (CELLS::CALL-WITH-INTEGRITY NIL NIL #<CLOSURE (LAMBDA (CELLS::OPCODE CELLS::DEFER-INFO)) {BBBD3DD}>) 5: (CELLS::CELL-READ (NIL . <vld>)=492/OPTIMIZED-AWAY/ROW/DEAD!NODE-TREE-NODE3440]) 6: (CELLS::MD-SLOT-VALUE DEAD!NODE-TREE-NODE3440 CELLS-GTK::ROW)
The last call is in
(defun ensure-value-is-current (c debug-id ensurer) (declare (ignorable debug-id ensurer)) (count-it :ensure-value-is-current) (when (and (not (symbolp (c-model c)))(eq :eternal-rest (md-state (c-model c)))) (break "model ~a of cell ~a is dead" (c-model c) c))
.... in particular the form:
(c-model c)
which breaks with:
The value NIL is not of type CELL. [Condition of type TYPE-ERROR]
To sum up, I believe the problem is that at a change of the kids list - First the kids are declared dead - Then not-to-be is called recursively and thus not-to-be is passed a dead self.
However, I wish to do some cleanup work when kids are kicked out, and for this I need to access a few slots.
What I'd like to have is - not-to-be being called before the object is declared dead or - another method (last-will?) to be executed right before the kids die or - an interims state (:zombie?) in which cell slots are still accessible with their last cached value
Or is the solution to have an observer on the kids slot instead of a not-to-be-method, which does the cleanup work for the kids?
I was thinking while doing the dishes. I can image the observer class being interesting in its own right, and slots over there ending up dependent on the same original kids-list in some way, as well of course as the value of the observer. Propagation would then try to update this slot and when it got to the value of the observer find a dead instance. Any rule that got to the value of the observer by accessing the list of observers (say something iterating over them) would not encounter such an observer/value, but rules lower down that get at the value directly will.
This is an interesting idea, but I doubt that this is my problem here. It really seems to be the relation between ruled kids slots, declaring cells dead, and not-to-be.
Now normally this is not a problem because such lower down rules would tend not to depend also on the original list, and indeed the reason I have left this unaddressed is that in most cases I have seen a simple way to rewrite my rules that was even better and which did not end up with these widespread dependencies (if you have followed me so far on that, and if I hasten to add all this guesswork is on the money). I like to hold out for Real Problems before whacking away at the code, I think that is a slippery slope.
That is surely true, as this case proves. I feel what we're looking for here is something like family-finalizers which are specified to be called everytime a kid dies.
btw, all that stuff in their that worries about dead instances is preemptive safeguard stuff -- I think if you disable that most things will just work. The rules that are failing now will run harmlessly and in a few cycles everything gets cleaned up anyway. Cells ran for /years/ with this happening to no ill effect (until RoboCup, of all things).
Yep, which is why it broke after introducing cells3 :-)
OTOH, I see why it is good to have these safe guards. I hacked a solution today to the fm-other tree searches which were all over cells-gtk -- now we have with-widget and with-widget-value which do the right thing without kicking off tree searches (I introduced an automatically maintained hashtable of active instances hashing by md-name, like I did in cells-ode).
If you want to send me your whole project I will look to see how I would rewrite the rules if that is even possible, and if not take a look at solving this formally.
As I said, I like to try things out in test-gtk first, so that I can isolate the error (and create a nice demo on the way). I will work through cvs and commit it tomorrow, I hope. I just don't want to force you to have to deal with my whole project -- and all the other issues it has.
fyi, in the past I have done silly things like having Cells just return nil on slot-value access to dead cells, but we may want to find something more elegant. :)
I have such code in my project, too:
(defun deadp (cell) (eql (slot-value cell 'cells::.md-state) :eternal-rest))
However, since I need the slot-value in my case, this does not help ;-)
Thanks again, Peter