From 96a8367667436feba8d1e3290a3d6838d582761b Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Fri, 12 Apr 2024 10:21:54 +0200 Subject: [PATCH] Update to new dataspace pattern language --- packages/core/src/runtime/pattern.ts | 208 +++++++++++++----------- packages/core/src/runtime/quasivalue.ts | 53 +++--- packages/core/src/runtime/skeleton.ts | 76 +++++---- 3 files changed, 187 insertions(+), 150 deletions(-) diff --git a/packages/core/src/runtime/pattern.ts b/packages/core/src/runtime/pattern.ts index 1d2c1de..df64a9f 100644 --- a/packages/core/src/runtime/pattern.ts +++ b/packages/core/src/runtime/pattern.ts @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones -import { canonicalString, KeyedDictionary, is, Record, RecordConstructorInfo, Value, _iterMap, DictionaryMap, Dictionary } from '@preserves/core'; +import { canonicalString, KeyedDictionary, is, Record, RecordConstructorInfo, Value, _iterMap, DictionaryMap, Dictionary, EncodableDictionary } from '@preserves/core'; import { AnyValue, Ref } from './actor.js'; import * as P from '../gen/dataspacePatterns.js'; @@ -21,7 +21,7 @@ export function classOfValue(v: any): Shape | null { } } -export function classOfCtor(v: P.DCompound): Shape { +export function classOfCtor(v: P.GroupType): Shape { switch (v._variant) { case 'rec': return canonicalString(v.label); @@ -46,44 +46,47 @@ export function step(v: AnyValue, index: AnyValue): AnyValue | undefined { } } +export type ConstantPositions = { + withValues: Array, + requiredToExist: Array, +} + export type PatternAnalysis = { - constPaths: Array, + constPositions: ConstantPositions, constValues: Array, capturePaths: Array, }; export function analysePattern(p: P.Pattern): PatternAnalysis { const result: PatternAnalysis = { - constPaths: [], + constPositions: { + withValues: [], + requiredToExist: [], + }, constValues: [], capturePaths: [], }; const path: Path = []; - function walkKey(p: P.Pattern, key: AnyValue) { - path.push(key); - walk(p); - path.pop(); - } - function walk(p: P.Pattern) { switch (p._variant) { - case 'DCompound': - 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; - } + case 'group': + p.entries.forEach((p, k) => { + path.push(k); + walk(p); + path.pop(); + }); break; - case 'DBind': + case 'bind': result.capturePaths.push(path.slice()); - walk(p.value.pattern); + walk(p.pattern); break; - case 'DDiscard': + case 'discard': + result.constPositions.requiredToExist.push(path.slice()); break; - case 'DLit': - result.constPaths.push(path.slice()); - result.constValues.push(P.fromAnyAtom(p.value.value)); + case 'lit': + result.constPositions.withValues.push(path.slice()); + result.constValues.push(P.fromAnyAtom(p.value)); break; } } @@ -97,32 +100,20 @@ export function match(p: P.Pattern, v: AnyValue): Array | false { function walk(p: P.Pattern, v: AnyValue): boolean { switch (p._variant) { - case 'DBind': { + case 'bind': { captures.push(v); - return walk(p.value.pattern, v); + return walk(p.pattern, v); } - case 'DDiscard': + case 'discard': return true; - case 'DLit': - return is(p.value.value, v); - case 'DCompound': { - const pcls = classOfCtor(p.value); + case 'lit': + return is(p.value, v); + case 'group': { + const pcls = classOfCtor(p.type); const vcls = classOfValue(v); if (pcls !== vcls) return false; - let items: Array; - switch (p.value._variant) { - case 'dict': - for (const [stepIndex, pp] of p.value.entries.entries()) { - const vv = step(v, stepIndex); - if (vv === void 0 || !walk(pp, vv)) return false; - } - return true; - case 'rec': items = p.value.fields; break; - case 'arr': items = p.value.items; break; - } - let index = 0; - for (const pp of items) { - const vv = step(v, index++); + for (const [stepIndex, pp] of p.entries.entries()) { + const vv = step(v, stepIndex); if (vv === void 0 || !walk(pp, vv)) return false; } return true; @@ -136,19 +127,14 @@ export function match(p: P.Pattern, v: AnyValue): Array | false { export function isCompletelyConcrete(p: P.Pattern): boolean { function walk(p: P.Pattern): boolean { switch (p._variant) { - case 'DBind': return false; - case 'DDiscard': return false; - case 'DLit': return true; - case 'DCompound': switch (p.value._variant) { - case 'rec': return p.value.fields.every(isCompletelyConcrete); - case 'arr': return p.value.items.every(isCompletelyConcrete); - case 'dict': { - for (const pp of p.value.entries.values()) { - if (!walk(pp)) return false; - } - return true; + case 'bind': return false; + case 'discard': return false; + case 'lit': return true; + case 'group': + for (const pp of p.entries.values()) { + if (!walk(pp)) return false; } - } + return true; } } return walk(p); @@ -157,26 +143,19 @@ export function isCompletelyConcrete(p: P.Pattern): boolean { export function withoutCaptures(p: P.Pattern): P.Pattern { function walk(p: P.Pattern): P.Pattern { switch (p._variant) { - case 'DBind': return walk(p.value.pattern); - case 'DDiscard': return p; - case 'DLit': return p; - case 'DCompound': - switch (p.value._variant) { - case 'rec': - return P.Pattern.DCompound(P.DCompound.rec({ - label: p.value.label, - fields: p.value.fields.map(walk), - })); - case 'arr': - return P.Pattern.DCompound(P.DCompound.arr(p.value.items.map(walk))); - case 'dict': { - const newDict = new KeyedDictionary, P.Pattern>(); - for (const [k, v] of p.value.entries) { - newDict.set(k, walk(v)); - } - return P.Pattern.DCompound(P.DCompound.dict(newDict)); - } + case 'bind': return walk(p.pattern); + case 'discard': return p; + case 'lit': return p; + case 'group': { + const newEntries = new KeyedDictionary, P.Pattern>(); + for (const [kk, pp] of p.entries) { + newEntries.set(kk, walk(pp)); } + return P.Pattern.group({ + type: p.type, + entries: newEntries, + }); + } } } return walk(p); @@ -186,24 +165,33 @@ export function withoutCaptures(p: P.Pattern): P.Pattern { // Constructor helpers export function bind(p?: P.Pattern): P.Pattern { - return P.Pattern.DBind(P.DBind(p ?? _)); + return P.Pattern.bind(p ?? _); } export function discard(): P.Pattern { - return P.Pattern.DDiscard(P.DDiscard()); + return P.Pattern.discard(); } export const _ = discard(); +function lit_seq_entries(vs: AnyValue[]): KeyedDictionary> { + const entries = new KeyedDictionary>(); + vs.forEach((v, i) => entries.set(i, lit(v))); + return entries; +} + export function lit(v: AnyValue): P.Pattern { if (Array.isArray(v)) { if ('label' in v) { - return P.Pattern.DCompound(P.DCompound.rec({ - label: v.label, - fields: v.map(lit), - })); + return P.Pattern.group({ + type: P.GroupType.rec(v.label), + entries: lit_seq_entries(v), + }); } else { - return P.Pattern.DCompound(P.DCompound.arr(v.map(lit))); + return P.Pattern.group({ + type: P.GroupType.arr(), + entries: lit_seq_entries(v), + }); } } @@ -211,38 +199,58 @@ export function lit(v: AnyValue): P.Pattern { if (vMap) { const r = new KeyedDictionary(); vMap.forEach((val, key) => r.set(key, lit(val))); - return P.Pattern.DCompound(P.DCompound.dict(r)); + return P.Pattern.group({ + type: P.GroupType.dict(), + entries: r, + }); } if (Set.isSet(v)) { throw new Error("Cannot express literal set in pattern"); } - return P.Pattern.DLit(P.DLit(P.asAnyAtom(v))); + return P.Pattern.lit(P.asAnyAtom(v)); } export function drop_lit(p: P.Pattern): AnyValue | null { const e = new Error(); + + function walkEntries(target: AnyValue[], entries: EncodableDictionary>): void { + let maxKey = -1; + for (const key of entries.keys()) { + if (typeof key !== 'number') throw e; + maxKey = Math.max(maxKey, key); + } + for (let i = 0; i < maxKey + 1; i++) { + const p = entries.get(i); + if (p === void 0) throw e; + target.push(walk(p)); + } + } + function walk(p: P.Pattern): AnyValue { switch (p._variant) { - case 'DCompound': - switch (p.value._variant) { + case 'group': + switch (p.type._variant) { case 'rec': { const v = [] as unknown as Record; - v.label = p.value.label; - p.value.fields.forEach(tt => v.push(walk(tt))); + v.label = p.type.label; + walkEntries(v, p.entries); + return v; + } + case 'arr': { + const v = [] as AnyValue[]; + walkEntries(v, p.entries); return v; } - case 'arr': - return p.value.items.map(walk); case 'dict': { const v = new DictionaryMap(); - p.value.entries.forEach((pp, key) => v.set(key, walk(pp))); + p.entries.forEach((pp, key) => v.set(key, walk(pp))); return v.simplifiedValue(); } } - case 'DLit': - return P.fromAnyAtom(p.value.value); + case 'lit': + return P.fromAnyAtom(p.value); default: throw e; } @@ -256,14 +264,22 @@ export function drop_lit(p: P.Pattern): AnyValue | null { } export function rec(label: AnyValue, ... fields: P.Pattern[]): P.Pattern { - return P.Pattern.DCompound(P.DCompound.rec({ label, fields })); + return P.Pattern.group({ + type: P.GroupType.rec(label), + entries: new KeyedDictionary(fields.map((p, i) => [i, p])), + }); } export function arr(... patterns: P.Pattern[]): P.Pattern { - return P.Pattern.DCompound(P.DCompound.arr(patterns)); + return P.Pattern.group({ + type: P.GroupType.arr(), + entries: new KeyedDictionary(patterns.map((p, i) => [i, p])), + }); } export function dict(... entries: [AnyValue, P.Pattern][]): P.Pattern { - return P.Pattern.DCompound(P.DCompound.dict( - new KeyedDictionary(entries))); + return P.Pattern.group({ + type: P.GroupType.dict(), + entries: new KeyedDictionary(entries), + }); } diff --git a/packages/core/src/runtime/quasivalue.ts b/packages/core/src/runtime/quasivalue.ts index 317a9ab..fecccac 100644 --- a/packages/core/src/runtime/quasivalue.ts +++ b/packages/core/src/runtime/quasivalue.ts @@ -2,7 +2,7 @@ /// SPDX-FileCopyrightText: Copyright © 2021-2024 Tony Garnock-Jones import { AnyValue, Ref } from './actor.js'; -import { Pattern, toPattern } from '../gen/dataspacePatterns.js'; +import { Pattern, toPattern, GroupType, fromGroupType } from '../gen/dataspacePatterns.js'; import * as P from './pattern.js'; import { RecordConstructorInfo, is, Record, JsDictionary } from '@preserves/core'; import { Meta, Type, GenType, SchemaDefinition } from '@preserves/schema'; @@ -17,9 +17,7 @@ export type QuasiValue = | { type: 'bind', inner: QuasiValue } | { type: 'discard' } | { type: 'lit', value: AnyValue } - | { type: 'rec', label: AnyValue, items: QuasiValue[] } - | { type: 'arr', items: QuasiValue[] } - | { type: 'dict', entries: [AnyValue, QuasiValue][] } + | { type: 'group', groupType: GroupType, entries: [AnyValue, QuasiValue][] } | { type: 'unquote', unquoted: QuasiValue } ; @@ -58,7 +56,8 @@ export function rec(label: AnyValue, ... items: QuasiValue[]): QuasiValue { if (literals.length === items.length) { return lit(Record(label, literals)); } else { - return { type: 'rec', label, items }; + const entries = items.map((v, i) => [i, v] as [number, QuasiValue]); + return { type: 'group', groupType: GroupType.rec(label), entries }; } } @@ -67,12 +66,13 @@ export function arr(... items: QuasiValue[]): QuasiValue { if (literals.length === items.length) { return lit(literals); } else { - return { type: 'arr', items }; + const entries = items.map((v, i) => [i, v] as [number, QuasiValue]); + return { type: 'group', groupType: GroupType.arr(), entries }; } } export function dict(... entries: [AnyValue, QuasiValue][]): QuasiValue { - return { type: 'dict', entries }; + return { type: 'group', groupType: GroupType.dict(), entries }; } export function quote(quoted: QuasiValue): QuasiValue { @@ -80,12 +80,9 @@ export function quote(quoted: QuasiValue): QuasiValue { case 'bind': return quote(bind.quasiValue(quoted.inner)); case 'discard': return quote(discard.quasiValue()); case 'lit': return rec(Symbol.for('lit'), lit(quoted.value)); - case 'arr': return rec(Symbol.for('arr'), arr( - ... quoted.items.map(quote))); - case 'rec': return rec(Symbol.for('rec'), lit(quoted.label), arr( - ... quoted.items.map(quote))); - case 'dict': return rec(Symbol.for('dict'), dict( - ... quoted.entries.map(([k, qq]) => [k, quote(qq)] as [AnyValue, QuasiValue]))); + case 'group': + return rec(Symbol.for('group'), lit(fromGroupType(quoted.groupType)), dict( + ... quoted.entries.map(([k, qq]) => [k, quote(qq)] as [AnyValue, QuasiValue]))); case 'unquote': return quoted.unquoted; } } @@ -94,6 +91,20 @@ export function unquote(unquoted: QuasiValue): QuasiValue { return { type: 'unquote', unquoted }; } +function entriesSeq(entries: [AnyValue, V][], defaultValue: V): V[] { + let maxKey = -1; + const result: V[] = []; + for (const [k, q] of entries) { + if (typeof k !== 'number') throw new Error("Invalid sequence quasivalue entries key"); + result[k] = q; + maxKey = Math.max(maxKey, k); + } + for (let i = 0; i < maxKey + 1; i++) { + if (result[i] === void 0) result[i] = defaultValue; + } + return result; +} + export function ctor(info: QuasiValueConstructorInfo, ... items: QuasiValue[]): QuasiValue { if ('constructorInfo' in info) { return rec(info.constructorInfo.label, ... items); @@ -158,7 +169,7 @@ export function ctor(info: QuasiValueConstructorInfo, ... items: QuasiValue[]): if (d.type === 'discard') { return d; } - if (d.type !== 'dict') { + if (d.type !== 'group' || d.groupType._variant !== 'dict') { throw new Error(`Dictionary argument needed to ${defNameStr}`); } for (const [k, p] of d.entries) { @@ -217,8 +228,8 @@ export function ctor(info: QuasiValueConstructorInfo, ... items: QuasiValue[]): } function qArr(q: QuasiValue): QuasiValue[] { - if (q.type === 'arr') { - return q.items; + if (q.type === 'group' && q.groupType._variant === 'arr') { + return entriesSeq(q.entries, discard()); } else if (q.type === 'lit' && Array.isArray(q.value)) { return q.value.map(lit); } else { @@ -284,10 +295,12 @@ function walk(q: QuasiValue): Pattern { case 'bind': return P.bind(walk(q.inner)); case 'discard': return P._; case 'lit': return P.lit(q.value); - case 'arr': return P.arr(... q.items.map(walk)); - case 'rec': return P.rec(q.label, ... q.items.map(walk)); - case 'dict': return P.dict(... q.entries.map( - ([k, qq]) => [k, walk(qq)] as [AnyValue, Pattern])); + case 'group': switch (q.groupType._variant) { + case 'arr': return P.arr(... entriesSeq(q.entries, discard()).map(walk)); + case 'rec': return P.rec(q.groupType.label, ... entriesSeq(q.entries, discard()).map(walk)); + case 'dict': return P.dict(... q.entries.map( + ([k, qq]) => [k, walk(qq)] as [AnyValue, Pattern])); + } case 'unquote': throw new Error('Unexpected unquote in QuasiValue'); } } diff --git a/packages/core/src/runtime/skeleton.ts b/packages/core/src/runtime/skeleton.ts index 548f23c..1cf39a0 100644 --- a/packages/core/src/runtime/skeleton.ts +++ b/packages/core/src/runtime/skeleton.ts @@ -6,7 +6,7 @@ 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, Shape } from './pattern.js'; +import { Path, analysePattern, classOfCtor, classOfValue, step, Shape, ConstantPositions } from './pattern.js'; enum EventType { ADDED = +1, @@ -31,13 +31,15 @@ export class Index { readonly root: Node = new Node(new Continuation(new Set())); addObserver(pattern: P.Pattern, observer: IndexObserver, parameter: T) { - let {constPaths, constValues, capturePaths} = analysePattern(pattern); + let {constPositions, constValues, capturePaths} = analysePattern(pattern); const continuation = this.root.extend(pattern); - let constValMap = continuation.leafMap.get(constPaths); + let constValMap = continuation.leafMap.get(constPositions); if (!constValMap) { constValMap = new KeyedDictionary(); continuation.cachedAssertions.forEach((a) => { - const key = projectPaths(a, constPaths); + if (projectPaths(a, constPositions.requiredToExist) === void 0) return; + const key = projectPaths(a, constPositions.withValues); + if (key === void 0) return; let leaf = constValMap!.get(key); if (!leaf) { leaf = new Leaf(); @@ -45,7 +47,7 @@ export class Index { } leaf.cachedAssertions.add(a); }); - continuation.leafMap.set(constPaths, constValMap); + continuation.leafMap.set(constPositions, constValMap); } let leaf = constValMap.get(constValues); if (!leaf) { @@ -55,8 +57,10 @@ export class Index { let observerGroup = leaf.observerGroups.get(capturePaths); if (!observerGroup) { const cachedCaptures = new Bag>(); - leaf.cachedAssertions.forEach((a) => - cachedCaptures._items.update(projectPaths(a, capturePaths), n => n! + 1, 0)); + leaf.cachedAssertions.forEach((a) => { + const vs = projectPaths(a, capturePaths); + if (vs !== void 0) cachedCaptures._items.update(vs, n => n! + 1, 0); + }); observerGroup = new ObserverGroup(cachedCaptures); leaf.observerGroups.set(capturePaths, observerGroup); } @@ -65,9 +69,9 @@ export class Index { } removeObserver(pattern: P.Pattern, observer: IndexObserver, parameter: T) { - let {constPaths, constValues, capturePaths} = analysePattern(pattern); + let {constPositions, constValues, capturePaths} = analysePattern(pattern); const continuation = this.root.extend(pattern); - let constValMap = continuation.leafMap.get(constPaths); + let constValMap = continuation.leafMap.get(constPositions); if (!constValMap) return; let leaf = constValMap.get(constValues); if (!leaf) return; @@ -82,7 +86,7 @@ export class Index { constValMap.delete(constValues); } if (constValMap.size === 0) { - continuation.leafMap.delete(constPaths); + continuation.leafMap.delete(constPositions); } } @@ -168,19 +172,19 @@ class Node { function walkNode(node: Node, popCount: number, stepIndex: AnyValue, p: P.Pattern): [number, Node] { switch (p._variant) { - case 'DDiscard': - case 'DLit': + case 'discard': + case 'lit': return [popCount, node]; - case 'DBind': - return walkNode(node, popCount, stepIndex, p.value.pattern); - case 'DCompound': { + case 'bind': + return walkNode(node, popCount, stepIndex, p.pattern); + case 'group': { 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 cls = classOfCtor(p.type); let nextNode = table[cls]; if (!nextNode) { nextNode = new Node(new Continuation( @@ -189,16 +193,11 @@ class Node { table[cls] = nextNode; } popCount = 0; - function walkKey(pp: P.Pattern, stepIndex: AnyValue) { + p.entries.forEach((pp, stepIndex) => { 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]; } } @@ -226,8 +225,10 @@ class Node { function walkContinuation(continuation: Continuation) { m_cont(continuation, outerValue); - continuation.leafMap.forEach((constValMap, constPaths) => { - let constValues = projectPaths(outerValue, constPaths); + continuation.leafMap.forEach((constValMap, constPositions) => { + if (projectPaths(outerValue, constPositions.requiredToExist) === void 0) return; + let constValues = projectPaths(outerValue, constPositions.withValues); + if (constValues === void 0) return; let leaf = constValMap.get(constValues); if (!leaf && operation === EventType.ADDED) { leaf = new Leaf(); @@ -236,12 +237,13 @@ class Node { if (leaf) { m_leaf(leaf, outerValue); leaf.observerGroups.forEach((observerGroup, capturePaths) => { - m_observerGroup(observerGroup, projectPaths(outerValue, capturePaths)); + const vs = projectPaths(outerValue, capturePaths); + if (vs !== void 0) m_observerGroup(observerGroup, vs); }); if (operation === EventType.REMOVED && leaf.isEmpty()) { constValMap.delete(constValues); if (constValMap.size === 0) { - continuation.leafMap.delete(constPaths); + continuation.leafMap.delete(constPositions); } } } @@ -267,7 +269,7 @@ class Node { class Continuation { readonly cachedAssertions: Set; - readonly leafMap: KeyedDictionary, KeyedDictionary>> = new KeyedDictionary(); + readonly leafMap: KeyedDictionary>> = new KeyedDictionary(); constructor(cachedAssertions: Set) { this.cachedAssertions = cachedAssertions; @@ -320,15 +322,21 @@ class ObserverGroup { } } -// Total by assumption that path is valid for v -function projectPath(v: AnyValue, path: Path): AnyValue { +function projectPath(v: AnyValue, path: Path): AnyValue | undefined { for (let index of path) { - v = step(v, index)!; + const next = step(v, index); + if (next === void 0) return void 0; + v = next; } return v; } -// Total by assumption that paths are valid for v -function projectPaths(v: AnyValue, paths: Array): AnyValue[] { - return paths.map((path) => projectPath(v, path)); +function projectPaths(v: AnyValue, paths: Array): AnyValue[] | undefined { + const result = []; + for (const path of paths) { + const w = projectPath(v, path); + if (w === void 0) return void 0; + result.push(w); + } + return result; }