syndicate-nim/src/syndicate/skeletons.nim

268 lines
8.7 KiB
Nim

# SPDX-FileCopyrightText: ☭ 2021 Emery Hemingway
# SPDX-License-Identifier: Unlicense
import ./assertions, ./bags, ./events
import preserves, preserves/records
import lists, options, sets, tables
type
NonEmptySkeleton*[Shape] = object
shape: Shape
members: seq[Skeleton[Shape]]
Skeleton*[Shape] = Option[NonEmptySkeleton[Shape]]
Shape = string
Value = Preserve
HandlerCallback* = proc (event: EventKind; bindings: seq[Value]) {.gcsafe.}
Path = seq[Natural]
Analysis* = object
skeleton: Skeleton[Shape]
constPaths: seq[Path]
constVals: seq[Value]
capturePaths: seq[Path]
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)
proc analyzeAssertion*(a: Value): Analysis =
var path: Path
proc walk(analysis: var Analysis; a: Value): Skeleton[Shape] =
if a.preserveTo(Discard).isSome:
discard
elif a.preserveTo(Capture).isSome:
analysis.capturePaths.add(path)
result = walk(analysis, a.fields[0])
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]
AssertionCache = HashSet[Value]
Leaf = ref object
cachedAssertions: AssertionCache
handlerMap: Table[seq[Path], Handler]
Continuation = ref object
cachedAssertions: AssertionCache
leafMap: Table[seq[Path], TableRef[seq[Value], Leaf]] # TODO: not TableRef?
Selector = tuple[popCount: int; index: int]
Node = ref object
edges: Table[Selector, TableRef[string, Node]]
continuation: Continuation
using
continuation: Continuation
leaf: Leaf
node: Node
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; popCount, index: int; skeleton: Skeleton[Shape]): tuple[popCount: int, node: Node] =
assert(not node.isNil)
if skeleton.isNone:
return (popCount, node)
else:
let selector: Selector = (popCount, index)
var
cls = skeleton.get.shape
var
table = node.edges.getOrDefault(selector)
if table.isNil:
table = newTable[string, Node]()
node.edges[selector] = table
var nextNode = table.getOrDefault(cls)
if nextNode.isNil:
nextNode = Node(continuation: Continuation())
table[cls] = nextNode
for a in node.continuation.cachedAssertions:
if $classOf(projectPath(a, path)) == cls:
nextNode.continuation.cachedAssertions.incl(a)
block:
var popCount, index: int
path.add(index)
for member in skeleton.get.members:
(popCount, nextNode) = walkNode(nextNode, result.popCount, index, member)
inc(index)
discard path.pop()
path.add(index)
discard path.pop()
result = (popCount.succ, nextNode)
walkNode(node, 0, 0, skeleton).node.continuation
type
Index* = object
allAssertions: Bag[Value]
root: Node
proc initIndex*(): Index =
Index(root: Node(continuation: Continuation()))
using index: 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)
var constValMap = continuation.leafMap.getOrDefault(constPaths)
if constValMap.isNil:
constValMap = newTable[seq[Value], Leaf]()
continuation.leafMap[constPaths] = constValMap
for a in continuation.cachedAssertions:
let key = projectPaths(a, constPaths)
var leaf = constValMap.getOrDefault(key)
if leaf.isNil:
new leaf
constValMap[key] = leaf
leaf.cachedAssertions.incl(a)
var leaf = constValMap.getOrDefault(constVals)
if leaf.isNil:
new leaf
constValMap[constVals] = leaf
var handler = leaf.handlerMap.getOrDefault(capturePaths)
if handler.isNil:
new handler
leaf.handlerMap[capturePaths] = handler
for a in leaf.cachedAssertions:
let a = projectPaths(a, capturePaths)
if handler.cachedCaptures.contains(a):
discard handler.cachedCaptures.change(a, +1)
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:
#debugEcho " assertion of ", outerValue
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:
#debugEcho "retraction of ", outerValue
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)