734 lines
27 KiB
Markdown
734 lines
27 KiB
Markdown
# Efficient, Imperative Dataspaces for Conversational Concurrency
|
||
|
||
Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||
20 October 2018
|
||
|
||
<p style="font-size:90%"><strong>Abstract.</strong> The dataspace
|
||
model of Conversational Concurrency [is great], but implementing it
|
||
efficiently has been difficult until now. Existing approaches use a
|
||
complex data structure that depends for its efficiency on
|
||
sophisticated run-time support. This paper presents a new approach
|
||
to implementation of the dataspace model that gives three benefits.
|
||
First, it avoids the complexity and run-time support requirements of
|
||
previous approaches, bringing dataspaces to a wider range of
|
||
environments. Second, it unlocks new types of conversational
|
||
interaction among concurrent components. Third, it dramatically
|
||
improves performance. Key to the new technique is a syntactic
|
||
treatment of assertions of interest, contrasting with the semantic
|
||
treatment of assertion sets used by the earlier approach.</p>
|
||
|
||
## Constructing assertions
|
||
|
||
Imagine a language for constructing data with embedded function calls
|
||
and variable references. Imagine that it is a fragment of a larger
|
||
language.
|
||
|
||
c ∈ assertions C ::= e | x(c, ...)
|
||
v ∈ values V ::= a | x(v, ...)
|
||
e ∈ expressions E ::= a | x | e e ...
|
||
x ∈ identifiers X
|
||
a ∈ atoms A = numbers ∪ strings ∪ ...
|
||
|
||
Here are some examples of assertions in `c`, along with suggested
|
||
interpretations:
|
||
|
||
present("Alice") Alice is present in the chat room
|
||
speak("Alice", "Hello!") Alice says "Hello!"
|
||
|
||
## Assertions of interest
|
||
|
||
In the dataspace model, "subscriptions" go hand in hand with
|
||
assertions of *interest* in subscribed-to data. The model includes two
|
||
special constructors for discussing interests. The first, `observe`,
|
||
is interpreted as a declaration of interest in assertions matching the
|
||
pattern given as its sole argument. The second, `discard`, is
|
||
interpreted as a "don't care" when part of a pattern within `observe`.
|
||
|
||
observe(present(discard())) Interest in the presence of any user
|
||
observe(speak("Alice", discard())) Interest in every time Alice speaks
|
||
|
||
We extend the dataspace model with an additional special constructor
|
||
which allows interested parties to declare the portions of matching
|
||
assertions that they specifically wish to examine further: *capturing*
|
||
positions. The `capture` constructor signals that the interested party
|
||
will treat specially the corresponding portion of a matching
|
||
assertion.[^capture-subpattern]
|
||
|
||
observe(present(capture())) Interest in each present user
|
||
observe(speak("Alice", capture())) Interest in the things Alice says
|
||
|
||
[^capture-subpattern]: The implemented system affords a single
|
||
argument to `capture` that restricts matches according to the
|
||
nested subpattern. What is written here as `capture()` corresponds
|
||
to `capture(discard())` in the full implementation. The pattern
|
||
language syntax is analogously extended.
|
||
|
||
There is an important difference between `observe(present(discard()))`
|
||
and `observe(present(capture()))`. The former declares that the
|
||
interested party cares only about whether any user at all is present,
|
||
while the latter declares an interest in the identities of the
|
||
specific users that are present. Similarly, `observe(speak("Alice",
|
||
discard()))` declares an interest in receiving a notification each
|
||
time Alice speaks, but no interest in the *content* of each utterance,
|
||
while `observe(speak("Alice", capture()))` declares interest in
|
||
learning the things Alice says.
|
||
|
||
To drive this point home, the following patterns both result in a
|
||
notification event for each utterance by any user. The first results
|
||
in notifications carrying the name of the speaker along with the
|
||
content of their speech. The second results in notifications carrying
|
||
only the name of the speaker.
|
||
|
||
observe(speak(capture(), capture())) Interest in who says what
|
||
observe(speak(capture(), discard())) Interest in who speaks
|
||
|
||
## Patterns
|
||
|
||
Imagine now an enriched version of our language that can construct
|
||
patterns over data, including captures and "don't care" positions.
|
||
|
||
p ∈ patterns P ::= e | x(p, ...) | $x | _
|
||
|
||
Syntactic patterns can be translated into assertions of interest
|
||
directly. Binding subpatterns `$x` are translated into `capture()`,
|
||
and "don't care" patterns `_` into `discard()`.
|
||
|
||
## Indexing assertions and patterns
|
||
|
||
There are two kinds of change in a running dataspace model program.
|
||
First, assertions can be added to and removed from the dataspace. When
|
||
this happens, interested facets must be informed of relevant changes.
|
||
Second, facets and their event handlers can be added to and removed
|
||
from the dataspace. When this happens, new handlers must be informed
|
||
of preexisting matching assertions.
|
||
|
||
To efficiently respond to these two kinds of change, we maintain a
|
||
special index. Every time an event handler within a facet is created,
|
||
we augment the index using a data structure called a *skeleton*. Each
|
||
skeleton contains information gleaned from static analysis of the
|
||
pattern associated with the event handler. The index also records
|
||
every assertion added to the dataspace, so as to correctly initialize
|
||
event handlers added later.
|
||
|
||
### Skeletons
|
||
|
||
A skeleton is comprised of three pieces: a *shape*, describing the
|
||
positions and arities of statically-known constructors in matching
|
||
assertions; a *constant map*, which places restrictions on fields
|
||
within constructors; and a *capture map*, which specifies locations of
|
||
captured positions.
|
||
|
||
Each time an assertion is added or removed, it is conceptually checked
|
||
against each handler's skeleton. First, the overall shape is checked.
|
||
If the assertion passes this check, the constant map is checked. If
|
||
all the constants match, the capture map is used to prepare an
|
||
argument vector, and the event handler's callback is invoked.
|
||
|
||
k ∈ skeletons K = S × [H×E] × [H]
|
||
s ∈ shapes S ::= * | x(s, ...)
|
||
h ∈ paths H = [𝐍]
|
||
|
||
Shapes retain only statically-known constructors and arities in a
|
||
pattern:
|
||
|
||
shape :: P -> S
|
||
shape e = *
|
||
shape x(p, ...) = x(shape p, ...)
|
||
shape $x = *
|
||
shape _ = *
|
||
|
||
A constant map extracts all non-capturing, non-discard positions in a
|
||
pattern. The expressions in the map are evaluated at the time the
|
||
corresponding event handler is installed; that is, at facet creation
|
||
time. They are not subsequently reevaluated; if any expression depends
|
||
on a dataflow variable, and that variable changes, the entire handler
|
||
is removed, reevaluated, and reinstalled.
|
||
|
||
constantmap :: P -> [(H, E)]
|
||
constantmap p = cmap [] p
|
||
where
|
||
cmap :: H -> P -> [(H, E)]
|
||
cmap h e = [(h, e)]
|
||
cmap h x(p_0, ..., p_i) = (cmap (h++[0]) p_0) ++
|
||
... ++
|
||
(cmap (h++[i]) p_i)
|
||
cmap h $x = []
|
||
cmap h _ = []
|
||
|
||
Finally, a capture map extracts all capturing positions in a pattern:
|
||
|
||
capturemap :: P -> [H]
|
||
capturemap p = vmap [] p
|
||
where
|
||
vmap :: H -> P -> [H]
|
||
vmap h e = []
|
||
vmap h x(p_0, ..., p_i) = (vmap (h++[0]) p_0) ++
|
||
... +
|
||
(vmap (h++[i]) p_i)
|
||
vmap h $x = [h]
|
||
vmap h _ = []
|
||
|
||
### The index
|
||
|
||
The index incorporates every active event handler and every active
|
||
assertion in the dataspace.
|
||
|
||
#### Overview and structures
|
||
|
||
An index is a pair of a bag of all currently-asserted
|
||
assertion-values, plus the root node of a trie-like structure.
|
||
Information from each indexed event handler's skeleton's shape is laid
|
||
out along edges connecting trie nodes.
|
||
|
||
Every node contains a "continuation", which embodies information from
|
||
a skeleton's constant map and capture map alongside handler callback
|
||
functions and caches of currently-asserted values.
|
||
|
||
Index = Bag(V) × Node
|
||
Node = Continuation × (Selector ⟼ Class ⟼ Node)
|
||
Selector = 𝐍 × 𝐍 -- pop-count and index
|
||
Class = X × 𝐍 -- label and arity
|
||
|
||
Continuation = 𝒫(V) × ([H] ⟼ [V] ⟼ Leaf)
|
||
Leaf = 𝒫(V) × ([H] ⟼ Handler)
|
||
|
||
Handler = Bag([V]) × 𝒫(Callback)
|
||
|
||
Callback = EventType -> [V] -> V
|
||
EventType ::= "+" | "-" | "!"
|
||
|
||
Bag(τ) = τ ⟼ 𝐍 -- bag of τ values
|
||
|
||
To use an index in the context of a single assertion—be it a new
|
||
addition, a removal, or a message to be delivered—follow a path from
|
||
the root `Node` of the index along `Selector`/`Class`-labelled edges,
|
||
collecting `Continuations` as you go. This yields a complete set of
|
||
event handlers that may match the assertion being considered. Further
|
||
investigating each collected `Continuation` by analyzing its constant
|
||
maps yields a set of matching `Leaf`s. Finally, each `Leaf` specifies
|
||
a set of captured positions in the assertion to extract and pass to
|
||
the contained callbacks.
|
||
|
||
At every `Continuation`, `Leaf` and `Handler` object, the index
|
||
maintains a set of currently-asserted values that conform to the
|
||
constraints implied by the object's position in the overall index.
|
||
|
||
Most of the components in an index are *mutable*: the `Bag(V)` in the
|
||
root; the assertion-value cache set in each `Continuation` or `Leaf`
|
||
object; the map from `Selector` to `Class` to `Node` within each
|
||
`Node`; the map from path list to value-list to `Leaf` in each
|
||
`Continuation`; the map from path list to `Handler` in each `Leaf`;
|
||
and the `Bag([V])` in every `Handler`. This reflects the fact that the
|
||
index directly reflects the current state of the dataspace it is
|
||
indexing.
|
||
|
||
#### Adding and removing event handlers
|
||
|
||
Every event handler is a pair of a skeleton and a callback function.
|
||
|
||
Adding or removing an event handler proceeds in two stages. First, the
|
||
index is extended to incorporate a path computed from the skeleton's
|
||
shape into the `Node`-based trie. Second, the capture map and callback
|
||
are installed into or removed from the `Continuation` within the
|
||
`Node` at the end of that path.
|
||
|
||
Because (statically-known) shapes are finite and not particularly
|
||
numerous in any given program, the implementation assumes that it is
|
||
never necessary to remove shapes from the index. Instead, it limits
|
||
itself to removal of handler functions, capture maps, and constant
|
||
maps. This assumption will have to be revisited in future broker-like
|
||
cases where handlers are dynamically installed.
|
||
|
||
**Definition.** The `project` function extracts the subvalue at a
|
||
given path `h` from an overall value `v`.
|
||
|
||
project :: V -> H -> V
|
||
project v [] = v
|
||
project x(v_0, ..., v_i) (n:h) = project v_n h
|
||
|
||
**Definition.** The `projectMany` function projects a sequence of
|
||
subvalues.
|
||
|
||
projectMany :: V -> [H] -> V
|
||
projectMany v [h_0, ...] = [project v h_0, ...]
|
||
|
||
**Definition.** The `classof` function extracts the constructor label
|
||
`x` and its arity `i` from a value `v`, yielding `()` if `v` is not
|
||
a record.
|
||
|
||
classof :: V -> 1 + Class
|
||
classof a = ()
|
||
classof x(v_0, ..., v_i) = (x,i)
|
||
|
||
**Definition.** The `extend` procedure augments an index with shape
|
||
information `s`, by imperatively updating the index structure. It
|
||
returns the `Continuation` associated with the deepest `Node`
|
||
visited in the path described by `s`.
|
||
|
||
extend :: Node -> S -> Continuation
|
||
extend node s =
|
||
let (_, (cont, _)) = walk-node [] node 0 0 s
|
||
cont
|
||
where
|
||
|
||
walk-edge :: H -> Node -> 𝐍 -> 𝐍 -> [S] -> (𝐍,Node)
|
||
walk-edge h node n_pop n_index [] =
|
||
(n_pop + 1, node)
|
||
walk-edge h node n_pop n_index (s:shapes) =
|
||
let (n_pop', node') = walk-node h node n_pop n_index s
|
||
let n_index' = n_index + 1
|
||
let h' = (dropRight h 1) ++ [n_index']
|
||
walk-edge h' node' n_pop' n_index' shapes
|
||
|
||
walk-node :: H -> Node -> 𝐍 -> 𝐍 -> S -> (𝐍,Node)
|
||
walk-node h node n_pop n_index * =
|
||
(n_pop, node)
|
||
walk-node h node n_pop n_index x(s_0, ... s_i) =
|
||
let (cont, edges) = node
|
||
let selector = (n_pop,n_index)
|
||
let class = (x,i)
|
||
if selector not in edges then
|
||
edges[selector] := {}
|
||
let table = edges[selector]
|
||
if class not in table then
|
||
let (outercache, constmap) = cont
|
||
let innercache =
|
||
{ v | v ∈ outercache,
|
||
classof (project v h) = class }
|
||
table[class] := ((innercache, {}), {})
|
||
let node' = table[class]
|
||
walk-edge (h ++ [0]) node' 0 0 [s_0, ..., s_i]
|
||
|
||
**Definition.** The `addHandler` procedure installs into an index an
|
||
event handler callback `f` expecting values matching and captured by
|
||
the given skeleton `k`. It then invokes `f` once for each distinct
|
||
sequence of captured values matching existing assertions in the
|
||
index.[^function-pointer-equality]
|
||
|
||
addHandler :: Index -> (S × [H×V] × [H]) -> Callback -> 1
|
||
addHandler index (s, constantMap, captureMap) f =
|
||
let (_, root) = index
|
||
let (cache, table) = extend root s
|
||
let constLocs = [h | (h,v) ∈ constantMap]
|
||
let constVals = [v | (h,v) ∈ constantMap]
|
||
if constLocs not in table then
|
||
table[constLocs] := {}
|
||
if constVals not in table[constLocs] then
|
||
let leafcache =
|
||
{ v | v ∈ cache,
|
||
projectMany v constLocs = constVals }
|
||
table[constLocs][constVals] := (leafcache, {})
|
||
let (leafcache, leaftable) = table[constLocs][constVals]
|
||
if captureMap not in leaftable then
|
||
let bag = empty_bag
|
||
for v in leafcache
|
||
bag[projectMany v captureMap] += 1
|
||
leaftable[captureMap] := (bag, {})
|
||
let (bag, f_table) = leaftable[captureMap]
|
||
f_table += f
|
||
for seq in bag
|
||
f "+" seq
|
||
()
|
||
|
||
[^function-pointer-equality]: Because we store *sets* of function
|
||
values, we rely on the general availability of a closure
|
||
equivalence relation. Pointer-equality of closures (`eq?`)
|
||
suffices.
|
||
|
||
**Definition.** The `removeHandler` procedure removes an event handler
|
||
from an index.
|
||
|
||
removeHandler :: Index -> (S × [H×V] × [H]) -> Callback -> 1
|
||
removeHandler index (s, constantMap, captureMap) f =
|
||
let (_, root) = index
|
||
let (cache, table) = extend root s
|
||
let constLocs = [h | (h,v) ∈ constantMap]
|
||
let constVals = [v | (h,v) ∈ constantMap]
|
||
if constLocs not in table then
|
||
return
|
||
if constVals not in table[constLocs] then
|
||
return
|
||
let (leafcache, leaftable) = table[constLocs][constVals]
|
||
if captureMap not in leaftable then
|
||
return
|
||
let (bag, f_table) = leaftable[captureMap]
|
||
if f not in f_table then
|
||
return
|
||
f_table -= f
|
||
if f_table = {} then
|
||
delete leaftable[captureMap]
|
||
if leaftable = {} then
|
||
delete table[constLocs][constVals]
|
||
if table[constLocs] = {} then
|
||
delete table[constLocs]
|
||
|
||
#### Adding assertions, removing assertions and sending messages
|
||
|
||
All three operations depend on a single traversal procedure,
|
||
parameterized with different update procedures.
|
||
|
||
**Definition.** The `modify` procedure traverses an index trie,
|
||
following the structure of `v` and updating cached assertion sets
|
||
according to the given update procedures. The update procedures act
|
||
by side-effect; in particular, the `m_handler` procedure may choose
|
||
to invoke the callback passed to it.
|
||
|
||
modify :: Node ->
|
||
V ->
|
||
(Continuation -> V -> 1) ->
|
||
(Leaf -> V -> 1) ->
|
||
(Handler -> [V] -> 1) ->
|
||
1
|
||
modify node v m_cont m_leaf m_handler =
|
||
walk-node node [outermost(v)]
|
||
where
|
||
walk-node :: Node -> [V] -> 1
|
||
walk-node (cont, edges) vs =
|
||
walk-cont cont
|
||
for sel@(n_pop, n_index) in edges
|
||
let vs' = dropLeft vs n_pop
|
||
let (x(v_0, ...) : _) = vs'
|
||
let v' = v_{n_index}
|
||
if classof v' in edges[sel] then
|
||
walk-node edges[sel][classof v'] (v':vs')
|
||
|
||
walk-cont :: Continuation -> 1
|
||
walk-cont cont@(cache, table) =
|
||
m_cont cont v
|
||
for constLocs in table
|
||
let consts = projectMany v constLocs
|
||
if consts in table[constLocs] then
|
||
let leaf@(leafcache, leaftable) =
|
||
table[constLocs][consts]
|
||
m_leaf leaf v
|
||
for captureMap in leaftable
|
||
let handler = leaftable[captureMap]
|
||
let vs = projectMany v captureMap
|
||
m_handler handler vs
|
||
|
||
The `outermost` constructor applied to `v` at the top of `modify` is
|
||
necessary because every path in the trie structure embodied in each
|
||
`node` is a sequence of zero or more (move, check) pairs. Each "move"
|
||
pops zero or more items from the stack and then pushes a sub-structure
|
||
of the topmost stack element onto the stack; the "check" then examines
|
||
the class of the new top element, abandoning the search if it does not
|
||
match. Without some outermost constructor, there would be no possible
|
||
"move", and the trie would have to be expressed as a single optional
|
||
check followed by zero or more (move, check) pairs.
|
||
|
||
**Definition.** The procedure `adjustAssertion` updates the copy-count
|
||
associated with `v` in the given index, invoking callbacks as a
|
||
side-effect if this changes the observable contents of the
|
||
dataspace.
|
||
|
||
adjustAssertion :: Index -> V -> 𝐍 -> 1
|
||
adjustAssertion (cache, root) v delta =
|
||
let was_present = v in cache
|
||
cache[v] += delta
|
||
let is_present = v in cache
|
||
if not was_present and is_present then
|
||
modify root v add_cont add_leaf add_handler
|
||
if was_present and not is_present then
|
||
modify root v del_cont del_leaf del_handler
|
||
where
|
||
add_cont (cache, _) v = cache += v
|
||
add_leaf (leafcache, _) v = leafcache += v
|
||
add_handler (bag, f_table) vs =
|
||
let was_present = vs in bag
|
||
bag[vs] += 1
|
||
if not was_present then
|
||
for f in f_table
|
||
f "+" vs
|
||
|
||
del_cont (cache, _) v = cache -= v
|
||
del_leaf (leafcache, _) v = leafcache -= v
|
||
del_handler (bag, f_table) vs =
|
||
bag[vs] -= 1
|
||
if vs not in bag then
|
||
for f in f_table
|
||
f "-" vs
|
||
|
||
**Definition.** The procedures `addAssertion` and `removeAssertion`
|
||
install and remove an assertion `v` into the given index,
|
||
respectively.
|
||
|
||
addAssertion :: Index -> V -> 1
|
||
addAssertion index v = adjustAssertion index v 1
|
||
|
||
removeAssertion :: Index -> V -> 1
|
||
removeAssertion index v = adjustAssertion index v -1
|
||
|
||
Care must be taken when applying entire *patches* to ensure that added
|
||
assertions are processed before removed assertions; otherwise, actors
|
||
will observe glitching in certain cases. For example, consider an
|
||
endpoint with a wildcard subscription `[_]` and a separate endpoint
|
||
asserting `[3]`. If a patch atomically replaces `[3]` with `[4]`, then
|
||
if the removal is processed first, it will briefly appear to the `[_]`
|
||
endpoint as if no assertions remain, whereas if the addition is
|
||
processed first, no glitch will be detected.
|
||
|
||
**Definition.** The procedure `sendMessage` delivers a message `v` to
|
||
event handlers in the given index.
|
||
|
||
sendMessage :: Index -> V -> 1
|
||
sendMessage (_, root) v =
|
||
modify root v send_cont send_leaf send_handler
|
||
where
|
||
send_cont _ _ = ()
|
||
send_leaf _ _ = ()
|
||
send (_, f_table) vs =
|
||
for f in f_table
|
||
f "!" vs
|
||
|
||
## Potential future optimizations
|
||
|
||
### Static analysis of messages and assertions
|
||
|
||
Static analysis of expressions under `(send! ...)` and `(assert ...)`
|
||
could cut out even more structural overhead.
|
||
|
||
For example, given interests
|
||
|
||
observe(message("actor1", capture()))
|
||
observe(message("actor2", capture()))
|
||
|
||
and a message
|
||
|
||
:: message(x, y)
|
||
|
||
static analysis could directly connect the sending site to a
|
||
hash-table lookup with `x` as the key, and invocation of the resulting
|
||
handlers with `y` as the argument. There would be no need to perform a
|
||
lookup based on the `message` constructor at runtime.
|
||
|
||
Similarly, given an interest
|
||
|
||
observe(present(capture()))
|
||
|
||
and an assertion
|
||
|
||
present(user)
|
||
|
||
static analysis could directly invoke handlers with `user` as the
|
||
argument, without needing to at runtime find the set of handlers
|
||
interested in the `present` constructor.
|
||
|
||
---
|
||
|
||
# TODO
|
||
|
||
- describe the cleanup function associated with a handler in the real implementation
|
||
- `relay.rkt` uses it. When an inner actor asserts interest in an
|
||
inbound assertion-set, the relay process pivots into the outer
|
||
dataspace's context, and adds a new endpoint that relays events
|
||
to the inner dataspace. The cleanup function attached to that
|
||
endpoint retracts (from the inner dataspace) any matching
|
||
assertions left over at the time the endpoint is removed.
|
||
- that appears to be it! Nowhere else in the code is a
|
||
`skeleton-interest` constructed with a non-`#f` cleanup
|
||
function.
|
||
- figure out and describe scoped assertions / visibility-restrictions
|
||
- (partial/sketchy answer:) It's to deal with the fact that
|
||
multiple endpoints may overlap. Within a single dataspace, an
|
||
assertion matching both endpoints will trigger each of them.
|
||
When relaying, the relay maintains an endpoint in the outer
|
||
space for each in the inner space. When both outer endpoints are
|
||
triggered, if they were to naively relay the matching assertion,
|
||
the problem isn't so much that they'd double up (because
|
||
dataspaces deduplicate!), the problem is that they don't have
|
||
enough information to reconstruct the triggering outer assertion
|
||
perfectly! So a visibility-restriction causes an assertion to
|
||
*only* trigger inner endpoints that capture *exactly* the
|
||
captures of the outer endpoint. One of the outer endpoints will
|
||
trigger its "matching" inner endpoint, but not the inner
|
||
endpoint of the other endpoint, even though you might expect the
|
||
relayed assertion to do so.
|
||
|
||
- there's more to say about the implementation of the *dataspace*
|
||
itself, not just the index structures. For example, the care that
|
||
must be taken regarding `cleanup-changes` and abandoning work
|
||
during exception handling.
|
||
|
||
<!--
|
||
|
||
# Example expansions
|
||
|
||
Starting at the top.
|
||
|
||
> (message-struct speak (who what))
|
||
> (assertion-struct present (who))
|
||
|
||
> (syntax->datum (expand-once '(on (message (speak $who $what)) (void))))
|
||
'(add-endpoint!
|
||
(current-facet)
|
||
""
|
||
#t
|
||
(lambda ()
|
||
(if #t
|
||
(values
|
||
(observe (speak (capture (discard)) (capture (discard))))
|
||
(skeleton-interest
|
||
(list struct:speak #f #f)
|
||
'()
|
||
(list)
|
||
'((0 0) (0 1))
|
||
(capture-facet-context
|
||
(lambda (op who what)
|
||
(when (eq? op '!)
|
||
(schedule-script!
|
||
#:priority
|
||
*normal-priority*
|
||
(current-actor)
|
||
(lambda () (begin/void-default (void)))))))
|
||
#f))
|
||
(values (void) #f))))
|
||
|
||
> (syntax->datum (expand-once '(on (asserted (present $who)) (void))))
|
||
'(add-endpoint!
|
||
(current-facet)
|
||
""
|
||
#t
|
||
(lambda ()
|
||
(if #t
|
||
(values
|
||
(observe (present (capture (discard))))
|
||
(skeleton-interest
|
||
(list struct:present #f)
|
||
'()
|
||
(list)
|
||
'((0 0))
|
||
(capture-facet-context
|
||
(lambda (op who)
|
||
(when (eq? op '+)
|
||
(schedule-script!
|
||
#:priority
|
||
*normal-priority*
|
||
(current-actor)
|
||
(lambda () (begin/void-default (void)))))))
|
||
#f))
|
||
(values (void) #f))))
|
||
|
||
> (syntax->datum (expand-once '(on (message `(speak ,$who ,$what)) (void))))
|
||
'(add-endpoint!
|
||
(current-facet)
|
||
""
|
||
#t
|
||
(lambda ()
|
||
(if #t
|
||
(values
|
||
(observe (list `speak (capture (discard)) (capture (discard))))
|
||
(skeleton-interest
|
||
(list 'list #f #f #f)
|
||
'((0 0))
|
||
(list `speak)
|
||
'((0 1) (0 2))
|
||
(capture-facet-context
|
||
(lambda (op who what)
|
||
(when (eq? op '!)
|
||
(schedule-script!
|
||
#:priority
|
||
*normal-priority*
|
||
(current-actor)
|
||
(lambda () (begin/void-default (void)))))))
|
||
#f))
|
||
(values (void) #f))))
|
||
|
||
> (syntax->datum (expand-once '(on (asserted `(present ,$who)) (void))))
|
||
'(add-endpoint!
|
||
(current-facet)
|
||
""
|
||
#t
|
||
(lambda ()
|
||
(if #t
|
||
(values
|
||
(observe (list `present (capture (discard))))
|
||
(skeleton-interest
|
||
(list 'list #f #f)
|
||
'((0 0))
|
||
(list `present)
|
||
'((0 1))
|
||
(capture-facet-context
|
||
(lambda (op who)
|
||
(when (eq? op '+)
|
||
(schedule-script!
|
||
#:priority
|
||
*normal-priority*
|
||
(current-actor)
|
||
(lambda () (begin/void-default (void)))))))
|
||
#f))
|
||
(values (void) #f))))
|
||
|
||
> (struct arp-query (protocol protocol-address interface-name link-address) #:prefab)
|
||
> (syntax->datum (expand-once '(on (asserted (observe (arp-query $protocol $protocol-address interface-name _))) (void))))
|
||
'(add-endpoint!
|
||
(current-facet)
|
||
""
|
||
#t
|
||
(lambda ()
|
||
(if #t
|
||
(values
|
||
(observe
|
||
(observe
|
||
(arp-query
|
||
(capture (discard))
|
||
(capture (discard))
|
||
interface-name
|
||
(discard))))
|
||
(skeleton-interest
|
||
(list struct:observe (list struct:arp-query #f #f #f #f))
|
||
'((0 0 2))
|
||
(list interface-name)
|
||
'((0 0 0) (0 0 1))
|
||
(capture-facet-context
|
||
(lambda (op protocol protocol-address)
|
||
(when (eq? op '+)
|
||
(schedule-script!
|
||
#:priority
|
||
*normal-priority*
|
||
(current-actor)
|
||
(lambda () (begin/void-default (void)))))))
|
||
#f))
|
||
(values (void) #f))))
|
||
|
||
> (struct tabular-layout (row col) #:prefab)
|
||
> (assertion-struct layout-solution (container-id spec size rectangle))
|
||
> (syntax->datum (expand-once '(on (asserted (observe (layout-solution container-id (tabular-layout $row $col) $size _))) (void))))
|
||
'(add-endpoint!
|
||
(current-facet)
|
||
""
|
||
#t
|
||
(lambda ()
|
||
(if #t
|
||
(values
|
||
(observe
|
||
(observe
|
||
(layout-solution
|
||
container-id
|
||
(tabular-layout (capture (discard)) (capture (discard)))
|
||
(capture (discard))
|
||
(discard))))
|
||
(skeleton-interest
|
||
(list
|
||
struct:observe
|
||
(list
|
||
struct:layout-solution
|
||
#f
|
||
(list struct:tabular-layout #f #f)
|
||
#f
|
||
#f))
|
||
'((0 0 0))
|
||
(list container-id)
|
||
'((0 0 1 0) (0 0 1 1) (0 0 2))
|
||
(capture-facet-context
|
||
(lambda (op row col size)
|
||
(when (eq? op '+)
|
||
(schedule-script!
|
||
#:priority
|
||
*normal-priority*
|
||
(current-actor)
|
||
(lambda () (begin/void-default (void)))))))
|
||
#f))
|
||
(values (void) #f))))
|
||
|
||
-->
|