@mark1russell7/coeval 0.22.0 → 0.24.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/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 }),
package/src/static.ts CHANGED
@@ -4,7 +4,7 @@ import { answerOf, ask } from "./ladder.ts";
4
4
  import { type PartSlot, partsOf, rulesCover, scopesOf } from "./parts.ts";
5
5
  import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
6
6
  import type { ErrorCode } from "./errors.ts";
7
- import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
7
+ import { type Machine, partCaps, partGrants, permits, waitedReason } from "./evaluate.ts";
8
8
  import type { ValueType } from "./types.ts";
9
9
  import { type RootTypes, type TypeNode, type Typed, machineTypeIssues, typePass } from "./typing.ts";
10
10
  import { cycleIssues } from "./instances.ts";
@@ -41,6 +41,8 @@ export interface DocVisit {
41
41
  * around it (`Kind.caps`). A walk without capabilities gives none here.
42
42
  */
43
43
  readonly caps?: readonly Cap[] | undefined;
44
+ /** The kinds of effects that a handler around the document answers (CAP.GRANT, D146). Such an effect does not reach the host. */
45
+ readonly handled?: readonly Cap[] | undefined;
44
46
  }
45
47
 
46
48
  /** A position that a walk cannot read as a document. */
@@ -103,6 +105,18 @@ function keysOf(trail: Trail | undefined): readonly Key[] {
103
105
 
104
106
  /** A visit with its paths as trails. It makes the keys of a path only when a reader asks for them. */
105
107
  class Visit implements DocVisit {
108
+ readonly doc: Envelope;
109
+ readonly declared: boolean;
110
+ readonly depth: number;
111
+ readonly names: ReadonlySet<string>;
112
+ readonly later: ReadonlySet<string>;
113
+ readonly slot: readonly Key[];
114
+ readonly parent: DocVisit | undefined;
115
+ readonly kind: Kind | undefined;
116
+ readonly typePath: readonly Key[] | undefined;
117
+ readonly address: true | undefined;
118
+ readonly caps: readonly Cap[] | undefined;
119
+ readonly handled: readonly Cap[] | undefined;
106
120
  readonly #place: Trail | undefined;
107
121
  readonly #at: Trail | undefined;
108
122
  readonly #bound: readonly string[];
@@ -111,20 +125,33 @@ class Visit implements DocVisit {
111
125
  constructor(
112
126
  place: Trail | undefined,
113
127
  at: Trail | undefined,
114
- readonly doc: Envelope,
115
- readonly declared: boolean,
116
- readonly depth: number,
117
- readonly names: ReadonlySet<string>,
118
- readonly later: ReadonlySet<string>,
119
- readonly slot: readonly Key[],
120
- readonly parent: DocVisit | undefined,
121
- readonly kind: Kind | undefined,
122
- readonly typePath: readonly Key[] | undefined,
123
- readonly address: true | undefined,
124
- readonly caps: readonly Cap[] | undefined,
128
+ doc: Envelope,
129
+ declared: boolean,
130
+ depth: number,
131
+ names: ReadonlySet<string>,
132
+ later: ReadonlySet<string>,
133
+ slot: readonly Key[],
134
+ parent: DocVisit | undefined,
135
+ kind: Kind | undefined,
136
+ typePath: readonly Key[] | undefined,
137
+ address: true | undefined,
138
+ caps: readonly Cap[] | undefined,
125
139
  bound: readonly string[],
126
140
  expect: ValueType | undefined,
141
+ handled: readonly Cap[] | undefined,
127
142
  ) {
143
+ this.doc = doc;
144
+ this.declared = declared;
145
+ this.depth = depth;
146
+ this.names = names;
147
+ this.later = later;
148
+ this.slot = slot;
149
+ this.parent = parent;
150
+ this.kind = kind;
151
+ this.typePath = typePath;
152
+ this.address = address;
153
+ this.caps = caps;
154
+ this.handled = handled;
128
155
  this.#place = place;
129
156
  this.#at = at;
130
157
  this.#bound = bound;
@@ -177,6 +204,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
177
204
  readonly caps: readonly Cap[] | undefined;
178
205
  readonly bound: readonly string[];
179
206
  readonly expect: ValueType | undefined;
207
+ readonly handled: readonly Cap[] | undefined;
180
208
  }
181
209
  const stack: Item[] = [
182
210
  {
@@ -191,6 +219,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
191
219
  slot: [],
192
220
  parent: undefined,
193
221
  caps: options.caps,
222
+ handled: undefined,
194
223
  bound: [],
195
224
  expect: undefined,
196
225
  },
@@ -219,6 +248,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
219
248
  item.caps,
220
249
  item.bound,
221
250
  item.expect,
251
+ item.handled,
222
252
  );
223
253
  const answer = visit(here);
224
254
  if (answer === "skip" || kind === undefined) continue;
@@ -232,6 +262,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
232
262
  const slot = split.parts[index] as PartSlot;
233
263
  const extra = sees[index] ?? [];
234
264
  const hidden = later[index] ?? [];
265
+ const grants = partGrants(kind.caps, slot.path, doc.value);
235
266
  stack.push({
236
267
  value: slot.item,
237
268
  place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
@@ -243,7 +274,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
243
274
  address: slot.address === true,
244
275
  slot: slot.path,
245
276
  parent: here,
246
- caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
277
+ caps: item.caps === undefined || kind.caps === undefined ? item.caps : [...partCaps(kind.caps, slot.path, item.caps), ...grants],
278
+ handled: grants.length === 0 ? item.handled : [...(item.handled ?? []), ...grants],
247
279
  bound: extra,
248
280
  expect: slot.expect,
249
281
  });
@@ -691,6 +723,8 @@ export function equationsOf(kind: Kind): readonly Equation[] {
691
723
  out.push({ label: `tree.attributes.${name}.fold.combine`, term: declared.fold.combine, names: FOLD_NAMES, attribute: name, down: false });
692
724
  }
693
725
  if (declared.default !== undefined) out.push({ label: `tree.attributes.${name}.default`, term: declared.default, names: [], attribute: name, down: false });
726
+ // Only the equation is known before the run. The document that it gives is not known (TREE.HIGHER-CHECK, D126).
727
+ if (declared.higher !== undefined) out.push({ label: `tree.attributes.${name}.higher`, term: declared.higher, names: [], attribute: name, down: false });
694
728
  const collect = declared.collect === undefined ? undefined : collectOf(declared.collect);
695
729
  if (collect?.ok === true && !collect.names) {
696
730
  out.push({ label: `tree.attributes.${name}.collect.unit`, term: collect.unit, names: [], attribute: name, down: false });
@@ -746,6 +780,7 @@ const KEEPS_SELF: ReadonlySet<string> = /* @__PURE__ */ new Set([
746
780
  "control.seq",
747
781
  "control.pure",
748
782
  "control.if",
783
+ "control.handle",
749
784
  "list.all",
750
785
  "effect.log",
751
786
  "effect.write",
@@ -885,15 +920,16 @@ function hasEntry(table: Readonly<Record<string, unknown>> | undefined, name: st
885
920
  /**
886
921
  * This function tells if a rung of the ladder can answer an attribute of a node (TREE.LADDER). An unknown parent can
887
922
  * be a node of each kind of the world, thus each name of `down` in the world can answer. A copy can always answer,
888
- * because `check` does not know a node without a parent.
923
+ * because `check` does not know a node without a parent. A kind with `forwards` can answer each other name, because the
924
+ * forwarded node is not known before the run (TREE.FORWARD-CHECK).
889
925
  */
890
926
  export function answered(world: World, node: SeenNode, name: string): boolean {
891
927
  if (node.own?.has(name) === true) return true;
892
928
  const tree = node.kind.tree;
893
- if (hasEntry(tree?.equations, name)) return true;
929
+ if (hasEntry(tree?.equations, name) || (tree?.forwards !== undefined && tree.forwards !== name)) return true;
894
930
  if (node.parent === undefined ? downNames(world).has(name) : hasEntry(node.parent.kind.tree?.down, name)) return true;
895
931
  const declared = tree?.attributes !== undefined && Object.hasOwn(tree.attributes, name) ? tree.attributes[name] : undefined;
896
- return declared !== undefined && (declared.copy === true || declared.fold !== undefined || declared.collect !== undefined || declared.default !== undefined);
932
+ return declared !== undefined && (declared.copy === true || declared.fold !== undefined || declared.collect !== undefined || declared.default !== undefined || declared.higher !== undefined);
897
933
  }
898
934
 
899
935
  /**
@@ -908,6 +944,7 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
908
944
  const needs: Need[] = [];
909
945
  state.needs.set(kind, needs);
910
946
  for (const name of Object.keys(kind.tree?.attributes ?? {})) needs.push({ name, why: "the kind declares it", hidden: undefined });
947
+ if (typeof kind.tree?.forwards === "string" && !Object.hasOwn(STRUCTURAL, kind.tree.forwards)) needs.push({ name: kind.tree.forwards, why: "the kind forwards to it", hidden: undefined });
911
948
  const self: SeenNode = { kind, text: kindText, own: undefined, parent: undefined };
912
949
  for (const { label, term, names, attribute, down, contributes, at } of equationsOf(kind)) {
913
950
  const inner: CheckOptions = { names, ...(of === undefined ? {} : { declaredType: of }), ...(options.caps === undefined ? {} : { caps: [] }) };
@@ -926,6 +963,7 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
926
963
  * structural step gets no read. A tree kind of the run side contradicts the evaluation on demand of its attributes. A
927
964
  * declaration `circular` without a start value or with a bad limit gives `#VALUE!` (TREE.CIRCULAR-CHECK). A declaration
928
965
  * `collect` without its form gives `#VALUE!` too (TREE.COLLECT). So does an entry of `at` without its form (TREE.AT-CHECK).
966
+ * A field `forwards` that is not a text gives `#VALUE!`, and a structural name gives `#NAME?` (TREE.FORWARD-CHECK).
929
967
  */
930
968
  function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
931
969
  const out: Issue[] = [];
@@ -947,6 +985,11 @@ function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
947
985
  const rule = atRuleOf(declared);
948
986
  if (!rule.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad entry tree.at.${name}. ${rule.reason}` });
949
987
  }
988
+ const forwards: unknown = tree?.forwards;
989
+ if (forwards !== undefined && typeof forwards !== "string") out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad field tree.forwards. It must be a text.` });
990
+ if (typeof forwards === "string" && Object.hasOwn(STRUCTURAL, forwards)) {
991
+ out.push({ code: "#NAME?", place: [], reason: `The kind ${kindText} gives the reserved attribute tree.forwards: ${String(STRUCTURAL[forwards])}.` });
992
+ }
950
993
  if (kind.side === "run") {
951
994
  out.push({ code: "#CALC!", place: [], reason: `The tree kind ${kindText} is on the run side: a node evaluates its attributes on demand, thus in no fixed order.` });
952
995
  }