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