@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/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 { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, atArgsOf, atRuleOf, circularOf, collectOf, equationType, lookupKind } from "./world.ts";
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
- return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
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 each node in the current data epoch: the context of the host, with the nodes at the root `world`. */
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 state of one data epoch: the context of the nodes, and the value of each node that a demand reached. */
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`, and the context of the host.
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 === 0) {
276
- for (const first of deepestFirst(id, edgesFor)) {
277
- ahead += 1;
278
- visit(first);
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 visit(id);
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
- close(from);
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 = demand(id);
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
- const nodesThing: HostThing = {
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 }),