//--------------------------------------------------------------------------- // @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. // Copyright (C) 2016-2021 Tony Garnock-Jones // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //--------------------------------------------------------------------------- import { IdentitySet } from './idcoll.js'; import { is, Value, Record, Set, Dictionary, canonicalString, preserves, RecordConstructorInfo } from 'preserves'; import { Bag, ChangeDescription } from './bag.js'; import { Discard, Capture, Observe } from './assertions.js'; import * as Stack from './stack.js'; import { Path, NonEmptySkeleton, Skeleton } from './api.js'; export enum EventType { ADDED = +1, REMOVED = -1, MESSAGE = 0, } export type HandlerCallback = (eventType: EventType, bindings: Array) => void; export type Shape = string; export interface Analysis { skeleton: Skeleton; constPaths: Array; constVals: Array; capturePaths: Array; callback?: HandlerCallback; } const _nop = () => {}; export class Index { readonly allAssertions: Bag = new Bag(); readonly root: Node = new Node(new Continuation(new Set())); addHandler(analysisResults: Analysis, callback: HandlerCallback) { let {skeleton, constPaths, constVals, capturePaths} = analysisResults; const continuation = this.root.extend(skeleton); 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(constVals); if (!leaf) { leaf = new Leaf(); constValMap.set(constVals, leaf); } let handler = leaf.handlerMap.get(capturePaths); if (!handler) { const cachedCaptures = new Bag(); leaf.cachedAssertions.forEach((a) => cachedCaptures._items.update(projectPaths(a, capturePaths), n => n! + 1, 0)); handler = new Handler(cachedCaptures); leaf.handlerMap.set(capturePaths, handler); } handler.callbacks.add(callback); handler.cachedCaptures.forEach((_count, captures) => callback(EventType.ADDED, captures as Array)); } removeHandler(analysisResults: Analysis, callback: HandlerCallback) { let {skeleton, constPaths, constVals, capturePaths} = analysisResults; const continuation = this.root.extend(skeleton); let constValMap = continuation.leafMap.get(constPaths); if (!constValMap) return; let leaf = constValMap.get(constVals); if (!leaf) return; let handler = leaf.handlerMap.get(capturePaths); if (!handler) return; handler.callbacks.delete(callback); if (handler.callbacks.size === 0) { leaf.handlerMap.delete(capturePaths); } if (leaf.isEmpty()) { constValMap.delete(constVals); } if (constValMap.size === 0) { continuation.leafMap.delete(constPaths); } } adjustAssertion(outerValue: Value, delta: number): ChangeDescription { let net = this.allAssertions.change(outerValue, delta); switch (net) { 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.callbacks.forEach(cb => cb(EventType.ADDED, vs)); }); break; 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.callbacks.forEach(cb => cb(EventType.REMOVED, vs)); }); break; } return net; } addAssertion(v: Value) { this.adjustAssertion(v, +1); } removeAssertion(v: Value) { this.adjustAssertion(v, -1); } deliverMessage(v: Value, leafCallback: (l: Leaf, v: Value) => void = _nop) { this.root.modify(EventType.MESSAGE, v, _nop, leafCallback, (h, vs) => h.callbacks.forEach(cb => cb(EventType.MESSAGE, vs))); } } class Node { readonly continuation: Continuation; readonly edges: { [selector: string]: { [shape: string]: Node } } = {}; constructor(continuation: Continuation) { this.continuation = continuation; } extend(skeleton: Skeleton): Continuation { const path: Path = []; function walkNode(node: Node, popCount: number, index: number, skeleton: Skeleton): [number, Node] { if (skeleton === null) { return [popCount, node]; } else { const selector = '' + popCount + ',' + index; const cls = skeleton.shape; let table = node.edges[selector]; if (!table) { table = {}; node.edges[selector] = table; } let nextNode = table[cls]; if (!nextNode) { nextNode = new Node(new Continuation( node.continuation.cachedAssertions.filter( (a) => classOf(projectPath(a, path)) === cls))); table[cls] = nextNode; } popCount = 0; index = 0; path.push(index); skeleton.members.forEach((member) => { [popCount, nextNode] = walkNode(nextNode, popCount, index, member); index++; path.pop(); path.push(index); }); path.pop(); return [popCount + 1, nextNode]; } } return walkNode(this, 0, 0, skeleton)[1].continuation; } modify(operation: EventType, outerValue: Value, m_cont: (c: Continuation, v: Value) => void, m_leaf: (l: Leaf, v: Value) => void, m_handler: (h: Handler, vs: Array) => void) { function walkNode(node: Node, termStack: Stack.NonEmptyStack>) { walkContinuation(node.continuation); Object.entries(node.edges).forEach(([selectorStr, table]) => { const selector = parseSelector(selectorStr); let nextStack = Stack.dropNonEmpty(termStack, selector.popCount); let nextValue = step(nextStack.item, selector.index); let nextNode = table[classOf(nextValue)]; if (nextNode) walkNode(nextNode, Stack.push(nextValue as Array, nextStack)); }); } function walkContinuation(continuation: Continuation) { m_cont(continuation, outerValue); continuation.leafMap.forEach((constValMap, constPaths) => { let constVals = projectPaths(outerValue, constPaths as Array); let leaf = constValMap.get(constVals); if (!leaf && operation === EventType.ADDED) { leaf = new Leaf(); constValMap.set(constVals, leaf); } if (leaf) { m_leaf(leaf, outerValue); leaf.handlerMap.forEach((handler, capturePaths) => { m_handler(handler, projectPaths(outerValue, capturePaths as Array)); }); if (operation === EventType.REMOVED && leaf.isEmpty()) { constValMap.delete(constVals); if (constValMap.size === 0) { continuation.leafMap.delete(constPaths); } } } return true; }); } walkNode(this, Stack.push([outerValue], Stack.empty())); } } function parseSelector(s: string): { popCount: number, index: number } { const pos = s.indexOf(','); return { popCount: parseInt(s.substr(0, pos)), index: parseInt(s.substr(pos + 1)) }; } class Continuation { readonly cachedAssertions: Set; readonly leafMap: Dictionary> = new Dictionary(); constructor(cachedAssertions: Set) { this.cachedAssertions = cachedAssertions; } } class Leaf { readonly cachedAssertions: Set = new Set(); readonly handlerMap: Dictionary = new Dictionary(); isEmpty(): boolean { return this.cachedAssertions.size === 0 && this.handlerMap.size === 0; } } class Handler { readonly cachedCaptures: Bag; readonly callbacks: IdentitySet = new IdentitySet(); constructor(cachedCaptures: Bag) { this.cachedCaptures = cachedCaptures; } } export function constructorInfoSignature(ci: RecordConstructorInfo): string { return canonicalString(ci.label) + '/' + ci.arity; } function classOf(v: any): Shape { if (Record.isRecord(v)) { return constructorInfoSignature(v.getConstructorInfo()); } else if (Array.isArray(v)) { return '' + v.length; } else { throw new Error(preserves`Cannot route based on ${v}`); } } function step(v: Array /* includes Record! */, index: number) { return v[index]; } function projectPath(v: Value, path: Path) { for (let index of path) { v = step(v as Array, index); } return v; } function projectPaths(v: Value, paths: Array) { return paths.map((path) => projectPath(v, path)); } export function analyzeAssertion(a: Value): Analysis { const constPaths: Path[] = []; const constVals: Value[] = []; const capturePaths: Path[] = []; const path: Path = []; function walk(a: Value): Skeleton { if (Capture.isClassOf(a)) { // NB. isUnrestricted relies on the specific order that // capturePaths is computed here. capturePaths.push(path.slice()); return walk(a[0]); } if (Discard.isClassOf(a)) { return null; } let cls = classOf(a); if (cls !== null) { let aa = a as Array; // ^ We know this is safe because it's either Record or Array let arity = aa.length; let result: NonEmptySkeleton = { shape: cls, members: [] }; path.push(0); for (let i = 0; i < arity; i++) { path[path.length - 1] = i; result.members.push(walk(step(aa, i))); } path.pop(); return result; } constPaths.push(path); constVals.push(a); return null; } let skeleton = walk(a); return { skeleton, constPaths, constVals, capturePaths }; } export function match(p: Value, v: Value): Array | false { const captures: Array = []; function walk(p: Value, v: Value): boolean { if (Capture.isClassOf(p)) { if (!walk(p[0], v)) return false; captures.push(v); return true; } if (Discard.isClassOf(p)) return true; const pcls = classOf(p); const vcls = classOf(v); if (pcls !== vcls) return false; if (pcls === null) return is(p, v); const pp = p as Array; const vv = v as Array; // ^ These are safe because classOf yielded nonnull for both return pp.every((pv, i) => walk(pv, vv[i])); } return walk(p, v) ? captures : false; } export function isCompletelyConcrete(p: Value): boolean { function walk(p: Value): boolean { if (Capture.isClassOf(p)) return false; if (Discard.isClassOf(p)) return false; const cls = classOf(p); if (cls === null) return true; return (p as Array).every(walk); } return walk(p); } export function withoutCaptures(p: Value): Value { function walk(p: Value): Value { if (Capture.isClassOf(p)) return walk(p[0]); if (Discard.isClassOf(p)) return p; const cls = classOf(p); if (cls === null) return p; if (Record.isRecord(p)) return new Record(p.label, p.map(walk)); return (p as Array).map(walk); } return walk(p); }