@mark1russell7/coeval 0.6.2 → 0.7.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.
@@ -0,0 +1,436 @@
1
+ import type { Key } from "./doc.ts";
2
+ import { type Context, type HostThing, type Thing, THING, context as contextOf } from "./deref.ts";
3
+ import { type Failed, type Result, Failure, fail } from "./errors.ts";
4
+ import { type Machine, type Perform, type StopSignal, rootContext } from "./evaluate.ts";
5
+ import { type DurableCounts, type DurableOptions, type Epochs, durable } from "./durable.ts";
6
+ import type { DocFacts, StaticRead } from "./facts.ts";
7
+ import { deepestFirst } from "./graph.ts";
8
+ import type { Side } from "./world.ts";
9
+
10
+ /*
11
+ * The interface of a schedule (S1, D31). A host gives named documents. Each document is a node, and the root `world`
12
+ * of each node reaches the other nodes. The static reads give the edges of the graph, and the contracts give the side
13
+ * of each node. A demand evaluates a node and each node that it reads, through `durable`. Thus after a change, a node
14
+ * evaluates again only when one of its reads changed. A demand of the host first demands the nodes that the eager
15
+ * edges reach, deepest first (S2a, D33). Thus a long chain does not use the stack of the host for each node.
16
+ */
17
+
18
+ /** One edge of a graph of nodes: a static read of a node from the document of another node (SCHED.EDGES). */
19
+ export interface NodeEdge {
20
+ /**
21
+ * The node that the read reaches: the first key after the root `world`. It is `undefined` when the read can reach
22
+ * each node, for example the path `["world"]`, or a step document right after the root.
23
+ */
24
+ readonly to: string | undefined;
25
+ /** The keys after the node, up to the first step document: the field of the node that the read reaches (SCHED.GRAIN). */
26
+ readonly field: readonly Key[];
27
+ /** `true` when only a lazy part reaches the read. An evaluation can then give a value without it (FACTS.LAZY). */
28
+ readonly lazy: boolean;
29
+ /** The static read of `factsOf`, with its path, its place and its mark `exact`. */
30
+ readonly read: StaticRead;
31
+ }
32
+
33
+ /** One node of a schedule, with what the static facts tell about it (SCHED.GRAPH). */
34
+ export interface NodeInfo {
35
+ /** The name of the node: its key under the root `world`. */
36
+ readonly id: string;
37
+ /** The document of the node. */
38
+ readonly doc: unknown;
39
+ /** The static facts of the document. */
40
+ readonly facts: DocFacts;
41
+ /** The side of the node, from the facts. A node on the run side is a sink: no node reads it (SCHED.SIDE). */
42
+ readonly side: Side;
43
+ /** The edges to the nodes that the document can read, in the order of its reads. */
44
+ readonly edges: readonly NodeEdge[];
45
+ }
46
+
47
+ /** The options of `schedule`: the options of `durable`, and the context of the host. */
48
+ export interface ScheduleOptions extends DurableOptions {
49
+ /** The context of the host: the names, `self`, `event` and the capabilities. The schedule sets the root `world` to the nodes. */
50
+ readonly context?: Context;
51
+ }
52
+
53
+ /** The work of a schedule since its start: the work of its `durable` cache, and the demands ahead of a node. */
54
+ export interface ScheduleCounts extends DurableCounts {
55
+ /**
56
+ * The demands that a demand of the host made before its own node (SCHED.LONG-CHAIN). Each node that the eager edges
57
+ * reach, and that has no value in the data epoch, adds one.
58
+ */
59
+ readonly ahead: number;
60
+ }
61
+
62
+ /** The options of a run of a node. */
63
+ export interface NodeRunOptions {
64
+ /** The event that starts the run, at the root `event`. The default is the event of the context of the host. */
65
+ readonly event?: Thing;
66
+ /** The host code that does each effect, as in `run`. */
67
+ readonly perform?: Perform;
68
+ /** A signal that stops the run. */
69
+ readonly signal?: StopSignal;
70
+ }
71
+
72
+ /**
73
+ * The demand schedule of a graph of named nodes (D31). The host tells it each change of a node, of the machine and of
74
+ * its data. The schedule evaluates a node only when a host or another node reads it.
75
+ */
76
+ export interface Schedule {
77
+ /** The epochs of the schedule (DURABLE.EPOCHS). A schedule builds again what depends on the structure when `structure` changes. */
78
+ readonly epochs: Epochs;
79
+ /** The work of the schedule since its start: the compilations, the evaluations, the verifications, the reuses and the demands ahead. */
80
+ readonly counts: ScheduleCounts;
81
+ /** The context of each node in the current data epoch: the context of the host, with the nodes at the root `world`. */
82
+ readonly context: Context;
83
+ /**
84
+ * This function gives each node with its facts, its side and its edges (SCHED.GRAPH). The map stays the same object
85
+ * in one structure epoch, thus a schedule can keep what it computes from it, for example an order.
86
+ *
87
+ * @returns The nodes, by name.
88
+ */
89
+ graph(): ReadonlyMap<string, NodeInfo>;
90
+ /**
91
+ * This function gives the value of a node on demand (SCHED.DEMAND). A read of `world` in the document demands that
92
+ * node first. Each node evaluates at most one time in a data epoch.
93
+ *
94
+ * - First, the schedule demands each node that the eager edges reach, deepest first (SCHED.LONG-CHAIN).
95
+ * - After a change, a pure node verifies its reads, and it keeps its result when each read is the same (SCHED.STALE).
96
+ * - Each node of a cycle gives `#CYCLE!`, in each order of demands (SCHED.CYCLE).
97
+ * - A node on the run side gives `#REF!`, because the schedule does not evaluate it (SCHED.SIDE).
98
+ *
99
+ * @param id - The name of the node.
100
+ * @returns The result, or `#N/A` for a node that the schedule does not have.
101
+ */
102
+ read(id: string): Result;
103
+ /**
104
+ * This function adds a node, or gives a node a new document. It is a change of the structure. Only a new document
105
+ * object compiles (DURABLE.STRUCTURE).
106
+ *
107
+ * @param id - The name of the node.
108
+ * @param doc - The document of the node. It must not change in place after this call.
109
+ */
110
+ set(id: string, doc: unknown): void;
111
+ /**
112
+ * This function removes a node. It is a change of the structure, and a read of the node then gives `#N/A`.
113
+ *
114
+ * @param id - The name of the node.
115
+ * @returns `true` when the schedule had the node.
116
+ */
117
+ remove(id: string): boolean;
118
+ /**
119
+ * This function tells the schedule that the data of the host changed (DURABLE.EPOCHS). The values of a context do
120
+ * not change in place, thus new data comes in a new context, or through a host thing.
121
+ *
122
+ * @param context - The new context of the host, or `undefined` to keep the context.
123
+ */
124
+ changeData(context?: Context): void;
125
+ /**
126
+ * This function gives the schedule a new machine. It drops each plan, each fact and each result.
127
+ *
128
+ * @param machine - The new machine.
129
+ */
130
+ changeMachine(machine: Machine): void;
131
+ /**
132
+ * This function starts a run of the document of a node, and the host does each effect (SCHED.RUN). Only the host
133
+ * starts a run, for example at an event. A change of data does not start or repeat a run (L3).
134
+ *
135
+ * @param id - The name of the node.
136
+ * @param options - The event, the host code that does the effects, and a signal to stop the run.
137
+ * @returns The result, when the run ends.
138
+ */
139
+ run(id: string, options?: NodeRunOptions): Promise<Result>;
140
+ }
141
+
142
+ /**
143
+ * This function gives the edges of a node from the static facts of its document (SCHED.EDGES). Each read of the root
144
+ * `world` is an edge to the node of its first key. The keys after that key are the field of the edge (SCHED.GRAIN).
145
+ * A read of another root reads the data of the host, thus it is not an edge.
146
+ *
147
+ * A reference in the value of a node can lead a read to a third node (DEREF.COLLAPSE). The edges do not show such a
148
+ * read, but the reads of an evaluation do (S1d).
149
+ *
150
+ * @param facts - The static facts of the document of the node.
151
+ * @returns The edges, in the order of the reads.
152
+ */
153
+ export function edgesOf(facts: DocFacts): readonly NodeEdge[] {
154
+ const edges: NodeEdge[] = [];
155
+ for (const read of facts.reads) {
156
+ if (read.root !== "world") continue;
157
+ const first = read.keys[0];
158
+ edges.push({ to: first === undefined ? undefined : String(first), field: read.keys.length > 1 ? read.keys.slice(1) : NO_KEYS, lazy: read.lazy, read });
159
+ }
160
+ // A list that grows by push keeps room for more edges. A copy of a short list is smaller (PLAN.LEAN).
161
+ return edges.length === 0 ? NO_EDGES : edges.slice();
162
+ }
163
+
164
+ /** No keys, and no edges. One frozen list of each serves each node, thus a schedule of many nodes keeps less. */
165
+ const NO_KEYS: readonly Key[] = /* @__PURE__ */ Object.freeze([]);
166
+ const NO_EDGES: readonly NodeEdge[] = /* @__PURE__ */ Object.freeze([]);
167
+
168
+ /** This function gives the error of a node on a cycle. It names only the node, thus each order of demands gives the same error. */
169
+ function cycleOf(id: string): Failed {
170
+ return fail("#CYCLE!", `The node ${id} is on a cycle of reads.`);
171
+ }
172
+
173
+ /** This function gives the error of a read of a node on the run side. */
174
+ function runSide(id: string): Failed {
175
+ return fail("#REF!", `The node ${id} is on the run side, thus it has no value for a reader.`);
176
+ }
177
+
178
+ /** This function gives the error of a node that the schedule does not have. */
179
+ function missing(id: string): Failed {
180
+ return fail("#N/A", `The schedule has no node ${id}.`);
181
+ }
182
+
183
+ /** The state of one data epoch: the context of the nodes, and the value of each node that a demand reached. */
184
+ interface Epoch {
185
+ readonly context: Context;
186
+ readonly values: Map<string, Result>;
187
+ }
188
+
189
+ /**
190
+ * This function makes the demand schedule of a graph of named nodes (S1, R4, D31). A host gives each node a name and
191
+ * a document. The root `world` of each node reaches the other nodes by name. The context of the host gives the other
192
+ * roots.
193
+ *
194
+ * - `graph()` gives the facts, the side and the edges of each node, from `factsOf` and `edgesOf`.
195
+ * - `read(id)` evaluates a node on demand, through `durable`. A pure node verifies its reads after a change, and a
196
+ * read of a node demands that node again. Thus only a node with a changed read evaluates again: the red-green
197
+ * algorithm of rustc.
198
+ * - A node is on a cycle when its demand reaches itself through the reads of the evaluations. Each node of such a
199
+ * strongly connected component gives `#CYCLE!`, whatever its document does with the error. The schedule finds the
200
+ * components with the algorithm of Tarjan over the demands. Thus each order of demands gives the same values.
201
+ * - A demand of the host, or of a run, first demands each node that the eager edges reach, deepest first. Thus each
202
+ * read of such a node finds a value, and a chain of 100000 nodes does not overflow the stack (SCHED.LONG-CHAIN).
203
+ * Each such demand starts with no node in evaluation, thus the order of demands keeps the values of D31.
204
+ * - A node on the run side is a sink. Only `run` evaluates it, and only the host uses `run`.
205
+ *
206
+ * The host keeps the promises of `durable`. The documents and the values of a context do not change in place.
207
+ *
208
+ * @param machine - The machine of the first structure epoch.
209
+ * @param options - The options of `durable`, and the context of the host.
210
+ * @returns The schedule.
211
+ */
212
+ export function schedule(machine: Machine, options: ScheduleOptions = {}): Schedule {
213
+ const { context: given, ...cacheOptions } = options;
214
+ const cache = durable(machine, cacheOptions);
215
+ const docs = new Map<string, unknown>();
216
+ const edges = new WeakMap<DocFacts, readonly NodeEdge[]>();
217
+ let base: Context = given ?? contextOf();
218
+ let now: Epoch | undefined;
219
+ let nodes: { readonly structure: number; readonly map: ReadonlyMap<string, NodeInfo> } | undefined;
220
+
221
+ // The nodes in evaluation, in the order of their demands. The top of the stack is the node whose read demands.
222
+ const stack: string[] = [];
223
+ const busy = new Set<string>();
224
+ // The algorithm of Tarjan over the demands: the order of each demand, the lowest order that the node reaches, and
225
+ // the nodes of the components that are still open.
226
+ const order = new Map<string, number>();
227
+ const low = new Map<string, number>();
228
+ const open: string[] = [];
229
+ const onOpen = new Set<string>();
230
+ // The nodes in evaluation that read themselves.
231
+ const looped = new Set<string>();
232
+ let count = 0;
233
+ let ahead = 0;
234
+
235
+ const lower = (id: string, to: number): void => {
236
+ if (to < (low.get(id) as number)) low.set(id, to);
237
+ };
238
+
239
+ /** This function closes the open components down to an index of the list `open`. */
240
+ const close = (from: number): number => {
241
+ let size = 0;
242
+ while (open.length > from) {
243
+ const id = open.pop() as string;
244
+ onOpen.delete(id);
245
+ order.delete(id);
246
+ low.delete(id);
247
+ size += 1;
248
+ }
249
+ return size;
250
+ };
251
+
252
+ /** This function gives the edges of the facts of a document. They stay for each facts object, as the facts stay for each document. */
253
+ const edgesFrom = (facts: DocFacts): readonly NodeEdge[] => {
254
+ let found = edges.get(facts);
255
+ if (found === undefined) {
256
+ found = edgesOf(facts);
257
+ edges.set(facts, found);
258
+ }
259
+ return found;
260
+ };
261
+
262
+ /** This function gives the edges of a node, for the walk of a demand. A known node, a missing node and a node on the run side need no walk. */
263
+ const edgesFor = (id: string): readonly NodeEdge[] | undefined => {
264
+ if (current().values.has(id) || !docs.has(id)) return undefined;
265
+ const facts = cache.facts(docs.get(id));
266
+ return facts.side === "run" ? undefined : edgesFrom(facts);
267
+ };
268
+
269
+ /**
270
+ * This function gives the value of a node. A demand of the host or of a run has no node in evaluation. It first
271
+ * demands each node that the eager edges reach, deepest first. Each of these demands also starts with no node in
272
+ * evaluation, thus it is a demand of the host too. D31 gives each order of such demands the same values.
273
+ */
274
+ const demand = (id: string): Result => {
275
+ if (stack.length === 0) {
276
+ for (const first of deepestFirst(id, edgesFor)) {
277
+ ahead += 1;
278
+ visit(first);
279
+ }
280
+ }
281
+ return visit(id);
282
+ };
283
+
284
+ /** This function demands one node, with the algorithm of Tarjan over the demands for the cycles of D31. */
285
+ const visit = (id: string): Result => {
286
+ const epoch = current();
287
+ const reader = stack[stack.length - 1];
288
+ const known = epoch.values.get(id);
289
+ if (known !== undefined) {
290
+ // A finished node of an open component is on a cycle with its reader too.
291
+ if (reader !== undefined && onOpen.has(id)) lower(reader, order.get(id) as number);
292
+ return known;
293
+ }
294
+ if (busy.has(id)) {
295
+ // A read of a node in evaluation closes a cycle. It gives the same error as the final result of that node.
296
+ if (reader !== undefined) lower(reader, order.get(id) as number);
297
+ if (reader === id) looped.add(id);
298
+ return cycleOf(id);
299
+ }
300
+ const doc = docs.get(id);
301
+ if (cache.facts(doc).side === "run") {
302
+ const refused = runSide(id);
303
+ epoch.values.set(id, refused);
304
+ return refused;
305
+ }
306
+ const mine = count++;
307
+ order.set(id, mine);
308
+ low.set(id, mine);
309
+ const from = open.length;
310
+ open.push(id);
311
+ onOpen.add(id);
312
+ busy.add(id);
313
+ stack.push(id);
314
+ let result: Result | undefined;
315
+ try {
316
+ result = cache.evaluate(id, doc, epoch.context);
317
+ } finally {
318
+ stack.pop();
319
+ busy.delete(id);
320
+ // An exception of the host leaves the node without a result. A later demand evaluates it again.
321
+ if (result === undefined) {
322
+ looped.delete(id);
323
+ close(from);
324
+ }
325
+ }
326
+ const reached = low.get(id) as number;
327
+ const self = looped.delete(id);
328
+ let value = result;
329
+ if (reached < mine) {
330
+ // The node reaches an earlier node that is still open, thus it is in that component.
331
+ value = cycleOf(id);
332
+ } else if (close(from) > 1 || self) {
333
+ value = cycleOf(id);
334
+ } else if (!result.ok && result.error.code === "#DEPTH!") {
335
+ // An overflow of the stack depends on the depth of the demand, not on the node (D15).
336
+ cache.forget(id);
337
+ }
338
+ epoch.values.set(id, value);
339
+ if (reader !== undefined) lower(reader, reached);
340
+ return value;
341
+ };
342
+
343
+ /** This function gives the record of the derive nodes, for a path that ends at the root `world`. */
344
+ const record = (): unknown => {
345
+ const out: Record<string, unknown> = {};
346
+ for (const id of [...docs.keys()].sort()) {
347
+ if (cache.facts(docs.get(id)).side === "run") continue;
348
+ const result = demand(id);
349
+ if (!result.ok) throw new Failure(result.error);
350
+ Object.defineProperty(out, id, { value: result.value, enumerable: true, writable: true, configurable: true });
351
+ }
352
+ return out;
353
+ };
354
+
355
+ /**
356
+ * This function gives the state of the current data epoch. Each data epoch has a new host thing at the root
357
+ * `world`. Thus a read that reaches the whole root changes at each change of data (READS.KEPT).
358
+ */
359
+ const current = (): Epoch => {
360
+ if (now !== undefined) return now;
361
+ const nodesThing: HostThing = {
362
+ [THING]: true,
363
+ label: "nodes",
364
+ deref(key) {
365
+ const id = String(key);
366
+ if (!docs.has(id)) return undefined;
367
+ const result = demand(id);
368
+ // A failed node gives its own error to each reader, as a failed attribute of a tree does (D16).
369
+ if (!result.ok) throw new Failure(result.error);
370
+ return result.value;
371
+ },
372
+ has: (key) => docs.has(String(key)),
373
+ toJson: record,
374
+ };
375
+ // The context has a world, thus `rootContext` only gives it its grant (CAP.NARROW-CODE). Each evaluation of the
376
+ // epoch then uses this object with no copy.
377
+ now = { context: rootContext(machine, { ...base, world: nodesThing }), values: new Map() };
378
+ return now;
379
+ };
380
+
381
+ const changed = (structure: boolean): void => {
382
+ if (structure) cache.changeStructure();
383
+ else cache.changeData();
384
+ now = undefined;
385
+ };
386
+
387
+ return {
388
+ get epochs() {
389
+ return cache.epochs;
390
+ },
391
+ get counts() {
392
+ return { ...cache.counts, ahead };
393
+ },
394
+ get context() {
395
+ return current().context;
396
+ },
397
+ graph() {
398
+ const structure = cache.epochs.structure;
399
+ if (nodes?.structure === structure) return nodes.map;
400
+ const map = new Map<string, NodeInfo>();
401
+ for (const [id, doc] of docs) {
402
+ const facts = cache.facts(doc);
403
+ map.set(id, { id, doc, facts, side: facts.side, edges: edgesFrom(facts) });
404
+ }
405
+ nodes = { structure, map };
406
+ return map;
407
+ },
408
+ read: (id) => (docs.has(id) ? demand(id) : missing(id)),
409
+ set(id, doc) {
410
+ docs.set(id, doc);
411
+ changed(true);
412
+ },
413
+ remove(id) {
414
+ if (!docs.delete(id)) return false;
415
+ cache.forget(id);
416
+ changed(true);
417
+ return true;
418
+ },
419
+ changeData(next) {
420
+ if (next !== undefined) base = next;
421
+ changed(false);
422
+ },
423
+ changeMachine(next) {
424
+ cache.changeStructure(next);
425
+ now = undefined;
426
+ },
427
+ run(id, runOptions = {}) {
428
+ if (!docs.has(id)) return Promise.resolve(missing(id));
429
+ const ctx = current().context;
430
+ return cache.plan(docs.get(id)).run(runOptions.event === undefined ? ctx : { ...ctx, event: runOptions.event }, {
431
+ ...(runOptions.perform === undefined ? {} : { perform: runOptions.perform }),
432
+ ...(runOptions.signal === undefined ? {} : { signal: runOptions.signal }),
433
+ });
434
+ },
435
+ };
436
+ }