Initial commit
A mostly verbatim translation of syndicate-js. https://git.syndicate-lang.org/syndicate-lang/syndicate-js
This commit is contained in:
commit
dd977991ad
|
@ -0,0 +1 @@
|
||||||
|
tests/test_box_and_client
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Syndicate
|
||||||
|
|
||||||
|
Nim implementation of [Syndicate](https://syndicate-lang.org/) dataspaces and actors.
|
|
@ -0,0 +1,649 @@
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
import ./syndicate/bags, ./syndicate/dataflow, ./syndicate/events, ./syndicate/skeletons
|
||||||
|
import preserves
|
||||||
|
import asyncdispatch, deques, hashes, macros, options, sets, strutils, tables
|
||||||
|
|
||||||
|
export dataflow.defineObservableProperty
|
||||||
|
export dataflow.recordObservation
|
||||||
|
export dataflow.recordDamage
|
||||||
|
|
||||||
|
template generateIdType(T: untyped) =
|
||||||
|
type T* = distinct Natural
|
||||||
|
proc `==`*(x, y: T): bool {.borrow.}
|
||||||
|
proc `$`*(id: T): string {.borrow.}
|
||||||
|
|
||||||
|
generateIdType(ActorId)
|
||||||
|
generateIdType(FacetId)
|
||||||
|
generateIdType(EndpointId)
|
||||||
|
generateIdType(FieldId)
|
||||||
|
|
||||||
|
type
|
||||||
|
Value* = Preserve
|
||||||
|
Bag = bags.Bag[Value]
|
||||||
|
|
||||||
|
Task[T] = proc (): T
|
||||||
|
Script[T] = proc (facet: Facet): T
|
||||||
|
ActivationScript* = Script[void]
|
||||||
|
|
||||||
|
ActionKind = enum
|
||||||
|
patchAction, messageAction, spawnAction, quitAction, deferredTurnAction, activationAction
|
||||||
|
|
||||||
|
Action = object
|
||||||
|
impl: proc (action: Action; ds: Dataspace; actor: Option[Actor]) {.gcsafe.}
|
||||||
|
case kind: ActionKind
|
||||||
|
of patchAction:
|
||||||
|
changes: Bag
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
Priority = enum
|
||||||
|
pQueryHigh = 0,
|
||||||
|
pQuery,
|
||||||
|
pQueryHandler,
|
||||||
|
pNormal,
|
||||||
|
pGC,
|
||||||
|
pIdle,
|
||||||
|
len
|
||||||
|
|
||||||
|
Actor = ref object
|
||||||
|
id: ActorId
|
||||||
|
name: string
|
||||||
|
dataspace*: Dataspace
|
||||||
|
rootFacet: ParentFacet
|
||||||
|
pendingTasks: array[Priority.len, Deque[Task[void]]]
|
||||||
|
pendingActions: seq[Action]
|
||||||
|
adhocAssertions: Bag
|
||||||
|
cleanupChanges: Bag
|
||||||
|
parentId: ActorId
|
||||||
|
isRunnable: bool
|
||||||
|
|
||||||
|
EndpointSpec* = object
|
||||||
|
assertion*: Option[Value]
|
||||||
|
analysis*: Option[Analysis]
|
||||||
|
|
||||||
|
Endpoint = ref object
|
||||||
|
id: EndpointId
|
||||||
|
facet: Facet
|
||||||
|
updateProc: Script[EndpointSpec]
|
||||||
|
spec: EndpointSpec
|
||||||
|
|
||||||
|
Field = tuple[name: string; value: Preserve]
|
||||||
|
Fields = seq[Field]
|
||||||
|
# TODO: compile-time tuples
|
||||||
|
|
||||||
|
Turn = object
|
||||||
|
actions: seq[Action]
|
||||||
|
actor: Option[Actor]
|
||||||
|
|
||||||
|
Dataspace* = ref object
|
||||||
|
ground*: Ground
|
||||||
|
index: Index
|
||||||
|
dataflow*: Graph[Endpoint, FieldId]
|
||||||
|
runnable: seq[Actor]
|
||||||
|
pendingTurns: seq[Turn]
|
||||||
|
actors: Table[ActorId, Actor]
|
||||||
|
activations: seq[ActivationScript]
|
||||||
|
nextId: Natural
|
||||||
|
|
||||||
|
StopHandler = proc (ds: Dataspace) {.gcsafe.}
|
||||||
|
|
||||||
|
Ground = ref object
|
||||||
|
dataspace: Dataspace
|
||||||
|
stopHandlers: seq[StopHandler]
|
||||||
|
future: Future[void]
|
||||||
|
|
||||||
|
ParentFacet = Option[Facet]
|
||||||
|
|
||||||
|
Facet* = ref FacetObj
|
||||||
|
FacetObj = object
|
||||||
|
id: FacetId
|
||||||
|
actor*: Actor
|
||||||
|
parent: ParentFacet
|
||||||
|
endpoints: Table[EndpointId, Endpoint]
|
||||||
|
stopScripts: seq[Script[void]]
|
||||||
|
children: Table[FacetId, Facet]
|
||||||
|
fields*: Fields
|
||||||
|
isLive, inScript: bool
|
||||||
|
|
||||||
|
# FacetImpl[Fields] = ref FacetImplObj[Fields]
|
||||||
|
# FacetImplObj[Fields] {.final.} = object of FacetBaseObj
|
||||||
|
|
||||||
|
using
|
||||||
|
dataspace: Dataspace
|
||||||
|
actor: Actor
|
||||||
|
facet: Facet
|
||||||
|
|
||||||
|
proc `$`*(spec: EndpointSpec): string =
|
||||||
|
result.add "{assertion: "
|
||||||
|
if spec.assertion.isSome:
|
||||||
|
result.add $(spec.assertion.get)
|
||||||
|
else:
|
||||||
|
result.add "nil"
|
||||||
|
result.add ", analysis: "
|
||||||
|
if spec.analysis.isSome:
|
||||||
|
result.add $spec.analysis.get
|
||||||
|
else:
|
||||||
|
result.add "nil"
|
||||||
|
result.add " }"
|
||||||
|
|
||||||
|
proc `$`*(actor): string =
|
||||||
|
result.add "Actor("
|
||||||
|
result.add $actor.id
|
||||||
|
result.add ','
|
||||||
|
result.add actor.name
|
||||||
|
result.add ')'
|
||||||
|
|
||||||
|
proc `$`*(facet): string =
|
||||||
|
result.add "Facet("
|
||||||
|
result.add $facet.actor.id
|
||||||
|
result.add ','
|
||||||
|
result.add facet.actor.name
|
||||||
|
result.add ','
|
||||||
|
result.add $facet.id
|
||||||
|
result.add ')'
|
||||||
|
|
||||||
|
proc hash*(ep: Endpoint): Hash =
|
||||||
|
!$(hash(ep.id) !& hash(ep.facet.id))
|
||||||
|
|
||||||
|
proc generateId*(ds: Dataspace): Natural =
|
||||||
|
# TODO: used by declareField, but should be hidden.
|
||||||
|
inc(ds.nextId)
|
||||||
|
ds.nextId
|
||||||
|
|
||||||
|
proc newActor(ds: Dataspace; name: string; initialAssertions: Value; parentId: ActorId): Actor =
|
||||||
|
assert(initialAssertions.kind == pkSet)
|
||||||
|
result = Actor(
|
||||||
|
id: ds.generateId.ActorId,
|
||||||
|
name: name,
|
||||||
|
dataspace: ds,
|
||||||
|
parentId: parentId)
|
||||||
|
for v in initialAssertions.set:
|
||||||
|
discard result.adhocAssertions.change(v, 1)
|
||||||
|
ds.actors[result.id] = result
|
||||||
|
|
||||||
|
proc newFacet(actor; parent: ParentFacet): Facet =
|
||||||
|
result = Facet(
|
||||||
|
id: actor.dataspace.generateId.FacetId,
|
||||||
|
actor: actor,
|
||||||
|
parent: parent,
|
||||||
|
isLive: true,
|
||||||
|
inScript: true)
|
||||||
|
if parent.isSome:
|
||||||
|
parent.get.children[result.id] = result
|
||||||
|
else:
|
||||||
|
actor.rootFacet = some result
|
||||||
|
|
||||||
|
proc applyPatch(ds: Dataspace; actor: Option[Actor]; changes: Bag) =
|
||||||
|
type Pair = tuple[val: Value; count: int]
|
||||||
|
var removals: seq[Pair]
|
||||||
|
for a, count in changes.pairs:
|
||||||
|
if count > 0:
|
||||||
|
# echo "applyPatch +", a
|
||||||
|
discard ds.index.adjustAssertion(a, count)
|
||||||
|
else:
|
||||||
|
removals.add((a, count))
|
||||||
|
actor.map do (ac: Actor):
|
||||||
|
discard ac.cleanupChanges.change(a, -count)
|
||||||
|
for (a, count) in removals:
|
||||||
|
# echo "applyPatch -", a
|
||||||
|
discard ds.index.adjustAssertion(a, count)
|
||||||
|
|
||||||
|
proc initPatch(): Action =
|
||||||
|
proc impl(patch: Action; ds: Dataspace; actor: Option[Actor]) {.gcsafe.} =
|
||||||
|
ds.applyPatch(actor, patch.changes)
|
||||||
|
Action(impl: impl, kind: patchAction)
|
||||||
|
|
||||||
|
proc pendingPatch(actor): var Action =
|
||||||
|
for a in actor.pendingActions.mitems:
|
||||||
|
if a.kind == patchAction: return a
|
||||||
|
actor.pendingActions.add(initPatch())
|
||||||
|
actor.pendingActions[actor.pendingActions.high]
|
||||||
|
|
||||||
|
proc adjust(patch: var Action; v: Value; delta: int) =
|
||||||
|
discard patch.changes.change(v, delta)
|
||||||
|
|
||||||
|
proc subscribe(ds: Dataspace; handler: Analysis) =
|
||||||
|
ds.index.addHandler(handler, handler.callback.get)
|
||||||
|
|
||||||
|
proc unsubscribe(ds: Dataspace; handler: Analysis) =
|
||||||
|
ds.index.removeHandler(handler, handler.callback.get)
|
||||||
|
|
||||||
|
proc assert(actor; a: Value) = actor.pendingPatch.adjust(a, +1)
|
||||||
|
|
||||||
|
proc retract(actor; a: Value) = actor.pendingPatch.adjust(a, -1)
|
||||||
|
|
||||||
|
proc install(ep: Endpoint; spec: EndpointSpec) =
|
||||||
|
ep.spec = spec
|
||||||
|
ep.spec.assertion.map do (a: Value):
|
||||||
|
ep.facet.actor.assert(a)
|
||||||
|
ep.spec.analysis.map do (a: Analysis):
|
||||||
|
ep.facet.actor.dataspace.subscribe(a)
|
||||||
|
|
||||||
|
proc scheduleTask(actor; prio: Priority; task: Task[void]) =
|
||||||
|
if not actor.isRunnable:
|
||||||
|
actor.isRunnable = true
|
||||||
|
actor.dataspace.runnable.add(actor)
|
||||||
|
actor.pendingTasks[prio].addLast(task)
|
||||||
|
|
||||||
|
proc scheduleTask(actor; task: Task[void]) =
|
||||||
|
scheduleTask(actor, pNormal, task)
|
||||||
|
|
||||||
|
proc abandonQueuedWork(actor) =
|
||||||
|
actor.pendingActions = @[]
|
||||||
|
for q in actor.pendingTasks.mitems: clear(q)
|
||||||
|
|
||||||
|
proc uninstall(ep: Endpoint; emitPatches: bool) =
|
||||||
|
if emitPatches:
|
||||||
|
ep.spec.assertion.map do (a: Value):
|
||||||
|
ep.facet.actor.retract(a)
|
||||||
|
ep.spec.analysis.map do (a: Analysis):
|
||||||
|
ep.facet.actor.dataspace.unsubscribe(a)
|
||||||
|
|
||||||
|
proc destroy(ep: Endpoint; emitPatches: bool) =
|
||||||
|
ep.facet.actor.dataspace.dataflow.forgetSubject(ep)
|
||||||
|
ep.uninstall(emitPatches)
|
||||||
|
ep.facet.actor.scheduleTask(pGC) do ():
|
||||||
|
ep.facet.endpoints.del(ep.id)
|
||||||
|
# TODO: cannot remove from ep.facet.endpoints during
|
||||||
|
# its iteration, defering remove is probably unecessary
|
||||||
|
# because the facet is going down.
|
||||||
|
|
||||||
|
proc retractAssertionsAndSubscriptions(facet; emitPatches: bool) =
|
||||||
|
facet.actor.scheduleTask do ():
|
||||||
|
for ep in facet.endpoints.values:
|
||||||
|
ep.destroy(emitPatches)
|
||||||
|
clear(facet.endpoints)
|
||||||
|
|
||||||
|
proc abort(facet; emitPatches: bool) =
|
||||||
|
facet.isLive = false
|
||||||
|
for child in facet.children.values:
|
||||||
|
child.abort(emitPatches)
|
||||||
|
facet.retractAssertionsAndSubscriptions(emitPatches)
|
||||||
|
|
||||||
|
proc enqueueScriptAction(actor; action: Action) =
|
||||||
|
actor.pendingActions.add(action)
|
||||||
|
|
||||||
|
proc enqueueScriptAction(facet; action: Action) =
|
||||||
|
enqueueScriptAction(facet.actor, action)
|
||||||
|
|
||||||
|
proc initQuitAction(): Action =
|
||||||
|
proc impl(action: Action; ds: Dataspace; actor: Option[Actor]) =
|
||||||
|
assert(actor.isSome)
|
||||||
|
ds.applyPatch(actor, actor.get.cleanupChanges)
|
||||||
|
ds.actors.del(actor.get.id)
|
||||||
|
Action(impl: impl, kind: quitAction)
|
||||||
|
|
||||||
|
proc terminate(actor; emitPatches: bool) =
|
||||||
|
if emitPatches:
|
||||||
|
actor.scheduleTask do ():
|
||||||
|
for a in actor.adhocAssertions.keys:
|
||||||
|
actor.retract(a)
|
||||||
|
actor.rootFacet.map do (root: Facet):
|
||||||
|
root.abort(emitPatches)
|
||||||
|
actor.scheduleTask do ():
|
||||||
|
actor.enqueueScriptAction(initQuitAction())
|
||||||
|
|
||||||
|
proc invokeScript(facet; script: Script[void]) =
|
||||||
|
try: script(facet)
|
||||||
|
except:
|
||||||
|
let e = getCurrentException()
|
||||||
|
# TODO: install an error handling callback at the facet?
|
||||||
|
facet.actor.abandonQueuedWork()
|
||||||
|
facet.actor.terminate(false)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
func isInert(facet): bool =
|
||||||
|
facet.endpoints.len == 0 and facet.children.len == 0
|
||||||
|
|
||||||
|
proc terminate(facet) =
|
||||||
|
if facet.isLive:
|
||||||
|
let
|
||||||
|
actor = facet.actor
|
||||||
|
parent = facet.parent
|
||||||
|
if parent.isSome:
|
||||||
|
parent.get.children.del(facet.id)
|
||||||
|
else:
|
||||||
|
reset actor.rootFacet
|
||||||
|
facet.isLive = false
|
||||||
|
for child in facet.children.values:
|
||||||
|
child.terminate()
|
||||||
|
actor.scheduleTask do ():
|
||||||
|
facet.invokeScript do (facet: Facet):
|
||||||
|
for s in facet.stopScripts:
|
||||||
|
s(facet)
|
||||||
|
|
||||||
|
facet.retractAssertionsAndSubscriptions(true)
|
||||||
|
actor.scheduleTask(pGC) do ():
|
||||||
|
if parent.isSome:
|
||||||
|
if parent.get.isInert:
|
||||||
|
parent.get.terminate()
|
||||||
|
else:
|
||||||
|
actor.terminate(true)
|
||||||
|
|
||||||
|
template withNonScriptContext(facet; body: untyped) =
|
||||||
|
let inScriptPrev = facet.inScript
|
||||||
|
facet.inScript = false
|
||||||
|
try: body
|
||||||
|
finally: facet.inScript = inScriptPrev
|
||||||
|
|
||||||
|
proc ensureFacetSetup(facet; s: string) =
|
||||||
|
assert(not facet.inScript, "Cannot " & s & "ouside facet setup")
|
||||||
|
|
||||||
|
proc ensureNonFacetSetup(facet; s: string) =
|
||||||
|
assert(facet.inScript, "Cannot " & s & " during facet setup")
|
||||||
|
|
||||||
|
proc wrap(facet; script: Script[void]): Task[void] =
|
||||||
|
proc task() = facet.invokeScript(script)
|
||||||
|
task
|
||||||
|
|
||||||
|
proc wrap*(facet; cb: proc(facet: Facet; event: EventKind; bindings: seq[Value]) {.gcsafe.}): HandlerCallback =
|
||||||
|
proc wrapper(event: EventKind; bindings: seq[Value]) =
|
||||||
|
facet.invokeScript do (facet: Facet):
|
||||||
|
cb(facet, event, bindings)
|
||||||
|
wrapper
|
||||||
|
|
||||||
|
proc scheduleScript*(facet; prio: Priority; script: Script[void]) =
|
||||||
|
facet.actor.scheduleTask(prio, facet.wrap(script))
|
||||||
|
|
||||||
|
proc scheduleScript*(facet; script: Script[void]) =
|
||||||
|
facet.actor.scheduleTask(pNormal, facet.wrap(script))
|
||||||
|
|
||||||
|
proc addStartScript(facet; s: Script[void]) =
|
||||||
|
facet.ensureFacetSetup("onStart")
|
||||||
|
facet.scheduleScript(pNormal, s)
|
||||||
|
|
||||||
|
proc addFacet(actor; parentFacet: Option[Facet]; bootScript: Script[void]; checkInScript = false) =
|
||||||
|
if checkInScript and parentFacet.isSome:
|
||||||
|
assert parentFacet.get.inScript
|
||||||
|
let f = Facet(
|
||||||
|
id: actor.dataspace.generateId.FacetId,
|
||||||
|
actor: actor,
|
||||||
|
parent: parentFacet,
|
||||||
|
isLive: true,
|
||||||
|
inScript: true)
|
||||||
|
if parentFacet.isSome:
|
||||||
|
parentFacet.get.children[f.id] = f
|
||||||
|
f.fields = parentFacet.get.fields
|
||||||
|
# inherit scope by copying fields of the parent
|
||||||
|
else:
|
||||||
|
actor.rootFacet = some f
|
||||||
|
f.invokeScript do (facet: Facet):
|
||||||
|
facet.withNonScriptContext:
|
||||||
|
bootScript(facet)
|
||||||
|
actor.scheduleTask do ():
|
||||||
|
if ((parentFacet.isSome) and (not parentFacet.get.isLive)) or f.isInert:
|
||||||
|
f.terminate()
|
||||||
|
|
||||||
|
proc deliverMessage(ds: Dataspace; msg: Value; ac: Option[Actor]) =
|
||||||
|
ds.index.deliverMessage(msg)
|
||||||
|
|
||||||
|
proc adhocRetract(actor; a: Value) =
|
||||||
|
if actor.adhocAssertions.change(a, -1, true) == cdPresentToAbsent:
|
||||||
|
actor.retract(a)
|
||||||
|
|
||||||
|
proc refresh(ep: Endpoint) =
|
||||||
|
let newSpec = ep.updateProc(ep.facet)
|
||||||
|
if newSpec.assertion != ep.spec.assertion:
|
||||||
|
ep.uninstall(true)
|
||||||
|
ep.install(newSpec)
|
||||||
|
|
||||||
|
proc refreshAssertions(ds: Dataspace) =
|
||||||
|
ds.dataflow.repairDamage do (ep: Endpoint):
|
||||||
|
let facet = ep.facet
|
||||||
|
assert(facet.isLive)
|
||||||
|
facet.invokeScript do (f: Facet):
|
||||||
|
f.withNonScriptContext:
|
||||||
|
refresh(ep)
|
||||||
|
|
||||||
|
proc addActor(ds: Dataspace; name: string; bootProc: Script[void]; initialAssertions: Value; parent: Option[Actor]) =
|
||||||
|
var parentId: ActorId
|
||||||
|
parent.map do (p: Actor): parentId = p.id
|
||||||
|
let ac = newActor(ds, name, initialAssertions, parentId)
|
||||||
|
ds.applyPatch(some ac, ac.adhocAssertions)
|
||||||
|
ac.addFacet(none Facet) do (systemFacet: Facet):
|
||||||
|
# Root facet is a dummy "system" facet that exists to hold
|
||||||
|
# one-or-more "user" "root" facets.
|
||||||
|
ac.addFacet(some systemFacet, bootProc)
|
||||||
|
# ^ The "true root", user-visible facet.
|
||||||
|
for a in initialAssertions.set:
|
||||||
|
ac.adhocRetract(a)
|
||||||
|
|
||||||
|
proc send*(facet; body: Value) =
|
||||||
|
facet.ensureNonFacetSetup("send")
|
||||||
|
proc impl(action: Action; ds: Dataspace; actor: Option[Actor]) =
|
||||||
|
ds.deliverMessage(body, actor)
|
||||||
|
facet.enqueueScriptAction(Action(impl: impl, kind: messageAction))
|
||||||
|
|
||||||
|
proc initSpawnAction(name: string; bootProc: Script[void], initialAssertions: Value): Action =
|
||||||
|
proc impl(action: Action; ds: Dataspace; actor: Option[Actor]) =
|
||||||
|
ds.addActor(name, bootProc, initialAssertions, actor)
|
||||||
|
Action(impl: impl, kind: spawnAction)
|
||||||
|
|
||||||
|
proc spawn*(facet; name: string; bootProc: Script[void], initialAssertions: Value) =
|
||||||
|
facet.ensureNonFacetSetup("spawn")
|
||||||
|
facet.enqueueScriptAction(initSpawnAction(name, bootProc, initialAssertions))
|
||||||
|
|
||||||
|
proc spawn*(facet; name: string; bootProc: Script[void]) =
|
||||||
|
spawn(facet, name, bootProc, Value(kind: pkSet))
|
||||||
|
|
||||||
|
#[
|
||||||
|
template spawn*(facet; name: string; fields: untyped; bootProc: Script[void]): untyped =
|
||||||
|
type Fields = typeof(fields)
|
||||||
|
spawn[Fields](facet, name, bootProc, Value(kind: pkSet))
|
||||||
|
]#
|
||||||
|
|
||||||
|
proc initActivationAction(script: ActivationScript; name: string): Action =
|
||||||
|
proc impl(action: Action; ds: Dataspace; actor: Option[Actor]) =
|
||||||
|
for s in ds.activations:
|
||||||
|
if s == script: return
|
||||||
|
ds.activations.add(script)
|
||||||
|
proc boot(root: Facet) =
|
||||||
|
root.addStartScript(script)
|
||||||
|
ds.addActor(name, boot, Preserve(kind: pkSet), actor)
|
||||||
|
Action(impl: impl, kind: activationAction)
|
||||||
|
|
||||||
|
proc activate(facet; script: ActivationScript; name = "") =
|
||||||
|
facet.ensureNonFacetSetup "`activate`"
|
||||||
|
facet.enqueueScriptAction(initActivationAction(script, name))
|
||||||
|
|
||||||
|
proc newDataspace(ground: Ground; bootProc: ActivationScript): Dataspace =
|
||||||
|
let turn = Turn(actions: @[initSpawnAction("", bootProc, Value(kind: pkSet))])
|
||||||
|
Dataspace(ground: ground, index: initIndex(), pendingTurns: @[turn])
|
||||||
|
|
||||||
|
proc addEndpoint*(facet; updateScript: Script[EndpointSpec], isDynamic = true): Endpoint =
|
||||||
|
facet.ensureFacetSetup("add endpoint")
|
||||||
|
let
|
||||||
|
actor = facet.actor
|
||||||
|
dataspace = actor.dataspace
|
||||||
|
result = Endpoint(
|
||||||
|
id: dataspace.generateId.EndpointId,
|
||||||
|
facet: facet,
|
||||||
|
updateProc: updateScript)
|
||||||
|
dataspace.dataflow.addSubject(result)
|
||||||
|
let
|
||||||
|
dyn = if isDynamic: some result else: none Endpoint
|
||||||
|
initialSpec = dataspace.dataflow.withSubject(dyn) do () -> EndpointSpec:
|
||||||
|
updateScript(facet)
|
||||||
|
result.install(initialSpec)
|
||||||
|
facet.endpoints[result.id] = result
|
||||||
|
# dataspace.endpointHook(facet, result)
|
||||||
|
# Subclasses may override
|
||||||
|
|
||||||
|
proc addDataflow*(facet; prio: Priority; subjectProc: Script[void]): Endpoint =
|
||||||
|
facet.addEndpoint do (fa: Facet) -> EndpointSpec:
|
||||||
|
let subjectId = facet.actor.dataspace.dataflow.currentSubjectId
|
||||||
|
facet.scheduleScript(prio) do (fa: Facet):
|
||||||
|
if facet.isLive:
|
||||||
|
facet.actor.dataspace.dataflow.withSubject(subjectId):
|
||||||
|
subjectProc(facet)
|
||||||
|
# result is the default EndpointSpec
|
||||||
|
|
||||||
|
proc addDataflow*(facet; subjectProc: Script[void]): Endpoint =
|
||||||
|
addDataflow(facet, pNormal, subjectProc)
|
||||||
|
|
||||||
|
proc commitActions(dataspace; actor; pending: seq[Action]) =
|
||||||
|
dataspace.pendingTurns.add(Turn(actor: some actor, actions: pending))
|
||||||
|
|
||||||
|
proc runPendingTask(actor): bool =
|
||||||
|
for deque in actor.pendingTasks.mitems:
|
||||||
|
if deque.len > 0:
|
||||||
|
let task = deque.popFirst()
|
||||||
|
task()
|
||||||
|
actor.dataspace.refreshAssertions()
|
||||||
|
return true
|
||||||
|
|
||||||
|
proc runPendingTasks(actor) =
|
||||||
|
while actor.runPendingTask(): discard
|
||||||
|
actor.isRunnable = false
|
||||||
|
if actor.pendingActions.len > 0:
|
||||||
|
var pending = move actor.pendingActions
|
||||||
|
actor.dataspace.commitActions(actor, pending)
|
||||||
|
|
||||||
|
proc runPendingTasks(ds: Dataspace) =
|
||||||
|
var runnable = move ds.runnable
|
||||||
|
for actor in runnable:
|
||||||
|
runPendingTasks(actor)
|
||||||
|
|
||||||
|
proc performPendingActions(ds: Dataspace) =
|
||||||
|
var turns = move ds.pendingTurns
|
||||||
|
for turn in turns:
|
||||||
|
for action in turn.actions:
|
||||||
|
action.impl(action, ds, turn.actor)
|
||||||
|
runPendingTasks(ds)
|
||||||
|
|
||||||
|
proc runTasks(ds: Dataspace): bool =
|
||||||
|
ds.runPendingTasks()
|
||||||
|
ds.performPendingActions()
|
||||||
|
result = ds.runnable.len > 0 or ds.pendingTurns.len > 0
|
||||||
|
|
||||||
|
proc stop*(facet; continuation: Script[void]) =
|
||||||
|
facet.parent.map do (parent: Facet):
|
||||||
|
facet.actor.scheduleTask do ():
|
||||||
|
facet.terminate()
|
||||||
|
parent.scheduleScript do (parent: Facet):
|
||||||
|
continuation(parent)
|
||||||
|
# ^ TODO: is this the correct scope to use??
|
||||||
|
|
||||||
|
proc stop*(facet) =
|
||||||
|
facet.parent.map do (parent: Facet):
|
||||||
|
facet.actor.scheduleTask do ():
|
||||||
|
facet.terminate()
|
||||||
|
|
||||||
|
proc addStopHandler*(g: Ground; h: StopHandler) =
|
||||||
|
g.stopHandlers.add(h)
|
||||||
|
|
||||||
|
proc step(g: Ground) =
|
||||||
|
if g.dataspace.runTasks():
|
||||||
|
asyncdispatch.callSoon: step(g)
|
||||||
|
else:
|
||||||
|
for sh in g.stopHandlers:
|
||||||
|
sh(g.dataspace)
|
||||||
|
reset g.stopHandlers
|
||||||
|
complete(g.future)
|
||||||
|
|
||||||
|
proc bootModule*(bootProc: ActivationScript): Future[void] =
|
||||||
|
# TODO: better integration with the async dispatcher
|
||||||
|
let g = Ground(future: newFuture[void]"bootModule")
|
||||||
|
g.dataspace = newDataspace(g) do (rootFacet: Facet):
|
||||||
|
rootFacet.addStartScript do (rootFacet: Facet):
|
||||||
|
rootFacet.activate(bootProc)
|
||||||
|
addTimer(1, true) do (fd: AsyncFD) -> bool:
|
||||||
|
step(g)
|
||||||
|
true
|
||||||
|
return g.future
|
||||||
|
|
||||||
|
proc registerField*(facet: Facet; field: Field): int =
|
||||||
|
# TODO: should be hidden, but used by declareField.
|
||||||
|
for i in 0..facet.fields.high:
|
||||||
|
if facet.fields[i].name == field.name:
|
||||||
|
facet.fields[i] = field
|
||||||
|
return i
|
||||||
|
facet.fields.add(field)
|
||||||
|
return facet.fields.high
|
||||||
|
|
||||||
|
template declareField*(facet: Facet; F: untyped; T: typedesc; init: T): untyped =
|
||||||
|
## Declare getter and setter procs for field `F` of type `T` initalized with `init`.
|
||||||
|
# TODO: do more at compile-time, use a tuple rather than a sequence of Preserves.
|
||||||
|
let
|
||||||
|
`F FieldId` = facet.actor.dataspace.generateId.FieldId
|
||||||
|
`F FeildOff` = registerField(facet,
|
||||||
|
(nimIdentNormalize("`F`"), toPreserve(init), ))
|
||||||
|
facet.actor.dataspace.dataflow.defineObservableProperty(`F FieldId`)
|
||||||
|
|
||||||
|
proc `F`(fields: Fields): T =
|
||||||
|
facet.actor.dataspace.dataflow.recordObservation(`F FieldId`)
|
||||||
|
fromPreserve[T](result, facet.fields[`F FeildOff`].value)
|
||||||
|
|
||||||
|
proc `F =`(fields: var Fields; x: T) =
|
||||||
|
facet.actor.dataspace.dataflow.recordDamage(`F FieldId`)
|
||||||
|
facet.fields[`F FeildOff`].value = toPreserve[T](x)
|
||||||
|
|
||||||
|
proc `F =`(fields: var Fields; x: Preserve) =
|
||||||
|
assert(x.kind == facet.fields[`F FeildOff`].value.kind, "invalid Preserves item for field type")
|
||||||
|
facet.actor.dataspace.dataflow.recordDamage(`F FieldId`)
|
||||||
|
facet.fields[`F FeildOff`].value = x
|
||||||
|
|
||||||
|
|
||||||
|
#[
|
||||||
|
# Some early macros
|
||||||
|
|
||||||
|
proc send() =
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc generateField(stmt: NimNode): NimNode =
|
||||||
|
stmt.expectLen 3
|
||||||
|
let l = stmt[1]
|
||||||
|
let r = stmt[2]
|
||||||
|
case r.kind
|
||||||
|
of nnkStmtList:
|
||||||
|
r.expectLen 1
|
||||||
|
result = newNimNode(nnkVarSection).add(newIdentDefs(l, r[0]))
|
||||||
|
else:
|
||||||
|
raiseAssert("unhandled field " & r.treeRepr)
|
||||||
|
|
||||||
|
proc generateStopOnTuple(output: var NimNode; stmt: NimNode): NimNode =
|
||||||
|
stmt.expectLen 3
|
||||||
|
let
|
||||||
|
cond = stmt[1]
|
||||||
|
body = stmt[2]
|
||||||
|
# symCond = genSym(ident="stopOnCond")
|
||||||
|
symStop = genSym(ident="stopOnCb")
|
||||||
|
# output.add(newProc(name = symCond, params = [ident"bool"], body = cond))
|
||||||
|
output.add(newProc(name = symStop, body = body))
|
||||||
|
newPar(newColonExpr(ident"cond", cond), newColonExpr(ident"body", symStop))
|
||||||
|
|
||||||
|
macro spawn*(label: string; body: untyped) =
|
||||||
|
## Spawn actor.
|
||||||
|
result = newNimNode(nnkStmtList)
|
||||||
|
let blockBody = newNimNode(nnkStmtList)
|
||||||
|
let actorSym = genSym(nskVar, label.strVal)
|
||||||
|
result.add(newNimNode(nnkVarSection).add(
|
||||||
|
newIdentDefs(actorSym, ident"Actor")))
|
||||||
|
var stopOnSeq = newNimNode(nnkBracket)
|
||||||
|
body.expectKind nnkStmtList
|
||||||
|
for stmt in body:
|
||||||
|
case stmt.kind
|
||||||
|
of nnkCommentStmt:
|
||||||
|
result.add(stmt)
|
||||||
|
of nnkCommand:
|
||||||
|
let cmd = stmt[0]
|
||||||
|
cmd.expectKind nnkIdent
|
||||||
|
if eqIdent(cmd, "field"):
|
||||||
|
result.add(generateField(stmt))
|
||||||
|
elif eqIdent(cmd, "stopOn"):
|
||||||
|
let stop = generateStopOnTuple(result, stmt)
|
||||||
|
stopOnSeq.add(stop)
|
||||||
|
else:
|
||||||
|
raiseAssert("unhandled spawn command " & cmd.repr)
|
||||||
|
else:
|
||||||
|
raiseAssert("unhandled statment " & stmt.treeRepr)
|
||||||
|
result.add(
|
||||||
|
newAssignment(newDotExpr(actorSym, ident"stops"), prefix(stopOnSeq, "@")))
|
||||||
|
#result.add(newBlockStmt(blockBody))
|
||||||
|
echo result.repr
|
||||||
|
# echo result.treeRepr
|
||||||
|
|
||||||
|
macro dumpStuff*(body: untyped): untyped =
|
||||||
|
echo body.treeRepr
|
||||||
|
]#
|
|
@ -0,0 +1,11 @@
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
import preserves
|
||||||
|
|
||||||
|
const
|
||||||
|
Discard* = RecordClass(label: symbol"discard", arity: 0)
|
||||||
|
Capture* = RecordClass(label: symbol"capture", arity: 1)
|
||||||
|
Observe* = RecordClass(label: symbol"observe", arity: 1)
|
||||||
|
Inbound* = RecordClass(label: symbol"inbound", arity: 1)
|
||||||
|
Outbound* = RecordClass(label: symbol"outbound", arity: 1)
|
||||||
|
Instance* = RecordClass(label: symbol"instance", arity: 1)
|
|
@ -0,0 +1,37 @@
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
## An unordered association of items to counts.
|
||||||
|
## An item count may be negative, unlike CountTable.
|
||||||
|
|
||||||
|
import tables
|
||||||
|
|
||||||
|
type
|
||||||
|
ChangeDescription* = enum
|
||||||
|
cdPresentToAbsent,
|
||||||
|
cdAbsentToAbsent,
|
||||||
|
cdAbsentToPresent,
|
||||||
|
cdPresentToPresent
|
||||||
|
|
||||||
|
Bag*[T] = Table[T, int]
|
||||||
|
|
||||||
|
proc change(count: var int; delta: int; clamp: bool): ChangeDescription =
|
||||||
|
var
|
||||||
|
oldCount = count
|
||||||
|
newCount = oldCount + delta
|
||||||
|
if clamp:
|
||||||
|
newCount = max(0, newCount)
|
||||||
|
if newCount == 0:
|
||||||
|
result =
|
||||||
|
if oldCount == 0: cdAbsentToAbsent
|
||||||
|
else: cdPresentToAbsent
|
||||||
|
else:
|
||||||
|
result =
|
||||||
|
if oldCount == 0: cdAbsentToPresent
|
||||||
|
else: cdPresentToPresent
|
||||||
|
count = newCount
|
||||||
|
|
||||||
|
proc change*[T](bag: var Bag[T]; key: T; delta: int; clamp = false): ChangeDescription =
|
||||||
|
assert(delta != 0)
|
||||||
|
result = change(bag.mGetOrPut(key, 0), delta, clamp)
|
||||||
|
if result in {cdAbsentToAbsent, cdPresentToAbsent}:
|
||||||
|
bag.del(key)
|
|
@ -0,0 +1,80 @@
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
import preserves
|
||||||
|
import options, sets, tables
|
||||||
|
|
||||||
|
type
|
||||||
|
Set = HashSet
|
||||||
|
|
||||||
|
Graph*[SubjectId, ObjectId] = object
|
||||||
|
edgesForward: Table[ObjectId, Set[SubjectId]]
|
||||||
|
edgesReverse: Table[SubjectId, Set[ObjectId]]
|
||||||
|
damagedNodes: Set[ObjectId]
|
||||||
|
currentSubjectId*: Option[SubjectId]
|
||||||
|
|
||||||
|
proc withSubject*[Sid, Oid, T](g: var Graph[Sid, Oid]; sid: Option[Sid];
|
||||||
|
body: proc (): T): T =
|
||||||
|
var oldSid = move g.currentSubjectId
|
||||||
|
g.currentSubjectId = sid
|
||||||
|
try: result = body()
|
||||||
|
finally: g.currentSubjectId = move oldSid
|
||||||
|
|
||||||
|
proc withSubject*[Sid, Oid](g: var Graph[Sid, Oid]; sid: Option[Sid];
|
||||||
|
body: proc ()) =
|
||||||
|
var oldSid = move g.currentSubjectId
|
||||||
|
g.currentSubjectId = sid
|
||||||
|
try: body()
|
||||||
|
finally: g.currentSubjectId = move oldSid
|
||||||
|
|
||||||
|
proc recordObservation*[Sid, Oid](g: var Graph[Sid, Oid]; oid: Oid) =
|
||||||
|
if g.currentSubjectId.isSome:
|
||||||
|
let sid = g.currentSubjectId.get
|
||||||
|
if not g.edgesForward.hasKey(oid):
|
||||||
|
g.edgesForward[oid] = initHashSet[Sid]()
|
||||||
|
g.edgesForward[oid].incl(sid)
|
||||||
|
if not g.edgesReverse.hasKey(sid):
|
||||||
|
g.edgesReverse[sid] = initHashSet[Oid]()
|
||||||
|
g.edgesReverse[sid].incl(oid)
|
||||||
|
# TODO: double lookups here
|
||||||
|
|
||||||
|
proc recordDamage*[Sid, Oid](g: var Graph[Sid, Oid]; oid: Oid) =
|
||||||
|
g.damagedNodes.incl(oid)
|
||||||
|
|
||||||
|
proc forgetSubject*[Sid, Oid](g: var Graph[Sid, Oid]; sid: Sid) =
|
||||||
|
var subjectObjects: Set[Oid]
|
||||||
|
if g.edgesReverse.pop(sid, subjectObjects):
|
||||||
|
for oid in subjectObjects:
|
||||||
|
g.edgesForward.del(oid)
|
||||||
|
|
||||||
|
iterator observersOf[Sid, Oid](g: Graph[Sid, Oid]; oid: Oid): Sid =
|
||||||
|
if g.edgesForward.hasKey(oid):
|
||||||
|
for sid in g.edgesForward[oid]: yield sid
|
||||||
|
|
||||||
|
proc repairDamage*[Sid, Oid](g: var Graph[Sid, Oid]; repairNode: proc (sid: Sid) {.gcsafe.}) =
|
||||||
|
var repairedThisRound: Set[Oid]
|
||||||
|
while true:
|
||||||
|
var workSet = move g.damagedNodes
|
||||||
|
assert(g.damagedNodes.len == 0)
|
||||||
|
|
||||||
|
var alreadyDamaged = workSet * repairedThisRound
|
||||||
|
if alreadyDamaged.len > 0:
|
||||||
|
echo "Cyclic dependencies involving ", alreadyDamaged
|
||||||
|
|
||||||
|
workSet = workSet - repairedThisRound
|
||||||
|
repairedThisRound = repairedThisRound + workSet
|
||||||
|
|
||||||
|
if workSet.len == 0: break
|
||||||
|
|
||||||
|
for oid in workSet:
|
||||||
|
for sid in g.observersOf(oid):
|
||||||
|
g.forgetSubject(sid)
|
||||||
|
g.withSubject(some sid) do: repairNode(sid)
|
||||||
|
|
||||||
|
proc defineObservableProperty*[Sid, Oid](g: var Graph[Sid, Oid]; oid: Oid) =
|
||||||
|
assert(not g.edgesForward.hasKey(oid))
|
||||||
|
g.edgesForward[oid] = initHashSet[Sid]()
|
||||||
|
g.recordDamage(oid)
|
||||||
|
|
||||||
|
proc addSubject*[Sid, Oid](g: var Graph[Sid, Oid]; sid: Sid) =
|
||||||
|
assert(not g.edgesReverse.hasKey(sid))
|
||||||
|
g.edgesReverse[sid] = initHashSet[Oid]()
|
|
@ -0,0 +1,3 @@
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
type EventKind* = enum addedEvent, removedEvent, messageEvent
|
|
@ -0,0 +1,297 @@
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
import ./assertions, ./bags, ./events
|
||||||
|
import preserves
|
||||||
|
import lists, options, sets, tables
|
||||||
|
|
||||||
|
type
|
||||||
|
Value = Preserve
|
||||||
|
|
||||||
|
NonEmptySkeleton*[Shape] = object
|
||||||
|
shape: Shape
|
||||||
|
members: seq[Skeleton[Shape]]
|
||||||
|
Skeleton*[Shape] = Option[NonEmptySkeleton[Shape]]
|
||||||
|
|
||||||
|
Path = seq[Natural]
|
||||||
|
|
||||||
|
proc projectPath(v: Value; path: Path): Value =
|
||||||
|
result = v
|
||||||
|
for index in path:
|
||||||
|
result = result[index]
|
||||||
|
|
||||||
|
proc projectPaths(v: Value; paths: seq[Path]): seq[Value] =
|
||||||
|
result.setLen(paths.len)
|
||||||
|
for i, path in paths: result[i] = projectPath(v, path)
|
||||||
|
|
||||||
|
type
|
||||||
|
Shape = string
|
||||||
|
|
||||||
|
HandlerCallback* = proc (event: EventKind; bindings: seq[Value]) {.gcsafe.}
|
||||||
|
|
||||||
|
Analysis* = object
|
||||||
|
skeleton: Skeleton[Shape]
|
||||||
|
constPaths: seq[Path]
|
||||||
|
constVals: seq[Value]
|
||||||
|
capturePaths: seq[Path]
|
||||||
|
callback*: Option[HandlerCallback]
|
||||||
|
|
||||||
|
proc `$`(a: Analysis): string =
|
||||||
|
result.add "\n\t skeleton: "
|
||||||
|
result.add $a.skeleton
|
||||||
|
result.add "\n\t constPaths: "
|
||||||
|
result.add $a.constPaths
|
||||||
|
result.add "\n\t constVals: "
|
||||||
|
result.add $a.constVals
|
||||||
|
result.add "\n\t capturePaths: "
|
||||||
|
result.add $a.capturePaths
|
||||||
|
|
||||||
|
proc analyzeAssertion*(a: Value): Analysis =
|
||||||
|
var path: Path
|
||||||
|
proc walk(analysis: var Analysis; a: Value): Skeleton[Shape] =
|
||||||
|
if Capture.isClassOf a:
|
||||||
|
analysis.capturePaths.add(path)
|
||||||
|
result = walk(analysis, a.fields[0])
|
||||||
|
elif Discard.isClassOf a:
|
||||||
|
discard
|
||||||
|
else:
|
||||||
|
if a.kind == pkRecord:
|
||||||
|
let class = classOf(a)
|
||||||
|
result = some NonEmptySkeleton[Shape](shape: $class)
|
||||||
|
path.add(0)
|
||||||
|
var i: int
|
||||||
|
for field in a.fields:
|
||||||
|
path[path.high] = i
|
||||||
|
result.get.members.add(walk(analysis, field))
|
||||||
|
inc(i)
|
||||||
|
discard path.pop
|
||||||
|
else:
|
||||||
|
analysis.constPaths.add(path)
|
||||||
|
analysis.constVals.add(a)
|
||||||
|
result.skeleton = walk(result, a)
|
||||||
|
|
||||||
|
type
|
||||||
|
Handler = ref object
|
||||||
|
cachedCaptures: Bag[seq[Value]]
|
||||||
|
callbacks: HashSet[HandlerCallback]
|
||||||
|
|
||||||
|
Leaf = ref object
|
||||||
|
cachedAssertions: HashSet[Value]
|
||||||
|
handlerMap: Table[seq[Path], Handler]
|
||||||
|
|
||||||
|
Continuation = ref object
|
||||||
|
cachedAssertions: HashSet[Value]
|
||||||
|
leafMap: Table[seq[Path], TableRef[seq[Value], Leaf]] # TODO: not TableRef?
|
||||||
|
|
||||||
|
Selector = tuple[popCount: int; index: int]
|
||||||
|
|
||||||
|
Node = ref object
|
||||||
|
edges: Table[Selector, Table[string, Node]]
|
||||||
|
continuation: Continuation
|
||||||
|
|
||||||
|
using
|
||||||
|
continuation: Continuation
|
||||||
|
leaf: Leaf
|
||||||
|
node: Node
|
||||||
|
|
||||||
|
proc `$`(leaf): string =
|
||||||
|
result.add "Leaf{ cached: "
|
||||||
|
result.add $leaf.cachedAssertions
|
||||||
|
result.add ", handler count: "
|
||||||
|
result.add $leaf.handlerMap.len
|
||||||
|
result.add " }"
|
||||||
|
|
||||||
|
proc `$`(continuation): string =
|
||||||
|
result.add "Continuation{ cached: "
|
||||||
|
result.add $continuation.cachedAssertions
|
||||||
|
result.add ", "
|
||||||
|
result.add $continuation.leafMap
|
||||||
|
result.add " }"
|
||||||
|
|
||||||
|
proc `$`(node): string =
|
||||||
|
result.add "Node{ "
|
||||||
|
result.add $node.continuation
|
||||||
|
result.add ", edges: "
|
||||||
|
result.add $node.edges
|
||||||
|
result.add "}"
|
||||||
|
|
||||||
|
proc isEmpty(leaf): bool =
|
||||||
|
leaf.cachedAssertions.len == 0 and leaf.handlerMap.len == 0
|
||||||
|
|
||||||
|
type
|
||||||
|
ContinuationProc = proc (c: Continuation; v: Value) {.gcsafe.}
|
||||||
|
LeafProc = proc (l: Leaf; v: Value) {.gcsafe.}
|
||||||
|
HandlerProc = proc (h: Handler; vs: seq[Value]) {.gcsafe.}
|
||||||
|
|
||||||
|
proc modify(node; operation: EventKind; outerValue: Value;
|
||||||
|
mCont: ContinuationProc; mLeaf: LeafProc; mHandler: HandlerProc) =
|
||||||
|
|
||||||
|
proc walkContinuation(continuation) {.gcsafe.}
|
||||||
|
|
||||||
|
proc walkNode(node; termStack: SinglyLinkedList[seq[Value]]) =
|
||||||
|
# TODO: use a seq for the stack?
|
||||||
|
walkContinuation(node.continuation)
|
||||||
|
for (selector, table) in node.edges.pairs:
|
||||||
|
var nextStack = termStack
|
||||||
|
for _ in 1..selector.popCount:
|
||||||
|
nextStack.head = nextStack.head.next
|
||||||
|
let nextValue = nextStack.head.value[selector.index]
|
||||||
|
if nextValue.isRecord:
|
||||||
|
let nextClass = classOf(nextValue)
|
||||||
|
let nextNode = table.getOrDefault($nextClass)
|
||||||
|
if not nextNode.isNil:
|
||||||
|
nextStack.prepend(nextValue.record)
|
||||||
|
walkNode(nextNode, nextStack)
|
||||||
|
|
||||||
|
proc walkContinuation(continuation: Continuation) =
|
||||||
|
mCont(continuation, outerValue)
|
||||||
|
for (constPaths, constValMap) in continuation.leafMap.pairs:
|
||||||
|
let constVals = projectPaths(outerValue, constPaths)
|
||||||
|
let leaf = constValMap.getOrDefault(constVals)
|
||||||
|
if leaf.isNil:
|
||||||
|
if operation == addedEvent:
|
||||||
|
constValMap[constVals] = Leaf()
|
||||||
|
else:
|
||||||
|
mLeaf(leaf, outerValue)
|
||||||
|
for (capturePaths, handler) in leaf.handlerMap.pairs:
|
||||||
|
mHandler(handler, projectPaths(outerValue, capturePaths))
|
||||||
|
if operation == removedEvent and leaf.isEmpty:
|
||||||
|
constValMap.del(constVals)
|
||||||
|
if constValMap.len == 0:
|
||||||
|
continuation.leafMap.del(constPaths)
|
||||||
|
var stack: SinglyLinkedList[seq[Value]]
|
||||||
|
stack.prepend(@[outerValue])
|
||||||
|
walkNode(node, stack)
|
||||||
|
|
||||||
|
proc extend*[Shape](node; skeleton: Skeleton[Shape]): Continuation =
|
||||||
|
var path: Path
|
||||||
|
proc walkNode(node: Node; popCount, index: int; skeleton: Skeleton[Shape]): tuple[popCount: int, node: Node] =
|
||||||
|
assert(not node.isNil)
|
||||||
|
if skeleton.isNone:
|
||||||
|
return (popCount, node)
|
||||||
|
else:
|
||||||
|
var
|
||||||
|
cls = skeleton.get.shape
|
||||||
|
table: Table[string, Node]
|
||||||
|
nextNode: Node
|
||||||
|
discard node.edges.pop((popCount, index), table)
|
||||||
|
if not table.pop(cls, nextNode):
|
||||||
|
nextNode = Node(continuation: Continuation())
|
||||||
|
for a in node.continuation.cachedAssertions:
|
||||||
|
if $classOf(projectPath(a, path)) == cls:
|
||||||
|
nextNode.continuation.cachedAssertions.incl(a)
|
||||||
|
block:
|
||||||
|
var
|
||||||
|
popCount = 0
|
||||||
|
index = 0
|
||||||
|
path.add(index)
|
||||||
|
for member in skeleton.get.members:
|
||||||
|
(popCount, nextNode) = walkNode(nextNode, popCount, index, member)
|
||||||
|
inc(index)
|
||||||
|
discard path.pop()
|
||||||
|
path.add(index)
|
||||||
|
discard path.pop()
|
||||||
|
result = (popCount.succ, nextNode)
|
||||||
|
table[cls] = nextNode
|
||||||
|
node.edges[(popCount, index)] = table
|
||||||
|
walkNode(node, 0, 0, skeleton).node.continuation
|
||||||
|
|
||||||
|
type
|
||||||
|
Index* = object
|
||||||
|
allAssertions: Bag[Value]
|
||||||
|
root: Node
|
||||||
|
|
||||||
|
proc initIndex*(): Index =
|
||||||
|
result.root = Node(continuation: Continuation())
|
||||||
|
|
||||||
|
using index: Index
|
||||||
|
|
||||||
|
proc `$`*(index): string =
|
||||||
|
result.add "Index("
|
||||||
|
result.add ")Index"
|
||||||
|
|
||||||
|
proc addHandler*(index; res: Analysis; callback: HandlerCallback) =
|
||||||
|
assert(not index.root.isNil)
|
||||||
|
let
|
||||||
|
constPaths = res.constPaths
|
||||||
|
constVals = res.constVals
|
||||||
|
capturePaths = res.capturePaths
|
||||||
|
continuation = index.root.extend(res.skeleton)
|
||||||
|
assert(not continuation.isNil)
|
||||||
|
var constValMap = continuation.leafMap.getOrDefault(constPaths)
|
||||||
|
if constValMap.isNil:
|
||||||
|
constValMap = newTable[seq[Value], Leaf]()
|
||||||
|
for a in continuation.cachedAssertions:
|
||||||
|
var leaf: Leaf
|
||||||
|
if not constValMap.pop(a.sequence, leaf):
|
||||||
|
new leaf
|
||||||
|
leaf.cachedAssertions.incl(a)
|
||||||
|
constValMap[a.sequence] = leaf
|
||||||
|
continuation.leafMap[constPaths] = constValMap
|
||||||
|
var leaf = constValMap.getOrDefault(constVals)
|
||||||
|
if leaf.isNil:
|
||||||
|
new leaf
|
||||||
|
constValMap[constVals] = leaf
|
||||||
|
var handler = leaf.handlerMap.getOrDefault(capturePaths)
|
||||||
|
if handler.isNil:
|
||||||
|
new handler
|
||||||
|
for a in leaf.cachedAssertions:
|
||||||
|
let a = projectPaths(a, capturePaths)
|
||||||
|
if handler.cachedCaptures.contains(a):
|
||||||
|
discard handler.cachedCaptures.change(a, +1)
|
||||||
|
leaf.handlerMap[capturePaths] = handler
|
||||||
|
handler.callbacks.incl(callback)
|
||||||
|
for captures, count in handler.cachedCaptures.pairs:
|
||||||
|
callback(addedEvent, captures)
|
||||||
|
|
||||||
|
proc removeHandler*(index; res: Analysis; callback: HandlerCallback) =
|
||||||
|
let continuation = index.root.extend(res.skeleton)
|
||||||
|
try:
|
||||||
|
let
|
||||||
|
constValMap = continuation.leafMap[res.constPaths]
|
||||||
|
leaf = constValMap[res.constVals]
|
||||||
|
handler = leaf.handlerMap[res.capturePaths]
|
||||||
|
handler.callbacks.excl(callback)
|
||||||
|
if handler.callbacks.len == 0:
|
||||||
|
leaf.handlerMap.del(res.capturePaths)
|
||||||
|
if leaf.isEmpty:
|
||||||
|
constValMap.del(res.constVals)
|
||||||
|
if constValMap.len == 0:
|
||||||
|
continuation.leafMap.del(res.constPaths)
|
||||||
|
except KeyError: discard
|
||||||
|
|
||||||
|
proc adjustAssertion*(index: var Index; outerValue: Value; delta: int): ChangeDescription =
|
||||||
|
result = index.allAssertions.change(outerValue, delta)
|
||||||
|
case result
|
||||||
|
of cdAbsentToPresent:
|
||||||
|
index.root.modify(
|
||||||
|
addedEvent,
|
||||||
|
outerValue,
|
||||||
|
(proc (c: Continuation; v: Value) = c.cachedAssertions.incl(v)),
|
||||||
|
(proc (l: Leaf; v: Value) = l.cachedAssertions.incl(v)),
|
||||||
|
(proc (h: Handler; vs: seq[Value]) =
|
||||||
|
if h.cachedCaptures.change(vs, +1) == cdAbsentToPresent:
|
||||||
|
for cb in h.callbacks: cb(addedEvent, vs)))
|
||||||
|
of cdPresentToAbsent:
|
||||||
|
index.root.modify(
|
||||||
|
removedEvent,
|
||||||
|
outerValue,
|
||||||
|
(proc (c: Continuation; v: Value) = c.cachedAssertions.excl(v)),
|
||||||
|
(proc (l: Leaf; v: Value) = l.cachedAssertions.excl(v)),
|
||||||
|
(proc (h: Handler; vs: seq[Value]) =
|
||||||
|
if h.cachedCaptures.change(vs, -1) == cdPresentToAbsent:
|
||||||
|
for cb in h.callbacks: cb(removedEvent, vs)))
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc continuationNoop(c: Continuation; v: Value) = discard
|
||||||
|
proc leafNoop(l: Leaf; v: Value) = discard
|
||||||
|
|
||||||
|
proc deliverMessage*(index; v: Value; leafCb: proc (l: Leaf; v: Value) {.gcsafe.}) =
|
||||||
|
proc handlerCb(h: Handler; vs: seq[Value]) =
|
||||||
|
for cb in h.callbacks: cb(messageEvent, vs)
|
||||||
|
index.root.modify(messageEvent, v, continuationNoop, leafCb, handlerCb)
|
||||||
|
|
||||||
|
proc deliverMessage*(index; v: Value) =
|
||||||
|
proc handlerCb(h: Handler; vs: seq[Value]) =
|
||||||
|
for cb in h.callbacks: cb(messageEvent, vs)
|
||||||
|
index.root.modify(messageEvent, v, continuationNoop, leafNoop, handlerCb)
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Package
|
||||||
|
|
||||||
|
version = "0.0.0"
|
||||||
|
author = "Emery Hemingway"
|
||||||
|
description = "Syndicated actors for conversational concurrency"
|
||||||
|
license = "ISC"
|
||||||
|
srcDir = "src"
|
||||||
|
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
requires "nim >= 1.4.8", "preserves >= 0.2.0"
|
|
@ -0,0 +1 @@
|
||||||
|
switch("path", "$projectDir/../src")
|
|
@ -0,0 +1,73 @@
|
||||||
|
# SPDX-License-Identifier: ISC
|
||||||
|
|
||||||
|
import syndicate, syndicate/assertions, syndicate/events, syndicate/skeletons
|
||||||
|
import preserves
|
||||||
|
import asyncdispatch, tables, options
|
||||||
|
|
||||||
|
const N = 100000
|
||||||
|
|
||||||
|
const
|
||||||
|
`?_` = init(Discard)
|
||||||
|
`?$` = init(Capture, `?_`)
|
||||||
|
BoxState = RecordClass(label: symbol"BoxState", arity: 1)
|
||||||
|
SetBox = RecordClass(label: symbol"SetBox", arity: 1)
|
||||||
|
|
||||||
|
|
||||||
|
proc boot(facet: Facet) =
|
||||||
|
|
||||||
|
facet.spawn("box") do (facet: Facet):
|
||||||
|
facet.declareField(value, int, 0)
|
||||||
|
|
||||||
|
discard facet.addEndpoint do (facet: Facet) -> EndpointSpec:
|
||||||
|
# echo "recomputing published BoxState ", facet.fields.value
|
||||||
|
let a = BoxState.init(facet.fields.value.toPreserve)
|
||||||
|
result.assertion = some a
|
||||||
|
|
||||||
|
discard facet.addDataflow do (facet: Facet):
|
||||||
|
# echo "box dataflow saw new value ", facet.fields.value
|
||||||
|
if facet.fields.value == N:
|
||||||
|
facet.stop do (facet: Facet):
|
||||||
|
echo "terminated box root facet"
|
||||||
|
|
||||||
|
discard facet.addEndpoint do (facet: Facet) -> EndpointSpec:
|
||||||
|
const a = SetBox.init(`?$`)
|
||||||
|
result.analysis = some analyzeAssertion(a)
|
||||||
|
proc cb(facet: Facet; evt: EventKind; vs: seq[Value]) =
|
||||||
|
if evt == messageEvent:
|
||||||
|
facet.scheduleScript do (facet: Facet):
|
||||||
|
facet.fields.value = vs[0]
|
||||||
|
# echo "box updated value ", vs[0]
|
||||||
|
result.analysis.get.callback = some (facet.wrap cb)
|
||||||
|
const o = Observe.init(SetBox.init(`?$`))
|
||||||
|
result.assertion = some o
|
||||||
|
|
||||||
|
facet.spawn("client") do (facet: Facet):
|
||||||
|
|
||||||
|
discard facet.addEndpoint do (facet: Facet) -> EndpointSpec:
|
||||||
|
const a = BoxState.init(`?$`)
|
||||||
|
result.analysis = some analyzeAssertion(a)
|
||||||
|
proc cb(facet: Facet; evt: EventKind; vs: seq[Value]) =
|
||||||
|
if evt == addedEvent:
|
||||||
|
facet.scheduleScript do (facet: Facet):
|
||||||
|
let v = SetBox.init(vs[0].int.succ.toPreserve)
|
||||||
|
# echo "client sending ", v
|
||||||
|
facet.send(v)
|
||||||
|
result.analysis.get.callback = some (facet.wrap cb)
|
||||||
|
const o = Observe.init(BoxState.init(`?$`))
|
||||||
|
result.assertion = some o
|
||||||
|
|
||||||
|
discard facet.addEndpoint do (facet: Facet) -> EndpointSpec:
|
||||||
|
const a = BoxState.init(`?_`)
|
||||||
|
result.analysis = some analyzeAssertion(a)
|
||||||
|
proc cb(facet: Facet; evt: EventKind; vs: seq[Value]) =
|
||||||
|
if evt == removedEvent:
|
||||||
|
facet.scheduleScript do (facet: Facet):
|
||||||
|
echo "box gone"
|
||||||
|
result.analysis.get.callback = some (facet.wrap cb)
|
||||||
|
const o = Observe.init(BoxState.init(`?_`))
|
||||||
|
result.assertion = some o
|
||||||
|
|
||||||
|
facet.actor.dataspace.ground.addStopHandler do (_: Dataspace):
|
||||||
|
echo "stopping box-and-client"
|
||||||
|
|
||||||
|
waitFor bootModule(boot)
|
Loading…
Reference in New Issue