# 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)