@mark1russell7/coeval 0.6.3 → 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.
- package/README.md +32 -5
- package/build/index.d.ts +1568 -271
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4779 -492
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +15 -3
- package/src/conform.ts +1068 -0
- package/src/deref.ts +284 -47
- package/src/durable.ts +238 -0
- package/src/effects.ts +110 -0
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +472 -111
- package/src/facts.ts +252 -0
- package/src/formula.ts +433 -36
- package/src/graph.ts +399 -0
- package/src/index.ts +28 -0
- package/src/ladder.ts +40 -1
- package/src/merge.ts +91 -4
- package/src/parts.ts +26 -11
- package/src/providers.ts +286 -0
- package/src/reads.ts +105 -0
- package/src/reference.ts +19 -7
- package/src/schedule.ts +436 -0
- package/src/static.ts +266 -29
- package/src/stdlib.ts +24 -7
- package/src/types.ts +741 -0
- package/src/typing.ts +390 -0
- package/src/world.ts +63 -9
package/src/reads.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { docEquals } from "./doc.ts";
|
|
2
|
+
import { type Result, ok } from "./errors.ts";
|
|
3
|
+
import type { Context, Hop } from "./deref.ts";
|
|
4
|
+
import { type Machine, type Plan, type Read, readAgain, rootContext } from "./evaluate.ts";
|
|
5
|
+
import { Held } from "./formula.ts";
|
|
6
|
+
import { sameResult } from "./same.ts";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* The verifying trace of a schedule (R4 P3, D26). An evaluation with the option `reads` records each path that it
|
|
10
|
+
* resolves, and what the path reached. A pure document is a function of its reads. Thus when each free read reaches
|
|
11
|
+
* the same thing in a new context, the old result stays valid, and the schedule keeps it without an evaluation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The answer of `verifyReads`: the old result stays valid, or the first read that reaches another thing. */
|
|
15
|
+
export type ReadsCheck =
|
|
16
|
+
| { readonly same: true }
|
|
17
|
+
| {
|
|
18
|
+
readonly same: false;
|
|
19
|
+
/** The index of the first changed read in the list. */
|
|
20
|
+
readonly index: number;
|
|
21
|
+
/** The first changed read, as the evaluation recorded it. */
|
|
22
|
+
readonly read: Read;
|
|
23
|
+
/** What the path of that read reaches in the new context. */
|
|
24
|
+
readonly now: Result;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The options of `verifyReads`. */
|
|
28
|
+
export interface VerifyOptions {
|
|
29
|
+
/**
|
|
30
|
+
* The rule of the host for two things, as in `sameResult`. It answers first, and `undefined` gives the rules of
|
|
31
|
+
* `sameResult`. An example is a rule for a host thing that keeps a version.
|
|
32
|
+
*
|
|
33
|
+
* @param a - The thing of the recorded read.
|
|
34
|
+
* @param b - The thing in the new context.
|
|
35
|
+
* @returns `true` when the two things are the same, or `undefined` for the rules of `sameResult`.
|
|
36
|
+
*/
|
|
37
|
+
readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One answer serves each list whose reads are all the same. */
|
|
41
|
+
const SAME: ReadsCheck = /* @__PURE__ */ Object.freeze({ same: true });
|
|
42
|
+
|
|
43
|
+
/** This function tells if two lists of hops are the same: the same steps, rungs and things, in order. */
|
|
44
|
+
function sameHops(a: readonly Hop[], b: readonly Hop[] | undefined, equal: VerifyOptions["equal"]): boolean {
|
|
45
|
+
if (b === undefined || a.length !== b.length) return false;
|
|
46
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
47
|
+
const x = a[index] as Hop;
|
|
48
|
+
const y = b[index] as Hop;
|
|
49
|
+
if (x.by !== y.by || !docEquals(x.step, y.step) || !sameResult(ok(x.thing), ok(y.thing), equal)) return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* This function gives the error of a name that holds a held error and stops a longer path, or `undefined`. A
|
|
56
|
+
* reference of the formula kit tests the binding of its first name before it resolves the rest of the path
|
|
57
|
+
* (FORMULA.LET). The resolution of the whole path does not show that test.
|
|
58
|
+
*/
|
|
59
|
+
function heldStop(root: Context, path: readonly unknown[]): Result | undefined {
|
|
60
|
+
const name = path[1];
|
|
61
|
+
if (path.length <= 2 || path[0] !== "names" || typeof name !== "string" || !Object.hasOwn(root.names, name)) return undefined;
|
|
62
|
+
const bound = root.names[name];
|
|
63
|
+
return bound instanceof Held ? { ok: false, error: bound.error } : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* This function tells if an old result stays valid in a new context (READS.VERIFY, D26). It resolves the path of
|
|
68
|
+
* each free read again, in the order of the list, and it compares what the path reaches with `sameResult`. It stops at
|
|
69
|
+
* the first read that reaches another thing, and it gives that read (READS.CHANGED).
|
|
70
|
+
*
|
|
71
|
+
* - A read that is not free reads a binding that a kind made from earlier reads, for example a name of `let`. The
|
|
72
|
+
* function does not resolve it again.
|
|
73
|
+
* - A thing compares as in `sameResult`. Thus a host thing or a date is the same only as itself. A host gives a new
|
|
74
|
+
* object for a new state, or the option `equal`.
|
|
75
|
+
* - A longer path through a name that holds a `Held` in the new context is a changed read. A reference of the formula
|
|
76
|
+
* kit gives the error of that binding, and the resolution of the path does not show it.
|
|
77
|
+
* - The answer is sound only for a pure document (FACTS.PURE). A document with an effect, a volatile operation or an
|
|
78
|
+
* extension of a profile can read outside `resolve`. The schedule asks `factsOf` first.
|
|
79
|
+
*
|
|
80
|
+
* The values of the context must not change in place, as PLAN.CACHE says for the documents. A value that changed in
|
|
81
|
+
* place is the same object, thus the function cannot see the change.
|
|
82
|
+
*
|
|
83
|
+
* @param target - The machine of the evaluation, or its plan.
|
|
84
|
+
* @param reads - The reads of the evaluation, from the option `reads`.
|
|
85
|
+
* @param context - The new context.
|
|
86
|
+
* @param options - The rule of the host for two things.
|
|
87
|
+
* @returns `{ same: true }` when each free read reaches the same thing, or else the first changed read.
|
|
88
|
+
*/
|
|
89
|
+
export function verifyReads(target: Machine | Plan, reads: readonly Read[], context: Context, options: VerifyOptions = {}): ReadsCheck {
|
|
90
|
+
const machine = "evaluators" in target ? target : target.machine;
|
|
91
|
+
const root = rootContext(machine, context);
|
|
92
|
+
const equal = options.equal;
|
|
93
|
+
for (let index = 0; index < reads.length; index += 1) {
|
|
94
|
+
const read = reads[index] as Read;
|
|
95
|
+
if (!read.free) continue;
|
|
96
|
+
const stop = heldStop(root, read.path);
|
|
97
|
+
if (stop !== undefined) return { same: false, index, read, now: stop };
|
|
98
|
+
const asked = read.hops !== undefined;
|
|
99
|
+
const now = readAgain(machine, root, read.path, asked);
|
|
100
|
+
if (!sameResult(read.reached, now.reached, equal) || (read.hops !== undefined && !sameHops(read.hops, now.hops, equal))) {
|
|
101
|
+
return { same: false, index, read, now: now.reached };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return SAME;
|
|
105
|
+
}
|
package/src/reference.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
makeContext,
|
|
22
22
|
permits,
|
|
23
23
|
placeText,
|
|
24
|
+
waitedReason,
|
|
24
25
|
} from "./evaluate.ts";
|
|
25
26
|
import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
|
|
26
27
|
|
|
@@ -49,6 +50,11 @@ export interface ReferenceOptions {
|
|
|
49
50
|
readonly effects?: Effect[];
|
|
50
51
|
/** The mock of the host (RUN.MOCK). It answers each effect at once. Without it, an effect gives its payload. */
|
|
51
52
|
readonly perform?: Perform;
|
|
53
|
+
/**
|
|
54
|
+
* The verb that goes up the ladder at each document, as in `prepare`. The default is `eval`. The reference reads
|
|
55
|
+
* each verb by the same rules (VERB.AGREE).
|
|
56
|
+
*/
|
|
57
|
+
readonly verb?: string;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/** The scope of the addresses of effects (RUN.ADDRESS): the root of the evaluation, or one item of `all`. */
|
|
@@ -68,6 +74,8 @@ interface Env {
|
|
|
68
74
|
readonly effects: Effect[];
|
|
69
75
|
readonly maxDepth: number;
|
|
70
76
|
readonly perform: Perform | undefined;
|
|
77
|
+
/** The verb that goes up the ladder at each document. Each part evaluates with the same verb. */
|
|
78
|
+
readonly verb: string;
|
|
71
79
|
/** The scope of the code that evaluates at this time. `all` changes it for each item. */
|
|
72
80
|
scope: Scope;
|
|
73
81
|
}
|
|
@@ -121,8 +129,8 @@ function choose(env: Env, name: string): Chosen | undefined {
|
|
|
121
129
|
|
|
122
130
|
/**
|
|
123
131
|
* This function reads one document by the rules, in their order. First come an envelope or a declared type
|
|
124
|
-
* (EVAL.ERRORS) and the limit of the depth (PLAN.DEPTH). Then come the ladder (LADDER
|
|
125
|
-
* of the parts (SHAPE.*).
|
|
132
|
+
* (EVAL.ERRORS) and the limit of the depth (PLAN.DEPTH). Then come the ladder of the verb (LADDER.*, VERB.LADDER), an
|
|
133
|
+
* evaluator and the shape of the parts (SHAPE.*).
|
|
126
134
|
*/
|
|
127
135
|
function examine(env: Env, at: At): Seen | Failed {
|
|
128
136
|
const { place, declared } = at;
|
|
@@ -133,7 +141,7 @@ function examine(env: Env, at: At): Seen | Failed {
|
|
|
133
141
|
if (at.depth > env.maxDepth) return fail("#DEPTH!", `The nesting is deeper than ${env.maxDepth} documents.`, { place });
|
|
134
142
|
const { doc, kind, split } = read;
|
|
135
143
|
const kindText = typeName(doc.type);
|
|
136
|
-
const answer = ask(world, read.declared ? {} : doc,
|
|
144
|
+
const answer = ask(world, read.declared ? {} : doc, env.verb, declared === undefined ? {} : { declaredType: declared });
|
|
137
145
|
if (answer.error === "unknown-kind") return fail("#KIND?", `The world has no kind ${kindText}.`, { place });
|
|
138
146
|
const name = answer.evaluator;
|
|
139
147
|
const chosen = name === undefined ? undefined : choose(env, name);
|
|
@@ -243,7 +251,7 @@ function evaluateSeen(env: Env, seen: Seen, at: At): Result {
|
|
|
243
251
|
const { chosen } = seen;
|
|
244
252
|
const { place } = at;
|
|
245
253
|
if ("gap" in chosen) return fail("#NOEVAL", chosen.gap, { place });
|
|
246
|
-
if ("runner" in chosen) return drive(runSeen(env, seen, at, chosen.runner), place);
|
|
254
|
+
if ("runner" in chosen) return drive(runSeen(env, seen, at, chosen.runner), place, seen.kindText);
|
|
247
255
|
const input = evalInput(env, seen, at);
|
|
248
256
|
try {
|
|
249
257
|
return placed(chosen.evaluator(input), place);
|
|
@@ -269,11 +277,11 @@ function* runSeen(env: Env, seen: Seen, at: At, runner: RunFn): Steps {
|
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
/** This function drives steps that cannot wait. A step that waits gives `#CALC!` at the document (RUN.WAIT). */
|
|
272
|
-
function drive(steps: Steps, place: readonly Key[]): Result {
|
|
280
|
+
function drive(steps: Steps, place: readonly Key[], kindText: string): Result {
|
|
273
281
|
const step = steps.next();
|
|
274
282
|
if (step.done === true) return step.value;
|
|
275
283
|
void step.value.catch(() => undefined);
|
|
276
|
-
const waited = fail("#CALC!",
|
|
284
|
+
const waited = fail("#CALC!", waitedReason(kindText), { place });
|
|
277
285
|
steps.return(waited);
|
|
278
286
|
return waited;
|
|
279
287
|
}
|
|
@@ -390,10 +398,13 @@ function* together(env: Env, list: readonly RunPart[], options: AllOptions = {})
|
|
|
390
398
|
* a traced plan and the second tier of a language must give its results. It covers evaluations, not runs: an effect
|
|
391
399
|
* gives the answer of the mock or its payload, and a part that waits gives `#CALC!`.
|
|
392
400
|
*
|
|
401
|
+
* The verb of the options goes up the ladder at each document, as in `prepare`. Each verb has the same rules: the
|
|
402
|
+
* ladder, the contract of the kind and the sealed parts (VERB.AGREE).
|
|
403
|
+
*
|
|
393
404
|
* @param machine - The world and the code of the evaluators.
|
|
394
405
|
* @param doc - The document.
|
|
395
406
|
* @param context - The names and the capabilities at the root.
|
|
396
|
-
* @param options - The type of a bare root value, the definitions of compiled code, a list for the effects and
|
|
407
|
+
* @param options - The type of a bare root value, the definitions of compiled code, a list for the effects, a mock and the verb.
|
|
397
408
|
* @returns The result.
|
|
398
409
|
*/
|
|
399
410
|
export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
|
|
@@ -403,6 +414,7 @@ export function reference(machine: Machine, doc: unknown, context: Context = mak
|
|
|
403
414
|
effects: options.effects ?? [],
|
|
404
415
|
maxDepth: machine.maxDepth ?? 1000,
|
|
405
416
|
perform: options.perform,
|
|
417
|
+
verb: options.verb ?? "eval",
|
|
406
418
|
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
407
419
|
};
|
|
408
420
|
const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
|
package/src/schedule.ts
ADDED
|
@@ -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
|
+
}
|