From 77e32a214ef44fb2cff5d08880f7ea011c31c1b0 Mon Sep 17 00:00:00 2001 From: Emery Hemingway Date: Tue, 29 Jun 2021 17:18:09 +0200 Subject: [PATCH] Initial Syndicate DSL --- .gitignore | 1 + README.md | 6 ++ src/syndicate/macros.nim | 178 ++++++++++++++++++++++++++++++++++ tests/test_box_and_client.nim | 7 +- tests/test_dsl.nim | 29 ++++++ 5 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 src/syndicate/macros.nim create mode 100644 tests/test_dsl.nim diff --git a/.gitignore b/.gitignore index a954504..2de5750 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ tests/test_box_and_client +tests/test_dsl diff --git a/README.md b/README.md index 7e59cc3..878ea10 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # Syndicate Nim implementation of [Syndicate](https://syndicate-lang.org/) dataspaces and actors. + +## TODO +* Complete Syndicate DSL +* Timer driver +* Remote dataspaces +* Async-dispatch integration diff --git a/src/syndicate/macros.nim b/src/syndicate/macros.nim new file mode 100644 index 0000000..a48f0e4 --- /dev/null +++ b/src/syndicate/macros.nim @@ -0,0 +1,178 @@ +# 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) diff --git a/tests/test_box_and_client.nim b/tests/test_box_and_client.nim index f4b70c1..775d689 100644 --- a/tests/test_box_and_client.nim +++ b/tests/test_box_and_client.nim @@ -2,7 +2,6 @@ import syndicate/assertions, syndicate/dataspaces, syndicate/events, syndicate/skeletons import preserves, preserves/records - import asyncdispatch, tables, options const N = 100000 @@ -21,12 +20,12 @@ proc boot(facet: Facet) = discard facet.addEndpoint do (facet: Facet) -> EndpointSpec: # echo "recomputing published BoxState ", facet.fields.value - let a = BoxState.init(facet.fields.value.toPreserve) + let a = BoxState.init(value.getPreserve) result.assertion = some a discard facet.addDataflow do (facet: Facet): # echo "box dataflow saw new value ", facet.fields.value - if facet.fields.value == N: + if value.get == N: facet.stop do (facet: Facet): echo "terminated box root facet" @@ -36,7 +35,7 @@ proc boot(facet: Facet) = proc cb(facet: Facet; evt: EventKind; vs: seq[Value]) = if evt == messageEvent: facet.scheduleScript do (facet: Facet): - facet.fields.value = vs[0] + value.set(vs[0]) # echo "box updated value ", vs[0] result.analysis.get.callback = facet.wrap cb const o = Observe.init(SetBox.init(`?$`)) diff --git a/tests/test_dsl.nim b/tests/test_dsl.nim new file mode 100644 index 0000000..dee6a74 --- /dev/null +++ b/tests/test_dsl.nim @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: ISC + +import syndicate/[assertions, macros] +import preserves, preserves/records +import asyncdispatch + +const + BoxState = RecordClass(label: symbol"box-state", arity: 1) + SetBox = RecordClass(label: symbol"set-box", arity: 1) + +syndicate "test_dsl": + + spawn "box": + field(currentValue, int, 0) + assert(BoxState, currentValue) + stopIf currentValue.get == 10: + echo "box: terminating" + onMessage(SetBox) do (newValue: int): + echo "box: taking on new value ", newValue + currentValue.set(newValue) + + spawn "client": + #stopIf retracted(observe(SetBox, _)): + # echo "client: box has gone" + onAsserted(BoxState) do (v: int): + echo "client: learned that box's value is now ", v + send(SetBox, v+1) + onRetracted(BoxState) do (_): + echo "client: box state disappeared"