syndicate-nim/src/syndicate/skeletons.nim

298 lines
9.3 KiB
Nim

# SPDX-License-Identifier: ISC
import ./assertions, ./bags, ./events
import preserves
import lists, options, sets, tables
type
Value = Preserve
NonEmptySkeleton*[Shape] = object
shape: Shape
members: seq[Skeleton[Shape]]
Skeleton*[Shape] = Option[NonEmptySkeleton[Shape]]
Path = seq[Natural]
proc projectPath(v: Value; path: Path): Value =
result = v
for index in path:
result = result[index]
proc projectPaths(v: Value; paths: seq[Path]): seq[Value] =
result.setLen(paths.len)
for i, path in paths: result[i] = projectPath(v, path)
type
Shape = string
HandlerCallback* = proc (event: EventKind; bindings: seq[Value]) {.gcsafe.}
Analysis* = object
skeleton: Skeleton[Shape]
constPaths: seq[Path]
constVals: seq[Value]
capturePaths: seq[Path]
callback*: Option[HandlerCallback]
proc `$`(a: Analysis): string =
result.add "\n\t skeleton: "
result.add $a.skeleton
result.add "\n\t constPaths: "
result.add $a.constPaths
result.add "\n\t constVals: "
result.add $a.constVals
result.add "\n\t capturePaths: "
result.add $a.capturePaths
proc analyzeAssertion*(a: Value): Analysis =
var path: Path
proc walk(analysis: var Analysis; a: Value): Skeleton[Shape] =
if Capture.isClassOf a:
analysis.capturePaths.add(path)
result = walk(analysis, a.fields[0])
elif Discard.isClassOf a:
discard
else:
if a.kind == pkRecord:
let class = classOf(a)
result = some NonEmptySkeleton[Shape](shape: $class)
path.add(0)
var i: int
for field in a.fields:
path[path.high] = i
result.get.members.add(walk(analysis, field))
inc(i)
discard path.pop
else:
analysis.constPaths.add(path)
analysis.constVals.add(a)
result.skeleton = walk(result, a)
type
Handler = ref object
cachedCaptures: Bag[seq[Value]]
callbacks: HashSet[HandlerCallback]
Leaf = ref object
cachedAssertions: HashSet[Value]
handlerMap: Table[seq[Path], Handler]
Continuation = ref object
cachedAssertions: HashSet[Value]
leafMap: Table[seq[Path], TableRef[seq[Value], Leaf]] # TODO: not TableRef?
Selector = tuple[popCount: int; index: int]
Node = ref object
edges: Table[Selector, Table[string, Node]]
continuation: Continuation
using
continuation: Continuation
leaf: Leaf
node: Node
proc `$`(leaf): string =
result.add "Leaf{ cached: "
result.add $leaf.cachedAssertions
result.add ", handler count: "
result.add $leaf.handlerMap.len
result.add " }"
proc `$`(continuation): string =
result.add "Continuation{ cached: "
result.add $continuation.cachedAssertions
result.add ", "
result.add $continuation.leafMap
result.add " }"
proc `$`(node): string =
result.add "Node{ "
result.add $node.continuation
result.add ", edges: "
result.add $node.edges
result.add "}"
proc isEmpty(leaf): bool =
leaf.cachedAssertions.len == 0 and leaf.handlerMap.len == 0
type
ContinuationProc = proc (c: Continuation; v: Value) {.gcsafe.}
LeafProc = proc (l: Leaf; v: Value) {.gcsafe.}
HandlerProc = proc (h: Handler; vs: seq[Value]) {.gcsafe.}
proc modify(node; operation: EventKind; outerValue: Value;
mCont: ContinuationProc; mLeaf: LeafProc; mHandler: HandlerProc) =
proc walkContinuation(continuation) {.gcsafe.}
proc walkNode(node; termStack: SinglyLinkedList[seq[Value]]) =
# TODO: use a seq for the stack?
walkContinuation(node.continuation)
for (selector, table) in node.edges.pairs:
var nextStack = termStack
for _ in 1..selector.popCount:
nextStack.head = nextStack.head.next
let nextValue = nextStack.head.value[selector.index]
if nextValue.isRecord:
let nextClass = classOf(nextValue)
let nextNode = table.getOrDefault($nextClass)
if not nextNode.isNil:
nextStack.prepend(nextValue.record)
walkNode(nextNode, nextStack)
proc walkContinuation(continuation: Continuation) =
mCont(continuation, outerValue)
for (constPaths, constValMap) in continuation.leafMap.pairs:
let constVals = projectPaths(outerValue, constPaths)
let leaf = constValMap.getOrDefault(constVals)
if leaf.isNil:
if operation == addedEvent:
constValMap[constVals] = Leaf()
else:
mLeaf(leaf, outerValue)
for (capturePaths, handler) in leaf.handlerMap.pairs:
mHandler(handler, projectPaths(outerValue, capturePaths))
if operation == removedEvent and leaf.isEmpty:
constValMap.del(constVals)
if constValMap.len == 0:
continuation.leafMap.del(constPaths)
var stack: SinglyLinkedList[seq[Value]]
stack.prepend(@[outerValue])
walkNode(node, stack)
proc extend*[Shape](node; skeleton: Skeleton[Shape]): Continuation =
var path: Path
proc walkNode(node: Node; popCount, index: int; skeleton: Skeleton[Shape]): tuple[popCount: int, node: Node] =
assert(not node.isNil)
if skeleton.isNone:
return (popCount, node)
else:
var
cls = skeleton.get.shape
table: Table[string, Node]
nextNode: Node
discard node.edges.pop((popCount, index), table)
if not table.pop(cls, nextNode):
nextNode = Node(continuation: Continuation())
for a in node.continuation.cachedAssertions:
if $classOf(projectPath(a, path)) == cls:
nextNode.continuation.cachedAssertions.incl(a)
block:
var
popCount = 0
index = 0
path.add(index)
for member in skeleton.get.members:
(popCount, nextNode) = walkNode(nextNode, popCount, index, member)
inc(index)
discard path.pop()
path.add(index)
discard path.pop()
result = (popCount.succ, nextNode)
table[cls] = nextNode
node.edges[(popCount, index)] = table
walkNode(node, 0, 0, skeleton).node.continuation
type
Index* = object
allAssertions: Bag[Value]
root: Node
proc initIndex*(): Index =
result.root = Node(continuation: Continuation())
using index: Index
proc `$`*(index): string =
result.add "Index("
result.add ")Index"
proc addHandler*(index; res: Analysis; callback: HandlerCallback) =
assert(not index.root.isNil)
let
constPaths = res.constPaths
constVals = res.constVals
capturePaths = res.capturePaths
continuation = index.root.extend(res.skeleton)
assert(not continuation.isNil)
var constValMap = continuation.leafMap.getOrDefault(constPaths)
if constValMap.isNil:
constValMap = newTable[seq[Value], Leaf]()
for a in continuation.cachedAssertions:
var leaf: Leaf
if not constValMap.pop(a.sequence, leaf):
new leaf
leaf.cachedAssertions.incl(a)
constValMap[a.sequence] = leaf
continuation.leafMap[constPaths] = constValMap
var leaf = constValMap.getOrDefault(constVals)
if leaf.isNil:
new leaf
constValMap[constVals] = leaf
var handler = leaf.handlerMap.getOrDefault(capturePaths)
if handler.isNil:
new handler
for a in leaf.cachedAssertions:
let a = projectPaths(a, capturePaths)
if handler.cachedCaptures.contains(a):
discard handler.cachedCaptures.change(a, +1)
leaf.handlerMap[capturePaths] = handler
handler.callbacks.incl(callback)
for captures, count in handler.cachedCaptures.pairs:
callback(addedEvent, captures)
proc removeHandler*(index; res: Analysis; callback: HandlerCallback) =
let continuation = index.root.extend(res.skeleton)
try:
let
constValMap = continuation.leafMap[res.constPaths]
leaf = constValMap[res.constVals]
handler = leaf.handlerMap[res.capturePaths]
handler.callbacks.excl(callback)
if handler.callbacks.len == 0:
leaf.handlerMap.del(res.capturePaths)
if leaf.isEmpty:
constValMap.del(res.constVals)
if constValMap.len == 0:
continuation.leafMap.del(res.constPaths)
except KeyError: discard
proc adjustAssertion*(index: var Index; outerValue: Value; delta: int): ChangeDescription =
result = index.allAssertions.change(outerValue, delta)
case result
of cdAbsentToPresent:
index.root.modify(
addedEvent,
outerValue,
(proc (c: Continuation; v: Value) = c.cachedAssertions.incl(v)),
(proc (l: Leaf; v: Value) = l.cachedAssertions.incl(v)),
(proc (h: Handler; vs: seq[Value]) =
if h.cachedCaptures.change(vs, +1) == cdAbsentToPresent:
for cb in h.callbacks: cb(addedEvent, vs)))
of cdPresentToAbsent:
index.root.modify(
removedEvent,
outerValue,
(proc (c: Continuation; v: Value) = c.cachedAssertions.excl(v)),
(proc (l: Leaf; v: Value) = l.cachedAssertions.excl(v)),
(proc (h: Handler; vs: seq[Value]) =
if h.cachedCaptures.change(vs, -1) == cdPresentToAbsent:
for cb in h.callbacks: cb(removedEvent, vs)))
else:
discard
proc continuationNoop(c: Continuation; v: Value) = discard
proc leafNoop(l: Leaf; v: Value) = discard
proc deliverMessage*(index; v: Value; leafCb: proc (l: Leaf; v: Value) {.gcsafe.}) =
proc handlerCb(h: Handler; vs: seq[Value]) =
for cb in h.callbacks: cb(messageEvent, vs)
index.root.modify(messageEvent, v, continuationNoop, leafCb, handlerCb)
proc deliverMessage*(index; v: Value) =
proc handlerCb(h: Handler; vs: seq[Value]) =
for cb in h.callbacks: cb(messageEvent, vs)
index.root.modify(messageEvent, v, continuationNoop, leafNoop, handlerCb)