syndicate-nim/src/syndicate/macros.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)