179 lines
6.2 KiB
Nim
179 lines
6.2 KiB
Nim
|
# SPDX-License-Identifier: ISC
|
||
|
|
||
|
import preserves, preserves/records
|
||
|
|
||
|
import ./assertions, ./dataspaces, ./events, ./skeletons
|
||
|
import std/[asyncdispatch, macros, options]
|
||
|
|
||
|
export assertions.Capture
|
||
|
export assertions.Discard
|
||
|
export assertions.Observe
|
||
|
export dataspaces.Facet
|
||
|
export dataspaces.FieldId
|
||
|
export dataspaces.Fields
|
||
|
export dataspaces.addEndpoint
|
||
|
export dataspaces.defineObservableProperty
|
||
|
export dataspaces.generateId
|
||
|
export dataspaces.hash
|
||
|
export dataspaces.recordDamage
|
||
|
export dataspaces.recordObservation
|
||
|
export dataspaces.scheduleScript
|
||
|
export events.EventKind
|
||
|
export skeletons.Analysis
|
||
|
|
||
|
export asyncdispatch.`callback=`
|
||
|
|
||
|
proc `==`*(x, y: FieldId): bool {.borrow.}
|
||
|
|
||
|
proc newLit(p: pointer): NimNode = ident"nil"
|
||
|
## Hack to make `newLit` work on `Presevere`.
|
||
|
|
||
|
proc getCurrentFacet*(): Facet =
|
||
|
## Return the current `Facet` for this context.
|
||
|
raiseAssert "getCurrentFacet called outside of a Syndicate context"
|
||
|
|
||
|
template stopIf*(cond, body: untyped): untyped =
|
||
|
## Stop the current facet if `cond` is true and
|
||
|
## invoke `body` after the facet has stopped.
|
||
|
mixin getCurrentFacet
|
||
|
discard getCurrentFacet().addDataflow do (facet: Facet):
|
||
|
if cond:
|
||
|
facet.stop do (facet: Facet):
|
||
|
body
|
||
|
|
||
|
template send*(class: RecordClass; fields: varargs[Preserve,
|
||
|
toPreserve]): untyped =
|
||
|
mixin getCurrentFacet
|
||
|
send(getCurrentFacet(), init(class, fields))
|
||
|
|
||
|
proc assertionForRecord(class: RecordClass; doHandler: NimNode): NimNode =
|
||
|
## Generate an assertion that captures or discards the items of record `class`
|
||
|
## according to the parameters of `doHandler`. `_` parameters are discarded.
|
||
|
let formalArgs = doHandler[3]
|
||
|
if formalArgs.len.pred != class.arity:
|
||
|
error($formalArgs.repr & " does not match record class " & $class, doHandler)
|
||
|
result = newCall("init", newLit(class))
|
||
|
for i, arg in formalArgs:
|
||
|
if i > 0:
|
||
|
arg.expectKind nnkIdentDefs
|
||
|
if arg[0] == ident"_":
|
||
|
result.add newCall("init", ident"Discard")
|
||
|
else:
|
||
|
result.add newCall("init", ident"Capture", newCall("init",
|
||
|
ident"Discard"))
|
||
|
|
||
|
proc callbackForEvent(event: EventKind; class: RecordClass; doHandler: NimNode;
|
||
|
assertion: NimNode): NimNode =
|
||
|
## Generate a procedure that checks an event kind, unpacks the fields of `class` to match the
|
||
|
## parameters of `doHandler`, and calls the body of `doHandler`.
|
||
|
# TODO: the assertion parameter is just for tracing.
|
||
|
let formalArgs = doHandler[3]
|
||
|
if formalArgs.len.pred != class.arity:
|
||
|
error($formalArgs.repr & " does not match record class " & $class, doHandler)
|
||
|
doHandler.expectKind nnkDo
|
||
|
let
|
||
|
cbFacetSym = genSym(nskParam, "facet")
|
||
|
scriptFacetSym = genSym(nskParam, "facet")
|
||
|
eventSym = genSym(nskParam, "event")
|
||
|
recSym = genSym(nskParam, "record")
|
||
|
var
|
||
|
letSection = newNimNode(nnkLetSection, doHandler)
|
||
|
captureCount: int
|
||
|
for i, arg in formalArgs:
|
||
|
if i > 0:
|
||
|
arg.expectKind nnkIdentDefs
|
||
|
if arg[0] == ident"_" or arg[0] == ident"*":
|
||
|
if arg[1].kind != nnkEmpty:
|
||
|
error("placeholders may not be typed", arg)
|
||
|
else:
|
||
|
if arg[1].kind == nnkEmpty:
|
||
|
error("type required for capture", arg)
|
||
|
var letDef = newNimNode(nnkIdentDefs, arg)
|
||
|
arg.copyChildrenTo letDef
|
||
|
letDef[2] = newCall("preserveTo",
|
||
|
newNimNode(nnkBracketExpr).add(recSym, newLit(pred i)),
|
||
|
letDef[1])
|
||
|
letSection.add(letDef)
|
||
|
inc(captureCount)
|
||
|
let script = newProc(
|
||
|
# the script scheduled by the callback when the event is matched
|
||
|
name = genSym(nskProc, "script"),
|
||
|
params = [
|
||
|
newEmptyNode(),
|
||
|
newIdentDefs(scriptFacetSym, ident"Facet"),
|
||
|
],
|
||
|
body = newStmtList(
|
||
|
newCall("assert",
|
||
|
infix(newCall("len", recSym), "==", newLit(captureCount))),
|
||
|
letSection,
|
||
|
doHandler[6]
|
||
|
)
|
||
|
)
|
||
|
newProc(
|
||
|
# the event handler that is called when an assertion matches
|
||
|
name = genSym(nskProc, "event_handler"),
|
||
|
params = [
|
||
|
newEmptyNode(),
|
||
|
newIdentDefs(cbFacetSym, ident"Facet"),
|
||
|
newIdentDefs(eventSym, ident"EventKind"),
|
||
|
newIdentDefs(recSym, newNimNode(nnkBracketExpr).add(ident"seq",
|
||
|
ident"Preserve")),
|
||
|
],
|
||
|
body = newStmtList(
|
||
|
newIfStmt((cond: infix(eventSym, "==", newLit(event)), body:
|
||
|
newStmtList(
|
||
|
script,
|
||
|
newCall("scheduleScript", cbFacetSym, script[0])
|
||
|
)))))
|
||
|
# TODO: this proc just checks the event type and then schedules a script,
|
||
|
# should the event check be done in skeletons instead?
|
||
|
|
||
|
proc onEvent(event: EventKind; class: RecordClass; doHandler: NimNode): NimNode =
|
||
|
let
|
||
|
assertion = assertionForRecord(class, doHandler)
|
||
|
handler = callbackForEvent(event, class, doHandler, assertion)
|
||
|
handlerSym = handler[0]
|
||
|
result = quote do:
|
||
|
`handler`
|
||
|
mixin getCurrentFacet
|
||
|
discard getCurrentFacet().addEndpoint do (facet: Facet) -> EndpointSpec:
|
||
|
let a = `assertion`
|
||
|
result.assertion = some(init(Observe, a))
|
||
|
result.analysis = some(analyzeAssertion(a))
|
||
|
result.analysis.get.callback = wrap(facet, `handlerSym`)
|
||
|
|
||
|
macro onAsserted*(class: static[RecordClass]; doHandler: untyped) =
|
||
|
onEvent(addedEvent, class, doHandler)
|
||
|
|
||
|
macro onRetracted*(class: static[RecordClass]; doHandler: untyped) =
|
||
|
onEvent(removedEvent, class, doHandler)
|
||
|
|
||
|
macro onMessage*(class: static[RecordClass]; doHandler: untyped) =
|
||
|
onEvent(messageEvent, class, doHandler)
|
||
|
|
||
|
template assert*(class: RecordClass; field: untyped): untyped =
|
||
|
mixin getCurrentFacet
|
||
|
let facet = getCurrentFacet()
|
||
|
discard facet.addEndpoint do (facet: Facet) -> EndpointSpec:
|
||
|
let a = init(class, getPreserve(field))
|
||
|
result.assertion = some(a)
|
||
|
|
||
|
template field*(F: untyped; T: typedesc; initial: T): untyped =
|
||
|
## Declare a field. The identifier `F` shall be a value with
|
||
|
## `get` and `set` procs.
|
||
|
mixin getCurrentFacet
|
||
|
declareField(getCurrentFacet(), F, T, initial)
|
||
|
# use the template defined in dataspaces
|
||
|
|
||
|
template spawn*(name: string; spawnBody: untyped): untyped =
|
||
|
mixin getCurrentFacet
|
||
|
spawn(getCurrentFacet(), name) do (spawnFacet: Facet):
|
||
|
proc getCurrentFacet(): Facet {.inject.} = spawnFacet
|
||
|
spawnBody
|
||
|
|
||
|
template syndicate*(name: string; dataspaceBody: untyped): untyped =
|
||
|
proc bootProc(rootFacet: Facet) =
|
||
|
proc getCurrentFacet(): Facet {.inject.} = rootFacet
|
||
|
dataspaceBody
|
||
|
waitFor bootModule(name, bootProc)
|