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.
305 lines
10 KiB
305 lines
10 KiB
|
7 years ago
|
* Brainstorming
|
||
|
|
|
||
|
7 years ago
|
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
|
||
|
|
|
||
|
7 years ago
|
;; This schedules a remote actor
|
||
|
|
(sys '<- (or/c local-actor-id? remote-actor-id?) args ...) => syscaller?
|
||
|
7 years ago
|
|
||
|
|
<- 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.
|
||
|
|
|
||
|
7 years ago
|
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?
|
||
|
|
|
||
|
7 years ago
|
** 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.
|
||
|
|
|
||
|
7 years ago
|
* 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.
|
||
|
7 years ago
|
|
||
|
|
* 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.
|
||
|
7 years ago
|
* 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.
|
||
|
7 years ago
|
|
||
|
|
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>)
|
||
|
7 years ago
|
|
||
|
|
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
|