Privilege levels

Another way of looking at the statically-checked cell borrowing approach used by Stakker (i.e. qcell-based borrowing) is to consider all the code associated with the actor system as belonging to one of three privilege levels.

Level 0: Code in the highest level of privilege has access to a &mut Stakker. This is the code outside of the actor system (the main loop and any associated external code that the actor system interfaces to) and the code called by the main loop that handles a queue-deferred action (actor call, return, forward, etc). In terms of statically-checked borrowing, both the Actor-owner and Share-owner are available.

Level 1: Actor methods all run in this level. They have a &mut Core (maybe via a &mut Cx), but have no access to Stakker methods. In terms of statically-checked borrowing, the Actor-owner is unavailable (because it was used to get access to the actor state), but the Share-owner is still free.

Level 2: Methods called on Share objects run in this level, as well as Drop handlers. In fact any code which doesn't accept a &mut Core or &mut Cx argument runs in this level. Neither of the cell owners are available. In the case of a method call on a Share object (e.g. share.rw(cx).method(args)), the &mut Core isn't available because it was used to get access to the share.

If you take a snapshot of the callstack at any point in time you'd find code running in one or more of these levels. At the base of the stack would be the main loop code, in level 0. If an actor method is running then this would be code in level 1. If the actor is calling out to Share code or arbitrary external libraries, this would be code in level 2. The levels form bands on the callstack.

Here are some examples of different things you can do at different levels:

Level:0: main loop1: actor methods2: share methods,
drop handlers
Available borrow:&mut Stakker&mut Core or
&mut Cx
none
Run the queuesYes--
Run an actor callYes--
query! an actorYes--
lazy!, idle!YesYes-
after!, at!, etcYesYes-
Access a ShareYesYes-
ret! data to RetYesYesYes
fwd! data to FwdYesYesYes
call! an actorYesYesYes
defer using DeferrerYesYesYes

Remember that these levels are statically enforced by the compiler (without any runtime overhead), so there is no way around it in safe code. However note that even in level 2, you can defer an operation, or forward data elsewhere. So the system of "privileges" only stops you doing things right now. It doesn't stop you doing them a little bit later. So it blocks synchronous operations when those could potentially cause issues, but doesn't stop you doing those same things asynchronously.

Also note that even in the internal code of Stakker it's impossible to break these rules. The rules are enforced by the Rust compiler and a tiny bit of code in qcell. If a borrow is performed to get access to an actor's state, then the &mut Stakker borrow is locked up until that actor borrow is released. Similarly a share borrow locks up the &mut Cx or &mut Core. See qcell documentation for more details. So this provides a very strong guarantee of correctness.