Initial Syndicate DSL
This commit is contained in:
parent
f745e8b53f
commit
77e32a214e
|
@ -1 +1,2 @@
|
|||
tests/test_box_and_client
|
||||
tests/test_dsl
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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(`?$`))
|
||||
|
|
|
@ -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"
|
Loading…
Reference in New Issue