@mark1russell7/render-biblo 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +43 -2
- package/build/index.d.ts +209 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +549 -0
- package/build/index.js.map +1 -0
- package/package.json +66 -3
- package/src/class.ts +64 -0
- package/src/index.ts +9 -0
- package/src/instance.ts +23 -0
- package/src/nodeops.ts +31 -0
- package/src/registry.ts +515 -0
- package/src/traits.ts +99 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import type { ComponentClass, CellDef } from "./class.ts";
|
|
2
|
+
import type { Instance, InstanceId } from "./instance.ts";
|
|
3
|
+
import type { PrimeTable, Trait, TraitResolution } from "./traits.ts";
|
|
4
|
+
import { fingerprintOf, primeIn, primeTable, selectTraits } from "./traits.ts";
|
|
5
|
+
import { docEquals, mapRefs } from "@mark1russell7/coeval";
|
|
6
|
+
import type { Expr } from "@mark1russell7/render-dsl";
|
|
7
|
+
import { isLiteral, lit, renderWorld } from "@mark1russell7/render-dsl";
|
|
8
|
+
import type { NodeId, NodeStore } from "@mark1russell7/render-node";
|
|
9
|
+
import { addNode, batch, removeNode, setExpr, setSlot } from "@mark1russell7/render-node";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The biblo is the class registry and the instance store.
|
|
13
|
+
*
|
|
14
|
+
* Classes are shared templates. Instances are small (an ID and a scope), and the values of their cells
|
|
15
|
+
* are in the node store. A trait gives methods to each class that has the cells of its list.
|
|
16
|
+
* Change a biblo only through its functions, for example `registerClass`, `registerTrait` and `instantiate`.
|
|
17
|
+
*/
|
|
18
|
+
export type Biblo = {
|
|
19
|
+
readonly classes: ReadonlyMap<string, ComponentClass>;
|
|
20
|
+
readonly instances: ReadonlyMap<InstanceId, Instance>;
|
|
21
|
+
readonly traits: ReadonlyMap<string, Trait>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type MutableInstance = {
|
|
25
|
+
readonly id: InstanceId;
|
|
26
|
+
classRef: string;
|
|
27
|
+
readonly scope: { readonly self: InstanceId; readonly parent: InstanceId | undefined; readonly children: InstanceId[] };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type BibloState = {
|
|
31
|
+
readonly classes: Map<string, ComponentClass>;
|
|
32
|
+
readonly instances: Map<InstanceId, MutableInstance>;
|
|
33
|
+
/** For each instance: the names of the cells that a binding gave. A change of the class does not change them. */
|
|
34
|
+
readonly bound: Map<InstanceId, Set<string>>;
|
|
35
|
+
readonly traits: Map<string, Trait>;
|
|
36
|
+
readonly primes: PrimeTable;
|
|
37
|
+
nextId: number;
|
|
38
|
+
readonly cellsCache: Map<string, Readonly<Record<string, CellDef>>>;
|
|
39
|
+
readonly methodsCache: Map<string, Readonly<Record<string, unknown>>>;
|
|
40
|
+
readonly traitsCache: Map<string, TraitResolution>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const bstate = (b: Biblo): BibloState => b as unknown as BibloState;
|
|
44
|
+
|
|
45
|
+
const hasOwn = (o: object, key: string): boolean => Object.prototype.hasOwnProperty.call(o, key);
|
|
46
|
+
|
|
47
|
+
/** This function gives an own property of a record, or `undefined`. It does not read `Object.prototype`. */
|
|
48
|
+
const own = <T>(o: Readonly<Record<string, T>> | undefined, key: string): T | undefined =>
|
|
49
|
+
o !== undefined && hasOwn(o, key) ? o[key] : undefined;
|
|
50
|
+
|
|
51
|
+
/** This function makes an empty biblo. */
|
|
52
|
+
export const biblo = (): Biblo => {
|
|
53
|
+
const s: BibloState = {
|
|
54
|
+
classes: new Map(),
|
|
55
|
+
instances: new Map(),
|
|
56
|
+
bound: new Map(),
|
|
57
|
+
traits: new Map(),
|
|
58
|
+
primes: primeTable(),
|
|
59
|
+
nextId: 0,
|
|
60
|
+
cellsCache: new Map(),
|
|
61
|
+
methodsCache: new Map(),
|
|
62
|
+
traitsCache: new Map(),
|
|
63
|
+
};
|
|
64
|
+
return s;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* This function gives an independent copy of a biblo: the same classes, traits, instances and counters. The
|
|
69
|
+
* classes and the traits are immutable, thus the copy shares them. Pair the copy with a copy of its store
|
|
70
|
+
* (`forkStore`), with the class semantics of the copy (`classNodeOps`).
|
|
71
|
+
*/
|
|
72
|
+
export const forkBiblo = (b: Biblo): Biblo => {
|
|
73
|
+
const s = bstate(b);
|
|
74
|
+
const copy: BibloState = {
|
|
75
|
+
classes: new Map(s.classes),
|
|
76
|
+
instances: new Map([...s.instances].map(([id, inst]): [InstanceId, MutableInstance] => [id, {
|
|
77
|
+
id: inst.id,
|
|
78
|
+
classRef: inst.classRef,
|
|
79
|
+
scope: { self: inst.scope.self, parent: inst.scope.parent, children: [...inst.scope.children] },
|
|
80
|
+
}])),
|
|
81
|
+
bound: new Map([...s.bound].map(([id, names]) => [id, new Set(names)])),
|
|
82
|
+
traits: new Map(s.traits),
|
|
83
|
+
primes: { primes: new Map(s.primes.primes), found: [...s.primes.found] },
|
|
84
|
+
nextId: s.nextId,
|
|
85
|
+
// The cached entries are frozen results, thus the copy can share them
|
|
86
|
+
cellsCache: new Map(s.cellsCache),
|
|
87
|
+
methodsCache: new Map(s.methodsCache),
|
|
88
|
+
traitsCache: new Map(s.traitsCache),
|
|
89
|
+
};
|
|
90
|
+
return copy;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const clearCaches = (s: BibloState): void => {
|
|
94
|
+
s.cellsCache.clear();
|
|
95
|
+
s.methodsCache.clear();
|
|
96
|
+
s.traitsCache.clear();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* This function registers a class, or replaces the class with the same name. The live instances do not change.
|
|
101
|
+
* The expressions of the class are coeval documents, thus a class from JSON text registers as it is.
|
|
102
|
+
*/
|
|
103
|
+
export const registerClass = (b: Biblo, cls: ComponentClass): void => {
|
|
104
|
+
const s = bstate(b);
|
|
105
|
+
s.classes.set(cls.name, cls);
|
|
106
|
+
clearCaches(s);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** This function registers a trait, or replaces the trait with the same name. The live instances use it at once. */
|
|
110
|
+
export const registerTrait = (b: Biblo, trait: Trait): void => {
|
|
111
|
+
const s = bstate(b);
|
|
112
|
+
s.traits.set(trait.name, trait);
|
|
113
|
+
clearCaches(s);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** This function removes a trait. */
|
|
117
|
+
export const unregisterTrait = (b: Biblo, name: string): void => {
|
|
118
|
+
const s = bstate(b);
|
|
119
|
+
s.traits.delete(name);
|
|
120
|
+
clearCaches(s);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* This function renames a class. Its subclasses, the typed cells that use it and its live instances use the
|
|
125
|
+
* new name. A trait whose list has a typed cell of the class (`kid:Point`) also uses the new name. The function
|
|
126
|
+
* does nothing and gives `false` when the class is missing or the new name is in use.
|
|
127
|
+
*/
|
|
128
|
+
export const renameClass = (b: Biblo, from: string, to: string): boolean => {
|
|
129
|
+
const s = bstate(b);
|
|
130
|
+
if (!s.classes.has(from) || to === "" || s.classes.has(to)) return false;
|
|
131
|
+
const next = new Map<string, ComponentClass>();
|
|
132
|
+
for (const [name, c] of s.classes) {
|
|
133
|
+
const extendsRenamed = c.extends === from;
|
|
134
|
+
const cellsRenamed = Object.values(c.cells).some((def) => def.type === from);
|
|
135
|
+
next.set(name === from ? to : name, {
|
|
136
|
+
...c,
|
|
137
|
+
name: name === from ? to : name,
|
|
138
|
+
...(extendsRenamed ? { extends: to } : {}),
|
|
139
|
+
cells: cellsRenamed
|
|
140
|
+
? Object.fromEntries(Object.entries(c.cells).map(([k, def]) => [k, def.type === from ? { ...def, type: to } : def]))
|
|
141
|
+
: c.cells,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// The new map keeps the order of registration, with the renamed class at the place of the old name
|
|
145
|
+
s.classes.clear();
|
|
146
|
+
for (const [name, c] of next) s.classes.set(name, c);
|
|
147
|
+
for (const inst of s.instances.values()) if (inst.classRef === from) inst.classRef = to;
|
|
148
|
+
const renamed = (requirement: string): string => {
|
|
149
|
+
const colon = requirement.indexOf(":");
|
|
150
|
+
return colon >= 0 && requirement.slice(colon + 1) === from ? `${requirement.slice(0, colon)}:${to}` : requirement;
|
|
151
|
+
};
|
|
152
|
+
for (const [name, trait] of s.traits) {
|
|
153
|
+
const requires = trait.requires.map(renamed);
|
|
154
|
+
if (requires.some((r, i) => r !== trait.requires[i])) s.traits.set(name, { ...trait, requires });
|
|
155
|
+
}
|
|
156
|
+
clearCaches(s);
|
|
157
|
+
return true;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** This function registers each class of a list, in order. */
|
|
161
|
+
export const registerClasses = (b: Biblo, classes: readonly ComponentClass[]): void => {
|
|
162
|
+
for (const cls of classes) registerClass(b, cls);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* This function gives the extends chain of a class, from the class to the root.
|
|
167
|
+
* A cycle (`A` extends `B` extends `A`) stops at the first class that repeats.
|
|
168
|
+
*/
|
|
169
|
+
const extendsChain = (b: Biblo, className: string): ComponentClass[] => {
|
|
170
|
+
const chain: ComponentClass[] = [];
|
|
171
|
+
const visited = new Set<string>();
|
|
172
|
+
let current: string | undefined = className;
|
|
173
|
+
while (current !== undefined && !visited.has(current)) {
|
|
174
|
+
visited.add(current);
|
|
175
|
+
const cls = b.classes.get(current);
|
|
176
|
+
if (!cls) break;
|
|
177
|
+
chain.push(cls);
|
|
178
|
+
current = cls.extends;
|
|
179
|
+
}
|
|
180
|
+
return chain;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* This function merges the records of an extends chain: the record of the class wins over the record of its parent.
|
|
185
|
+
* `Object.fromEntries` makes own data properties, thus a cell named `__proto__` is a normal cell.
|
|
186
|
+
*/
|
|
187
|
+
const mergeChain = <T>(chain: readonly ComponentClass[], pick: (cls: ComponentClass) => Readonly<Record<string, T>> | undefined): Readonly<Record<string, T>> => {
|
|
188
|
+
const entries: [string, T][] = [];
|
|
189
|
+
for (let i = chain.length - 1; i >= 0; i--) entries.push(...Object.entries(pick(chain[i]!) ?? {}));
|
|
190
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** This function gives all cell templates of a class through its extends chain. A subclass cell replaces a parent cell. */
|
|
194
|
+
export const resolveCells = (b: Biblo, className: string): Readonly<Record<string, CellDef>> => {
|
|
195
|
+
const s = bstate(b);
|
|
196
|
+
let out = s.cellsCache.get(className);
|
|
197
|
+
if (!out) {
|
|
198
|
+
out = mergeChain(extendsChain(b, className), (cls) => cls.cells);
|
|
199
|
+
s.cellsCache.set(className, out);
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** This function gives the fingerprint of a class: the product of the primes of its cells and typed cells. */
|
|
205
|
+
export const classFingerprint = (b: Biblo, className: string): bigint => {
|
|
206
|
+
const atoms: string[] = [];
|
|
207
|
+
for (const [name, def] of Object.entries(resolveCells(b, className))) {
|
|
208
|
+
atoms.push(`cell:${name}`);
|
|
209
|
+
if (def.type !== undefined) atoms.push(`cell:${name}:${def.type}`);
|
|
210
|
+
}
|
|
211
|
+
return fingerprintOf(bstate(b).primes, atoms);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** This function gives the prime of an atomic feature, for example `cell:value`. */
|
|
215
|
+
export const primeOf = (b: Biblo, atom: string): bigint => primeIn(bstate(b).primes, atom);
|
|
216
|
+
|
|
217
|
+
/** This function gives the traits that apply to a class (the most specific ones), and the ambiguous method names. */
|
|
218
|
+
export const resolveTraits = (b: Biblo, className: string): TraitResolution => {
|
|
219
|
+
const s = bstate(b);
|
|
220
|
+
let out = s.traitsCache.get(className);
|
|
221
|
+
if (!out) {
|
|
222
|
+
out = selectTraits(s.primes, s.traits.values(), classFingerprint(b, className));
|
|
223
|
+
s.traitsCache.set(className, out);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* This function gives all methods of a class. There are three layers, from the lowest to the highest.
|
|
230
|
+
* The first layer is the root of the extends chain, for example `Top`. The second layer is the traits that apply.
|
|
231
|
+
* The third layer is the other classes of the chain, to the class itself.
|
|
232
|
+
*
|
|
233
|
+
* Thus a trait changes the defaults of the root, and a class changes a trait.
|
|
234
|
+
* A method that two traits give is ambiguous: no trait gives it.
|
|
235
|
+
*/
|
|
236
|
+
export const resolveMethods = (b: Biblo, className: string): Readonly<Record<string, unknown>> => {
|
|
237
|
+
const s = bstate(b);
|
|
238
|
+
let out = s.methodsCache.get(className);
|
|
239
|
+
if (!out) {
|
|
240
|
+
const chain = extendsChain(b, className);
|
|
241
|
+
const root = chain.length > 1 ? chain.slice(-1) : [];
|
|
242
|
+
const upper = chain.length > 1 ? chain.slice(0, -1) : chain;
|
|
243
|
+
const { applied, ambiguous } = resolveTraits(b, className);
|
|
244
|
+
const traitEntries = applied
|
|
245
|
+
.flatMap((t) => Object.entries(t.methods))
|
|
246
|
+
.filter(([name]) => !ambiguous.includes(name));
|
|
247
|
+
out = Object.freeze(Object.fromEntries([
|
|
248
|
+
...Object.entries(mergeChain(root, (cls) => cls.methods)),
|
|
249
|
+
...traitEntries,
|
|
250
|
+
...Object.entries(mergeChain(upper, (cls) => cls.methods)),
|
|
251
|
+
]));
|
|
252
|
+
s.methodsCache.set(className, out);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** This function gives an instance ID that is free in the biblo and in the store. */
|
|
258
|
+
const newInstanceId = (s: BibloState, store: NodeStore): InstanceId => {
|
|
259
|
+
for (;;) {
|
|
260
|
+
const id = `i_${String(s.nextId++)}`;
|
|
261
|
+
if (!s.instances.has(id) && !store.nodes.has(id)) return id;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* This function changes the scope names of the free `world` references to node IDs with `mapRefs` of the kernel.
|
|
267
|
+
* `self` becomes the instance, and `parent` becomes the parent instance. The parameters of a `fn` stay.
|
|
268
|
+
*/
|
|
269
|
+
const scopeExpr = (expr: Expr, selfId: InstanceId, parentId: InstanceId | undefined): Expr =>
|
|
270
|
+
isLiteral(expr) ? expr : (mapRefs(renderWorld, expr, (use) => {
|
|
271
|
+
if (use.root !== "world") return undefined;
|
|
272
|
+
const [, head, ...rest] = use.path;
|
|
273
|
+
if (head === "self") return { type: "ref", value: ["world", selfId, ...rest] };
|
|
274
|
+
if (head === "parent" && parentId !== undefined) return { type: "ref", value: ["world", parentId, ...rest] };
|
|
275
|
+
return undefined;
|
|
276
|
+
}) as Expr);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* This function adds one cell to an instance. A typed cell becomes a child instance. The exception is a class
|
|
280
|
+
* that is already on the chain of typed cells above the cell. Then the cell is plain, thus the instance is finite.
|
|
281
|
+
*/
|
|
282
|
+
const addCell = (
|
|
283
|
+
s: BibloState,
|
|
284
|
+
b: Biblo,
|
|
285
|
+
store: NodeStore,
|
|
286
|
+
inst: MutableInstance,
|
|
287
|
+
name: string,
|
|
288
|
+
def: CellDef,
|
|
289
|
+
bound: Expr | undefined,
|
|
290
|
+
chain: ReadonlySet<string>,
|
|
291
|
+
): void => {
|
|
292
|
+
let target: NodeId;
|
|
293
|
+
if (bound === undefined && def.type !== undefined && !chain.has(def.type)) {
|
|
294
|
+
target = instantiateIn(s, b, store, def.type, inst.id, def.bindings, chain).id;
|
|
295
|
+
} else {
|
|
296
|
+
target = addNode(store, scopeExpr(bound ?? def.expr, inst.id, inst.scope.parent), `${inst.id}.${name}`);
|
|
297
|
+
}
|
|
298
|
+
setSlot(store, inst.id, name, target, { own: true });
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const instantiateIn = (
|
|
302
|
+
s: BibloState,
|
|
303
|
+
b: Biblo,
|
|
304
|
+
store: NodeStore,
|
|
305
|
+
className: string,
|
|
306
|
+
parentId: InstanceId | undefined,
|
|
307
|
+
bindings: Readonly<Record<string, Expr>> | undefined,
|
|
308
|
+
chain: ReadonlySet<string>,
|
|
309
|
+
): MutableInstance => {
|
|
310
|
+
const parent = parentId === undefined ? undefined : s.instances.get(parentId);
|
|
311
|
+
const id = newInstanceId(s, store);
|
|
312
|
+
const inst: MutableInstance = { id, classRef: className, scope: { self: id, parent: parent?.id, children: [] } };
|
|
313
|
+
s.instances.set(id, inst);
|
|
314
|
+
parent?.scope.children.push(id);
|
|
315
|
+
|
|
316
|
+
addNode(store, lit(undefined), id);
|
|
317
|
+
const inner = new Set([...chain, className]);
|
|
318
|
+
const bound = new Set<string>();
|
|
319
|
+
for (const [name, def] of Object.entries(resolveCells(b, className))) {
|
|
320
|
+
const binding = own(bindings, name);
|
|
321
|
+
if (binding !== undefined) bound.add(name);
|
|
322
|
+
addCell(s, b, store, inst, name, def, binding, inner);
|
|
323
|
+
}
|
|
324
|
+
if (bound.size > 0) s.bound.set(id, bound);
|
|
325
|
+
return inst;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* This function makes an instance of a class. The class is not copied: the instance gets an ID, a scope,
|
|
330
|
+
* and a root node with one slot for each cell. All nodes are evaluated before the end of the function.
|
|
331
|
+
*
|
|
332
|
+
* - A typed cell becomes a child instance of its class, with the bindings of the cell. The slot of the cell
|
|
333
|
+
* holds the root node of the child, thus `ref("self", "kid", "value")` reads a cell of the child.
|
|
334
|
+
* A typed cell whose class is already above it on the chain of typed cells is a plain cell.
|
|
335
|
+
* - `bindings` replace cells of this instance. They are in the scope of this instance. A binding for a typed
|
|
336
|
+
* cell makes it a plain cell with the bound expression.
|
|
337
|
+
* - An unknown class gives an instance with no cells.
|
|
338
|
+
*/
|
|
339
|
+
export const instantiate = (
|
|
340
|
+
b: Biblo,
|
|
341
|
+
store: NodeStore,
|
|
342
|
+
className: string,
|
|
343
|
+
parentId?: InstanceId,
|
|
344
|
+
bindings?: Readonly<Record<string, Expr>>,
|
|
345
|
+
): Instance => batch(store, () => instantiateIn(bstate(b), b, store, className, parentId, bindings, new Set()));
|
|
346
|
+
|
|
347
|
+
const destroyIn = (s: BibloState, store: NodeStore, id: InstanceId): void => {
|
|
348
|
+
const inst = s.instances.get(id);
|
|
349
|
+
if (!inst) return;
|
|
350
|
+
// A copy, because each destroy removes the child from this array
|
|
351
|
+
for (const childId of inst.scope.children.slice()) destroyIn(s, store, childId);
|
|
352
|
+
removeNode(store, id);
|
|
353
|
+
const parent = inst.scope.parent === undefined ? undefined : s.instances.get(inst.scope.parent);
|
|
354
|
+
if (parent) {
|
|
355
|
+
const idx = parent.scope.children.indexOf(id);
|
|
356
|
+
if (idx >= 0) parent.scope.children.splice(idx, 1);
|
|
357
|
+
}
|
|
358
|
+
s.instances.delete(id);
|
|
359
|
+
s.bound.delete(id);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* This function moves a child instance to a position in the children of its parent. A position past the end
|
|
364
|
+
* moves it to the end. It does nothing for an instance without a parent.
|
|
365
|
+
*/
|
|
366
|
+
export const moveChild = (b: Biblo, childId: InstanceId, index: number): void => {
|
|
367
|
+
const s = bstate(b);
|
|
368
|
+
const child = s.instances.get(childId);
|
|
369
|
+
const parent = child?.scope.parent === undefined ? undefined : s.instances.get(child.scope.parent);
|
|
370
|
+
if (!parent) return;
|
|
371
|
+
const children = parent.scope.children;
|
|
372
|
+
const from = children.indexOf(childId);
|
|
373
|
+
if (from < 0) return;
|
|
374
|
+
children.splice(from, 1);
|
|
375
|
+
children.splice(Math.max(0, Math.min(index, children.length)), 0, childId);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* This function destroys an instance, the inverse of `instantiate`. It destroys the children first, then
|
|
380
|
+
* removes the nodes of the instance and detaches it from its parent. The readers of the removed nodes
|
|
381
|
+
* become `none` in one epoch.
|
|
382
|
+
*/
|
|
383
|
+
export const destroyInstance = (b: Biblo, store: NodeStore, instanceId: InstanceId): void => {
|
|
384
|
+
batch(store, () => { destroyIn(bstate(b), store, instanceId); });
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/** This function gives the classes on the chain of typed cells above an instance, with the class of the instance. */
|
|
388
|
+
const typedChain = (s: BibloState, store: NodeStore, inst: MutableInstance): Set<string> => {
|
|
389
|
+
const chain = new Set<string>([inst.classRef]);
|
|
390
|
+
let current: MutableInstance | undefined = inst;
|
|
391
|
+
while (current?.scope.parent !== undefined) {
|
|
392
|
+
const parent = s.instances.get(current.scope.parent);
|
|
393
|
+
const childId: InstanceId = current.id;
|
|
394
|
+
const holds = parent !== undefined && [...(store.nodes.get(parent.id)?.slots.values() ?? [])].includes(childId);
|
|
395
|
+
if (!parent || !holds || chain.has(parent.classRef)) break;
|
|
396
|
+
chain.add(parent.classRef);
|
|
397
|
+
current = parent;
|
|
398
|
+
}
|
|
399
|
+
return chain;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/** This function removes a cell of an instance: an owned cell node goes, and a shared one only loses the slot. */
|
|
403
|
+
const removeCell = (s: BibloState, store: NodeStore, inst: MutableInstance, name: string, slot: NodeId): void => {
|
|
404
|
+
const child = s.instances.get(slot);
|
|
405
|
+
if (child) destroyIn(s, store, child.id);
|
|
406
|
+
else if (store.nodes.get(slot)?.parent === inst.id) removeNode(store, slot);
|
|
407
|
+
else setSlot(store, inst.id, name, undefined);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* This function updates the cells of a child instance after a change of the bindings of its typed cell.
|
|
412
|
+
* A child cell that still has the old binding (or the old default of its class) gets the new one.
|
|
413
|
+
*/
|
|
414
|
+
const updateBindings = (
|
|
415
|
+
s: BibloState,
|
|
416
|
+
b: Biblo,
|
|
417
|
+
store: NodeStore,
|
|
418
|
+
child: MutableInstance,
|
|
419
|
+
oldBindings: Readonly<Record<string, Expr>> | undefined,
|
|
420
|
+
newBindings: Readonly<Record<string, Expr>> | undefined,
|
|
421
|
+
): void => {
|
|
422
|
+
const cells = resolveCells(b, child.classRef);
|
|
423
|
+
const root = store.nodes.get(child.id);
|
|
424
|
+
if (!root) return;
|
|
425
|
+
const bound = s.bound.get(child.id) ?? new Set<string>();
|
|
426
|
+
for (const k of new Set([...Object.keys(oldBindings ?? {}), ...Object.keys(newBindings ?? {})])) {
|
|
427
|
+
const oldE = own(oldBindings, k) ?? own(cells, k)?.expr;
|
|
428
|
+
const newE = own(newBindings, k) ?? own(cells, k)?.expr;
|
|
429
|
+
if (own(newBindings, k) !== undefined) bound.add(k);
|
|
430
|
+
else bound.delete(k);
|
|
431
|
+
const slot = root.slots.get(k);
|
|
432
|
+
const cell = slot === undefined ? undefined : store.nodes.get(slot);
|
|
433
|
+
if (!cell || !oldE || !newE || s.instances.has(cell.id)) continue;
|
|
434
|
+
if (docEquals(cell.expr, scopeExpr(oldE, child.id, child.scope.parent))) {
|
|
435
|
+
setExpr(store, cell.id, scopeExpr(newE, child.id, child.scope.parent));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (bound.size > 0) s.bound.set(child.id, bound);
|
|
439
|
+
else s.bound.delete(child.id);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* This function registers a new version of a class and updates the live instances: the class is a template,
|
|
444
|
+
* and the instances follow it. The update applies to each instance of the class and of its subclasses.
|
|
445
|
+
* When a cell of the resolved class changes, the instance cell changes too, if it still has the old expression.
|
|
446
|
+
*
|
|
447
|
+
* - A cell with an edit of its own keeps it. A cell that a binding gave keeps the binding.
|
|
448
|
+
* - A new cell is added, and a removed cell is removed. A shared cell node only loses its slot.
|
|
449
|
+
* - A typed cell that changes its type gets a new child instance. A change of its bindings reaches the cells of
|
|
450
|
+
* the child that still follow the old bindings. A change between a typed cell and a plain cell replaces the cell.
|
|
451
|
+
*
|
|
452
|
+
* All changes evaluate in one epoch.
|
|
453
|
+
*/
|
|
454
|
+
export const updateClass = (b: Biblo, store: NodeStore, cls: ComponentClass): void => {
|
|
455
|
+
const s = bstate(b);
|
|
456
|
+
const before = new Map<string, Readonly<Record<string, CellDef>>>();
|
|
457
|
+
for (const inst of s.instances.values()) {
|
|
458
|
+
if (!before.has(inst.classRef)) before.set(inst.classRef, resolveCells(b, inst.classRef));
|
|
459
|
+
}
|
|
460
|
+
registerClass(b, cls);
|
|
461
|
+
batch(store, () => {
|
|
462
|
+
for (const inst of Array.from(s.instances.values())) {
|
|
463
|
+
const root = store.nodes.get(inst.id);
|
|
464
|
+
const old = before.get(inst.classRef);
|
|
465
|
+
if (!root || !old || !s.instances.has(inst.id)) continue;
|
|
466
|
+
const next = resolveCells(b, inst.classRef);
|
|
467
|
+
const bound = s.bound.get(inst.id);
|
|
468
|
+
for (const name of new Set([...Object.keys(old), ...Object.keys(next)])) {
|
|
469
|
+
const o = own(old, name);
|
|
470
|
+
const n = own(next, name);
|
|
471
|
+
if (o === n || bound?.has(name) === true) continue;
|
|
472
|
+
const slot = root.slots.get(name);
|
|
473
|
+
const target = slot === undefined ? undefined : store.nodes.get(slot);
|
|
474
|
+
const child = slot === undefined ? undefined : s.instances.get(slot);
|
|
475
|
+
const follows = (def: CellDef): boolean =>
|
|
476
|
+
target !== undefined && child === undefined && docEquals(target.expr, scopeExpr(def.expr, inst.id, inst.scope.parent));
|
|
477
|
+
if (!n) {
|
|
478
|
+
if (slot !== undefined) removeCell(s, store, inst, name, slot);
|
|
479
|
+
} else if (!o) {
|
|
480
|
+
if (slot === undefined) addCell(s, b, store, inst, name, n, undefined, typedChain(s, store, inst));
|
|
481
|
+
} else if (o.type !== undefined && n.type !== undefined) {
|
|
482
|
+
if (!child) continue;
|
|
483
|
+
if (o.type !== n.type) {
|
|
484
|
+
removeCell(s, store, inst, name, child.id);
|
|
485
|
+
addCell(s, b, store, inst, name, n, undefined, typedChain(s, store, inst));
|
|
486
|
+
} else {
|
|
487
|
+
updateBindings(s, b, store, child, o.bindings, n.bindings);
|
|
488
|
+
}
|
|
489
|
+
} else if (o.type !== undefined || n.type !== undefined) {
|
|
490
|
+
// A change between a typed cell and a plain cell: the cell follows the class only in its old form
|
|
491
|
+
const followsOld = o.type !== undefined ? child !== undefined : follows(o);
|
|
492
|
+
if (slot !== undefined && followsOld) {
|
|
493
|
+
removeCell(s, store, inst, name, slot);
|
|
494
|
+
addCell(s, b, store, inst, name, n, undefined, typedChain(s, store, inst));
|
|
495
|
+
}
|
|
496
|
+
} else if (slot !== undefined && follows(o)) {
|
|
497
|
+
setExpr(store, slot, scopeExpr(n.expr, inst.id, inst.scope.parent));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* This function gives the instance that owns a node. For an instance root, it is the node itself.
|
|
506
|
+
* For another node, it is the nearest owner up the slot tree. A node outside all instances gives `undefined`.
|
|
507
|
+
*/
|
|
508
|
+
export const ownerOf = (b: Biblo, store: NodeStore, nodeId: NodeId): InstanceId | undefined => {
|
|
509
|
+
let current: NodeId | undefined = nodeId;
|
|
510
|
+
for (let guard = 0; current !== undefined && guard < 10_000; guard++) {
|
|
511
|
+
if (b.instances.has(current)) return current;
|
|
512
|
+
current = store.nodes.get(current)?.parent;
|
|
513
|
+
}
|
|
514
|
+
return undefined;
|
|
515
|
+
};
|
package/src/traits.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural traits with prime fingerprints: a granular polymorphism.
|
|
3
|
+
*
|
|
4
|
+
* Each atomic feature of a class gets a unique prime: a cell (`value`), or a typed cell (`kid:Point`).
|
|
5
|
+
* The fingerprint of a class is the product of the primes of its features. The product does not depend on
|
|
6
|
+
* the order of the features, thus two classes with the same features have the same fingerprint.
|
|
7
|
+
*
|
|
8
|
+
* A trait gives methods to each class that has the features of its list. Then the fingerprint of the trait
|
|
9
|
+
* divides the fingerprint of the class. Among the traits that apply, only the most specific ones count
|
|
10
|
+
* (a trait whose fingerprint divides the fingerprint of another applicable trait is dominated). When two of
|
|
11
|
+
* them give the same method, the method is ambiguous: no trait gives it, and the ambiguity is visible.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A trait: methods for each class that has the cells of its list. */
|
|
15
|
+
export type Trait = {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
/** The features that a class must have: a cell name (`label`), or a typed cell (`kid:Point`). */
|
|
18
|
+
readonly requires: readonly string[];
|
|
19
|
+
readonly methods: Readonly<Record<string, unknown>>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The traits that apply to a class, and the names of the methods that two of them give. */
|
|
23
|
+
export type TraitResolution = {
|
|
24
|
+
readonly applied: readonly Trait[];
|
|
25
|
+
readonly ambiguous: readonly string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** The table of primes: one unique prime for each atomic feature. */
|
|
29
|
+
export type PrimeTable = {
|
|
30
|
+
readonly primes: Map<string, bigint>;
|
|
31
|
+
readonly found: bigint[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** This function makes an empty table of primes. */
|
|
35
|
+
export const primeTable = (): PrimeTable => ({ primes: new Map(), found: [] });
|
|
36
|
+
|
|
37
|
+
/** This function gives the next prime after the last prime of the table (trial division by the found primes). */
|
|
38
|
+
const nextPrime = (t: PrimeTable): bigint => {
|
|
39
|
+
let candidate = t.found.length === 0 ? 2n : t.found[t.found.length - 1]! + 1n;
|
|
40
|
+
for (;;) {
|
|
41
|
+
let prime = true;
|
|
42
|
+
for (const p of t.found) {
|
|
43
|
+
if (p * p > candidate) break;
|
|
44
|
+
if (candidate % p === 0n) {
|
|
45
|
+
prime = false;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (prime) {
|
|
50
|
+
t.found.push(candidate);
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
candidate++;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** This function gives the prime of an atomic feature. A new feature gets the next free prime. */
|
|
58
|
+
export const primeIn = (t: PrimeTable, atom: string): bigint => {
|
|
59
|
+
let p = t.primes.get(atom);
|
|
60
|
+
if (p === undefined) {
|
|
61
|
+
p = nextPrime(t);
|
|
62
|
+
t.primes.set(atom, p);
|
|
63
|
+
}
|
|
64
|
+
return p;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** This function gives the product of the primes of a set of features (the fingerprint). */
|
|
68
|
+
export const fingerprintOf = (t: PrimeTable, atoms: Iterable<string>): bigint => {
|
|
69
|
+
let product = 1n;
|
|
70
|
+
for (const atom of new Set(atoms)) product *= primeIn(t, atom);
|
|
71
|
+
return product;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** This function gives the features of an item of the list of a trait: `kid:Point` is the cell `kid` with the type `Point`. */
|
|
75
|
+
export const requirementAtoms = (requirement: string): string[] => {
|
|
76
|
+
const colon = requirement.indexOf(":");
|
|
77
|
+
return colon < 0 ? [`cell:${requirement}`] : [`cell:${requirement.slice(0, colon)}`, `cell:${requirement}`];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* This function selects the traits for a fingerprint. It keeps each trait whose fingerprint divides the class
|
|
82
|
+
* fingerprint, removes the dominated ones, and finds the methods that two remaining traits give.
|
|
83
|
+
*/
|
|
84
|
+
export const selectTraits = (
|
|
85
|
+
t: PrimeTable,
|
|
86
|
+
traits: Iterable<Trait>,
|
|
87
|
+
classFingerprint: bigint,
|
|
88
|
+
): TraitResolution => {
|
|
89
|
+
const applicable: { trait: Trait; d: bigint }[] = [];
|
|
90
|
+
for (const trait of traits) {
|
|
91
|
+
const d = fingerprintOf(t, trait.requires.flatMap(requirementAtoms));
|
|
92
|
+
if (classFingerprint % d === 0n) applicable.push({ trait, d });
|
|
93
|
+
}
|
|
94
|
+
const maximal = applicable.filter((a) => !applicable.some((b) => b.d !== a.d && b.d % a.d === 0n));
|
|
95
|
+
const owners = new Map<string, number>();
|
|
96
|
+
for (const { trait } of maximal) for (const name of Object.keys(trait.methods)) owners.set(name, (owners.get(name) ?? 0) + 1);
|
|
97
|
+
const ambiguous = [...owners].filter(([, count]) => count > 1).map(([name]) => name).toSorted();
|
|
98
|
+
return { applied: maximal.map((m) => m.trait), ambiguous };
|
|
99
|
+
};
|