@mark1russell7/coeval 0.20.1 → 0.22.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/providers.ts CHANGED
@@ -33,13 +33,106 @@ const NOTES: ReadonlySet<string> = /* @__PURE__ */ new Set([
33
33
  ]);
34
34
 
35
35
  /** The keywords that the reading reads. Each other keyword gives an issue and tests nothing. */
36
- const READ: ReadonlySet<string> = /* @__PURE__ */ new Set(["type", "properties", "required", "additionalProperties", "items", "enum", "const", "anyOf", "oneOf"]);
36
+ const READ: ReadonlySet<string> = /* @__PURE__ */ new Set([
37
+ "type",
38
+ "properties",
39
+ "required",
40
+ "additionalProperties",
41
+ "items",
42
+ "enum",
43
+ "const",
44
+ "anyOf",
45
+ "oneOf",
46
+ "allOf",
47
+ "$ref",
48
+ ]);
49
+
50
+ /**
51
+ * The keywords that can permit more values than the other keywords permit. A dynamic reference can reach a schema
52
+ * outside this schema, and `nullable` of OpenAPI 3.0 permits `null`. Thus a schema with one of them reads as `unknown`.
53
+ */
54
+ const WIDENING: ReadonlySet<string> = /* @__PURE__ */ new Set(["$dynamicRef", "$recursiveRef", "nullable"]);
55
+
56
+ /** The keywords of an object without the keyword `type`. They test only the values that are records. */
57
+ const OBJECT_KEYS: readonly string[] = ["properties", "required", "additionalProperties"];
58
+
59
+ /** The form of a local reference: a definition in `$defs` or `definitions` of the root schema. */
60
+ const LOCAL_REF = /^#\/(\$defs|definitions)\/([^/]*)$/;
61
+
62
+ /** The most pairs of members that the meet of two unions builds. A larger meet keeps the first type, which is sound. */
63
+ const MAX_MEET_PAIRS = 64;
64
+
65
+ /** The keywords of one JSON type without the keyword `type`: they test only the records or only the lists. */
66
+ interface Condition {
67
+ readonly on: "record" | "list";
68
+ readonly type: ValueType;
69
+ readonly at: readonly Key[];
70
+ }
71
+
72
+ /** A record type of the reading. */
73
+ type RecordType = Extract<ValueType, { readonly record: unknown }>;
74
+
75
+ /** This function tells if a field of a record type can be absent. */
76
+ function isOptional(type: RecordType, key: string): boolean {
77
+ return type.optional?.includes(key) === true;
78
+ }
37
79
 
38
80
  /**
39
- * The keywords that can permit more values than the other keywords permit. Older drafts ignore each keyword beside
40
- * `$ref`, and `nullable` of OpenAPI 3.0 permits `null`. Thus a schema with one of them reads as `unknown`.
81
+ * This function gives the meet of two types of a reading: a type that holds each value of both types. A schema is
82
+ * the conjunction of its keywords, thus its type is the meet of their types. The meet is exact for the forms that a
83
+ * reading makes: base types, lists, records, unions and `one`. A meet of two different forms is `never`.
41
84
  */
42
- const WIDENING: ReadonlySet<string> = /* @__PURE__ */ new Set(["$ref", "$dynamicRef", "$recursiveRef", "nullable"]);
85
+ function meet(a: ValueType, b: ValueType, depth: number): ValueType {
86
+ if (depth > MAX_SCHEMA_DEPTH) return a;
87
+ if (isUnknown(a, undefined)) return b;
88
+ if (isUnknown(b, undefined)) return a;
89
+ if (a === "never" || b === "never") return "never";
90
+ if (typeof a !== "string" && "one" in a) return { one: a.one.filter((value) => member(value, b)) };
91
+ if (typeof b !== "string" && "one" in b) return { one: b.one.filter((value) => member(value, a)) };
92
+ if (typeof b !== "string" && "union" in b && !(typeof a !== "string" && "union" in a)) return meet(b, a, depth);
93
+ if (typeof a !== "string" && "union" in a) {
94
+ const others = typeof b !== "string" && "union" in b ? b.union : [b];
95
+ if (a.union.length * others.length > MAX_MEET_PAIRS) return a;
96
+ return unionOf(a.union.flatMap((item) => others.map((other) => meet(item, other, depth + 1))));
97
+ }
98
+ if (typeof a === "string" || typeof b === "string") return a === b ? a : "never";
99
+ if ("list" in a && "list" in b) return { list: meet(a.list, b.list, depth + 1) };
100
+ if ("record" in a && "record" in b) return meetRecords(a, b, depth + 1);
101
+ // A list and a record have no common value. Each other pair keeps the first type, which is sound.
102
+ return ("list" in a || "record" in a) && ("list" in b || "record" in b) ? "never" : a;
103
+ }
104
+
105
+ /**
106
+ * This function gives the meet of two record types. A field of both records gets the meet of its types, and it is
107
+ * optional only when both records permit its absence. A closed record has no other field, thus a field of only the
108
+ * other record is absent. When the other record needs that field, no value fits both records.
109
+ */
110
+ function meetRecords(a: RecordType, b: RecordType, depth: number): ValueType {
111
+ const fields: Record<string, ValueType> = {};
112
+ const optional: string[] = [];
113
+ const keys = [...Object.keys(a.record), ...Object.keys(b.record).filter((key) => !Object.hasOwn(a.record, key))];
114
+ for (const key of keys) {
115
+ const inA = Object.hasOwn(a.record, key);
116
+ const inB = Object.hasOwn(b.record, key);
117
+ if ((!inA && a.closed === true) || (!inB && b.closed === true)) {
118
+ if ((inA && !isOptional(a, key)) || (inB && !isOptional(b, key))) return "never";
119
+ continue;
120
+ }
121
+ const type = inA && inB ? meet(a.record[key] as ValueType, b.record[key] as ValueType, depth) : ((inA ? a.record[key] : b.record[key]) as ValueType);
122
+ setOwn(fields, key, type);
123
+ if ((!inA || isOptional(a, key)) && (!inB || isOptional(b, key))) optional.push(key);
124
+ }
125
+ const closed = a.closed === true || b.closed === true;
126
+ return { record: fields, ...(optional.length === 0 ? {} : { optional }), ...(closed ? { closed: true } : {}) };
127
+ }
128
+
129
+ /** This function applies a condition to a type: each member of the JSON type of the condition meets its type. */
130
+ function narrow(type: ValueType, condition: Condition): ValueType {
131
+ if (typeof type === "string") return type;
132
+ if ("union" in type) return unionOf(type.union.map((item) => narrow(item, condition)));
133
+ if ("one" in type) return { one: type.one.filter((value) => !(condition.on === "list" ? Array.isArray(value) : isRecord(value)) || member(value, condition.type)) };
134
+ return condition.on in type ? meet(type, condition.type, 0) : type;
135
+ }
43
136
 
44
137
  /** The base type of each name of the keyword `type`. */
45
138
  const BASES: Readonly<Record<string, ValueType>> = { null: "null", boolean: "bool", number: "number", integer: "number", string: "text" };
@@ -55,25 +148,55 @@ function unionOf(types: readonly ValueType[]): ValueType {
55
148
  return types.reduce<ValueType>((all, type) => join(all, type), "never");
56
149
  }
57
150
 
58
- /** The state of one reading of a schema: the issues that it found. */
151
+ /** The state of one reading of a schema: the root, the issues and the definitions that it read. */
59
152
  class SchemaReader {
60
153
  readonly issues: TypeIssue[] = [];
154
+ /** The type of each definition that a reference reached. Each reading of a definition is sound, thus the first stays. */
155
+ readonly defs = new Map<string, ValueType>();
156
+ /** The definitions that the reading has not finished. A reference to one of them is a cycle. */
157
+ readonly open = new Set<string>();
158
+ /** The number of schemas with an own `$id` around the current schema. A reference inside them has another base. */
159
+ inner = 0;
160
+
161
+ constructor(readonly root: unknown) {}
61
162
 
62
163
  issue(at: readonly Key[], reason: string): void {
63
164
  this.issues.push({ at, reason });
64
165
  }
65
166
 
66
- /** This method reads one schema. */
167
+ /** This method reads one schema: the meet of the types of its keywords, then its conditions. */
67
168
  read(schema: unknown, at: readonly Key[], depth: number): ValueType {
169
+ const types: ValueType[] = [];
170
+ const conditions: Condition[] = [];
171
+ this.parts(schema, at, depth, types, conditions);
172
+ let type = types.reduce<ValueType>((all, item) => meet(all, item, 0), "unknown");
173
+ for (const condition of conditions) {
174
+ if (isUnknown(type, undefined)) {
175
+ this.issue(condition.at, "The schema has no keyword type. Thus a value of each JSON type fits it, and the type reads it as unknown.");
176
+ break;
177
+ }
178
+ type = narrow(type, condition);
179
+ }
180
+ return type;
181
+ }
182
+
183
+ /**
184
+ * This method adds the type of each keyword of a schema to `types`, and its conditions to `conditions`. Each type
185
+ * holds each value that the schema accepts. The parts of `allOf` add their keywords too.
186
+ */
187
+ parts(schema: unknown, at: readonly Key[], depth: number, types: ValueType[], conditions: Condition[]): void {
68
188
  if (depth > MAX_SCHEMA_DEPTH) {
69
189
  this.issue(at, `The schema is deeper than ${MAX_SCHEMA_DEPTH} levels. The type reads it as unknown.`);
70
- return "unknown";
190
+ return;
191
+ }
192
+ if (schema === true) return;
193
+ if (schema === false) {
194
+ types.push("never");
195
+ return;
71
196
  }
72
- if (schema === true) return "unknown";
73
- if (schema === false) return "never";
74
197
  if (!isRecord(schema)) {
75
198
  this.issue(at, "A schema is a record or a truth value. The type reads this value as unknown.");
76
- return "unknown";
199
+ return;
77
200
  }
78
201
  let widens = false;
79
202
  for (const key of Object.keys(schema)) {
@@ -85,31 +208,85 @@ class SchemaReader {
85
208
  this.issue([...at, key], `The type does not read the keyword ${key}. It does not test what the keyword tests.`);
86
209
  }
87
210
  }
88
- if (widens) return "unknown";
89
- // A schema is the conjunction of its keywords. The reading keeps one of them, and each one alone holds each value.
90
- const sides: { readonly key: string; readonly type: ValueType }[] = [];
91
- if (Object.hasOwn(schema, "type")) sides.push({ key: "type", type: this.byType(schema, at, depth) });
92
- else if (["properties", "required", "additionalProperties", "items"].some((key) => Object.hasOwn(schema, key)) && !["enum", "const", "anyOf", "oneOf"].some((key) => Object.hasOwn(schema, key))) {
93
- this.issue(at, "The schema has no keyword type. Thus a value of each JSON type fits it, and the type reads it as unknown.");
211
+ if (widens) return;
212
+ const nested = schema !== this.root && Object.hasOwn(schema, "$id");
213
+ if (nested) this.inner += 1;
214
+ try {
215
+ this.keywords(schema, at, depth, types, conditions);
216
+ } finally {
217
+ if (nested) this.inner -= 1;
218
+ }
219
+ }
220
+
221
+ /** This method reads the keywords of a schema that does not permit more values (WIDENING). */
222
+ keywords(schema: Readonly<Record<string, unknown>>, at: readonly Key[], depth: number, types: ValueType[], conditions: Condition[]): void {
223
+ if (Object.hasOwn(schema, "$ref")) {
224
+ // Older drafts ignore each keyword beside $ref. The target alone holds each value in each draft.
225
+ for (const key of Object.keys(schema)) if (READ.has(key) && key !== "$ref") this.issue([...at, key], `The type reads only the target of $ref. It does not test the keyword ${key} beside it.`);
226
+ const type = this.target(schema["$ref"], [...at, "$ref"], depth);
227
+ if (type !== undefined) types.push(type);
228
+ return;
229
+ }
230
+ if (Object.hasOwn(schema, "type")) types.push(this.byType(schema, at, depth));
231
+ else {
232
+ if (OBJECT_KEYS.some((key) => Object.hasOwn(schema, key))) conditions.push({ on: "record", type: this.object(schema, at, depth), at });
233
+ if (Object.hasOwn(schema, "items")) conditions.push({ on: "list", type: this.array(schema, at, depth), at });
94
234
  }
95
- for (const key of ["anyOf", "oneOf"]) {
235
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
96
236
  if (!Object.hasOwn(schema, key)) continue;
97
237
  const items = schema[key];
98
238
  if (!Array.isArray(items)) {
99
239
  this.issue([...at, key], `The value of ${key} is a list of schemas.`);
100
240
  continue;
101
241
  }
242
+ // Each part of allOf holds each value of the schema, thus its keywords join the keywords of the schema.
243
+ if (key === "allOf") for (const [index, item] of items.entries()) this.parts(item, [...at, key, index], depth + 1, types, conditions);
102
244
  // A union holds each value of oneOf too. It does not test that only one schema fits.
103
- sides.push({ key, type: unionOf(items.map((item, index) => this.read(item, [...at, key, index], depth + 1))) });
245
+ else types.push(unionOf(items.map((item, index) => this.read(item, [...at, key, index], depth + 1))));
104
246
  }
105
- // A finite set of values meets each other keyword exactly: the type keeps each value that each side holds.
106
247
  const values = this.values(schema, at);
107
- if (values !== undefined) return { one: values.filter((value) => sides.every((side) => member(value, side.type))) };
108
- const known = sides.filter((side) => !isUnknown(side.type, undefined));
109
- const kept = known[0];
110
- if (kept === undefined) return "unknown";
111
- for (const side of known.slice(1)) this.issue([...at, side.key], `The type reads the keyword ${kept.key}, and not the keyword ${side.key}.`);
112
- return kept.type;
248
+ if (values !== undefined) types.push({ one: values });
249
+ }
250
+
251
+ /** This method reads the target of a reference: a definition of the root schema, or `undefined` for each value. */
252
+ target(ref: unknown, at: readonly Key[], depth: number): ValueType | undefined {
253
+ const match = typeof ref === "string" ? LOCAL_REF.exec(ref) : null;
254
+ let name: string | undefined;
255
+ try {
256
+ // The name is a step of a JSON Pointer in a URI fragment.
257
+ name = match === null ? undefined : decodeURIComponent(match[2] ?? "").replace(/~1/g, "/").replace(/~0/g, "~");
258
+ } catch {
259
+ name = undefined;
260
+ }
261
+ if (match === null || name === undefined) {
262
+ this.issue(at, "The type reads only a $ref to #/$defs/<name> or #/definitions/<name> of the same schema. Thus it reads the schema as unknown.");
263
+ return undefined;
264
+ }
265
+ if (this.inner > 0) {
266
+ this.issue(at, "A $ref inside a schema with its own $id has another base. Thus the type reads the schema as unknown.");
267
+ return undefined;
268
+ }
269
+ const group = match[1] as string;
270
+ const defs = isRecord(this.root) ? this.root[group] : undefined;
271
+ if (!isRecord(defs) || !Object.hasOwn(defs, name)) {
272
+ this.issue(at, `The schema has no definition ${String(ref)}. Thus the type reads the reference as unknown.`);
273
+ return undefined;
274
+ }
275
+ const key = `${group}/${name}`;
276
+ const known = this.defs.get(key);
277
+ if (known !== undefined) return known;
278
+ if (this.open.has(key)) {
279
+ this.issue(at, `The reference ${String(ref)} is in a cycle. The type reads this reference as unknown.`);
280
+ return undefined;
281
+ }
282
+ this.open.add(key);
283
+ try {
284
+ const type = this.read(defs[name], [group, name], depth + 1);
285
+ this.defs.set(key, type);
286
+ return type;
287
+ } finally {
288
+ this.open.delete(key);
289
+ }
113
290
  }
114
291
 
115
292
  /** This method reads `enum` and `const`: the values that the schema permits, or `undefined` without them. */
@@ -196,17 +373,20 @@ class SchemaReader {
196
373
  * - `properties`, `required` and `additionalProperties` give a record. `false` closes it, and a property that is not
197
374
  * in `required` is optional.
198
375
  * - `items` gives a list. `enum` and `const` give `one`. `anyOf` and `oneOf` give a union.
376
+ * - `$ref` reads a definition in `$defs` or `definitions` of the same schema (TYPE.SCHEMA-REF). Another reference, a
377
+ * missing definition and a reference in a cycle read as `unknown`.
378
+ * - `allOf` and the other keywords of a schema give the meet of their types (TYPE.SCHEMA-ALLOF). Records merge their
379
+ * fields, and a closed part keeps the merge closed.
199
380
  *
200
381
  * The type holds each value that the schema accepts. Each keyword that the function does not read tests nothing, as
201
- * `unknown`, and the issues name it. Examples are `minimum`, `pattern` and `allOf`. A schema with `$ref` or `nullable`
202
- * reads as `unknown`, because these keywords can permit more values. A schema with more than one of `type`, `anyOf`
203
- * and `oneOf` gives the type of the first, and an issue for each other. The function does not throw.
382
+ * `unknown`, and the issues name it. Examples are `minimum` and `pattern`. A schema with `nullable` or `$dynamicRef`
383
+ * reads as `unknown`, because these keywords can permit more values. The function does not throw.
204
384
  *
205
385
  * @param schema - The schema, as JSON.
206
386
  * @returns The type, and the problems of the reading with their keys in the schema.
207
387
  */
208
388
  export function typeFromSchema(schema: unknown): TypeReading {
209
- const reader = new SchemaReader();
389
+ const reader = new SchemaReader(schema);
210
390
  try {
211
391
  return { type: reader.read(schema, [], 0), issues: reader.issues };
212
392
  } catch (error) {
package/src/reference.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type Envelope, type Json, type Key, docEquals, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
2
  import { ask } from "./ladder.ts";
3
- import { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, circularOf, equationType, lookupKind } from "./world.ts";
3
+ import { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, atArgsOf, atRuleOf, circularOf, collectOf, 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
- import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
6
+ import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
7
7
  import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
8
8
  import {
9
9
  type AllOptions,
@@ -113,6 +113,8 @@ interface Env {
113
113
  scope: Scope;
114
114
  /** The count of the drives without a wait that are active. In a run, an effect under one does not reach the host (RUN.HELD). */
115
115
  held: number;
116
+ /** The count of the applications of closures that run at this time: the clock of `call` (FORMULA.CALL-DEPTH, D125). */
117
+ applying: number;
116
118
  /** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
117
119
  readonly reading: Reading | undefined;
118
120
  }
@@ -674,6 +676,7 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
674
676
  verb: options.verb ?? "eval",
675
677
  scope: { prefix: "", counts: new Map(), alls: 0 },
676
678
  held: 0,
679
+ applying: 0,
677
680
  reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
678
681
  };
679
682
  }
@@ -754,6 +757,8 @@ interface RefTree {
754
757
  interface RefNode {
755
758
  readonly tree: RefTree;
756
759
  readonly kind: string;
760
+ /** The binding name from the header of the document of the node (TREE.NAMED). */
761
+ readonly name: string | undefined;
757
762
  readonly place: readonly Key[];
758
763
  readonly parts: readonly Part[];
759
764
  readonly ctx: Context;
@@ -831,6 +836,52 @@ function foldOf(node: RefNode, name: string, fold: NonNullable<AttributeDeclarat
831
836
  return acc;
832
837
  }
833
838
 
839
+ /**
840
+ * TREE.COLLECT and TREE.NAMED: the node, then each node of this tree in each list `children`, in the order of the
841
+ * lists. Each node counts one time. The registry `names` maps each binding name to its node.
842
+ */
843
+ function collectIn(node: RefNode, name: string, declared: unknown): Result {
844
+ const rule = collectOf(declared);
845
+ if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, name)} has a bad declaration collect. ${rule.reason}`);
846
+ const registry: Record<string, unknown> = {};
847
+ const first = new Map<string, RefNode>();
848
+ let acc: Result = rule.names ? ok(registry) : equationOf(node, rule.unit, {});
849
+ if (!acc.ok) return acc;
850
+ const done: RefNode[] = [];
851
+ // The contribution of a node comes before the nodes of its list children. A failure ends the collection.
852
+ const visit = (at: RefNode): Result | undefined => {
853
+ if (done.includes(at)) return undefined;
854
+ done.push(at);
855
+ if (rule.names && at.name !== undefined) {
856
+ const earlier = first.get(at.name);
857
+ if (earlier !== undefined) {
858
+ return fail("#VALUE!", `Two nodes have the name ${at.name} in the registry ${nameOfAttribute(node, name)}: ${earlier.kind} at [${earlier.place.join(", ")}] and ${at.kind} at [${at.place.join(", ")}].`);
859
+ }
860
+ first.set(at.name, at);
861
+ Object.defineProperty(registry, at.name, { value: at.thing, enumerable: true, writable: true, configurable: true });
862
+ }
863
+ const term = rule.names ? undefined : termIn(at.contract?.contributes, name);
864
+ if (!rule.names && term !== undefined) {
865
+ const item = equationOf(at, term, {});
866
+ if (!item.ok) return item;
867
+ acc = equationOf(node, rule.combine, { acc: acc.ok ? acc.value : null, item: item.value });
868
+ if (!acc.ok) return acc;
869
+ }
870
+ const children = attributeOf(at, "children");
871
+ if (children === undefined) return undefined;
872
+ if (!children.ok) return children;
873
+ if (!Array.isArray(children.value)) return fail("#VALUE!", `The collection of ${name} needs a list in children of ${at.kind} at [${at.place.join(", ")}].`);
874
+ for (const item of children.value) {
875
+ const child = refNodeOf(item);
876
+ if (child === undefined || child.tree !== node.tree) continue;
877
+ const failed = visit(child);
878
+ if (failed !== undefined) return failed;
879
+ }
880
+ return undefined;
881
+ };
882
+ return visit(node) ?? acc;
883
+ }
884
+
834
885
  /**
835
886
  * TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
836
887
  * answer, and the next rung then tries.
@@ -860,6 +911,7 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
860
911
  (node, name) => {
861
912
  const declared = termIn(node.contract?.attributes, name);
862
913
  if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
914
+ if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
863
915
  return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
864
916
  },
865
917
  ],
@@ -1020,6 +1072,53 @@ function attributeOf(node: RefNode, name: string): Result | undefined {
1020
1072
  return result;
1021
1073
  }
1022
1074
 
1075
+ /** TREE.AT: one instance of an attribute with parameters of a node. Its result is `undefined` while it evaluates. */
1076
+ interface RefInstance {
1077
+ readonly attr: string;
1078
+ readonly names: unknown;
1079
+ result: Result | undefined;
1080
+ }
1081
+
1082
+ /** TREE.AT: the instances of each node of the reference. Two arguments are the same instance by DOC.EQUALS. */
1083
+ const refInstances = /* @__PURE__ */ new WeakMap<RefNode, RefInstance[]>();
1084
+
1085
+ /**
1086
+ * TREE.AT: the instance of an attribute with parameters, or `undefined` when the kind has no entry in `tree.at`. The
1087
+ * equation sees the arguments as names. A demand of an instance in evaluation gives `#CYCLE!`, also in a fixed point.
1088
+ */
1089
+ function instanceOf(node: RefNode, attr: string, args: unknown): Result | undefined {
1090
+ const declared = termIn(node.contract?.at, attr);
1091
+ if (declared === undefined) return undefined;
1092
+ const rule = atRuleOf(declared);
1093
+ if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, attr)} has a bad entry tree.at. ${rule.reason}`);
1094
+ const given = atArgsOf(rule.params, args);
1095
+ if (!given.ok) return fail("#VALUE!", `The step tree.at of ${nameOfAttribute(node, attr)} has bad arguments. ${given.reason}`);
1096
+ const name = `${attr} with ${given.key}`;
1097
+ const list = refInstances.get(node) ?? [];
1098
+ refInstances.set(node, list);
1099
+ const found = list.find((item) => item.attr === attr && docEquals(item.names, given.names));
1100
+ if (found?.result !== undefined) return found.result;
1101
+ if (found !== undefined) {
1102
+ const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
1103
+ const steps = inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
1104
+ return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
1105
+ }
1106
+ const item: RefInstance = { attr, names: given.names, result: undefined };
1107
+ list.push(item);
1108
+ const entry = { node, name, circular: false, tentative: false };
1109
+ inEvaluation.push(entry);
1110
+ let result: Result | undefined;
1111
+ try {
1112
+ result = equationOf(node, rule.equation, given.names);
1113
+ } finally {
1114
+ inEvaluation.pop();
1115
+ // A result that read an approximation is not kept (TREE.CIRCULAR).
1116
+ if (result === undefined || entry.tentative) list.splice(list.indexOf(item), 1);
1117
+ else item.result = result;
1118
+ }
1119
+ return result;
1120
+ }
1121
+
1023
1122
  /** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
1024
1123
  function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
1025
1124
  const record: Record<string, unknown> = {};
@@ -1065,7 +1164,7 @@ function plainValue(value: unknown, open: readonly object[]): Result {
1065
1164
  * tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error. The node
1066
1165
  * reads `self` only through `resolve` (C4, TREE.SELF).
1067
1166
  */
1068
- const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk }) => {
1167
+ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk, doc }) => {
1069
1168
  const reached = walk(["self"]);
1070
1169
  const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
1071
1170
  const outer = refNodeOf(parent);
@@ -1076,6 +1175,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1076
1175
  const node: RefNode = {
1077
1176
  tree: nested ? outer.tree : { open: true },
1078
1177
  kind,
1178
+ name: doc.name,
1079
1179
  place,
1080
1180
  parts,
1081
1181
  ctx: context,
@@ -1107,6 +1207,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1107
1207
  };
1108
1208
  node.thing = self;
1109
1209
  refNodes.set(self, node);
1210
+ AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
1110
1211
  if (nested) return ok(self);
1111
1212
  try {
1112
1213
  return jsonOfNode(node, self, []);
@@ -1235,13 +1336,22 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
1235
1336
  }
1236
1337
  const names = params as readonly string[];
1237
1338
  const body = partNamed(parts, "body") as Part;
1339
+ // FORMULA.CALL-DEPTH: the evaluation that makes the closure counts its applications. A quarter of maxDepth is the limit.
1340
+ const clock = activeEnvs[activeEnvs.length - 1] ?? { applying: 0, maxDepth: 1000 };
1341
+ const limit = Math.ceil(clock.maxDepth / 4);
1238
1342
  const closure = (...args: readonly unknown[]): unknown => {
1239
- const scope: Record<string, unknown> = {};
1240
- for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
1241
- names.forEach((name, index) => setField(scope, name, args[index]));
1242
- const result = body.evaluate({ ...context, names: scope });
1243
- if (!result.ok) throw new Failure(result.error);
1244
- return result.value;
1343
+ if (clock.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
1344
+ clock.applying += 1;
1345
+ try {
1346
+ const scope: Record<string, unknown> = {};
1347
+ for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
1348
+ names.forEach((name, index) => setField(scope, name, args[index]));
1349
+ const result = body.evaluate({ ...context, names: scope });
1350
+ if (!result.ok) throw new Failure(result.error);
1351
+ return result.value;
1352
+ } finally {
1353
+ clock.applying -= 1;
1354
+ }
1245
1355
  };
1246
1356
  Object.defineProperty(closure, CLOSURE, { value: { params: names } });
1247
1357
  return ok(closure);