Subject: doing :initialize/:recycle "right"

From: Roger Crew <[email-masked]>
Date: Thu, 10 Sep 1992 15:05:30 -0700

...Erik/JoeF just triggered one of my backlogged drafts...  
...Sorry about the length on this, it's been growing for a while...

>	 2) chparent().  ... is a major pain because, unlike create()
>    and recycle(), it doesn't let any of the objects involved know
>    what's going on.

The problem isn't just chparent().  create() and recycle() don't always let
the object know either.  Yes, :initialize and :recycle get called
respectively, but it may not necessarily be the :initialize/:recycle you wrote
if we're talking about an object that is grandchild or lower in the hierarchy.

Consider, for example, the current problem we have with .owned_objects where 
we rely on all :initialize and :recycle verbs to actually pass(@args)
somewhere in order to ensure that #1:initialize/recycle eventually get
called to update .owned_objects.  There are lots of other things that should 
be handled using :initialize/:recycle (e.g., the quota system) that can't 
currently be done that way precisely because we can't trust those routines 
to always be called

The purpose of :initialize/:recycle as I see it:

  making it possible to maintain some invariant governing the descendants 
  of a given object, 

much the way :accept/:enterfunc/:exitfunc can be used to maintain an
invariant governing the contents of a given room.  A typical invariant might be

  "for every descendant of this object, 
   there is an entry on such-and-such a list".

Consider LambdaCore's $exit:recycle(), which tries to keep the corresponding 
room's .exits and .entrances lists up to date and thus guarantee that these 
lists don't contain invalid objects.

Problems:

1.  [as noted above]  There is no guarantee that a particular object's
    :initialize/:recycle verb gets called, whether because of a descendant
    refusing to pass(@args) or someone's use of chparent().

Adding hooks to chparent() is only a partial solution and partial solutions
are basically worthless, since if you can't trust the mechanism you have to 
be constantly checking that it hasn't been circumvented, which usually ends 
up being more effort than to simply do things a different way.  

So I think we need something more comprehensive.  First let's brush aside an
easier problem:

2.  There is no way to distinguish between a legitimate call to :initialize(),
    one resulting from a call to the create() builtin and an ordinary verb 
    call.  The callers() stack looks the same in both cases.  

This is actually a general problem with server hooks, though for top-level
hooks like :server_started or :do_login_command, it suffices to make sure
there aren't any callers.  However, for :accept, :exitfunc, :enterfunc,
:initialize and :recycle, life is not so easy.

Seeing as the task_permissions and object-id of the entity attempting the
move(), create() or recycle() operation are relevant in :exitfunc, etc...,
modifying the behavior of caller_perms() or caller is probably not the right
way to go here.  Nor do I like the idea of modifying callers(), never mind
that anything that increases the dependence on callers() is a lose IMHO.

I think the simplest answer here is to put the x verbflag to its intended 
use, i.e., !x means only the server can call this verb.  The only change
required would be to have the server recognize -x verbs when searching for 
hook verbs the way it does when it searches for command verbs.  This way,
if, say, I were to make an object:exitfunc -x, I would be guaranteed that 
only a move(object) call could invoke it.

To get back to problem (1) and what to do about it, the key point is that in
order for hook routines (:initialize, etc...) to be useful there has to be a
way to guarantee that they always get called, whether you use create() or some
bizarre combination of chparent()s.  What exactly this means is unclear
though:

      a        suppose I want to chparent(x,e), and suppose each of
     / \       b, c, d, and e is trying to enforce some particular condition
    b   d      on each of their descendants, then something has to ensure
   /     \     that all 4 of those hooks (call them :gain_a_kid/:lose_a_kid 
  c       e    for now) get called.  Then we're in for trouble when these hook
 /             routines can themselves do arbitrarily bad things to the 
x              inheritance hierarchy.

Consider the case where one of c:lose_a_kid, b:lose_a_kid, or d:gain_a_kid 
does chparent(e,x).  With the restriction on properties, there are other fun 
possibilities in which b:lose_a_kid adds a property to x that is defined on e.

Where should x be left in the heirarchy when bad things happen?  If we don't
choose a good place, then that leaves open potential ways to circumvent these
hooks.

[...I just deleted 3 huge paragraphs describing possible locking mechanisms
    to prevent hook routines from screwing with the inheritance hierarchy.  
    I've decided *that* cure is worse than the disease...]

The bottom line is that chparent() is just too damn complicated; if it has
to call a sequence of hooks --- and I think there's no way around this ---
then there's no way it should be viewed as an atomic operation.  Likewise for
create() and recycle(), they are all fairly complicated beasts that we should
reduce to a simpler set of building blocks.

So here's the proposed set of builtins to take their places:

new(owner)   (I'd really like to call this one "create" but the name is taken.)
  raise E_PERM unless caller_perms().wizard
  => an anonymous child of #-1 having the given owner

destroy(object)  
  raise E_PERM unless caller_perms().wizard
  raise E_INVARG if (parent(object)!=#-1)
  remove all properties and verbs from object
  render object invalid

specialize(object,newparent)
  (raise E_PERM unless caller_perms() controls object i.e. == owner or wizard)
  (raise E_PERM unless either newparent.f or caller_perms() controls newparent)
   raise E_RECMOVE if object == newparent
   raise E_INVARG  if parent(object)!=parent(newparent)
   raise E_INVARG  if some property of newparent is defined 
                      on object or a descendant
    1. change the parent of object to newparent, 
    2. initialize slots for each of properties(newparent).
    3. If newparent has its own :initialize verb, call it with this==object.
     (object may have its own :initialize verb but that should be ignored here)

generalize(object)
   raise E_INVARG unless valid(parent(object))
  (raise E_PERM unless caller_perms() controls object OR parent(object))
    1. if the parent has its own :finalize verb, call it with this==object,
       (object may have its own :finalize verb but that should be ignored here)
   raise E_SCREWED if parent(object) has changed
    2. remove all parent property slots
    3. change object's parent to parent(parent(object))

Then, create() is really a new() followed by a succession of specialize()s,
recycle() is really a succession of generalize()s followed by a destroy(),
chparent() is... you get the idea.  Now, if I have an object and you want to
make a child of it, you have no choice but to first create a sibling --- nor
do I have to concern myself with what contortions you have to go through to
obtain that sibling.  Then you have to call specialize() and then *my*
:initialize() verb is called.  Likewise if you have a descendent of my object
and you want to recycle it or otherwise get it out from under, you first have
to work it up to being a direct child --- again, I won't care how you do this
--- and then do generalize() which unavoidably calls my :finalize() verb.

With generalize, there is an added wrinkle in that, in the vanilla scheme,
:finalize is free to punt your object back down to being a grandchild or
lower.  Note that :finalize really does have to go first (i.e., before the
actual parent change) since it will probably need access to property values
that will go away when the parent change happens.  Strictly speaking this is
not a problem in that, wherever my finalize decides to move your object within
its family, the appropriate :initialize/:finalize on descendent objects will
be called and things will remain in a consistent state even though the error
E_SCREWED eventually gets raised.

Still, having me being able to trap your object underneath mine in the
inheritance hierarchy --- which, by the way, would also prevent you from
recycling it --- might be regarded as a problem, much like trapping exitfuncs.
If one does not wish to regard it as strictly a "social" problem (i.e., best
answered by putting "don't do this" in `help manners'), there is a relatively
simple locking mechanism that will take care of it: give every object a lock
bit, set it before calling :finalize, reset it when :finalize returns or bombs
out, then have any attempt to specialize or generalize a locked object raise
an error.  That way :finalize has all of the information it needs and can
still chparent other objects (e.g., voodoo dolls) when necessary.

There isn't a similar problem with specialize() since the :initialize call is
at the very end so nothing can be left inconsistent.  Anyway, the worst that
:initialize can do is bounce the object back above the parent or kick it
down lower.  Neither of these can even be considered a "social" problem; the
former provides a way to do away with the check for

  (raise E_PERM unless either newparent.f or caller_perms() controls newparent)

that is, every object becomes +f by default and if you don't want it to be +f,
you put in a killer :initialize().  Notice now that having :initialize verbs be inherited takes on a different meaning than before.  If I want to make an entire subtree effectively -f, it's merely a matter of installing an :initialize on the root object that does

   root = ...;
   if(parent(this) == root && !$perm_utils:controls(caller_perms(),root))
     generalize(this);
   endif

The .f flag, by the way, doesn't currently exist; I'm just lobbying for a way
to control fertility independently of readablity (.r)

Other details:

(*) generalize() being usable by the parent object's owner as well as the
object's owner makes it possible to painlessly get rid of unwanted children,
which I think would be a good thing.

(*) The E_PERM checks that I've got in parentheses in specialize() and
generalize() are by analogy with what the current server does.  One may
instead want to make these new builtins be wizard-only and then leave the db
owner to decide who gets to use them and when.  

(*) I could easily see using specialize(#-1) in place of new() and having
generalize(obj) recycle obj if it has an invalid parent.  Thus we'd just
have 2 builtin functions for messing with the object hierarchy.  
I see this as a matter of taste.

(*) Yes, that *is* supposed to be :finalize rather than :recycle.  
In short, 
    :finalize => what my child should do when it wants to leave home
                 (i.e., clean up and lock the door behind him)
    :recycle  => what I should do before I die...
                 (i.e., write a will, etc...).  

Currently, :recycle is doing double duty.  With all of the child-cleanup 
functions pushed off onto :finalize, there's probably no need for a :recycle 
server hook.

(*) Re: backwards compatibility.  create() recycle() and chparent() can be
written in terms of the above builtins in moo-code.  A short term solution to
the problem of keeping existing code workable would be to have the create(),
recycle() and chparent() builtins continue to exist but merely call the
corresponding verbs on #0.

(*) Re: multiple inheritance: I think this should carry over without too much
difficulty.  Just read "add_parent()" in place of "specialize()" and
"delete_parent()" in place of "generalize()".  The general theme remains the
same:

   Don't try to create() all in one shot.  Rather, take a new() object and
   gradually maneuver it down the object tree/dag until it's where you want it.
   Likewise for recycle() and whatever the analogue to chparent() is 
   going to be (if any).