@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/build/index.js CHANGED
@@ -354,6 +354,54 @@ function describe(value) {
354
354
  }
355
355
  }
356
356
  // src/world.ts
357
+ /** The most rounds of a circular attribute without its own `limit` (TREE.CIRCULAR-LIMIT, D113). */
358
+ const CIRCULAR_LIMIT = 100;
359
+ /** This function tells if a value is JSON data: `null`, a truth value, a finite number, a text, or a container of them. */
360
+ function isJsonData(value) {
361
+ const stack = [value];
362
+ const seen = /* @__PURE__ */ new Set();
363
+ while (stack.length > 0) {
364
+ const item = stack.pop();
365
+ if (item === null || typeof item === "string" || typeof item === "boolean") continue;
366
+ if (typeof item === "number") {
367
+ if (!Number.isFinite(item)) return false;
368
+ continue;
369
+ }
370
+ if (!Array.isArray(item) && !isRecord(item)) return false;
371
+ if (seen.has(item)) continue;
372
+ seen.add(item);
373
+ for (const inner of Object.values(item)) stack.push(inner);
374
+ }
375
+ return true;
376
+ }
377
+ /**
378
+ * This function reads the declaration `circular` of an attribute (D113). The tree kit, the reference, `check` and the
379
+ * graph of attribute instances read it with this one function.
380
+ *
381
+ * @param declared - The value of the field `circular`.
382
+ * @returns The start value and the limit, or the reason when the declaration has no correct form.
383
+ */
384
+ function circularOf(declared) {
385
+ if (!isRecord(declared) || !Object.hasOwn(declared, "start")) return {
386
+ ok: false,
387
+ reason: "The declaration circular needs a record with the field start."
388
+ };
389
+ const start = field(declared, "start");
390
+ if (!isJsonData(start)) return {
391
+ ok: false,
392
+ reason: "The start value of the declaration circular must be JSON data."
393
+ };
394
+ const limit = Object.hasOwn(declared, "limit") ? field(declared, "limit") : 100;
395
+ if (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1) return {
396
+ ok: false,
397
+ reason: "The limit of the declaration circular must be a whole number of 1 or more."
398
+ };
399
+ return {
400
+ ok: true,
401
+ start,
402
+ limit
403
+ };
404
+ }
357
405
  /**
358
406
  * This function gives the type of a bare equation of a kind (D42). It is the type `of` of the shape of its parts, or
359
407
  * else the type `of` of the shape `rest` of a record. The plan, the reference and `check` read it with this one function.
@@ -5622,12 +5670,55 @@ const NO_ANSWER = /* @__PURE__ */ Object.freeze({
5622
5670
  by: void 0
5623
5671
  });
5624
5672
  /**
5625
- * The marks of the attributes in evaluation, for each place of a plan, with the cells of a host (TREE.CELL). Nodes of
5626
- * two evaluations share their place and their kept cells, thus they share their marks.
5673
+ * The marks of the attributes in evaluation, for each key of a node, with the cells of a host (TREE.CELL). Nodes of
5674
+ * two evaluations share their key and their kept cells, thus they share their marks. Nodes of two documents have
5675
+ * different keys, thus a nested evaluation gets its own marks (TREE.CELL-NESTED, D115).
5627
5676
  */
5628
5677
  const marksByPlace = /* @__PURE__ */ new WeakMap();
5629
- /** 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. */
5678
+ /** The trees of the cells of a host, for each key of a root. Nodes of two evaluations stay in one tree. */
5630
5679
  const buildsByPlace = /* @__PURE__ */ new WeakMap();
5680
+ /** The keys of the nodes with the cells of a host, for each scope and each place of a plan (D115). */
5681
+ const keysByScope = /* @__PURE__ */ new WeakMap();
5682
+ /** The scopes of the roots that a demand made, for each marks of a node and each name of an attribute (D115). */
5683
+ const demandScopes = /* @__PURE__ */ new WeakMap();
5684
+ /**
5685
+ * This function gives the key of a node with the cells of a host (TREE.CELL-DOCUMENT, D115). The key of the node
5686
+ * around the node is its scope. A root without such a node has the scope of the open demand, or no scope.
5687
+ * Without a scope, the key is the place of the plan. Thus a cached plan gives a new key for the nodes of each
5688
+ * document, and each evaluation of a document gives the same keys.
5689
+ *
5690
+ * @param place - The place of the document of the node in its plan.
5691
+ * @param above - The state of the node around the node, or `undefined`.
5692
+ * @returns The key: an array with the keys of `place`.
5693
+ */
5694
+ function cellPlace(place, above) {
5695
+ let scope = above?.place;
5696
+ if (scope === void 0) {
5697
+ const demand = demands[demands.length - 1];
5698
+ if (demand === void 0) return place;
5699
+ let byName = demandScopes.get(demand.cells);
5700
+ if (byName === void 0) {
5701
+ byName = /* @__PURE__ */ new Map();
5702
+ demandScopes.set(demand.cells, byName);
5703
+ }
5704
+ scope = byName.get(demand.key);
5705
+ if (scope === void 0) {
5706
+ scope = {};
5707
+ byName.set(demand.key, scope);
5708
+ }
5709
+ }
5710
+ let keys = keysByScope.get(scope);
5711
+ if (keys === void 0) {
5712
+ keys = /* @__PURE__ */ new WeakMap();
5713
+ keysByScope.set(scope, keys);
5714
+ }
5715
+ let key = keys.get(place);
5716
+ if (key === void 0) {
5717
+ key = Object.freeze([...place]);
5718
+ keys.set(place, key);
5719
+ }
5720
+ return key;
5721
+ }
5631
5722
  /** This function gives the value of a weak map, or puts a new value in it. */
5632
5723
  function keptAt(map, place, make) {
5633
5724
  let value = map.get(place);
@@ -5641,6 +5732,277 @@ function keptAt(map, place, make) {
5641
5732
  const NOT_THROWN = /* @__PURE__ */ Symbol("coeval.tree.not-thrown");
5642
5733
  /** The attributes in evaluation, in the order of their demands. An evaluation does not wait, thus one list serves. */
5643
5734
  const demands = [];
5735
+ /** The open fixed point, or `undefined`. */
5736
+ let openPoint;
5737
+ /** For each table `tree.attributes`, `true` when an attribute of it declares `circular`. */
5738
+ const circularTables = /* @__PURE__ */ new WeakMap();
5739
+ /** For each declaration `circular` that is an object, its rule. */
5740
+ const circularRules = /* @__PURE__ */ new WeakMap();
5741
+ /** This function tells if a tree contract declares a circular attribute. The answer for a table is computed one time. */
5742
+ function hasCircular(tree) {
5743
+ const table = tree?.attributes;
5744
+ if (table === void 0) return false;
5745
+ let known = circularTables.get(table);
5746
+ if (known === void 0) {
5747
+ known = Object.keys(table).some((key) => {
5748
+ const declared = entryOf(table, key);
5749
+ return typeof declared === "object" && declared !== null && declared.circular !== void 0;
5750
+ });
5751
+ circularTables.set(table, known);
5752
+ }
5753
+ return known;
5754
+ }
5755
+ /** This function gives the declaration `circular` of an attribute of a tree contract (D113), or `undefined`. */
5756
+ function circularRule(tree, key) {
5757
+ const declared = entryOf(tree?.attributes, key)?.circular;
5758
+ if (declared === void 0) return void 0;
5759
+ if (typeof declared !== "object" || declared === null) return circularOf(declared);
5760
+ let rule = circularRules.get(declared);
5761
+ if (rule === void 0) {
5762
+ rule = circularOf(declared);
5763
+ circularRules.set(declared, rule);
5764
+ }
5765
+ return rule;
5766
+ }
5767
+ /** This function gives the values of a fixed point for the marks of a node. */
5768
+ function loopValues(loop, marks) {
5769
+ let values = loop.values.get(marks);
5770
+ if (values === void 0) {
5771
+ values = /* @__PURE__ */ new Map();
5772
+ loop.values.set(marks, values);
5773
+ }
5774
+ return values;
5775
+ }
5776
+ /** This function marks each demand above the leader: each one read an approximation, thus its result is not final. */
5777
+ function markTentative(loop) {
5778
+ for (let index = loop.depth + 1; index < demands.length; index += 1) loop.tentative.add(demands[index]);
5779
+ }
5780
+ /** This function tells if two approximations are the same: two values by DOC.EQUALS, two errors by code and reason. */
5781
+ function sameApproximation$1(a, b) {
5782
+ if (a === void 0 || b === void 0) return a === b;
5783
+ if (a.ok && b.ok) return docEquals(a.value, b.value);
5784
+ if (!a.ok && !b.ok) return a.error.code === b.error.code && a.error.reason === b.error.reason;
5785
+ return false;
5786
+ }
5787
+ /** This function writes the final value of each circular attribute of the last round in the plain memo of its node. */
5788
+ function settle(loop) {
5789
+ for (const values of loop.values.values()) for (const [key, value] of values) if (value.memo !== void 0 && value.round === loop.round) value.memo.set(key, {
5790
+ busy: false,
5791
+ result: value.result,
5792
+ by: value.by
5793
+ });
5794
+ }
5795
+ /** The text of the fixed point of a circular attribute that does not become stable (TREE.CIRCULAR-LIMIT). */
5796
+ function limitReason(text, limit) {
5797
+ return `The cycle of the circular attribute ${text} does not become stable within its limit of ${limit} ${limit === 1 ? "round" : "rounds"}.`;
5798
+ }
5799
+ /**
5800
+ * This function answers a demand that reached an attribute in evaluation in an open fixed point (TREE.CYCLE). A chain
5801
+ * of circular attributes gives the current approximation (TREE.CIRCULAR). Each other chain gives `#CYCLE!`.
5802
+ */
5803
+ function reachedIn(loop, n, key) {
5804
+ const first = demands.findIndex((demand) => demand.cells === n.marks && demand.key === key);
5805
+ let circular = first >= loop.depth;
5806
+ for (let index = first; circular && index < demands.length; index += 1) circular = loop.circular.has(demands[index]);
5807
+ if (!circular) return cycleAt(n.marks, key, attributeText(key, n.type, n.place));
5808
+ const rule = circularRule(n.tree, key);
5809
+ const kept = loop.values.get(n.marks)?.get(key);
5810
+ markTentative(loop);
5811
+ return kept !== void 0 ? kept.result : ok(rule?.ok === true ? rule.start : null);
5812
+ }
5813
+ /**
5814
+ * This function evaluates the leader of a fixed point (TREE.CIRCULAR, D113). Its demand is the last one. Each round
5815
+ * visits the ladder again, and the rounds end when no circular attribute gets a new value. An error ends the rounds
5816
+ * at once. After the last round of the limit, the leader gives `#CYCLE!` at the place of its node.
5817
+ */
5818
+ function roundsOf(n, key, rule) {
5819
+ const leader = demands[demands.length - 1];
5820
+ const loop = {
5821
+ depth: demands.length - 1,
5822
+ round: 0,
5823
+ changed: false,
5824
+ values: /* @__PURE__ */ new Map(),
5825
+ circular: /* @__PURE__ */ new Set([leader]),
5826
+ tentative: /* @__PURE__ */ new Set()
5827
+ };
5828
+ openPoint = loop;
5829
+ try {
5830
+ let before = ok(rule.start);
5831
+ for (;;) {
5832
+ loop.round += 1;
5833
+ loop.changed = false;
5834
+ const answer = n.climb(key) ?? NO_ANSWER;
5835
+ if (answer.result !== void 0 && !answer.result.ok) return answer;
5836
+ if (!sameApproximation$1(before, answer.result)) loop.changed = true;
5837
+ before = answer.result;
5838
+ loopValues(loop, n.marks).set(key, {
5839
+ result: answer.result,
5840
+ by: answer.by,
5841
+ round: loop.round,
5842
+ memo: void 0
5843
+ });
5844
+ if (!loop.changed) {
5845
+ settle(loop);
5846
+ return answer;
5847
+ }
5848
+ if (loop.round >= rule.limit) return {
5849
+ result: fail("#CYCLE!", limitReason(attributeText(key, n.type, n.place), rule.limit), { place: n.place }),
5850
+ by: answer.by
5851
+ };
5852
+ }
5853
+ } finally {
5854
+ openPoint = void 0;
5855
+ }
5856
+ }
5857
+ /**
5858
+ * This function evaluates a circular attribute in the open fixed point of another leader, one time in each round
5859
+ * (D113). A later demand in the same round gets the value of the round. Each value is an approximation.
5860
+ */
5861
+ function roundIn(loop, n, key, rule, kept) {
5862
+ let value = kept;
5863
+ if (value === void 0 || value.round !== loop.round) {
5864
+ const demand = {
5865
+ cells: n.marks,
5866
+ key,
5867
+ text: attributeText(key, n.type, n.place)
5868
+ };
5869
+ n.marks.set(key, BUSY);
5870
+ demands.push(demand);
5871
+ loop.circular.add(demand);
5872
+ let answer;
5873
+ try {
5874
+ answer = n.climb(key) ?? NO_ANSWER;
5875
+ } finally {
5876
+ demands.pop();
5877
+ n.marks.delete(key);
5878
+ }
5879
+ if (!sameApproximation$1(value === void 0 ? ok(rule.start) : value.result, answer.result)) loop.changed = true;
5880
+ value = {
5881
+ result: answer.result,
5882
+ by: answer.by,
5883
+ round: loop.round,
5884
+ memo: n.host === void 0 ? n.cells : void 0
5885
+ };
5886
+ loopValues(loop, n.marks).set(key, value);
5887
+ }
5888
+ markTentative(loop);
5889
+ return value.result;
5890
+ }
5891
+ /**
5892
+ * This function evaluates an attribute without the cell of the host: with the plain memo, or in an open fixed point
5893
+ * (D113). A result that read an approximation is not final, thus no memo keeps it.
5894
+ */
5895
+ function localIn(n, key, rule) {
5896
+ const demand = {
5897
+ cells: n.marks,
5898
+ key,
5899
+ text: attributeText(key, n.type, n.place)
5900
+ };
5901
+ const loop = openPoint;
5902
+ n.marks.set(key, BUSY);
5903
+ demands.push(demand);
5904
+ let answer;
5905
+ try {
5906
+ answer = rule === void 0 ? n.climb(key) ?? NO_ANSWER : roundsOf(n, key, rule);
5907
+ } catch (error) {
5908
+ n.marks.delete(key);
5909
+ throw error;
5910
+ } finally {
5911
+ demands.pop();
5912
+ }
5913
+ if (loop !== void 0 && loop.tentative.has(demand)) {
5914
+ n.marks.delete(key);
5915
+ return answer.result;
5916
+ }
5917
+ if (n.host !== void 0) {
5918
+ n.marks.delete(key);
5919
+ if (loop !== void 0) loopValues(loop, n.marks).set(key, {
5920
+ result: answer.result,
5921
+ by: answer.by,
5922
+ round: 0,
5923
+ memo: void 0
5924
+ });
5925
+ }
5926
+ n.cells.set(key, {
5927
+ busy: false,
5928
+ result: answer.result,
5929
+ by: answer.by
5930
+ });
5931
+ return answer.result;
5932
+ }
5933
+ /**
5934
+ * This function answers a demand that reached an attribute in evaluation (TREE.CYCLE). Outside a fixed point, it gives
5935
+ * `#CYCLE!` with the chain. In an open fixed point, `reachedIn` answers (TREE.CIRCULAR).
5936
+ */
5937
+ function busyAt(n, key) {
5938
+ const loop = openPoint;
5939
+ return loop === void 0 ? cycleAt(n.marks, key, attributeText(key, n.type, n.place)) : reachedIn(loop, n, key);
5940
+ }
5941
+ /**
5942
+ * This function evaluates the leader of a fixed point through the cell of the host (TREE.CELL, D113). The cell of the
5943
+ * leader gets the final value of the fixed point, from a computation that contains each round.
5944
+ */
5945
+ function leaderOnHost(n, key, rule, host) {
5946
+ const text = attributeText(key, n.type, n.place);
5947
+ n.marks.set(key, BUSY);
5948
+ demands.push({
5949
+ cells: n.marks,
5950
+ key,
5951
+ text
5952
+ });
5953
+ let thrown = NOT_THROWN;
5954
+ let answer;
5955
+ try {
5956
+ answer = host(n.place, key, () => {
5957
+ try {
5958
+ return roundsOf(n, key, rule);
5959
+ } catch (error) {
5960
+ thrown = error;
5961
+ throw error;
5962
+ }
5963
+ });
5964
+ } catch (error) {
5965
+ if (error === thrown) throw error;
5966
+ answer = {
5967
+ result: fail("#CYCLE!", `The cells of the host found a cycle in the demands: ${demands.map((demand) => demand.text).join(" -> ")}.`, { cause: error }),
5968
+ by: void 0
5969
+ };
5970
+ } finally {
5971
+ demands.pop();
5972
+ n.marks.delete(key);
5973
+ }
5974
+ n.cells.set(key, {
5975
+ busy: false,
5976
+ result: answer.result,
5977
+ by: answer.by
5978
+ });
5979
+ return answer.result;
5980
+ }
5981
+ /**
5982
+ * This function gives an attribute of a node with circular attributes, or an attribute in an open fixed point (D113).
5983
+ * 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.
5984
+ */
5985
+ function circularRead(n, key) {
5986
+ const rule = circularRule(n.tree, key);
5987
+ if (rule !== void 0 && !rule.ok) {
5988
+ const result = fail("#VALUE!", `The attribute ${attributeText(key, n.type, n.place)} has a bad declaration circular. ${rule.reason}`);
5989
+ n.cells.set(key, {
5990
+ busy: false,
5991
+ result,
5992
+ by: void 0
5993
+ });
5994
+ return result;
5995
+ }
5996
+ const loop = openPoint;
5997
+ if (loop !== void 0) {
5998
+ const kept = loop.values.get(n.marks)?.get(key);
5999
+ if (rule !== void 0) return roundIn(loop, n, key, rule, kept);
6000
+ if (kept !== void 0) return kept.result;
6001
+ return localIn(n, key, void 0);
6002
+ }
6003
+ if (rule === void 0 || n.host === void 0) return localIn(n, key, rule);
6004
+ return leaderOnHost(n, key, rule, n.host);
6005
+ }
5644
6006
  function attributeText(key, type, place) {
5645
6007
  return `${key} of ${type} at [${place.join(", ")}]`;
5646
6008
  }
@@ -5710,16 +6072,18 @@ function plainOf(value, path) {
5710
6072
  * fold or a default, come last. A failed attribute throws `Failure` with its own error, thus a reference to it gives that error.
5711
6073
  * The argument `outer` is the self of the context, that `treeNode` resolved. The node reads no root of the context directly (C4).
5712
6074
  *
5713
- * The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of the node.
6075
+ * The argument `place` is the key of the node. With the cells of a host, `cellPlace` gives it. Else it is the place of
6076
+ * the plan. The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of the node.
5714
6077
  */
5715
- function treeThing(input, build, outer, host) {
5716
- const { parts, context, kind: type, place, machine } = input;
6078
+ function treeThing(input, place, build, outer, host) {
6079
+ const { parts, context, kind: type, machine } = input;
5717
6080
  const runtime = input[RUNTIME];
5718
6081
  const { tree, of } = contractOf$1(machine, input.typePath);
5719
6082
  const above = stateOf(outer);
5720
6083
  const up = above !== void 0 && above.build === build ? above : void 0;
5721
6084
  const cells = /* @__PURE__ */ new Map();
5722
6085
  const marks = host === void 0 ? cells : keptAt(marksByPlace, place, () => /* @__PURE__ */ new Map());
6086
+ const circular = hasCircular(tree);
5723
6087
  /** The positions of the items of the list `children`, for the names of the children. */
5724
6088
  let positions;
5725
6089
  const fold = (key, declared) => {
@@ -5789,8 +6153,9 @@ function treeThing(input, build, outer, host) {
5789
6153
  */
5790
6154
  const through = host === void 0 ? void 0 : (key) => {
5791
6155
  const text = attributeText(key, type, place);
5792
- if (marks.has(key)) return cycleAt(marks, key, text);
6156
+ if (marks.has(key)) return busyAt(state, key);
5793
6157
  if (tree === void 0 && up?.tree === void 0 && !parts.some((item) => item.key === key)) return void 0;
6158
+ if (circular || openPoint !== void 0) return circularRead(state, key);
5794
6159
  marks.set(key, BUSY);
5795
6160
  demands.push({
5796
6161
  cells: marks,
@@ -5828,8 +6193,9 @@ function treeThing(input, build, outer, host) {
5828
6193
  const read = (key) => {
5829
6194
  if (through !== void 0) return through(key);
5830
6195
  const cell = cells.get(key);
5831
- if (cell !== void 0) return cell.busy ? cycleAt(cells, key, attributeText(key, type, place)) : cell.result;
6196
+ if (cell !== void 0) return cell.busy ? busyAt(state, key) : cell.result;
5832
6197
  if (tree === void 0 && up?.tree === void 0 && !parts.some((item) => item.key === key)) return void 0;
6198
+ if (circular || openPoint !== void 0) return circularRead(state, key);
5833
6199
  cells.set(key, BUSY);
5834
6200
  demands.push({
5835
6201
  cells,
@@ -5857,6 +6223,10 @@ function treeThing(input, build, outer, host) {
5857
6223
  type,
5858
6224
  place,
5859
6225
  tree,
6226
+ marks,
6227
+ cells,
6228
+ host,
6229
+ climb,
5860
6230
  read,
5861
6231
  equation(term, names) {
5862
6232
  if (machine === void 0) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
@@ -5980,10 +6350,11 @@ function treeNode(input, host) {
5980
6350
  const found = input.resolve(SELF_PATH, NO_HOPS);
5981
6351
  const outer = found.ok ? found.thing : void 0;
5982
6352
  const above = stateOf(outer);
5983
- if (above !== void 0 && above.build.open) return ok(treeThing(input, above.build, outer, host));
5984
- const build = host === void 0 ? { open: true } : keptAt(buildsByPlace, input.place, () => ({ open: true }));
6353
+ const place = host === void 0 ? input.place : cellPlace(input.place, above);
6354
+ if (above !== void 0 && above.build.open) return ok(treeThing(input, place, above.build, outer, host));
6355
+ const build = host === void 0 ? { open: true } : keptAt(buildsByPlace, place, () => ({ open: true }));
5985
6356
  build.open = true;
5986
- const root = treeThing(input, build, outer, host);
6357
+ const root = treeThing(input, place, build, outer, host);
5987
6358
  try {
5988
6359
  return stateOf(root).json(/* @__PURE__ */ new Set());
5989
6360
  } finally {
@@ -7465,16 +7836,24 @@ function valueOf$1(info) {
7465
7836
  function hasOwnEntry(table, name) {
7466
7837
  return table !== void 0 && Object.hasOwn(table, name);
7467
7838
  }
7839
+ /** This function tells if the kind of a node declares an attribute `circular` (D113). */
7840
+ function isCircular(kind, attribute) {
7841
+ return (hasOwnEntry(kind.tree?.attributes, attribute) ? kind.tree?.attributes?.[attribute] : void 0)?.circular !== void 0;
7842
+ }
7468
7843
  /** This function gives the vertex of an attribute of a node. A new vertex goes on the work list for its rungs. */
7469
7844
  function vertexOf(b, node, attribute) {
7470
7845
  const id = attributeVertexId(node.visit.place, attribute);
7471
7846
  if (!b.vertices.has(id)) {
7472
- b.vertices.set(id, {
7847
+ const vertex = {
7473
7848
  id,
7474
7849
  place: node.visit.place,
7475
7850
  attribute,
7476
7851
  type: node.text
7477
- });
7852
+ };
7853
+ b.vertices.set(id, isCircular(node.kind, attribute) ? {
7854
+ ...vertex,
7855
+ circular: true
7856
+ } : vertex);
7478
7857
  b.nodes.set(id, node);
7479
7858
  if (!node.parts.has(attribute)) b.work.push(id);
7480
7859
  }
@@ -7873,12 +8252,22 @@ function attributeGraph(machine, doc, options = {}) {
7873
8252
  edges: b.edges,
7874
8253
  graph,
7875
8254
  unknown: b.edges.filter((e) => e.from === void 0),
7876
- cycles: cycles(graph).map((cycle) => ({
7877
- vertices: cycle.nodes,
7878
- certain: cycle.certain
7879
- }))
8255
+ cycles: cycles(graph).map((cycle) => {
8256
+ const found = {
8257
+ vertices: cycle.nodes,
8258
+ certain: cycle.certain
8259
+ };
8260
+ return allCircular(b, cycle.nodes) ? {
8261
+ ...found,
8262
+ circular: true
8263
+ } : found;
8264
+ })
7880
8265
  };
7881
8266
  }
8267
+ /** This function tells if each vertex of a list is a circular attribute (D113). */
8268
+ function allCircular(b, ids) {
8269
+ return ids.every((id) => b.vertices.get(id)?.circular === true);
8270
+ }
7882
8271
  function vertexText(vertex) {
7883
8272
  return `${vertex.attribute} of ${vertex.type} at [${vertex.place.join(", ")}]`;
7884
8273
  }
@@ -7888,6 +8277,9 @@ function vertexText(vertex) {
7888
8277
  * certain cycle that these reads reach always gives `#CYCLE!`. The issue gets the place of the read that closes the
7889
8278
  * 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.
7890
8279
  *
8280
+ * A cycle whose attributes are each circular gives a fixed point, thus it is not an issue (TREE.CIRCULAR-CHECK). In a
8281
+ * cycle with a plain attribute, the run gives `#CYCLE!` at the first read that closes a chain with a plain attribute.
8282
+ *
7891
8283
  * @param machine - The world and the code of the evaluators.
7892
8284
  * @param doc - The document, as JSON.
7893
8285
  * @param options - The type of a bare root value.
@@ -7898,7 +8290,7 @@ function cycleIssues(machine, doc, options) {
7898
8290
  const graph = nodeGraph(b);
7899
8291
  const certain = /* @__PURE__ */ new Map();
7900
8292
  cycles(graph).forEach((cycle, index) => {
7901
- if (cycle.certain) for (const id of cycle.nodes) certain.set(id, index);
8293
+ if (cycle.certain && !allCircular(b, cycle.nodes)) for (const id of cycle.nodes) certain.set(id, index);
7902
8294
  });
7903
8295
  if (certain.size === 0) return [];
7904
8296
  const eager = /* @__PURE__ */ new Map();
@@ -7946,9 +8338,11 @@ function cycleIssues(machine, doc, options) {
7946
8338
  if (seen === "done") continue;
7947
8339
  const cycle = certain.get(from);
7948
8340
  if (cycle === void 0 || reported.has(cycle)) continue;
7949
- reported.add(cycle);
7950
8341
  const first = stack.findIndex((frame) => frame.id === from);
7951
- const chain = [...stack.slice(first).map((frame) => frame.id), from].map((id) => vertexText(b.vertices.get(id)));
8342
+ const ids = stack.slice(first).map((frame) => frame.id);
8343
+ if (allCircular(b, ids)) continue;
8344
+ reported.add(cycle);
8345
+ const chain = [...ids, from].map((id) => vertexText(b.vertices.get(id)));
7952
8346
  out.push({
7953
8347
  code: "#CYCLE!",
7954
8348
  place: e.place,
@@ -8592,7 +8986,8 @@ function equationIssues(machine, kind, kindText, options, state) {
8592
8986
  }
8593
8987
  /**
8594
8988
  * This function finds the issues of the contract of a tree kind (TREE.CHECK-RESERVED). An attribute with the name of a
8595
- * structural step gets no read. A tree kind of the run side contradicts the evaluation on demand of its attributes.
8989
+ * structural step gets no read. A tree kind of the run side contradicts the evaluation on demand of its attributes. A
8990
+ * declaration `circular` without a start value or with a bad limit gives `#VALUE!` (TREE.CIRCULAR-CHECK).
8596
8991
  */
8597
8992
  function contractIssues(kind, kindText) {
8598
8993
  const out = [];
@@ -8609,6 +9004,14 @@ function contractIssues(kind, kindText) {
8609
9004
  reason: `The kind ${kindText} gives the reserved attribute tree.${table}.${name}: ${why}.`
8610
9005
  });
8611
9006
  }
9007
+ for (const [name, declared] of Object.entries(tree?.attributes ?? {})) {
9008
+ const rule = declared.circular === void 0 ? void 0 : circularOf(declared.circular);
9009
+ if (rule !== void 0 && !rule.ok) out.push({
9010
+ code: "#VALUE!",
9011
+ place: [],
9012
+ reason: `The kind ${kindText} has a bad declaration tree.attributes.${name}.circular. ${rule.reason}`
9013
+ });
9014
+ }
8612
9015
  if (kind.side === "run") out.push({
8613
9016
  code: "#CALC!",
8614
9017
  place: [],
@@ -12124,31 +12527,146 @@ const REF_RUNGS = [
12124
12527
  return declared?.default === void 0 ? void 0 : equationOf(node, declared.default, {});
12125
12528
  }]
12126
12529
  ];
12127
- /** TREE.ONCE, TREE.MEMO and TREE.CYCLE: the result of one attribute, or `undefined` when no rung of its ladder answers. */
12530
+ /** TREE.LADDER: the answer of the first rung that answers, or `undefined`. */
12531
+ function ladderOf(node, name) {
12532
+ for (const [, rung] of REF_RUNGS) {
12533
+ const result = rung(node, name);
12534
+ if (result !== void 0) return result;
12535
+ }
12536
+ }
12537
+ /** The open fixed point of the reference, or `undefined`. */
12538
+ let refPoint;
12539
+ /** TREE.CIRCULAR-CHECK: the declaration `circular` of an attribute of a node, or `undefined`. */
12540
+ function circularRuleOf(node, name) {
12541
+ const declared = termIn(node.contract?.attributes, name)?.circular;
12542
+ return declared === void 0 ? void 0 : circularOf(declared);
12543
+ }
12544
+ /** TREE.CIRCULAR: two approximations are the same when two values are equal (DOC.EQUALS) or two errors have one code and reason. */
12545
+ function sameApproximation(a, b) {
12546
+ if (a === void 0 || b === void 0) return a === b;
12547
+ if (a.ok && b.ok) return docEquals(a.value, b.value);
12548
+ if (!a.ok && !b.ok) return a.error.code === b.error.code && a.error.reason === b.error.reason;
12549
+ return false;
12550
+ }
12551
+ /** TREE.CIRCULAR: the attributes above the leader read an approximation, thus their results are not final. */
12552
+ function readApproximation(point) {
12553
+ for (const entry of inEvaluation.slice(point.depth + 1)) entry.tentative = true;
12554
+ }
12555
+ function approximationsOf(point, node) {
12556
+ const known = point.values.get(node);
12557
+ if (known !== void 0) return known;
12558
+ const made = /* @__PURE__ */ new Map();
12559
+ point.values.set(node, made);
12560
+ return made;
12561
+ }
12562
+ /**
12563
+ * TREE.CIRCULAR and TREE.CIRCULAR-LIMIT: the rounds of the leader of a fixed point. A round without a new value ends
12564
+ * the rounds, and each circular attribute of that round keeps its value. An error ends the rounds at once. The last
12565
+ * round of the limit gives `#CYCLE!` at the place of the node of the leader.
12566
+ */
12567
+ function fixedPointOf(node, name, rule) {
12568
+ const point = {
12569
+ depth: inEvaluation.length - 1,
12570
+ round: 0,
12571
+ changed: false,
12572
+ values: /* @__PURE__ */ new Map()
12573
+ };
12574
+ refPoint = point;
12575
+ try {
12576
+ let before = ok(rule.start);
12577
+ for (;;) {
12578
+ point.round += 1;
12579
+ point.changed = false;
12580
+ const result = ladderOf(node, name);
12581
+ if (result !== void 0 && !result.ok) return result;
12582
+ if (!sameApproximation(before, result)) point.changed = true;
12583
+ before = result;
12584
+ approximationsOf(point, node).set(name, {
12585
+ result,
12586
+ round: point.round
12587
+ });
12588
+ if (!point.changed) {
12589
+ for (const [member, values] of point.values) for (const [key, value] of values) if (value.round === point.round && value.result !== void 0) member.values.set(key, value.result);
12590
+ return result;
12591
+ }
12592
+ if (point.round >= rule.limit) 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"}.`, { place: node.place });
12593
+ }
12594
+ } finally {
12595
+ refPoint = void 0;
12596
+ }
12597
+ }
12598
+ /** TREE.CIRCULAR: a circular attribute in the open fixed point of another leader evaluates one time in each round. */
12599
+ function roundOf(point, node, name, rule) {
12600
+ let value = point.values.get(node)?.get(name);
12601
+ if (value === void 0 || value.round !== point.round) {
12602
+ node.pending.add(name);
12603
+ inEvaluation.push({
12604
+ node,
12605
+ name,
12606
+ circular: true,
12607
+ tentative: false
12608
+ });
12609
+ let result;
12610
+ try {
12611
+ result = ladderOf(node, name);
12612
+ } finally {
12613
+ node.pending.delete(name);
12614
+ inEvaluation.pop();
12615
+ }
12616
+ if (!sameApproximation(value === void 0 ? ok(rule.start) : value.result, result)) point.changed = true;
12617
+ value = {
12618
+ result,
12619
+ round: point.round
12620
+ };
12621
+ approximationsOf(point, node).set(name, value);
12622
+ }
12623
+ readApproximation(point);
12624
+ return value.result;
12625
+ }
12626
+ /**
12627
+ * TREE.ONCE, TREE.MEMO, TREE.CYCLE and TREE.CIRCULAR: the result of one attribute, or `undefined` when no rung of its
12628
+ * ladder answers. A result that read an approximation of an open fixed point is not kept.
12629
+ */
12128
12630
  function attributeOf(node, name) {
12129
12631
  const done = node.values.get(name);
12130
12632
  if (done !== void 0) return done;
12131
12633
  if (node.pending.has(name)) {
12132
12634
  const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
12635
+ const point = refPoint;
12636
+ if (point !== void 0 && from >= point.depth && inEvaluation.slice(from).every((entry) => entry.circular)) {
12637
+ readApproximation(point);
12638
+ const kept = point.values.get(node)?.get(name);
12639
+ if (kept !== void 0) return kept.result;
12640
+ const rule = circularRuleOf(node, name);
12641
+ return ok(rule?.ok === true ? rule.start : null);
12642
+ }
12133
12643
  return fail("#CYCLE!", `The attributes form a cycle: ${[...from < 0 ? [nameOfAttribute(node, name)] : inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name)), nameOfAttribute(node, name)].join(" -> ")}.`);
12134
12644
  }
12135
- node.pending.add(name);
12136
- inEvaluation.push({
12645
+ const rule = circularRuleOf(node, name);
12646
+ if (rule !== void 0 && !rule.ok) {
12647
+ const bad = fail("#VALUE!", `The attribute ${nameOfAttribute(node, name)} has a bad declaration circular. ${rule.reason}`);
12648
+ node.values.set(name, bad);
12649
+ return bad;
12650
+ }
12651
+ const point = refPoint;
12652
+ if (point !== void 0 && rule !== void 0) return roundOf(point, node, name, rule);
12653
+ const entry = {
12137
12654
  node,
12138
- name
12139
- });
12655
+ name,
12656
+ circular: rule !== void 0,
12657
+ tentative: false
12658
+ };
12659
+ node.pending.add(name);
12660
+ inEvaluation.push(entry);
12661
+ let result;
12140
12662
  try {
12141
- for (const [, rung] of REF_RUNGS) {
12142
- const result = rung(node, name);
12143
- if (result === void 0) continue;
12144
- node.values.set(name, result);
12145
- return result;
12146
- }
12147
- return;
12663
+ result = rule === void 0 ? ladderOf(node, name) : fixedPointOf(node, name, rule);
12148
12664
  } finally {
12149
12665
  node.pending.delete(name);
12150
12666
  inEvaluation.pop();
12151
12667
  }
12668
+ if (!entry.tentative && result !== void 0) node.values.set(name, result);
12669
+ return result;
12152
12670
  }
12153
12671
  /** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
12154
12672
  function jsonOfNode(node, self, open) {
@@ -16814,6 +17332,6 @@ function samplesAt(values, closed, depth) {
16814
17332
  function typeFromSamples(values, options = {}) {
16815
17333
  return samplesAt(values, options.closed === true, 0);
16816
17334
  }
16817
- export { ATTRIBUTE_RUNGS, CAPS_TYPE, CONSTRUCTS, ENVELOPE_KEYS, Failure, Held, JOURNAL, MergeError, PARTS, PART_INFO, ROOTS, RUNGS, STD_CAPS, STD_DEFINITIONS, STD_DOMAINS, STD_EVALUATORS, STD_EXT, STD_MACHINE, STD_RUNNERS, STD_SYNTAX, STD_WALKERS, STD_WORLD, THING, WORLD_SYNTAX, agree, agreeRun, agreeSchedule, ask, attributeGraph, attributeTrace, attributeVertexId, capKeys, check, checkLatent, checkSteps, checkWorld, classify, components, conforms, context, cycles, dataEdges, decodeDomain, decodeLog, decodeValue, derefStep, describe, docEquals, durable, edgesOf, effectEdges, effectsOf, effectsOfVerb, encodeLog, encodeValue, entriesAsJson, equationType, evaluate, extendDomain, factsOf, fail, failures, field, fits, flatten, formulaDefinitions, formulaEvaluators, formulaFacts, formulaKinds, formulaKnown, formulaMachine, fromJson, generate, heights, intersectCaps, isAlias, isAllowedOrder, isClosure, isEnvelope, isHostThing, isKey, isKind, isNamespace, isRecord, isRef, isStackOverflow, isTypeRef, join, jsTruthy, kindDocument, kindNamed, kindOf, layers, looksLikeKind, lookupKind, makeContext, mapRefs, member, mergeMachines, mergeWorlds, narrow, nearestWalker, necessary, nodeAt, nodeLayers, ok, parse, partCaps, partsOf, pathMatches, permits, placeOf, placed, planner, prepare, print, printReads, random, recorder, ref, reference, referenceRun, refsOf, replayer, resolve, restrictMachine, restrictWorld, run, sameResult, sameValue, schedule, splitParts, stored, syntaxFormat, tag, thrown, toJson, treeNodeEvaluator, typeCheck, typeFromSamples, typeFromSchema, typeIssues, typeName, typePath, typeText, typed, typesOf, valueOfThing, verifyReads, walk, worldThing };
17335
+ export { ATTRIBUTE_RUNGS, CAPS_TYPE, CIRCULAR_LIMIT, CONSTRUCTS, ENVELOPE_KEYS, Failure, Held, JOURNAL, MergeError, PARTS, PART_INFO, ROOTS, RUNGS, STD_CAPS, STD_DEFINITIONS, STD_DOMAINS, STD_EVALUATORS, STD_EXT, STD_MACHINE, STD_RUNNERS, STD_SYNTAX, STD_WALKERS, STD_WORLD, THING, WORLD_SYNTAX, agree, agreeRun, agreeSchedule, ask, attributeGraph, attributeTrace, attributeVertexId, capKeys, check, checkLatent, checkSteps, checkWorld, circularOf, classify, components, conforms, context, cycles, dataEdges, decodeDomain, decodeLog, decodeValue, derefStep, describe, docEquals, durable, edgesOf, effectEdges, effectsOf, effectsOfVerb, encodeLog, encodeValue, entriesAsJson, equationType, evaluate, extendDomain, factsOf, fail, failures, field, fits, flatten, formulaDefinitions, formulaEvaluators, formulaFacts, formulaKinds, formulaKnown, formulaMachine, fromJson, generate, heights, intersectCaps, isAlias, isAllowedOrder, isClosure, isEnvelope, isHostThing, isKey, isKind, isNamespace, isRecord, isRef, isStackOverflow, isTypeRef, join, jsTruthy, kindDocument, kindNamed, kindOf, layers, looksLikeKind, lookupKind, makeContext, mapRefs, member, mergeMachines, mergeWorlds, narrow, nearestWalker, necessary, nodeAt, nodeLayers, ok, parse, partCaps, partsOf, pathMatches, permits, placeOf, placed, planner, prepare, print, printReads, random, recorder, ref, reference, referenceRun, refsOf, replayer, resolve, restrictMachine, restrictWorld, run, sameResult, sameValue, schedule, splitParts, stored, syntaxFormat, tag, thrown, toJson, treeNodeEvaluator, typeCheck, typeFromSamples, typeFromSchema, typeIssues, typeName, typePath, typeText, typed, typesOf, valueOfThing, verifyReads, walk, worldThing };
16818
17336
 
16819
17337
  //# sourceMappingURL=index.js.map