@mark1russell7/coeval 0.19.0 → 0.20.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.19.0",
4
+ "version": "0.20.1",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
@@ -36,6 +36,8 @@
36
36
  ],
37
37
  "devDependencies": {
38
38
  "@mark1russell7/cue": "^1.0.2",
39
+ "@mark1russell7/rigor-bench": "^0.2.1",
40
+ "@mark1russell7/rigor-statistics": "^0.1.5",
39
41
  "@types/node": "^26.6.4",
40
42
  "esbuild": "^0.28.2",
41
43
  "typescript": "^7.0.2",
package/src/index.ts CHANGED
@@ -38,6 +38,8 @@ export {
38
38
  type Alias,
39
39
  type AttributeDeclaration,
40
40
  type CapsRule,
41
+ type CircularDeclaration,
42
+ type CircularRule,
41
43
  type Kind,
42
44
  type KindFacts,
43
45
  type LazyRule,
@@ -52,6 +54,8 @@ export {
52
54
  type VerbEffects,
53
55
  type World,
54
56
  type WorldEntry,
57
+ CIRCULAR_LIMIT,
58
+ circularOf,
55
59
  effectsOfVerb,
56
60
  entriesAsJson,
57
61
  equationType,
@@ -88,6 +92,7 @@ export {
88
92
  kindNamed,
89
93
  kindOf,
90
94
  } from "./ladder.ts";
95
+ export { type StepKey, type StepLadder, type StepNeed, type StepReason, type StepRefusal, type StepWalk, checkStep, isStep, stepLadder, stepOf, stepRank } from "./steps.ts";
91
96
  export {
92
97
  type Cap,
93
98
  type Context,
package/src/instances.ts CHANGED
@@ -37,6 +37,8 @@ export interface AttributeVertex {
37
37
  readonly attribute: string;
38
38
  /** The text of the type of the node. */
39
39
  readonly type: string;
40
+ /** `true` when the kind of the node declares the attribute `circular` (TREE.CIRCULAR-CHECK). Else the field is absent. */
41
+ readonly circular?: true;
40
42
  }
41
43
 
42
44
  /** One read of an attribute by the code of another attribute (D107). */
@@ -57,6 +59,11 @@ export interface AttributeCycle {
57
59
  readonly vertices: readonly string[];
58
60
  /** `true` when the eager reads with a known source close the cycle (GRAPH.CYCLE). */
59
61
  readonly certain: boolean;
62
+ /**
63
+ * `true` when each attribute of the cycle is circular: the run gives its fixed point, thus `check` gives no issue
64
+ * (TREE.CIRCULAR-CHECK). Else the field is absent.
65
+ */
66
+ readonly circular?: true;
60
67
  }
61
68
 
62
69
  /** The graph of attribute instances of a document (TREE.GRAPH). */
@@ -195,11 +202,18 @@ interface Reader {
195
202
  readonly at: readonly Key[];
196
203
  }
197
204
 
205
+ /** This function tells if the kind of a node declares an attribute `circular` (D113). */
206
+ function isCircular(kind: Kind, attribute: string): boolean {
207
+ const declared = hasOwnEntry(kind.tree?.attributes, attribute) ? kind.tree?.attributes?.[attribute] : undefined;
208
+ return declared?.circular !== undefined;
209
+ }
210
+
198
211
  /** This function gives the vertex of an attribute of a node. A new vertex goes on the work list for its rungs. */
199
212
  function vertexOf(b: Builder, node: NodeInfo, attribute: string): string {
200
213
  const id = attributeVertexId(node.visit.place, attribute);
201
214
  if (!b.vertices.has(id)) {
202
- b.vertices.set(id, { id, place: node.visit.place, attribute, type: node.text });
215
+ const vertex: AttributeVertex = { id, place: node.visit.place, attribute, type: node.text };
216
+ b.vertices.set(id, isCircular(node.kind, attribute) ? { ...vertex, circular: true } : vertex);
203
217
  b.nodes.set(id, node);
204
218
  if (!node.parts.has(attribute)) b.work.push(id);
205
219
  }
@@ -558,10 +572,18 @@ export function attributeGraph(machine: Machine, doc: unknown, options: Attribut
558
572
  edges: b.edges,
559
573
  graph,
560
574
  unknown: b.edges.filter((e) => e.from === undefined),
561
- cycles: cycles(graph).map((cycle) => ({ vertices: cycle.nodes, certain: cycle.certain })),
575
+ cycles: cycles(graph).map((cycle) => {
576
+ const found: AttributeCycle = { vertices: cycle.nodes, certain: cycle.certain };
577
+ return allCircular(b, cycle.nodes) ? { ...found, circular: true } : found;
578
+ }),
562
579
  };
563
580
  }
564
581
 
582
+ /** This function tells if each vertex of a list is a circular attribute (D113). */
583
+ function allCircular(b: Builder, ids: readonly string[]): boolean {
584
+ return ids.every((id) => b.vertices.get(id)?.circular === true);
585
+ }
586
+
565
587
  function vertexText(vertex: AttributeVertex): string {
566
588
  return `${vertex.attribute} of ${vertex.type} at [${vertex.place.join(", ")}]`;
567
589
  }
@@ -572,6 +594,9 @@ function vertexText(vertex: AttributeVertex): string {
572
594
  * certain cycle that these reads reach always gives `#CYCLE!`. The issue gets the place of the read that closes the
573
595
  * cycle in the order of the run, and the reason of the run. A cycle through a lazy read or an unknown read is not an issue.
574
596
  *
597
+ * A cycle whose attributes are each circular gives a fixed point, thus it is not an issue (TREE.CIRCULAR-CHECK). In a
598
+ * cycle with a plain attribute, the run gives `#CYCLE!` at the first read that closes a chain with a plain attribute.
599
+ *
575
600
  * @param machine - The world and the code of the evaluators.
576
601
  * @param doc - The document, as JSON.
577
602
  * @param options - The type of a bare root value.
@@ -582,7 +607,7 @@ export function cycleIssues(machine: Machine, doc: unknown, options: CheckOption
582
607
  const graph = nodeGraph(b);
583
608
  const certain = new Map<string, number>();
584
609
  cycles(graph).forEach((cycle, index) => {
585
- if (cycle.certain) for (const id of cycle.nodes) certain.set(id, index);
610
+ if (cycle.certain && !allCircular(b, cycle.nodes)) for (const id of cycle.nodes) certain.set(id, index);
586
611
  });
587
612
  if (certain.size === 0) return [];
588
613
  // The eager reads with a known source of each vertex, in the order of its code, with the index of each edge.
@@ -627,9 +652,12 @@ export function cycleIssues(machine: Machine, doc: unknown, options: CheckOption
627
652
  // The read reaches an attribute in evaluation: the run gives #CYCLE! here, with the chain of the demands.
628
653
  const cycle = certain.get(from);
629
654
  if (cycle === undefined || reported.has(cycle)) continue;
630
- reported.add(cycle);
631
655
  const first = stack.findIndex((frame) => frame.id === from);
632
- const chain = [...stack.slice(first).map((frame) => frame.id), from].map((id) => vertexText(b.vertices.get(id) as AttributeVertex));
656
+ const ids = stack.slice(first).map((frame) => frame.id);
657
+ // A chain of circular attributes reads an approximation in the run, thus it does not close the cycle (D113).
658
+ if (allCircular(b, ids)) continue;
659
+ reported.add(cycle);
660
+ const chain = [...ids, from].map((id) => vertexText(b.vertices.get(id) as AttributeVertex));
633
661
  out.push({ code: "#CYCLE!", place: e.place, reason: `The attributes form a cycle: ${chain.join(" -> ")}.`, at: b.ats[index] as readonly Key[] });
634
662
  }
635
663
  }
package/src/reference.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
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 Kind, type TreeContract, equationType, lookupKind } from "./world.ts";
3
+ import { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, circularOf, equationType, lookupKind } from "./world.ts";
4
4
  import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
5
5
  import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, ROOTS, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
6
6
  import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
@@ -776,7 +776,7 @@ interface RefNode {
776
776
  const refNodes = /* @__PURE__ */ new WeakMap<object, RefNode>();
777
777
 
778
778
  /** The attributes in evaluation in the reference, in the order of their demands (TREE.CYCLE). */
779
- const inEvaluation: { readonly node: RefNode; readonly name: string }[] = [];
779
+ const inEvaluation: { readonly node: RefNode; readonly name: string; readonly circular: boolean; tentative: boolean }[] = [];
780
780
 
781
781
  function nameOfAttribute(node: RefNode, name: string): string {
782
782
  return `${name} of ${node.kind} at [${node.place.join(", ")}]`;
@@ -865,29 +865,159 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
865
865
  ],
866
866
  ];
867
867
 
868
- /** TREE.ONCE, TREE.MEMO and TREE.CYCLE: the result of one attribute, or `undefined` when no rung of its ladder answers. */
868
+ /** TREE.LADDER: the answer of the first rung that answers, or `undefined`. */
869
+ function ladderOf(node: RefNode, name: string): Result | undefined {
870
+ for (const [, rung] of REF_RUNGS) {
871
+ const result = rung(node, name);
872
+ if (result !== undefined) return result;
873
+ }
874
+ return undefined;
875
+ }
876
+
877
+ /** One approximation of a circular attribute in the reference, with its round (TREE.CIRCULAR). */
878
+ interface RefApproximation {
879
+ readonly result: Result | undefined;
880
+ readonly round: number;
881
+ }
882
+
883
+ /**
884
+ * TREE.CIRCULAR: a fixed point in evaluation in the reference. The first circular attribute of a demand leads it, and
885
+ * each circular attribute that a demand reaches during its rounds joins it.
886
+ */
887
+ interface RefPoint {
888
+ /** The index of the leader in `inEvaluation`. */
889
+ readonly depth: number;
890
+ round: number;
891
+ changed: boolean;
892
+ readonly values: Map<RefNode, Map<string, RefApproximation>>;
893
+ }
894
+
895
+ /** The open fixed point of the reference, or `undefined`. */
896
+ let refPoint: RefPoint | undefined;
897
+
898
+ /** TREE.CIRCULAR-CHECK: the declaration `circular` of an attribute of a node, or `undefined`. */
899
+ function circularRuleOf(node: RefNode, name: string): CircularRule | undefined {
900
+ const declared = termIn(node.contract?.attributes, name)?.circular;
901
+ return declared === undefined ? undefined : circularOf(declared);
902
+ }
903
+
904
+ /** TREE.CIRCULAR: two approximations are the same when two values are equal (DOC.EQUALS) or two errors have one code and reason. */
905
+ function sameApproximation(a: Result | undefined, b: Result | undefined): boolean {
906
+ if (a === undefined || b === undefined) return a === b;
907
+ if (a.ok && b.ok) return docEquals(a.value, b.value);
908
+ if (!a.ok && !b.ok) return a.error.code === b.error.code && a.error.reason === b.error.reason;
909
+ return false;
910
+ }
911
+
912
+ /** TREE.CIRCULAR: the attributes above the leader read an approximation, thus their results are not final. */
913
+ function readApproximation(point: RefPoint): void {
914
+ for (const entry of inEvaluation.slice(point.depth + 1)) entry.tentative = true;
915
+ }
916
+
917
+ function approximationsOf(point: RefPoint, node: RefNode): Map<string, RefApproximation> {
918
+ const known = point.values.get(node);
919
+ if (known !== undefined) return known;
920
+ const made = new Map<string, RefApproximation>();
921
+ point.values.set(node, made);
922
+ return made;
923
+ }
924
+
925
+ /**
926
+ * TREE.CIRCULAR and TREE.CIRCULAR-LIMIT: the rounds of the leader of a fixed point. A round without a new value ends
927
+ * the rounds, and each circular attribute of that round keeps its value. An error ends the rounds at once. The last
928
+ * round of the limit gives `#CYCLE!` at the place of the node of the leader.
929
+ */
930
+ function fixedPointOf(node: RefNode, name: string, rule: CircularRule & { readonly ok: true }): Result | undefined {
931
+ const point: RefPoint = { depth: inEvaluation.length - 1, round: 0, changed: false, values: new Map() };
932
+ refPoint = point;
933
+ try {
934
+ let before: Result | undefined = ok(rule.start);
935
+ for (;;) {
936
+ point.round += 1;
937
+ point.changed = false;
938
+ const result = ladderOf(node, name);
939
+ if (result !== undefined && !result.ok) return result;
940
+ if (!sameApproximation(before, result)) point.changed = true;
941
+ before = result;
942
+ approximationsOf(point, node).set(name, { result, round: point.round });
943
+ if (!point.changed) {
944
+ for (const [member, values] of point.values) {
945
+ for (const [key, value] of values) if (value.round === point.round && value.result !== undefined) member.values.set(key, value.result);
946
+ }
947
+ return result;
948
+ }
949
+ if (point.round >= rule.limit) {
950
+ return fail("#CYCLE!", `The cycle of the circular attribute ${nameOfAttribute(node, name)} does not become stable within its limit of ${rule.limit} ${rule.limit === 1 ? "round" : "rounds"}.`, {
951
+ place: node.place,
952
+ });
953
+ }
954
+ }
955
+ } finally {
956
+ refPoint = undefined;
957
+ }
958
+ }
959
+
960
+ /** TREE.CIRCULAR: a circular attribute in the open fixed point of another leader evaluates one time in each round. */
961
+ function roundOf(point: RefPoint, node: RefNode, name: string, rule: CircularRule & { readonly ok: true }): Result | undefined {
962
+ let value = point.values.get(node)?.get(name);
963
+ if (value === undefined || value.round !== point.round) {
964
+ node.pending.add(name);
965
+ inEvaluation.push({ node, name, circular: true, tentative: false });
966
+ let result: Result | undefined;
967
+ try {
968
+ result = ladderOf(node, name);
969
+ } finally {
970
+ node.pending.delete(name);
971
+ inEvaluation.pop();
972
+ }
973
+ if (!sameApproximation(value === undefined ? ok(rule.start) : value.result, result)) point.changed = true;
974
+ value = { result, round: point.round };
975
+ approximationsOf(point, node).set(name, value);
976
+ }
977
+ readApproximation(point);
978
+ return value.result;
979
+ }
980
+
981
+ /**
982
+ * TREE.ONCE, TREE.MEMO, TREE.CYCLE and TREE.CIRCULAR: the result of one attribute, or `undefined` when no rung of its
983
+ * ladder answers. A result that read an approximation of an open fixed point is not kept.
984
+ */
869
985
  function attributeOf(node: RefNode, name: string): Result | undefined {
870
986
  const done = node.values.get(name);
871
987
  if (done !== undefined) return done;
872
988
  if (node.pending.has(name)) {
873
989
  const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
990
+ const point = refPoint;
991
+ if (point !== undefined && from >= point.depth && inEvaluation.slice(from).every((entry) => entry.circular)) {
992
+ readApproximation(point);
993
+ const kept = point.values.get(node)?.get(name);
994
+ if (kept !== undefined) return kept.result;
995
+ const rule = circularRuleOf(node, name);
996
+ return ok(rule?.ok === true ? rule.start : null);
997
+ }
874
998
  const steps = from < 0 ? [nameOfAttribute(node, name)] : inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
875
999
  return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
876
1000
  }
1001
+ const rule = circularRuleOf(node, name);
1002
+ if (rule !== undefined && !rule.ok) {
1003
+ const bad = fail("#VALUE!", `The attribute ${nameOfAttribute(node, name)} has a bad declaration circular. ${rule.reason}`);
1004
+ node.values.set(name, bad);
1005
+ return bad;
1006
+ }
1007
+ const point = refPoint;
1008
+ if (point !== undefined && rule !== undefined) return roundOf(point, node, name, rule);
1009
+ const entry = { node, name, circular: rule !== undefined, tentative: false };
877
1010
  node.pending.add(name);
878
- inEvaluation.push({ node, name });
1011
+ inEvaluation.push(entry);
1012
+ let result: Result | undefined;
879
1013
  try {
880
- for (const [, rung] of REF_RUNGS) {
881
- const result = rung(node, name);
882
- if (result === undefined) continue;
883
- node.values.set(name, result);
884
- return result;
885
- }
886
- return undefined;
1014
+ result = rule === undefined ? ladderOf(node, name) : fixedPointOf(node, name, rule);
887
1015
  } finally {
888
1016
  node.pending.delete(name);
889
1017
  inEvaluation.pop();
890
1018
  }
1019
+ if (!entry.tentative && result !== undefined) node.values.set(name, result);
1020
+ return result;
891
1021
  }
892
1022
 
893
1023
  /** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
package/src/static.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, isKey, isRecord, typeName } from "./doc.ts";
2
- import { type Kind, type World, type WorldEntry, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
2
+ import { type Kind, type World, type WorldEntry, circularOf, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
4
  import { type PartSlot, partsOf, scopesOf } from "./parts.ts";
5
5
  import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
@@ -867,7 +867,8 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
867
867
 
868
868
  /**
869
869
  * This function finds the issues of the contract of a tree kind (TREE.CHECK-RESERVED). An attribute with the name of a
870
- * structural step gets no read. A tree kind of the run side contradicts the evaluation on demand of its attributes.
870
+ * structural step gets no read. A tree kind of the run side contradicts the evaluation on demand of its attributes. A
871
+ * declaration `circular` without a start value or with a bad limit gives `#VALUE!` (TREE.CIRCULAR-CHECK).
871
872
  */
872
873
  function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
873
874
  const out: Issue[] = [];
@@ -878,6 +879,11 @@ function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
878
879
  if (why !== undefined) out.push({ code: "#NAME?", place: [], reason: `The kind ${kindText} gives the reserved attribute tree.${table}.${name}: ${why}.` });
879
880
  }
880
881
  }
882
+ // A declaration circular needs a start value and a correct limit (TREE.CIRCULAR-CHECK, D113).
883
+ for (const [name, declared] of Object.entries(tree?.attributes ?? {})) {
884
+ const rule = declared.circular === undefined ? undefined : circularOf(declared.circular);
885
+ if (rule !== undefined && !rule.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad declaration tree.attributes.${name}.circular. ${rule.reason}` });
886
+ }
881
887
  if (kind.side === "run") {
882
888
  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.` });
883
889
  }