You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

10 KiB

Brainstorming

E style vats, in the pure lambda calculus (plus sealer/unsealers, with weak-maps desireable for efficiency)

It does assume the existence of "eq?".

syscaller

Passed into every actor, and returned from every actor.

(sys 'call actor-id args …) => (values result syscaller?)

This returned syscaller must be trademarked by sys? I guess that only matters for the turn level

Every turn has a sys with a new trademark. THIS IS IMPORTANT because it prevents actors from maliciously "rolling back" actors.

Then we just return the new sys.

(sys 'spawn actor-handler) => (values actor-id syscaller?)

actor-handler is just a procedure, the initial handler of the actor

;; This schedules a remote actor (sys '<- (or/c local-actor-id? remote-actor-id?) args …) => syscaller?

<- might either not be exposed or may be ignored by some syscallers.

An actor is spawned with a sys, and whatever other arguments are appropriate.

Return values for actor: (values return-val [new-sys [next-self]])

with new-sys defaulting to called-with sys and next-self defaulting to current self.

One downside of this design: it allows callers to intentionally "refuse" to propagate a callee's changes to self while still extracting information. This might be a significant problem in some cases but I'm not sure which they are.

the vat

Separated into two stages:

  • churns: process all current messages
  • turns: each individual message processing

A message looks like: (struct local-message (to-id args kws kw-args))

Or simplified when w/o keywords, could be as simple as: (list 'local-message to-id args …)

Where keywords could be in key/value pairs.

The foundational function:

(vat 'turn message?) => (values any/c ; return-val vat? ; next vat (listof message?) ; to-local messages (listof message?)) ; to-remote messages

Here's some sugar for (vat 'turn message?) (vat-turn vat actor-id args …)

Strictly speaking, churn is sugar, running each turn and accumulating messages.

(churn vat? (treeof message?)) => (values vat? ; next vat (treeof message?) ; to-local messages (treeof message?)) ; to-remote messages

The reason for treeof instead of listof is it avoids an unnecessary append.

We might need to do something about promises… not sure.

We also need a low-level way to add initial actors to the vat:

(vat 'spawn actor-handler) => (values actor-id vat?)

What info does the syscaller contain?

How does a turn work?

drivers and inputs?

Drivers could be perceived as <- to "imaginary vats".

fixed-rate game

Simple. Ignore <- messages and have an object that knows about all active gameobjs that need to be run.

(sys 'call spawn-gameobj make-my-gameobj)

adds a new actor, registered by the gameobj manager calls

(define ((make-my-gameobj kill-me) sys args …) …)

This gameobj is then registered so that it will run every game tick.

The gameobj manager can be manually invoked with a single turn to find out everyone that should run:

(define (game-tick vat) (define-values (tick-gameobjs _ _ _) (vat-turn vat gameobj-manager 'get-active-gameobjs)) (define tick-messages (map (lambda (gobj) (local-message to-id '(tick) '() '())) tick-gameobjs)) (define-values (next-vat _ _) ; we ignore <- messages in this example (vat-churn vat tick-messages)) next-vat)

other stuff

sugar for (sys 'call actor-id?)

maybe (sys actor-id args …) is considered sugar for (sys 'call actor-id args …) because it's so common.

romeo and juliet confuse the guard

Here's a problem with the purely functional approach: callees can sometimes conspire with callers further up the stack to have themselves be committed while preventing the committing of their intermediate objects… which should be against the theoretical realm of possibility.

Here's the scenario:

  • romeo would like to serenade juliet
  • juliet is within the capulet orchard (has state 'wherefore-art-thou)
  • the capulet orchard is surveiled by the capulets (spy of tybalt)?
  • To get to juliet, one must go through the orchard, but the orchard would like to log romeo and juliet's interaction. Normally, if romeo simply calls the orchard to get to juliet, the orchard will log that information, and a capulet can find out later.
  • But Romeo and Juliet would like to meet completely undetected and still share the experience. Is there a way?
  • If Romeo, in calling Juliet through the Capulet Orchard, passes Juliet a "gift" which inside contains Romeo's syscaller object (as opposed to the one Juliet was given), Juliet can perform her write based off of Romeo's. Juliet can then return her updated sys object as well, also wrapped in a gift (sealed?).
  • Romeo can then then return the syscaller with the updated Juliet, ignoring the state changes / logging attempted by the capulet observer.

That's a win for love, but a loss for security; it could, for instance, break Horton.

Transactional, only semi-functional version

In this version, most things work the same except that the syscaller is an object whose state is mutated by the syscaller itself. Updates only happen in the syscaller itself, and thus are transactional. This should avoid the romeo-and-juliet attack and should be considerably easier to use as a user to bot; both (sys 'spawn handler) and (sys 'call foo-ref …) can now skip returning the syscaller. It no longer needs to be "threaded" through the program. However since each turn generates its own separate syscaller, this should still be safe to do.

We might need to be able to mark an inactive syscaller as "stale" however, since old syscallers will still be in scope.

Parameterization of the syscaller?

Parameterization of the syscaller would result in the easiest to use design (now we can simply call actors directly and use <- and spawn directly as well). It's a bit less interesting to show off to groups that don't have access to parameterization though. This is probably the "nicest to use" design, and more or less gets us the api we want to use with goblins.

promises, on, and <-

  • Need to allow a message to specify a resolver
  • <- then spawns promise and resolver, specifies that resolver on the message it sends
  • messages, upon being handled, will send messages to resolvers finalizing them.

how promises and resolvers work:

  • there's a state
  • there's a set of observers

what does on do?

  • It registers a new observer, but how?
  • It seems like this is in conflict with (<- promise …) because both of them require sending a message to a promise but they have different behavior:

    • (<- promise …) appears that it will capture all args and keyword args, making message dispatch difficult. And yet message dispatch appears to be necessary if we want to distinguish between (<- promise …) and (on promise …) which both appear to need to send a message to the promise.
    • one way to resolve this could be that on uses a miranda method.
  • It seems that on needs to be a syscaller method.
  • It also seems likely that messages to promises need to be treated specially.

what does sending a message to a promise do?

  • Sending a message to promise A should give us promise B, which is waiting on the resolution of promise A.

what about remote promises?

  • seems to throw a wrench in the works because (<- remote-promise-ref foo bar) should be able to immediately also return another promise.
  • on also requires local behavior for what involves a remote action.

what about caching promise results?

  • it could be that the actormap needs to be updated so that refs map to actor objects.
  • (struct actor (handler miranda-handler))
  • next can then accept not just a handler but a full actor object, including the miranda-handler
  • Promises get some extra miranda handlers:

    • promise-state
    • promise-resolution

I should read the E docs…

Ok, the E docs have a miranda method called whenMoreResolved which sounds like what we want to use for on.

OK ok I'm reading some more. So we have two "primary" ref categories:

  • near-ref (immediate calls OR eventual sends)
  • eventual-ref (includes everything else, eventual sends only)

Nope! Instead, we have

  • live-ref
  • sturdy-ref

And THOSE are the two ref categories. However, the actormap should only care about life-refs!

live-refs map to meta-actor types:

  • mactor:near
  • mactor:far
  • mactor:near-promise
  • mactor:far-promise
  • mactor:symlink <- in case we have a promise resolve to some existing object but we want to just rely on that object's value Symlinks should also collapse where possible; if we observe somehow that we've got a symlink to a symlink to an object, we should simply remap to a symlink to an object.
  • mactor:encased <- for non-actor values like integers, strings, etc

to remove from encasing, we run (extract <encased-ref>)

So, now we could theoretically set up promises. Attaching listeners: not so hard.

The problem: the resolver! How do we resolve it?

Different ways to solve it:

  • Have the promise and resolver both be facades for the "real" promise object, which has the following methods:

    • get-state
    • fulfill
    • smash
    • on-fulfilled
    • on-broken

    Advantage: it's easy Disadvantage: an extra object and indirection

  • Have both the state, and fulfill and break observers/listeners be their own separate cells.
  • Have the resolver get a special sealer and the promise get an associated unsealer (and brand). Only an object sealed by that sealer will correctly be "opened" by the promise, kicking off the appropriate fulfill/smash action. Actually I like this quite a bit… Keep in mind that there's no need for a "handler"; only mactor:near needs a handler, so this is clean to implement. Instead, there can be a "miranda method" just for near promises which is this kind of resolver.

Okay I like this last one. Also, I think that resolvers should work the same way as all other observers. We don't need separate observers for fulfilled / broken; we just have one that can accept either 'resolve/'smash as methods. The nice thing then is that for an "on" statement, we don't need two separate listeners, we just need one that can respond to either potential method.

So we can make a new special miranda method:

  • resolve