@mark1russell7/coeval 0.22.0 → 0.23.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/README.md +23 -5
- package/build/index.d.ts +359 -116
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5308 -3680
- package/build/index.js.map +1 -1
- package/package.json +4 -4
- package/src/codec-laws.ts +152 -0
- package/src/codec.ts +2 -2
- package/src/conform.ts +662 -70
- package/src/deref.ts +17 -3
- package/src/durable.ts +21 -9
- package/src/errors.ts +17 -1
- package/src/evaluate.ts +55 -21
- package/src/formula.ts +60 -17
- package/src/index.ts +10 -0
- package/src/instances.ts +16 -1
- package/src/meaning.ts +43 -10
- package/src/parts.ts +7 -4
- package/src/providers.ts +4 -1
- package/src/reference.ts +67 -2
- package/src/schedule.ts +286 -45
- package/src/static.ts +46 -14
- package/src/stdlib.ts +127 -5
- package/src/syntax.ts +430 -48
- package/src/types.ts +2 -1
- package/src/world.ts +24 -3
package/src/reference.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, docEquals, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import { ask } from "./ladder.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AttributeDeclaration,
|
|
5
|
+
type CapsRule,
|
|
6
|
+
type CircularRule,
|
|
7
|
+
type Kind,
|
|
8
|
+
type TreeContract,
|
|
9
|
+
GENERATION_LIMIT,
|
|
10
|
+
atArgsOf,
|
|
11
|
+
atRuleOf,
|
|
12
|
+
circularOf,
|
|
13
|
+
collectOf,
|
|
14
|
+
documentKey,
|
|
15
|
+
equationType,
|
|
16
|
+
lookupKind,
|
|
17
|
+
} from "./world.ts";
|
|
4
18
|
import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
|
|
5
19
|
import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, ROOTS, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
|
|
6
20
|
import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
|
|
@@ -882,6 +896,55 @@ function collectIn(node: RefNode, name: string, declared: unknown): Result {
|
|
|
882
896
|
return visit(node) ?? acc;
|
|
883
897
|
}
|
|
884
898
|
|
|
899
|
+
/** TREE.HIGHER-DEPTH: the depth of each root of a generated document in the reference. */
|
|
900
|
+
const refGenerations = /* @__PURE__ */ new WeakMap<RefNode, number>();
|
|
901
|
+
|
|
902
|
+
/** TREE.HIGHER-DEPTH: the depth of the nearest root of a generated document at or above a node of the reference, or 0. */
|
|
903
|
+
function generationIn(node: RefNode): number {
|
|
904
|
+
for (let at: RefNode | undefined = node; at !== undefined; at = at.up) {
|
|
905
|
+
const depth = refGenerations.get(at);
|
|
906
|
+
if (depth !== undefined) return depth;
|
|
907
|
+
}
|
|
908
|
+
return 0;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* TREE.HIGHER: a higher-order attribute. The equation gives a document as data, and the document evaluates with the
|
|
913
|
+
* node as `self`. The reference compiles each document again, thus a structural key is not a part of the meaning.
|
|
914
|
+
*/
|
|
915
|
+
function higherOf(node: RefNode, name: string, term: Json): Result {
|
|
916
|
+
const value = equationOf(node, term, {});
|
|
917
|
+
if (!value.ok) return value;
|
|
918
|
+
const text = nameOfAttribute(node, name);
|
|
919
|
+
if (documentKey(value.value) === undefined) return fail("#VALUE!", `The equation of the higher-order attribute ${text} gives a value that is not JSON data.`);
|
|
920
|
+
const depth = generationIn(node) + 1;
|
|
921
|
+
if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The higher-order attribute ${text} is under more than ${GENERATION_LIMIT} generated documents.`);
|
|
922
|
+
const root = equationOf(node, value.value as Json, {});
|
|
923
|
+
if (!root.ok) return root;
|
|
924
|
+
const made = refNodeOf(root.value);
|
|
925
|
+
if (made === undefined) return fail("#VALUE!", `The document of the higher-order attribute ${text} gives no node of the tree.`);
|
|
926
|
+
refGenerations.set(made, depth);
|
|
927
|
+
return root;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** TREE.FORWARD: the last rung. The attribute `forwards` gives a node, and that node answers through its own ladder. */
|
|
931
|
+
function forwardOf(node: RefNode, name: string): Result | undefined {
|
|
932
|
+
const target: unknown = node.contract?.forwards;
|
|
933
|
+
if (target === undefined || target === name) return undefined;
|
|
934
|
+
const text = nameOfAttribute(node, name);
|
|
935
|
+
if (typeof target !== "string") return fail("#VALUE!", `The attribute ${text} has a contract with a bad field forwards. It must be a text.`);
|
|
936
|
+
const value = attributeOf(node, target);
|
|
937
|
+
if (value === undefined || !value.ok) return value;
|
|
938
|
+
const to = refNodeOf(value.value);
|
|
939
|
+
if (to === undefined) return fail("#VALUE!", `The attribute ${target} that ${text} forwards to gives no node of a tree.`);
|
|
940
|
+
if (!refGenerations.has(to)) {
|
|
941
|
+
const depth = generationIn(node) + 1;
|
|
942
|
+
if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The attribute ${text} forwards through more than ${GENERATION_LIMIT} generated documents.`);
|
|
943
|
+
refGenerations.set(to, depth);
|
|
944
|
+
}
|
|
945
|
+
return attributeOf(to, name);
|
|
946
|
+
}
|
|
947
|
+
|
|
885
948
|
/**
|
|
886
949
|
* TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
|
|
887
950
|
* answer, and the next rung then tries.
|
|
@@ -912,9 +975,11 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
|
|
|
912
975
|
const declared = termIn(node.contract?.attributes, name);
|
|
913
976
|
if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
|
|
914
977
|
if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
|
|
915
|
-
|
|
978
|
+
if (declared?.default !== undefined) return equationOf(node, declared.default, {});
|
|
979
|
+
return declared?.higher === undefined ? undefined : higherOf(node, name, declared.higher);
|
|
916
980
|
},
|
|
917
981
|
],
|
|
982
|
+
["forward", forwardOf],
|
|
918
983
|
];
|
|
919
984
|
|
|
920
985
|
/** TREE.LADDER: the answer of the first rung that answers, or `undefined`. */
|
package/src/schedule.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Key } from "./doc.ts";
|
|
2
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";
|
|
3
|
+
import { type Failed, type Result, Failure, fail, overflowCount } from "./errors.ts";
|
|
4
4
|
import { type Machine, type Perform, type StopSignal, rootContext } from "./evaluate.ts";
|
|
5
5
|
import { type DurableCounts, type DurableOptions, type Epochs, durable } from "./durable.ts";
|
|
6
6
|
import type { DocFacts, StaticRead } from "./facts.ts";
|
|
7
|
-
import { deepestFirst } from "./graph.ts";
|
|
7
|
+
import { type NodeGraph, cycles, deepestFirst } from "./graph.ts";
|
|
8
|
+
import type { Issue } from "./static.ts";
|
|
8
9
|
import type { Side } from "./world.ts";
|
|
9
10
|
|
|
10
11
|
/*
|
|
@@ -48,6 +49,17 @@ export interface NodeInfo {
|
|
|
48
49
|
export interface ScheduleOptions extends DurableOptions {
|
|
49
50
|
/** The context of the host: the names, `self`, `event` and the capabilities. The schedule sets the root `world` to the nodes. */
|
|
50
51
|
readonly context?: Context;
|
|
52
|
+
/**
|
|
53
|
+
* This function gives a node its own `self` (SCHED.NODE-CONTEXT, D135). Thus one document at each cell of a grid
|
|
54
|
+
* shares one plan, and each cell has its own value and its own reads. In one data epoch, the function gives the same
|
|
55
|
+
* data for a node. New data comes with `changeData`, and each node then verifies its reads with its new `self`.
|
|
56
|
+
* Without the function, each node gets the context of the host.
|
|
57
|
+
*
|
|
58
|
+
* @param id - The name of the node.
|
|
59
|
+
* @param host - The context of the host, without the nodes at the root `world`.
|
|
60
|
+
* @returns The `self` of the node, or `undefined` for the `self` of the host.
|
|
61
|
+
*/
|
|
62
|
+
readonly selfOf?: (id: string, host: Context) => Thing;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
/** The work of a schedule since its start: the work of its `durable` cache, and the demands ahead of a node. */
|
|
@@ -78,7 +90,7 @@ export interface Schedule {
|
|
|
78
90
|
readonly epochs: Epochs;
|
|
79
91
|
/** The work of the schedule since its start: the compilations, the evaluations, the verifications, the reuses and the demands ahead. */
|
|
80
92
|
readonly counts: ScheduleCounts;
|
|
81
|
-
/** The context of
|
|
93
|
+
/** The context of the current data epoch: the context of the host, with the nodes at the root `world`. The option `selfOf` changes only the `self` of a node. */
|
|
82
94
|
readonly context: Context;
|
|
83
95
|
/**
|
|
84
96
|
* This function gives each node with its facts, its side and its edges (SCHED.GRAPH). The map stays the same object
|
|
@@ -93,7 +105,7 @@ export interface Schedule {
|
|
|
93
105
|
*
|
|
94
106
|
* - First, the schedule demands each node that the eager edges reach, deepest first (SCHED.LONG-CHAIN).
|
|
95
107
|
* - 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).
|
|
108
|
+
* - Each node of a cycle gives `#CYCLE!`, in each order of demands (SCHED.CYCLE), at each length (SCHED.DEEP-CYCLE).
|
|
97
109
|
* - A node on the run side gives `#REF!`, because the schedule does not evaluate it (SCHED.SIDE).
|
|
98
110
|
*
|
|
99
111
|
* @param id - The name of the node.
|
|
@@ -117,7 +129,8 @@ export interface Schedule {
|
|
|
117
129
|
remove(id: string): boolean;
|
|
118
130
|
/**
|
|
119
131
|
* 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.
|
|
132
|
+
* not change in place, thus new data comes in a new context, or through a host thing. A new `self` of a node is a
|
|
133
|
+
* change of data too (SCHED.NODE-STALE).
|
|
121
134
|
*
|
|
122
135
|
* @param context - The new context of the host, or `undefined` to keep the context.
|
|
123
136
|
*/
|
|
@@ -130,7 +143,8 @@ export interface Schedule {
|
|
|
130
143
|
changeMachine(machine: Machine): void;
|
|
131
144
|
/**
|
|
132
145
|
* 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).
|
|
146
|
+
* starts a run, for example at an event. A change of data does not start or repeat a run (L3). The run gets the
|
|
147
|
+
* `self` of its node (SCHED.NODE-CONTEXT).
|
|
134
148
|
*
|
|
135
149
|
* @param id - The name of the node.
|
|
136
150
|
* @param options - The event, the host code that does the effects, and a signal to stop the run.
|
|
@@ -180,10 +194,52 @@ function missing(id: string): Failed {
|
|
|
180
194
|
return fail("#N/A", `The schedule has no node ${id}.`);
|
|
181
195
|
}
|
|
182
196
|
|
|
183
|
-
/** The
|
|
197
|
+
/** The walk of a data epoch (D139): the result of each node that the walk finished, and the root `world` of the walk. */
|
|
198
|
+
interface Walk {
|
|
199
|
+
readonly results: Map<string, Result>;
|
|
200
|
+
readonly context: Context;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The state of one data epoch: the context of the nodes, the value of each reached node, and the walk after an overflow. */
|
|
184
204
|
interface Epoch {
|
|
185
205
|
readonly context: Context;
|
|
186
206
|
readonly values: Map<string, Result>;
|
|
207
|
+
walk?: Walk;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* One certain cycle of a schedule, in the form of an issue of `check` (SCHED.CYCLE-ISSUES). The first node of `nodes`
|
|
212
|
+
* gives the place and the reason. The place is the key of that node under the root `world`, and the reason has the
|
|
213
|
+
* text of `#CYCLE!` for that node.
|
|
214
|
+
*/
|
|
215
|
+
export interface CycleIssue extends Issue {
|
|
216
|
+
readonly code: "#CYCLE!";
|
|
217
|
+
/** The nodes of the cycle, sorted by name as the record of the root `world` sorts them. */
|
|
218
|
+
readonly nodes: readonly string[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* This function gives the certain cycles of a schedule before a run (SCHED.CYCLE-ISSUES, D131). It reads only `graph()`,
|
|
223
|
+
* thus it evaluates no node. A host calls it when it needs the answer. A run does not call it.
|
|
224
|
+
*
|
|
225
|
+
* - Each certain cycle of `cycles` gives one issue. The issue has the place `[id]` of its first node.
|
|
226
|
+
* - A possible cycle gives no issue, because a run can skip its lazy read (SCHED.CYCLE-CERTAIN).
|
|
227
|
+
* - The order of the `set` calls does not change the answer (SCHED.CYCLE-ORDER).
|
|
228
|
+
* - No node of an issue gives a value. A read can fail before it reaches the cycle, and the node then gives that
|
|
229
|
+
* error (SCHED.CYCLE-AGREES). Thus the demand of D33 still gives the result of each node.
|
|
230
|
+
*
|
|
231
|
+
* @param target - The schedule, or another object that gives a graph of nodes.
|
|
232
|
+
* @returns One issue for each certain cycle, sorted by the first node of each issue.
|
|
233
|
+
*/
|
|
234
|
+
export function checkCycles(target: { graph(): NodeGraph<string> }): readonly CycleIssue[] {
|
|
235
|
+
const out: CycleIssue[] = [];
|
|
236
|
+
for (const cycle of cycles(target.graph())) {
|
|
237
|
+
if (!cycle.certain) continue;
|
|
238
|
+
const nodes = [...cycle.nodes].sort();
|
|
239
|
+
const first = nodes[0] as string;
|
|
240
|
+
out.push({ code: "#CYCLE!", place: [first], reason: cycleOf(first).error.reason, nodes });
|
|
241
|
+
}
|
|
242
|
+
return out.sort((a, b) => ((a.nodes[0] as string) < (b.nodes[0] as string) ? -1 : 1));
|
|
187
243
|
}
|
|
188
244
|
|
|
189
245
|
/**
|
|
@@ -201,16 +257,20 @@ interface Epoch {
|
|
|
201
257
|
* - A demand of the host, or of a run, first demands each node that the eager edges reach, deepest first. Thus each
|
|
202
258
|
* read of such a node finds a value, and a chain of 100000 nodes does not overflow the stack (SCHED.LONG-CHAIN).
|
|
203
259
|
* Each such demand starts with no node in evaluation, thus the order of demands keeps the values of D31.
|
|
260
|
+
* - After an overflow of the stack, a walk with its own stack follows the same reads. It gives each node of a cycle
|
|
261
|
+
* `#CYCLE!` at each length (SCHED.DEEP-CYCLE, D139). A demand without an overflow does no such work.
|
|
204
262
|
* - A node on the run side is a sink. Only `run` evaluates it, and only the host uses `run`.
|
|
263
|
+
* - The option `selfOf` gives a node its own `self` (D135). The plan, the facts and the edges stay for each document
|
|
264
|
+
* object, thus nodes with one document share them. The value, the reads and the place on a cycle stay for each node.
|
|
205
265
|
*
|
|
206
266
|
* The host keeps the promises of `durable`. The documents and the values of a context do not change in place.
|
|
207
267
|
*
|
|
208
268
|
* @param machine - The machine of the first structure epoch.
|
|
209
|
-
* @param options - The options of `durable`,
|
|
269
|
+
* @param options - The options of `durable`, the context of the host and the `self` of each node.
|
|
210
270
|
* @returns The schedule.
|
|
211
271
|
*/
|
|
212
272
|
export function schedule(machine: Machine, options: ScheduleOptions = {}): Schedule {
|
|
213
|
-
const { context: given, ...cacheOptions } = options;
|
|
273
|
+
const { context: given, selfOf, ...cacheOptions } = options;
|
|
214
274
|
const cache = durable(machine, cacheOptions);
|
|
215
275
|
const docs = new Map<string, unknown>();
|
|
216
276
|
const edges = new WeakMap<DocFacts, readonly NodeEdge[]>();
|
|
@@ -229,8 +289,15 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
229
289
|
const onOpen = new Set<string>();
|
|
230
290
|
// The nodes in evaluation that read themselves.
|
|
231
291
|
const looped = new Set<string>();
|
|
292
|
+
// The length of the stack at the kept read that resolves at this time, or -1 without such a read (SCHED.OWN-NODE).
|
|
293
|
+
let keptAt = -1;
|
|
294
|
+
// The nodes that finished in a kept read, and if that read met a node in evaluation below it (D143).
|
|
295
|
+
const keptNodes: string[] = [];
|
|
296
|
+
let keptMet = false;
|
|
232
297
|
let count = 0;
|
|
233
298
|
let ahead = 0;
|
|
299
|
+
// The node that stopped the evaluation of a walk, because the walk has no result for it.
|
|
300
|
+
let wanted: string | undefined;
|
|
234
301
|
|
|
235
302
|
const lower = (id: string, to: number): void => {
|
|
236
303
|
if (to < (low.get(id) as number)) low.set(id, to);
|
|
@@ -266,19 +333,35 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
266
333
|
return facts.side === "run" ? undefined : edgesFrom(facts);
|
|
267
334
|
};
|
|
268
335
|
|
|
336
|
+
/** This function gives the context of a node: the context of the epoch, or a copy with the `self` of the node. A copy keeps the grant. */
|
|
337
|
+
const contextFor = (id: string, shared: Context): Context => {
|
|
338
|
+
if (selfOf === undefined) return shared;
|
|
339
|
+
const self = selfOf(id, base);
|
|
340
|
+
return self === undefined ? shared : { ...shared, self };
|
|
341
|
+
};
|
|
342
|
+
|
|
269
343
|
/**
|
|
270
344
|
* 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
345
|
* demands each node that the eager edges reach, deepest first. Each of these demands also starts with no node in
|
|
272
346
|
* evaluation, thus it is a demand of the host too. D31 gives each order of such demands the same values.
|
|
273
347
|
*/
|
|
274
348
|
const demand = (id: string): Result => {
|
|
275
|
-
if (stack.length
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
349
|
+
if (stack.length > 0) return visit(id);
|
|
350
|
+
for (const first of deepestFirst(id, edgesFor)) {
|
|
351
|
+
ahead += 1;
|
|
352
|
+
hostDemand(first);
|
|
280
353
|
}
|
|
281
|
-
return
|
|
354
|
+
return hostDemand(id);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* This function demands one node with no node in evaluation. An overflow of the stack, in this demand or earlier in
|
|
359
|
+
* the data epoch, makes a walk from the node. A `#DEPTH!` of an earlier demand can stop a read of this demand.
|
|
360
|
+
*/
|
|
361
|
+
const hostDemand = (id: string): Result => {
|
|
362
|
+
const before = overflowCount();
|
|
363
|
+
const result = visit(id);
|
|
364
|
+
return overflowCount() === before && current().walk === undefined ? result : settle(id, result);
|
|
282
365
|
};
|
|
283
366
|
|
|
284
367
|
/** This function demands one node, with the algorithm of Tarjan over the demands for the cycles of D31. */
|
|
@@ -292,6 +375,13 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
292
375
|
return known;
|
|
293
376
|
}
|
|
294
377
|
if (busy.has(id)) {
|
|
378
|
+
// The evaluation of a node did not ask for its kept read of the node itself, thus that read closes no cycle (SCHED.OWN-NODE).
|
|
379
|
+
if (reader === id && keptAt === stack.length) return cycleOf(id);
|
|
380
|
+
// The evaluation of the node below did not ask for this demand either, thus it closes no cycle (SCHED.KEPT-BELOW).
|
|
381
|
+
if (keptAt >= 0 && stack.indexOf(id) < keptAt) {
|
|
382
|
+
keptMet = true;
|
|
383
|
+
return cycleOf(id);
|
|
384
|
+
}
|
|
295
385
|
// A read of a node in evaluation closes a cycle. It gives the same error as the final result of that node.
|
|
296
386
|
if (reader !== undefined) lower(reader, order.get(id) as number);
|
|
297
387
|
if (reader === id) looped.add(id);
|
|
@@ -313,14 +403,21 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
313
403
|
stack.push(id);
|
|
314
404
|
let result: Result | undefined;
|
|
315
405
|
try {
|
|
316
|
-
result = cache.evaluate(id, doc, epoch.context);
|
|
406
|
+
result = cache.evaluate(id, doc, contextFor(id, epoch.context));
|
|
317
407
|
} finally {
|
|
318
408
|
stack.pop();
|
|
319
409
|
busy.delete(id);
|
|
320
410
|
// An exception of the host leaves the node without a result. A later demand evaluates it again.
|
|
321
411
|
if (result === undefined) {
|
|
322
412
|
looped.delete(id);
|
|
323
|
-
|
|
413
|
+
// This loop calls no function of the module. Near the limit of the stack such a call can fail. Then this node
|
|
414
|
+
// stays open, and its reader finds two nodes in its component, as on a cycle (SCHED.DEEP-CYCLE-CHAIN).
|
|
415
|
+
while (open.length > from) {
|
|
416
|
+
const left = open.pop() as string;
|
|
417
|
+
onOpen.delete(left);
|
|
418
|
+
order.delete(left);
|
|
419
|
+
low.delete(left);
|
|
420
|
+
}
|
|
324
421
|
}
|
|
325
422
|
}
|
|
326
423
|
const reached = low.get(id) as number;
|
|
@@ -336,22 +433,190 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
336
433
|
cache.forget(id);
|
|
337
434
|
}
|
|
338
435
|
epoch.values.set(id, value);
|
|
436
|
+
if (keptAt >= 0) keptNodes.push(id);
|
|
339
437
|
if (reader !== undefined) lower(reader, reached);
|
|
340
438
|
return value;
|
|
341
439
|
};
|
|
342
440
|
|
|
441
|
+
/** This function gives the walk of a data epoch. Its root `world` answers from the walk, and each other read stops the evaluation. */
|
|
442
|
+
const startWalk = (): Walk => {
|
|
443
|
+
const results = new Map<string, Result>();
|
|
444
|
+
const stop = new Failure(fail("#CALC!", "A walk stopped this evaluation at a read.").error);
|
|
445
|
+
const read = (id: string): Result => {
|
|
446
|
+
// After the first stop, a document that catches the error reads nothing more: such a read does not happen.
|
|
447
|
+
if (wanted !== undefined) throw stop;
|
|
448
|
+
const reader = stack[stack.length - 1] as string;
|
|
449
|
+
const known = results.get(id);
|
|
450
|
+
if (known !== undefined) {
|
|
451
|
+
if (onOpen.has(id)) lower(reader, order.get(id) as number);
|
|
452
|
+
return known;
|
|
453
|
+
}
|
|
454
|
+
if (busy.has(id)) {
|
|
455
|
+
lower(reader, order.get(id) as number);
|
|
456
|
+
if (reader === id) looped.add(id);
|
|
457
|
+
return cycleOf(id);
|
|
458
|
+
}
|
|
459
|
+
if (cache.facts(docs.get(id)).side === "run") {
|
|
460
|
+
const refused = runSide(id);
|
|
461
|
+
results.set(id, refused);
|
|
462
|
+
return refused;
|
|
463
|
+
}
|
|
464
|
+
wanted = id;
|
|
465
|
+
throw stop;
|
|
466
|
+
};
|
|
467
|
+
return { results, context: rooted(worldOf(results, read)) };
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/** This function gives the node that stopped the last evaluation of a walk, and it clears the mark. */
|
|
471
|
+
const takeWanted = (): string | undefined => {
|
|
472
|
+
const id = wanted;
|
|
473
|
+
wanted = undefined;
|
|
474
|
+
return id;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* This function follows the reads of the evaluations from a node after an overflow of the stack (SCHED.DEEP-CYCLE,
|
|
479
|
+
* D139). `visit` uses the stack of the host for each node. This walk uses the lists of `visit` as its own stack.
|
|
480
|
+
*
|
|
481
|
+
* An evaluation has no nested demand. A read of a node without a result stops the evaluation, and the walk finishes
|
|
482
|
+
* that node. Then the reader evaluates again. Thus the algorithm of Tarjan of `visit` finds each cycle of real reads.
|
|
483
|
+
*
|
|
484
|
+
* Each evaluation enters a new node or finishes a node. Thus the walks of a data epoch do at most `2V` evaluations
|
|
485
|
+
* for `V` entered nodes. They keep one result and one entry in each list for each such node.
|
|
486
|
+
*
|
|
487
|
+
* A walk that reaches no cycle changes nothing, thus a chain keeps its `#DEPTH!`. After a cycle, each entered node
|
|
488
|
+
* keeps only its error. A node with a value evaluates again at its next demand, because a value can hold the root of
|
|
489
|
+
* the walk.
|
|
490
|
+
*
|
|
491
|
+
* @param id - The node of the demand.
|
|
492
|
+
* @param given - The result of the demand.
|
|
493
|
+
* @returns The final result of the demand.
|
|
494
|
+
*/
|
|
495
|
+
const settle = (id: string, given: Result): Result => {
|
|
496
|
+
const epoch = current();
|
|
497
|
+
const walk = (epoch.walk ??= startWalk());
|
|
498
|
+
const { results } = walk;
|
|
499
|
+
if (results.has(id) || cache.facts(docs.get(id)).side === "run") return given;
|
|
500
|
+
const entered: string[] = [];
|
|
501
|
+
const starts: number[] = [];
|
|
502
|
+
const enter = (node: string): void => {
|
|
503
|
+
const mine = count++;
|
|
504
|
+
order.set(node, mine);
|
|
505
|
+
low.set(node, mine);
|
|
506
|
+
starts.push(open.length);
|
|
507
|
+
open.push(node);
|
|
508
|
+
onOpen.add(node);
|
|
509
|
+
busy.add(node);
|
|
510
|
+
stack.push(node);
|
|
511
|
+
entered.push(node);
|
|
512
|
+
};
|
|
513
|
+
let found = false;
|
|
514
|
+
// An overflow can stop `visit` between two steps of its lists. The walk starts with empty lists.
|
|
515
|
+
close(0);
|
|
516
|
+
looped.clear();
|
|
517
|
+
enter(id);
|
|
518
|
+
try {
|
|
519
|
+
while (stack.length > 0) {
|
|
520
|
+
const node = stack[stack.length - 1] as string;
|
|
521
|
+
const result = cache.plan(docs.get(node)).evaluate(contextFor(node, walk.context));
|
|
522
|
+
const next = takeWanted();
|
|
523
|
+
if (next !== undefined) {
|
|
524
|
+
enter(next);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
stack.pop();
|
|
528
|
+
busy.delete(node);
|
|
529
|
+
const from = starts.pop() as number;
|
|
530
|
+
const reached = low.get(node) as number;
|
|
531
|
+
const self = looped.delete(node);
|
|
532
|
+
const onCycle = reached < (order.get(node) as number) || close(from) > 1 || self;
|
|
533
|
+
results.set(node, onCycle ? cycleOf(node) : result);
|
|
534
|
+
// A node behind a cycle of an earlier walk finds that cycle too.
|
|
535
|
+
found ||= onCycle || (!result.ok && result.error.code === "#CYCLE!");
|
|
536
|
+
const reader = stack[stack.length - 1];
|
|
537
|
+
if (reader !== undefined) lower(reader, reached);
|
|
538
|
+
}
|
|
539
|
+
} finally {
|
|
540
|
+
takeWanted();
|
|
541
|
+
stack.length = 0;
|
|
542
|
+
busy.clear();
|
|
543
|
+
looped.clear();
|
|
544
|
+
close(0);
|
|
545
|
+
}
|
|
546
|
+
if (!found) return given;
|
|
547
|
+
for (const node of entered) {
|
|
548
|
+
const result = results.get(node) as Result;
|
|
549
|
+
cache.forget(node);
|
|
550
|
+
if (result.ok) epoch.values.delete(node);
|
|
551
|
+
else epoch.values.set(node, result);
|
|
552
|
+
}
|
|
553
|
+
return visit(id);
|
|
554
|
+
};
|
|
555
|
+
|
|
343
556
|
/** This function gives the record of the derive nodes, for a path that ends at the root `world`. */
|
|
344
|
-
const record = (): unknown => {
|
|
557
|
+
const record = (get: (id: string) => Result): unknown => {
|
|
345
558
|
const out: Record<string, unknown> = {};
|
|
346
559
|
for (const id of [...docs.keys()].sort()) {
|
|
347
560
|
if (cache.facts(docs.get(id)).side === "run") continue;
|
|
348
|
-
const result =
|
|
561
|
+
const result = get(id);
|
|
349
562
|
if (!result.ok) throw new Failure(result.error);
|
|
350
563
|
Object.defineProperty(out, id, { value: result.value, enumerable: true, writable: true, configurable: true });
|
|
351
564
|
}
|
|
352
565
|
return out;
|
|
353
566
|
};
|
|
354
567
|
|
|
568
|
+
/** This function gives the host thing at the root `world`: `get` gives the result of a node, and `values` holds each final result. */
|
|
569
|
+
const worldOf = (values: ReadonlyMap<string, Result>, get: (id: string) => Result): HostThing => ({
|
|
570
|
+
[THING]: true,
|
|
571
|
+
label: "nodes",
|
|
572
|
+
deref(key) {
|
|
573
|
+
const id = String(key);
|
|
574
|
+
if (!docs.has(id)) return undefined;
|
|
575
|
+
const result = get(id);
|
|
576
|
+
// A failed node gives its own error to each reader, as a failed attribute of a tree does (D16).
|
|
577
|
+
if (!result.ok) throw new Failure(result.error);
|
|
578
|
+
return result.value;
|
|
579
|
+
},
|
|
580
|
+
has: (key) => docs.has(String(key)),
|
|
581
|
+
toJson: () => record(get),
|
|
582
|
+
// A demand from a kept read can reach the node in evaluation, and that cycle is false (SCHED.ONE-MEANING).
|
|
583
|
+
// Thus only a node with its result in this epoch gives a value. Each such value is final (D31).
|
|
584
|
+
known(key) {
|
|
585
|
+
const result = values.get(String(key));
|
|
586
|
+
if (result === undefined) return undefined;
|
|
587
|
+
if (!result.ok) throw new Failure(result.error);
|
|
588
|
+
return { thing: result.value };
|
|
589
|
+
},
|
|
590
|
+
// A kept read that a reference leads to a node still demands that node, and only its own node gives no cycle.
|
|
591
|
+
// A nested demand has a longer stack, thus each read of its evaluation counts (SCHED.OWN-NODE).
|
|
592
|
+
keeping(read) {
|
|
593
|
+
const outer = keptAt;
|
|
594
|
+
const outerMet = keptMet;
|
|
595
|
+
const from = keptNodes.length;
|
|
596
|
+
keptAt = stack.length;
|
|
597
|
+
keptMet = false;
|
|
598
|
+
try {
|
|
599
|
+
return read();
|
|
600
|
+
} finally {
|
|
601
|
+
// These nodes read an error that a run does not give, thus a real demand evaluates them again (SCHED.KEPT-BELOW).
|
|
602
|
+
// A node of an open component stays: it is on a true cycle, and its value is final (D31).
|
|
603
|
+
if (keptMet) {
|
|
604
|
+
for (const id of keptNodes.splice(from)) {
|
|
605
|
+
if (onOpen.has(id)) continue;
|
|
606
|
+
current().values.delete(id);
|
|
607
|
+
cache.forget(id);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
keptAt = outer;
|
|
611
|
+
keptMet = outerMet;
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// The context has a world, thus `rootContext` only gives it its grant (CAP.NARROW-CODE). Each evaluation of the
|
|
617
|
+
// epoch then uses this object with no copy.
|
|
618
|
+
const rooted = (world: HostThing): Context => rootContext(machine, { ...base, world });
|
|
619
|
+
|
|
355
620
|
/**
|
|
356
621
|
* This function gives the state of the current data epoch. Each data epoch has a new host thing at the root
|
|
357
622
|
* `world`. Thus a read that reaches the whole root changes at each change of data (READS.KEPT). A kept read of a
|
|
@@ -360,31 +625,7 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
360
625
|
const current = (): Epoch => {
|
|
361
626
|
if (now !== undefined) return now;
|
|
362
627
|
const values = new Map<string, Result>();
|
|
363
|
-
|
|
364
|
-
[THING]: true,
|
|
365
|
-
label: "nodes",
|
|
366
|
-
deref(key) {
|
|
367
|
-
const id = String(key);
|
|
368
|
-
if (!docs.has(id)) return undefined;
|
|
369
|
-
const result = demand(id);
|
|
370
|
-
// A failed node gives its own error to each reader, as a failed attribute of a tree does (D16).
|
|
371
|
-
if (!result.ok) throw new Failure(result.error);
|
|
372
|
-
return result.value;
|
|
373
|
-
},
|
|
374
|
-
has: (key) => docs.has(String(key)),
|
|
375
|
-
toJson: record,
|
|
376
|
-
// A demand from a kept read can reach the node in evaluation, and that cycle is false (SCHED.ONE-MEANING).
|
|
377
|
-
// Thus only a node with its result in this epoch gives a value. Each such value is final (D31).
|
|
378
|
-
known(key) {
|
|
379
|
-
const result = values.get(String(key));
|
|
380
|
-
if (result === undefined) return undefined;
|
|
381
|
-
if (!result.ok) throw new Failure(result.error);
|
|
382
|
-
return { thing: result.value };
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
// The context has a world, thus `rootContext` only gives it its grant (CAP.NARROW-CODE). Each evaluation of the
|
|
386
|
-
// epoch then uses this object with no copy.
|
|
387
|
-
now = { context: rootContext(machine, { ...base, world: nodesThing }), values };
|
|
628
|
+
now = { context: rooted(worldOf(values, demand)), values };
|
|
388
629
|
return now;
|
|
389
630
|
};
|
|
390
631
|
|
|
@@ -436,7 +677,7 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
436
677
|
},
|
|
437
678
|
run(id, runOptions = {}) {
|
|
438
679
|
if (!docs.has(id)) return Promise.resolve(missing(id));
|
|
439
|
-
const ctx = current().context;
|
|
680
|
+
const ctx = contextFor(id, current().context);
|
|
440
681
|
return cache.plan(docs.get(id)).run(runOptions.event === undefined ? ctx : { ...ctx, event: runOptions.event }, {
|
|
441
682
|
...(runOptions.perform === undefined ? {} : { perform: runOptions.perform }),
|
|
442
683
|
...(runOptions.signal === undefined ? {} : { signal: runOptions.signal }),
|