syndicate-js/packages/core/src/runtime/skeleton.ts

335 lines
13 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { Set, Dictionary, KeyedDictionary, IdentitySet, stringify, Value } from '@preserves/core';
import { AnyValue, Assertion, Ref } from './actor.js';
import { Bag, ChangeDescription } from './bag.js';
import * as Stack from './stack.js';
import * as P from '../gen/dataspacePatterns.js';
import { Path, analysePattern, classOfCtor, classOfValue, step } from './pattern.js';
enum EventType {
ADDED = +1,
REMOVED = -1,
MESSAGE = 0,
}
const _nop = function() {};
const INDENT = ' ';
export interface IndexObserver<T> {
onAssert(captures: Assertion[], parameter: T): void;
onRetract(captures: Assertion[], parameter: T): void;
onMessage(captures: Assertion[], parameter: T): void;
onRemoval(parameter: T): void;
dump?(): string;
}
export class Index<T> {
readonly allAssertions: Bag<Ref> = new Bag();
readonly root: Node<T> = new Node(new Continuation(new Set()));
addObserver(pattern: P.Pattern, observer: IndexObserver<T>, parameter: T) {
let {constPaths, constValues, capturePaths} = analysePattern(pattern);
const continuation = this.root.extend(pattern);
let constValMap = continuation.leafMap.get(constPaths);
if (!constValMap) {
constValMap = new Dictionary();
continuation.cachedAssertions.forEach((a) => {
const key = projectPaths(a, constPaths);
let leaf = constValMap!.get(key);
if (!leaf) {
leaf = new Leaf();
constValMap!.set(key, leaf);
}
leaf.cachedAssertions.add(a);
});
continuation.leafMap.set(constPaths, constValMap);
}
let leaf = constValMap.get(constValues);
if (!leaf) {
leaf = new Leaf();
constValMap.set(constValues, leaf);
}
let observerGroup = leaf.observerGroups.get(capturePaths);
if (!observerGroup) {
const cachedCaptures = new Bag<Ref, Array<AnyValue>>();
leaf.cachedAssertions.forEach((a) =>
cachedCaptures._items.update(projectPaths(a, capturePaths), n => n! + 1, 0));
observerGroup = new ObserverGroup(cachedCaptures);
leaf.observerGroups.set(capturePaths, observerGroup);
}
observerGroup.observers.add(observer);
observerGroup.cachedCaptures.forEach((_count, captures) => observer.onAssert(captures, parameter));
}
removeObserver(pattern: P.Pattern, observer: IndexObserver<T>, parameter: T) {
let {constPaths, constValues, capturePaths} = analysePattern(pattern);
const continuation = this.root.extend(pattern);
let constValMap = continuation.leafMap.get(constPaths);
if (!constValMap) return;
let leaf = constValMap.get(constValues);
if (!leaf) return;
let observerGroup = leaf.observerGroups.get(capturePaths);
if (!observerGroup) return;
observer.onRemoval(parameter);
observerGroup.observers.delete(observer);
if (observerGroup.observers.size === 0) {
leaf.observerGroups.delete(capturePaths);
}
if (leaf.isEmpty()) {
constValMap.delete(constValues);
}
if (constValMap.size === 0) {
continuation.leafMap.delete(constPaths);
}
}
adjustAssertion(outerValue: Assertion, delta: number, parameter: T): boolean {
switch (this.allAssertions.change(outerValue, delta)) {
case ChangeDescription.ABSENT_TO_PRESENT:
this.root.modify(
EventType.ADDED,
outerValue,
(c, v) => c.cachedAssertions.add(v),
(l, v) => l.cachedAssertions.add(v),
(h, vs) => {
if (h.cachedCaptures.change(vs, +1) === ChangeDescription.ABSENT_TO_PRESENT) {
h.observers.forEach(observer => observer.onAssert(vs, parameter));
}
});
return true;
case ChangeDescription.PRESENT_TO_ABSENT:
this.root.modify(
EventType.REMOVED,
outerValue,
(c, v) => c.cachedAssertions.delete(v),
(l, v) => l.cachedAssertions.delete(v),
(h, vs) => {
if (h.cachedCaptures.change(vs, -1) === ChangeDescription.PRESENT_TO_ABSENT) {
h.observers.forEach(observer => observer.onRetract(vs, parameter));
}
});
return true;
default:
return false;
}
}
addAssertion(v: Assertion, parameter: T): boolean {
return this.adjustAssertion(v, +1, parameter);
}
removeAssertion(v: Assertion, parameter: T): boolean {
return this.adjustAssertion(v, -1, parameter);
}
deliverMessage(v: Assertion, parameter: T, leafCallback: (l: Leaf<T>, v: Assertion) => void = _nop) {
this.root.modify(EventType.MESSAGE, v, _nop, leafCallback, (h, vs) =>
h.observers.forEach(observer => observer.onMessage(vs, parameter)));
}
dump() {
console.log('INDEX');
// console.log('allAssertions:');
// dumpBag(this.allAssertions, 4);
console.log('tree:');
this.root.dump(INDENT);
console.log();
}
}
function dumpBag<T, V extends Value<T>>(b: Bag<T, V>, indent: string) {
for (const [v, count] of b.entries()) {
console.log(indent + stringify(v) + ' = ' + count);
}
}
function dumpSet<V>(s: Set<V>, indent: string) {
s.forEach(v => console.log(indent + stringify(v)));
}
type Selector = [number, AnyValue];
class Node<T> {
readonly continuation: Continuation<T>;
readonly edges: KeyedDictionary<Selector, { [shape: string]: Node<T> }, Ref> = new KeyedDictionary();
constructor(continuation: Continuation<T>) {
this.continuation = continuation;
}
extend(p: P.Pattern): Continuation<T> {
const path: Path = [];
function walkNode(node: Node<T>, popCount: number, stepIndex: AnyValue, p: P.Pattern): [number, Node<T>]
{
switch (p._variant) {
case 'DDiscard':
case 'DLit':
return [popCount, node];
case 'DBind':
return walkNode(node, popCount, stepIndex, p.value.pattern);
case 'DCompound': {
const selector: Selector = [popCount, stepIndex];
let table = node.edges.get(selector);
if (!table) {
table = {};
node.edges.set(selector, table);
}
let cls = classOfCtor(p.value);
let nextNode = table[cls];
if (!nextNode) {
nextNode = new Node(new Continuation(
node.continuation.cachedAssertions.filter(
(a) => classOfValue(projectPath(a, path)) === cls)));
table[cls] = nextNode;
}
popCount = 0;
function walkKey(pp: P.Pattern, stepIndex: AnyValue) {
path.push(stepIndex);
[popCount, nextNode] = walkNode(nextNode, popCount, stepIndex, pp);
path.pop();
}
switch (p.value._variant) {
case 'rec': p.value.fields.forEach(walkKey); break;
case 'arr': p.value.items.forEach(walkKey); break;
case 'dict': p.value.entries.forEach(walkKey); break;
}
return [popCount + 1, nextNode];
}
}
}
return walkNode(this, 0, 0, p)[1].continuation;
}
modify(operation: EventType,
outerValue: Assertion,
m_cont: (c: Continuation<T>, v: Assertion) => void,
m_leaf: (l: Leaf<T>, v: Assertion) => void,
m_observerGroup: (h: ObserverGroup<T>, vs: Array<Assertion>) => void)
{
function walkNode(node: Node<T>, termStack: Stack.NonEmptyStack<Assertion>) {
walkContinuation(node.continuation);
node.edges.forEach((table, [popCount, stepIndex]) => {
const nextStack = Stack.dropNonEmpty(termStack, popCount);
const nextValue = step(nextStack.item, stepIndex);
const nextClass = classOfValue(nextValue);
const nextNode = nextClass && table[nextClass];
if (nextNode) walkNode(nextNode, Stack.push(nextValue!, nextStack));
});
}
function walkContinuation(continuation: Continuation<T>) {
m_cont(continuation, outerValue);
continuation.leafMap.forEach((constValMap, constPaths) => {
let constValues = projectPaths(outerValue, constPaths);
let leaf = constValMap.get(constValues);
if (!leaf && operation === EventType.ADDED) {
leaf = new Leaf();
constValMap.set(constValues, leaf);
}
if (leaf) {
m_leaf(leaf, outerValue);
leaf.observerGroups.forEach((observerGroup, capturePaths) => {
m_observerGroup(observerGroup, projectPaths(outerValue, capturePaths));
});
if (operation === EventType.REMOVED && leaf.isEmpty()) {
constValMap.delete(constValues);
if (constValMap.size === 0) {
continuation.leafMap.delete(constPaths);
}
}
}
return true;
});
}
walkNode(this, Stack.push([outerValue], Stack.empty()));
}
dump(indent: string) {
this.continuation.dump(indent);
this.edges.forEach((shapeMap, selector) => {
const [popCount, stepIndex] = selector;
console.log(indent + `popCount ${popCount} stepIndex ${stringify(stepIndex)} -->`);
for (const shape in shapeMap) {
console.log(indent + INDENT + stringify(shape));
shapeMap[shape].dump(indent + INDENT + INDENT);
}
});
}
}
class Continuation<T> {
readonly cachedAssertions: Set<Ref>;
readonly leafMap: KeyedDictionary<Array<Path>, Dictionary<Ref, Leaf<T>>, Ref> = new KeyedDictionary();
constructor(cachedAssertions: Set<Ref>) {
this.cachedAssertions = cachedAssertions;
}
dump(indent: string) {
dumpSet(this.cachedAssertions, indent);
this.leafMap.forEach((valueMap, paths) => {
valueMap.forEach((leaf, values) => {
console.log(indent + `when ${stringify(paths)} == ${stringify(values)} -->`);
leaf.dump(indent + INDENT);
});
});
}
}
class Leaf<T> {
readonly cachedAssertions: Set<Ref> = new Set();
readonly observerGroups: KeyedDictionary<Array<Path>, ObserverGroup<T>, Ref> = new KeyedDictionary();
isEmpty(): boolean {
return this.cachedAssertions.size === 0 && this.observerGroups.size === 0;
}
dump(indent: string) {
dumpSet(this.cachedAssertions, indent);
this.observerGroups.forEach((observerGroup, paths) => {
observerGroup.dump(paths, indent);
});
}
}
class ObserverGroup<T> {
readonly cachedCaptures: Bag<Ref, Array<AnyValue>>;
readonly observers = new IdentitySet<IndexObserver<T>>();
constructor(cachedCaptures: Bag<Ref, Array<AnyValue>>) {
this.cachedCaptures = cachedCaptures;
}
dump(paths: Path[], indent: string) {
dumpBag(this.cachedCaptures, indent);
this.observers.forEach(observer => {
console.log(indent + `${observer} projecting ${stringify(paths)}`);
const d = observer.dump?.();
if (d) {
console.log(indent + INDENT + d.split(/\n/).join(indent + INDENT + '\n'));
}
});
}
}
// Total by assumption that path is valid for v
function projectPath(v: AnyValue, path: Path): AnyValue {
for (let index of path) {
v = step(v, index)!;
}
return v;
}
// Total by assumption that paths are valid for v
function projectPaths(v: AnyValue, paths: Array<Path>): AnyValue[] {
return paths.map((path) => projectPath(v, path));
}