@mark1russell7/coeval 0.19.0 → 0.20.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/stdlib.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Json, type Key, field, isRecord } from "./doc.ts";
1
+ import { type Json, type Key, docEquals, field, isRecord } from "./doc.ts";
2
2
  import {
3
3
  type EvalInput,
4
4
  type Evaluator,
@@ -18,7 +18,7 @@ import {
18
18
  import { Failure } from "./errors.ts";
19
19
  import { type Cap, type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
20
20
  import type { World } from "./ladder.ts";
21
- import { type AttributeDeclaration, type TreeContract, equationType, lookupKind } from "./world.ts";
21
+ import { type AttributeDeclaration, type CircularRule, type TreeContract, circularOf, equationType, lookupKind } from "./world.ts";
22
22
  import { STD_DOMAINS } from "./types.ts";
23
23
  import type { SyntaxProfile } from "./syntax.ts";
24
24
 
@@ -270,12 +270,8 @@ export interface AttributeTrace {
270
270
  }
271
271
 
272
272
  /** The state of a node behind its brand. */
273
- interface NodeState {
273
+ interface NodeState extends LoopNode {
274
274
  readonly build: TreeBuild;
275
- readonly type: string;
276
- readonly place: readonly Key[];
277
- /** The tree contract of the kind of the node (D42). */
278
- readonly tree: TreeContract | undefined;
279
275
  /**
280
276
  * This function gives an attribute through its ladder (TREE.LADDER).
281
277
  *
@@ -328,9 +324,12 @@ export interface AttributeAnswer {
328
324
  * This function gives the memo cell of one attribute of one node, for a host that keeps its cells between evaluations
329
325
  * (TREE.CELL, D108). The kit uses it at each demand of an attribute in an evaluation.
330
326
  *
331
- * The array `place` is one object for each document of a plan, and it stays the same across the evaluations of that
332
- * plan. A new plan gives new arrays. Thus a host keeps its cells by the identity of `place` and by `attribute`.
333
- * A host that keeps its cells by the keys of `place` can give a stale answer after a change of the document.
327
+ * The array `place` is one object for each node of a plan in its scope. It stays the same across the evaluations of
328
+ * that plan. The scope of a node is the node around it, or the demand that made its tree (TREE.CELL-DOCUMENT,
329
+ * D115). Thus a cached plan, for example the plan of an equation, gives a new array for the nodes of each document.
330
+ *
331
+ * A new plan gives new arrays. Thus a host keeps its cells by the identity of `place` and by `attribute`.
332
+ * A host that keeps its cells by the keys of `place` can give the answer of another node or a stale answer.
334
333
  *
335
334
  * The function gives the answer of `compute`, or a kept answer. A host with signals gives a computed signal: a change
336
335
  * of its store then makes only the attributes that read the store compute again. Each demand gives a new `compute`, and
@@ -339,7 +338,7 @@ export interface AttributeAnswer {
339
338
  * The kit marks an attribute in evaluation before it uses the cell. Thus a cycle gives `#CYCLE!` before it reaches the
340
339
  * cell again. An exception of the function that `compute` did not throw gives `#CYCLE!` with the chain of the demands.
341
340
  *
342
- * @param place - The keys from the root document to the document of the node.
341
+ * @param place - The key of the node. Its items are the keys from the root document to the document of the node.
343
342
  * @param attribute - The name of the attribute.
344
343
  * @param compute - The function that visits the rungs of the ladder.
345
344
  * @returns The answer.
@@ -428,14 +427,60 @@ const BUSY: Cell = /* @__PURE__ */ Object.freeze({ busy: true });
428
427
  const NO_ANSWER: AttributeAnswer = /* @__PURE__ */ Object.freeze({ result: undefined, by: undefined });
429
428
 
430
429
  /**
431
- * The marks of the attributes in evaluation, for each place of a plan, with the cells of a host (TREE.CELL). Nodes of
432
- * two evaluations share their place and their kept cells, thus they share their marks.
430
+ * The marks of the attributes in evaluation, for each key of a node, with the cells of a host (TREE.CELL). Nodes of
431
+ * two evaluations share their key and their kept cells, thus they share their marks. Nodes of two documents have
432
+ * different keys, thus a nested evaluation gets its own marks (TREE.CELL-NESTED, D115).
433
433
  */
434
434
  const marksByPlace = /* @__PURE__ */ new WeakMap<readonly Key[], Map<string, Cell>>();
435
435
 
436
- /** The trees of the cells of a host, for each place of a root in a plan. Nodes of two evaluations stay in one tree. */
436
+ /** The trees of the cells of a host, for each key of a root. Nodes of two evaluations stay in one tree. */
437
437
  const buildsByPlace = /* @__PURE__ */ new WeakMap<readonly Key[], TreeBuild>();
438
438
 
439
+ /** The keys of the nodes with the cells of a host, for each scope and each place of a plan (D115). */
440
+ const keysByScope = /* @__PURE__ */ new WeakMap<object, WeakMap<readonly Key[], readonly Key[]>>();
441
+
442
+ /** The scopes of the roots that a demand made, for each marks of a node and each name of an attribute (D115). */
443
+ const demandScopes = /* @__PURE__ */ new WeakMap<Map<string, Cell>, Map<string, object>>();
444
+
445
+ /**
446
+ * This function gives the key of a node with the cells of a host (TREE.CELL-DOCUMENT, D115). The key of the node
447
+ * around the node is its scope. A root without such a node has the scope of the open demand, or no scope.
448
+ * Without a scope, the key is the place of the plan. Thus a cached plan gives a new key for the nodes of each
449
+ * document, and each evaluation of a document gives the same keys.
450
+ *
451
+ * @param place - The place of the document of the node in its plan.
452
+ * @param above - The state of the node around the node, or `undefined`.
453
+ * @returns The key: an array with the keys of `place`.
454
+ */
455
+ function cellPlace(place: readonly Key[], above: NodeState | undefined): readonly Key[] {
456
+ let scope: object | undefined = above?.place;
457
+ if (scope === undefined) {
458
+ const demand = demands[demands.length - 1];
459
+ if (demand === undefined) return place;
460
+ let byName = demandScopes.get(demand.cells);
461
+ if (byName === undefined) {
462
+ byName = new Map();
463
+ demandScopes.set(demand.cells, byName);
464
+ }
465
+ scope = byName.get(demand.key);
466
+ if (scope === undefined) {
467
+ scope = {};
468
+ byName.set(demand.key, scope);
469
+ }
470
+ }
471
+ let keys = keysByScope.get(scope);
472
+ if (keys === undefined) {
473
+ keys = new WeakMap();
474
+ keysByScope.set(scope, keys);
475
+ }
476
+ let key = keys.get(place);
477
+ if (key === undefined) {
478
+ key = Object.freeze([...place]);
479
+ keys.set(place, key);
480
+ }
481
+ return key;
482
+ }
483
+
439
484
  /** This function gives the value of a weak map, or puts a new value in it. */
440
485
  function keptAt<T>(map: WeakMap<readonly Key[], T>, place: readonly Key[], make: () => T): T {
441
486
  let value = map.get(place);
@@ -459,6 +504,293 @@ interface Demand {
459
504
  /** The attributes in evaluation, in the order of their demands. An evaluation does not wait, thus one list serves. */
460
505
  const demands: Demand[] = [];
461
506
 
507
+ /** One value of an attribute in an open fixed point (D113). */
508
+ interface LoopValue {
509
+ readonly result: Result | undefined;
510
+ readonly by: AttributeRung | undefined;
511
+ /** The round of an approximation. The round `0` marks a final answer of a node with the cells of a host. */
512
+ readonly round: number;
513
+ /** The plain memo of the node. A stable fixed point writes its final value there. A node with host cells has none. */
514
+ readonly memo: Map<string, Cell> | undefined;
515
+ }
516
+
517
+ /**
518
+ * A fixed point in evaluation (TREE.CIRCULAR, D113). The circular attribute that a demand reaches first leads it. Each
519
+ * circular attribute that a demand reaches during its rounds joins it, thus only one fixed point is open at a time.
520
+ */
521
+ interface FixedPoint {
522
+ /** The index of the demand of the leader in `demands`. */
523
+ readonly depth: number;
524
+ round: number;
525
+ /** `true` when a circular attribute got a new value in this round. */
526
+ changed: boolean;
527
+ /** The values of the fixed point, by the marks of a node and by the name of the attribute. */
528
+ readonly values: Map<Map<string, Cell>, Map<string, LoopValue>>;
529
+ /** The demands of circular attributes, from the leader up. */
530
+ readonly circular: Set<Demand>;
531
+ /** The demands above the leader that read an approximation. Their results are not final. */
532
+ readonly tentative: Set<Demand>;
533
+ }
534
+
535
+ /** The open fixed point, or `undefined`. */
536
+ let openPoint: FixedPoint | undefined;
537
+
538
+ /**
539
+ * What the code of a fixed point needs of a node (D113). The state of each node holds it, thus the functions of a
540
+ * fixed point are module functions, and a node makes no closure for them.
541
+ */
542
+ interface LoopNode {
543
+ readonly type: string;
544
+ readonly place: readonly Key[];
545
+ /** The tree contract of the kind of the node (D42). */
546
+ readonly tree: TreeContract | undefined;
547
+ /** The marks of the attributes in evaluation. Without the cells of a host, they are the plain memo. */
548
+ readonly marks: Map<string, Cell>;
549
+ /** The plain memo of the node. With the cells of a host, it keeps the last answer only for `has` and `trace`. */
550
+ readonly cells: Map<string, Cell>;
551
+ readonly host: AttributeCell | undefined;
552
+ /** This function visits the rungs of the ladder of an attribute in their order, until one answers (TREE.LADDER). */
553
+ readonly climb: (key: string) => AttributeAnswer | undefined;
554
+ }
555
+
556
+ /** For each table `tree.attributes`, `true` when an attribute of it declares `circular`. */
557
+ const circularTables = /* @__PURE__ */ new WeakMap<object, boolean>();
558
+
559
+ /** For each declaration `circular` that is an object, its rule. */
560
+ const circularRules = /* @__PURE__ */ new WeakMap<object, CircularRule>();
561
+
562
+ /** This function tells if a tree contract declares a circular attribute. The answer for a table is computed one time. */
563
+ function hasCircular(tree: TreeContract | undefined): boolean {
564
+ const table = tree?.attributes;
565
+ if (table === undefined) return false;
566
+ let known = circularTables.get(table);
567
+ if (known === undefined) {
568
+ known = Object.keys(table).some((key) => {
569
+ const declared: unknown = entryOf(table, key);
570
+ return typeof declared === "object" && declared !== null && (declared as AttributeDeclaration).circular !== undefined;
571
+ });
572
+ circularTables.set(table, known);
573
+ }
574
+ return known;
575
+ }
576
+
577
+ /** This function gives the declaration `circular` of an attribute of a tree contract (D113), or `undefined`. */
578
+ function circularRule(tree: TreeContract | undefined, key: string): CircularRule | undefined {
579
+ const declared: unknown = entryOf(tree?.attributes, key)?.circular;
580
+ if (declared === undefined) return undefined;
581
+ if (typeof declared !== "object" || declared === null) return circularOf(declared);
582
+ let rule = circularRules.get(declared);
583
+ if (rule === undefined) {
584
+ rule = circularOf(declared);
585
+ circularRules.set(declared, rule);
586
+ }
587
+ return rule;
588
+ }
589
+
590
+ /** This function gives the values of a fixed point for the marks of a node. */
591
+ function loopValues(loop: FixedPoint, marks: Map<string, Cell>): Map<string, LoopValue> {
592
+ let values = loop.values.get(marks);
593
+ if (values === undefined) {
594
+ values = new Map();
595
+ loop.values.set(marks, values);
596
+ }
597
+ return values;
598
+ }
599
+
600
+ /** This function marks each demand above the leader: each one read an approximation, thus its result is not final. */
601
+ function markTentative(loop: FixedPoint): void {
602
+ for (let index = loop.depth + 1; index < demands.length; index += 1) loop.tentative.add(demands[index] as Demand);
603
+ }
604
+
605
+ /** This function tells if two approximations are the same: two values by DOC.EQUALS, two errors by code and reason. */
606
+ function sameApproximation(a: Result | undefined, b: Result | undefined): boolean {
607
+ if (a === undefined || b === undefined) return a === b;
608
+ if (a.ok && b.ok) return docEquals(a.value, b.value);
609
+ if (!a.ok && !b.ok) return a.error.code === b.error.code && a.error.reason === b.error.reason;
610
+ return false;
611
+ }
612
+
613
+ /** This function writes the final value of each circular attribute of the last round in the plain memo of its node. */
614
+ function settle(loop: FixedPoint): void {
615
+ for (const values of loop.values.values()) {
616
+ for (const [key, value] of values) {
617
+ if (value.memo !== undefined && value.round === loop.round) value.memo.set(key, { busy: false, result: value.result, by: value.by });
618
+ }
619
+ }
620
+ }
621
+
622
+ /** The text of the fixed point of a circular attribute that does not become stable (TREE.CIRCULAR-LIMIT). */
623
+ function limitReason(text: string, limit: number): string {
624
+ return `The cycle of the circular attribute ${text} does not become stable within its limit of ${limit} ${limit === 1 ? "round" : "rounds"}.`;
625
+ }
626
+
627
+ /**
628
+ * This function answers a demand that reached an attribute in evaluation in an open fixed point (TREE.CYCLE). A chain
629
+ * of circular attributes gives the current approximation (TREE.CIRCULAR). Each other chain gives `#CYCLE!`.
630
+ */
631
+ function reachedIn(loop: FixedPoint, n: LoopNode, key: string): Result | undefined {
632
+ const first = demands.findIndex((demand) => demand.cells === n.marks && demand.key === key);
633
+ let circular = first >= loop.depth;
634
+ for (let index = first; circular && index < demands.length; index += 1) circular = loop.circular.has(demands[index] as Demand);
635
+ if (!circular) return cycleAt(n.marks, key, attributeText(key, n.type, n.place));
636
+ const rule = circularRule(n.tree, key);
637
+ const kept = loop.values.get(n.marks)?.get(key);
638
+ markTentative(loop);
639
+ return kept !== undefined ? kept.result : ok(rule?.ok === true ? rule.start : null);
640
+ }
641
+
642
+ /**
643
+ * This function evaluates the leader of a fixed point (TREE.CIRCULAR, D113). Its demand is the last one. Each round
644
+ * visits the ladder again, and the rounds end when no circular attribute gets a new value. An error ends the rounds
645
+ * at once. After the last round of the limit, the leader gives `#CYCLE!` at the place of its node.
646
+ */
647
+ function roundsOf(n: LoopNode, key: string, rule: CircularRule & { readonly ok: true }): AttributeAnswer {
648
+ const leader = demands[demands.length - 1] as Demand;
649
+ const loop: FixedPoint = { depth: demands.length - 1, round: 0, changed: false, values: new Map(), circular: new Set([leader]), tentative: new Set() };
650
+ openPoint = loop;
651
+ try {
652
+ let before: Result | undefined = ok(rule.start);
653
+ for (;;) {
654
+ loop.round += 1;
655
+ loop.changed = false;
656
+ const answer = n.climb(key) ?? NO_ANSWER;
657
+ // An error is not a value of the fixed point. The other attributes of the cycle evaluate again at their next demand.
658
+ if (answer.result !== undefined && !answer.result.ok) return answer;
659
+ if (!sameApproximation(before, answer.result)) loop.changed = true;
660
+ before = answer.result;
661
+ loopValues(loop, n.marks).set(key, { result: answer.result, by: answer.by, round: loop.round, memo: undefined });
662
+ if (!loop.changed) {
663
+ settle(loop);
664
+ return answer;
665
+ }
666
+ if (loop.round >= rule.limit) return { result: fail("#CYCLE!", limitReason(attributeText(key, n.type, n.place), rule.limit), { place: n.place }), by: answer.by };
667
+ }
668
+ } finally {
669
+ openPoint = undefined;
670
+ }
671
+ }
672
+
673
+ /**
674
+ * This function evaluates a circular attribute in the open fixed point of another leader, one time in each round
675
+ * (D113). A later demand in the same round gets the value of the round. Each value is an approximation.
676
+ */
677
+ function roundIn(loop: FixedPoint, n: LoopNode, key: string, rule: CircularRule & { readonly ok: true }, kept: LoopValue | undefined): Result | undefined {
678
+ let value = kept;
679
+ if (value === undefined || value.round !== loop.round) {
680
+ const demand: Demand = { cells: n.marks, key, text: attributeText(key, n.type, n.place) };
681
+ n.marks.set(key, BUSY);
682
+ demands.push(demand);
683
+ loop.circular.add(demand);
684
+ let answer: AttributeAnswer;
685
+ try {
686
+ answer = n.climb(key) ?? NO_ANSWER;
687
+ } finally {
688
+ demands.pop();
689
+ n.marks.delete(key);
690
+ }
691
+ if (!sameApproximation(value === undefined ? ok(rule.start) : value.result, answer.result)) loop.changed = true;
692
+ value = { result: answer.result, by: answer.by, round: loop.round, memo: n.host === undefined ? n.cells : undefined };
693
+ loopValues(loop, n.marks).set(key, value);
694
+ }
695
+ markTentative(loop);
696
+ return value.result;
697
+ }
698
+
699
+ /**
700
+ * This function evaluates an attribute without the cell of the host: with the plain memo, or in an open fixed point
701
+ * (D113). A result that read an approximation is not final, thus no memo keeps it.
702
+ */
703
+ function localIn(n: LoopNode, key: string, rule: (CircularRule & { readonly ok: true }) | undefined): Result | undefined {
704
+ const demand: Demand = { cells: n.marks, key, text: attributeText(key, n.type, n.place) };
705
+ const loop = openPoint;
706
+ n.marks.set(key, BUSY);
707
+ demands.push(demand);
708
+ let answer: AttributeAnswer;
709
+ try {
710
+ answer = rule === undefined ? (n.climb(key) ?? NO_ANSWER) : roundsOf(n, key, rule);
711
+ } catch (error) {
712
+ // An exception of the host, for example an overflow of the stack, leaves the attribute without a result.
713
+ n.marks.delete(key);
714
+ throw error;
715
+ } finally {
716
+ demands.pop();
717
+ }
718
+ if (loop !== undefined && loop.tentative.has(demand)) {
719
+ n.marks.delete(key);
720
+ return answer.result;
721
+ }
722
+ if (n.host !== undefined) {
723
+ // The fixed point keeps a final answer of a node with host cells until it ends (D113).
724
+ n.marks.delete(key);
725
+ if (loop !== undefined) loopValues(loop, n.marks).set(key, { result: answer.result, by: answer.by, round: 0, memo: undefined });
726
+ }
727
+ n.cells.set(key, { busy: false, result: answer.result, by: answer.by });
728
+ return answer.result;
729
+ }
730
+
731
+ /**
732
+ * This function answers a demand that reached an attribute in evaluation (TREE.CYCLE). Outside a fixed point, it gives
733
+ * `#CYCLE!` with the chain. In an open fixed point, `reachedIn` answers (TREE.CIRCULAR).
734
+ */
735
+ function busyAt(n: LoopNode, key: string): Result | undefined {
736
+ const loop = openPoint;
737
+ return loop === undefined ? cycleAt(n.marks, key, attributeText(key, n.type, n.place)) : reachedIn(loop, n, key);
738
+ }
739
+
740
+ /**
741
+ * This function evaluates the leader of a fixed point through the cell of the host (TREE.CELL, D113). The cell of the
742
+ * leader gets the final value of the fixed point, from a computation that contains each round.
743
+ */
744
+ function leaderOnHost(n: LoopNode, key: string, rule: CircularRule & { readonly ok: true }, host: AttributeCell): Result | undefined {
745
+ const text = attributeText(key, n.type, n.place);
746
+ n.marks.set(key, BUSY);
747
+ demands.push({ cells: n.marks, key, text });
748
+ let thrown: unknown = NOT_THROWN;
749
+ let answer: AttributeAnswer;
750
+ try {
751
+ answer = host(n.place, key, () => {
752
+ try {
753
+ return roundsOf(n, key, rule);
754
+ } catch (error) {
755
+ thrown = error;
756
+ throw error;
757
+ }
758
+ });
759
+ } catch (error) {
760
+ // An exception of the kit or of the stack goes on, as without a host. Another exception is a cycle of the host.
761
+ if (error === thrown) throw error;
762
+ const chain = demands.map((demand) => demand.text).join(" -> ");
763
+ answer = { result: fail("#CYCLE!", `The cells of the host found a cycle in the demands: ${chain}.`, { cause: error }), by: undefined };
764
+ } finally {
765
+ demands.pop();
766
+ n.marks.delete(key);
767
+ }
768
+ n.cells.set(key, { busy: false, result: answer.result, by: answer.by });
769
+ return answer.result;
770
+ }
771
+
772
+ /**
773
+ * This function gives an attribute of a node with circular attributes, or an attribute in an open fixed point (D113).
774
+ * The memo and the marks of the node do not hold the attribute. An open fixed point does not use the cells of the host.
775
+ */
776
+ function circularRead(n: LoopNode, key: string): Result | undefined {
777
+ const rule = circularRule(n.tree, key);
778
+ if (rule !== undefined && !rule.ok) {
779
+ const result = fail("#VALUE!", `The attribute ${attributeText(key, n.type, n.place)} has a bad declaration circular. ${rule.reason}`);
780
+ n.cells.set(key, { busy: false, result, by: undefined });
781
+ return result;
782
+ }
783
+ const loop = openPoint;
784
+ if (loop !== undefined) {
785
+ const kept = loop.values.get(n.marks)?.get(key);
786
+ if (rule !== undefined) return roundIn(loop, n, key, rule, kept);
787
+ if (kept !== undefined) return kept.result;
788
+ return localIn(n, key, undefined);
789
+ }
790
+ if (rule === undefined || n.host === undefined) return localIn(n, key, rule);
791
+ return leaderOnHost(n, key, rule, n.host);
792
+ }
793
+
462
794
  function attributeText(key: string, type: string, place: readonly Key[]): string {
463
795
  return `${key} of ${type} at [${place.join(", ")}]`;
464
796
  }
@@ -528,10 +860,11 @@ function plainOf(value: unknown, path: Set<object>): Result {
528
860
  * fold or a default, come last. A failed attribute throws `Failure` with its own error, thus a reference to it gives that error.
529
861
  * The argument `outer` is the self of the context, that `treeNode` resolved. The node reads no root of the context directly (C4).
530
862
  *
531
- * The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of the node.
863
+ * The argument `place` is the key of the node. With the cells of a host, `cellPlace` gives it. Else it is the place of
864
+ * the plan. The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of the node.
532
865
  */
533
- function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined, host: AttributeCell | undefined): HostThing {
534
- const { parts, context, kind: type, place, machine } = input;
866
+ function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, outer: Thing | undefined, host: AttributeCell | undefined): HostThing {
867
+ const { parts, context, kind: type, machine } = input;
535
868
  // The equations evaluate in the evaluation that made the node, thus their reads go in its list (READS.EQUATION).
536
869
  const runtime = (input as InputWithRuntime)[RUNTIME];
537
870
  const { tree, of } = contractOf(machine, input.typePath);
@@ -539,8 +872,10 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
539
872
  // Only a node of the same tree is a parent for the rungs `parent` and `copy`.
540
873
  const up = above !== undefined && above.build === build ? above : undefined;
541
874
  const cells = new Map<string, Cell>();
542
- // With the cells of a host, the marks of a place serve each node of the place (TREE.CELL).
875
+ // With the cells of a host, the marks of a key serve each node of the key (TREE.CELL).
543
876
  const marks = host === undefined ? cells : keptAt(marksByPlace, place, () => new Map<string, Cell>());
877
+ // Only a node whose kind declares a circular attribute takes the path of D113 outside a fixed point.
878
+ const circular = hasCircular(tree);
544
879
  /** The positions of the items of the list `children`, for the names of the children. */
545
880
  let positions: { readonly list: readonly unknown[]; readonly at: Map<unknown, number> } | undefined;
546
881
 
@@ -594,8 +929,9 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
594
929
  ? undefined
595
930
  : (key: string): Result | undefined => {
596
931
  const text = attributeText(key, type, place);
597
- if (marks.has(key)) return cycleAt(marks, key, text);
932
+ if (marks.has(key)) return busyAt(state, key);
598
933
  if (tree === undefined && up?.tree === undefined && !parts.some((item) => item.key === key)) return undefined;
934
+ if (circular || openPoint !== undefined) return circularRead(state, key);
599
935
  marks.set(key, BUSY);
600
936
  demands.push({ cells: marks, key, text });
601
937
  let thrown: unknown = NOT_THROWN;
@@ -625,9 +961,11 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
625
961
  const read = (key: string): Result | undefined => {
626
962
  if (through !== undefined) return through(key);
627
963
  const cell = cells.get(key);
628
- if (cell !== undefined) return cell.busy ? cycleAt(cells, key, attributeText(key, type, place)) : cell.result;
964
+ if (cell !== undefined) return cell.busy ? busyAt(state, key) : cell.result;
629
965
  // A node without a contract has only its parts, as before D42. Thus a missing part needs no cell.
630
966
  if (tree === undefined && up?.tree === undefined && !parts.some((item) => item.key === key)) return undefined;
967
+ // A circular attribute and each demand in an open fixed point take the code of D113 (TREE.CIRCULAR).
968
+ if (circular || openPoint !== undefined) return circularRead(state, key);
631
969
  cells.set(key, BUSY);
632
970
  demands.push({ cells, key, text: attributeText(key, type, place) });
633
971
  let answer: { readonly result: Result; readonly by: AttributeRung } | undefined;
@@ -649,6 +987,10 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
649
987
  type,
650
988
  place,
651
989
  tree,
990
+ marks,
991
+ cells,
992
+ host,
993
+ climb,
652
994
  read,
653
995
  equation(term, names) {
654
996
  if (machine === undefined) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
@@ -759,11 +1101,13 @@ function treeNode(input: EvalInput, host: AttributeCell | undefined): Result {
759
1101
  const found = input.resolve(SELF_PATH, NO_HOPS);
760
1102
  const outer = found.ok ? found.thing : undefined;
761
1103
  const above = stateOf(outer);
762
- if (above !== undefined && above.build.open) return ok(treeThing(input, above.build, outer, host));
1104
+ // With the cells of a host, the key of a node holds its scope (D115). Without them, the place of the plan serves.
1105
+ const place = host === undefined ? input.place : cellPlace(input.place, above);
1106
+ if (above !== undefined && above.build.open) return ok(treeThing(input, place, above.build, outer, host));
763
1107
  // With the cells of a host, a kept value can hold a node of an earlier evaluation. Thus that node stays in the tree.
764
- const build: TreeBuild = host === undefined ? { open: true } : keptAt(buildsByPlace, input.place, () => ({ open: true }));
1108
+ const build: TreeBuild = host === undefined ? { open: true } : keptAt(buildsByPlace, place, () => ({ open: true }));
765
1109
  build.open = true;
766
- const root = treeThing(input, build, outer, host);
1110
+ const root = treeThing(input, place, build, outer, host);
767
1111
  try {
768
1112
  return (stateOf(root) as NodeState).json(new Set());
769
1113
  } finally {
package/src/world.ts CHANGED
@@ -134,11 +134,70 @@ export interface KindFacts {
134
134
  * Then the term `combine` adds the attribute of each node in the list `children`, one at a time (TREE.FOLD).
135
135
  * `combine` sees the names `acc` and `item`.
136
136
  * - `default`: the term that gives the value when no rung before it answers and the declaration has no fold.
137
+ * - `circular`: a cycle of this attribute gives its fixed point, not `#CYCLE!` (TREE.CIRCULAR, D113). Only a cycle
138
+ * whose attributes each declare `circular` gives a fixed point.
137
139
  */
138
140
  export interface AttributeDeclaration {
139
141
  readonly copy?: boolean;
140
142
  readonly fold?: { readonly unit: Json; readonly combine: Json };
141
143
  readonly default?: Json;
144
+ readonly circular?: CircularDeclaration;
145
+ }
146
+
147
+ /**
148
+ * The declaration `circular` of an attribute (TREE.CIRCULAR, D113). The attribute that a cycle reaches first leads it.
149
+ * The leader evaluates again until no value of the cycle changes.
150
+ *
151
+ * - `start`: the value of the attribute before its first round, as JSON data. The kit does not evaluate it.
152
+ * - `limit`: the most rounds of the leader, a whole number of 1 or more. The default is `CIRCULAR_LIMIT`. A cycle that
153
+ * is not stable after the last round gives `#CYCLE!` (TREE.CIRCULAR-LIMIT).
154
+ */
155
+ export interface CircularDeclaration {
156
+ readonly start: Json;
157
+ readonly limit?: number;
158
+ }
159
+
160
+ /** The most rounds of a circular attribute without its own `limit` (TREE.CIRCULAR-LIMIT, D113). */
161
+ export const CIRCULAR_LIMIT = 100;
162
+
163
+ /** A declaration `circular` that the kit can use: its start value and its limit, or the reason of a bad declaration. */
164
+ export type CircularRule = { readonly ok: true; readonly start: Json; readonly limit: number } | { readonly ok: false; readonly reason: string };
165
+
166
+ /** This function tells if a value is JSON data: `null`, a truth value, a finite number, a text, or a container of them. */
167
+ function isJsonData(value: unknown): boolean {
168
+ const stack: unknown[] = [value];
169
+ const seen = new Set<object>();
170
+ while (stack.length > 0) {
171
+ const item = stack.pop();
172
+ if (item === null || typeof item === "string" || typeof item === "boolean") continue;
173
+ if (typeof item === "number") {
174
+ if (!Number.isFinite(item)) return false;
175
+ continue;
176
+ }
177
+ if (!Array.isArray(item) && !isRecord(item)) return false;
178
+ if (seen.has(item)) continue;
179
+ seen.add(item);
180
+ for (const inner of Object.values(item)) stack.push(inner);
181
+ }
182
+ return true;
183
+ }
184
+
185
+ /**
186
+ * This function reads the declaration `circular` of an attribute (D113). The tree kit, the reference, `check` and the
187
+ * graph of attribute instances read it with this one function.
188
+ *
189
+ * @param declared - The value of the field `circular`.
190
+ * @returns The start value and the limit, or the reason when the declaration has no correct form.
191
+ */
192
+ export function circularOf(declared: unknown): CircularRule {
193
+ if (!isRecord(declared) || !Object.hasOwn(declared, "start")) return { ok: false, reason: "The declaration circular needs a record with the field start." };
194
+ const start = field(declared, "start");
195
+ if (!isJsonData(start)) return { ok: false, reason: "The start value of the declaration circular must be JSON data." };
196
+ const limit: unknown = Object.hasOwn(declared, "limit") ? field(declared, "limit") : CIRCULAR_LIMIT;
197
+ if (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1) {
198
+ return { ok: false, reason: "The limit of the declaration circular must be a whole number of 1 or more." };
199
+ }
200
+ return { ok: true, start: start as Json, limit };
142
201
  }
143
202
 
144
203
  /**