@mark1russell7/coeval 0.20.0 → 0.21.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,6 +1,6 @@
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, 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
6
  import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
@@ -754,6 +754,8 @@ interface RefTree {
754
754
  interface RefNode {
755
755
  readonly tree: RefTree;
756
756
  readonly kind: string;
757
+ /** The binding name from the header of the document of the node (TREE.NAMED). */
758
+ readonly name: string | undefined;
757
759
  readonly place: readonly Key[];
758
760
  readonly parts: readonly Part[];
759
761
  readonly ctx: Context;
@@ -831,6 +833,52 @@ function foldOf(node: RefNode, name: string, fold: NonNullable<AttributeDeclarat
831
833
  return acc;
832
834
  }
833
835
 
836
+ /**
837
+ * TREE.COLLECT and TREE.NAMED: the node, then each node of this tree in each list `children`, in the order of the
838
+ * lists. Each node counts one time. The registry `names` maps each binding name to its node.
839
+ */
840
+ function collectIn(node: RefNode, name: string, declared: unknown): Result {
841
+ const rule = collectOf(declared);
842
+ if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, name)} has a bad declaration collect. ${rule.reason}`);
843
+ const registry: Record<string, unknown> = {};
844
+ const first = new Map<string, RefNode>();
845
+ let acc: Result = rule.names ? ok(registry) : equationOf(node, rule.unit, {});
846
+ if (!acc.ok) return acc;
847
+ const done: RefNode[] = [];
848
+ // The contribution of a node comes before the nodes of its list children. A failure ends the collection.
849
+ const visit = (at: RefNode): Result | undefined => {
850
+ if (done.includes(at)) return undefined;
851
+ done.push(at);
852
+ if (rule.names && at.name !== undefined) {
853
+ const earlier = first.get(at.name);
854
+ if (earlier !== undefined) {
855
+ 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(", ")}].`);
856
+ }
857
+ first.set(at.name, at);
858
+ Object.defineProperty(registry, at.name, { value: at.thing, enumerable: true, writable: true, configurable: true });
859
+ }
860
+ const term = rule.names ? undefined : termIn(at.contract?.contributes, name);
861
+ if (!rule.names && term !== undefined) {
862
+ const item = equationOf(at, term, {});
863
+ if (!item.ok) return item;
864
+ acc = equationOf(node, rule.combine, { acc: acc.ok ? acc.value : null, item: item.value });
865
+ if (!acc.ok) return acc;
866
+ }
867
+ const children = attributeOf(at, "children");
868
+ if (children === undefined) return undefined;
869
+ if (!children.ok) return children;
870
+ if (!Array.isArray(children.value)) return fail("#VALUE!", `The collection of ${name} needs a list in children of ${at.kind} at [${at.place.join(", ")}].`);
871
+ for (const item of children.value) {
872
+ const child = refNodeOf(item);
873
+ if (child === undefined || child.tree !== node.tree) continue;
874
+ const failed = visit(child);
875
+ if (failed !== undefined) return failed;
876
+ }
877
+ return undefined;
878
+ };
879
+ return visit(node) ?? acc;
880
+ }
881
+
834
882
  /**
835
883
  * TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
836
884
  * answer, and the next rung then tries.
@@ -860,6 +908,7 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
860
908
  (node, name) => {
861
909
  const declared = termIn(node.contract?.attributes, name);
862
910
  if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
911
+ if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
863
912
  return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
864
913
  },
865
914
  ],
@@ -1065,7 +1114,7 @@ function plainValue(value: unknown, open: readonly object[]): Result {
1065
1114
  * tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error. The node
1066
1115
  * reads `self` only through `resolve` (C4, TREE.SELF).
1067
1116
  */
1068
- const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk }) => {
1117
+ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk, doc }) => {
1069
1118
  const reached = walk(["self"]);
1070
1119
  const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
1071
1120
  const outer = refNodeOf(parent);
@@ -1076,6 +1125,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1076
1125
  const node: RefNode = {
1077
1126
  tree: nested ? outer.tree : { open: true },
1078
1127
  kind,
1128
+ name: doc.name,
1079
1129
  place,
1080
1130
  parts,
1081
1131
  ctx: context,
package/src/static.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Envelope, type Json, type Key, isKey, isRecord, typeName } from "./doc.ts";
2
- import { type Kind, type World, type WorldEntry, circularOf, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
3
- import { ask } from "./ladder.ts";
4
- import { type PartSlot, partsOf, scopesOf } from "./parts.ts";
2
+ import { type Kind, type LazyRule, type World, type WorldEntry, circularOf, collectOf, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
3
+ import { answerOf, ask } from "./ladder.ts";
4
+ import { type PartSlot, partsOf, rulesCover, scopesOf } from "./parts.ts";
5
5
  import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
6
6
  import type { ErrorCode } from "./errors.ts";
7
7
  import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
@@ -405,6 +405,19 @@ export interface Typing {
405
405
  readonly places: readonly Typed[];
406
406
  }
407
407
 
408
+ /**
409
+ * This function gives the parts that the code of a runner evaluates itself, without a wait (RUN.DRIVES, D119). The
410
+ * answer of the hook `facts` for the code wins over the contract. The contract describes only the code of its kind for
411
+ * `eval`. Without a declaration, the runner passes each wait of its parts through.
412
+ */
413
+ function driveRules(machine: Machine, v: DocVisit, kind: Kind, name: string, rung: string | undefined, verb: string): readonly LazyRule[] | undefined {
414
+ if (verb !== "eval") return undefined;
415
+ const hook = machine.facts?.(v.typePath ?? [], name)?.drives;
416
+ if (hook !== undefined) return hook;
417
+ const described = v.declared || rung !== "own" || name === answerOf(machine.world, undefined, kind, verb)?.name;
418
+ return described ? kind.drives : undefined;
419
+ }
420
+
408
421
  /** The least authority: the capability `[]` permits each effect (CAP.KEYS). */
409
422
  const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
410
423
 
@@ -412,16 +425,42 @@ const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
412
425
  const NO_EFFECTS: readonly (readonly Key[])[] = /* @__PURE__ */ Object.freeze([]);
413
426
 
414
427
  /**
415
- * This function gives the effects that the contract of the kind of a visit declares for a verb, as keys
416
- * (FACTS.EFFECTS, VERB.EFFECTS). The text `"type"` gives the type of the document after each alias. A visit without a
417
- * kind declares no effect. `check`, `factsOf` and `effectsOf` read the declarations with this one function (D27).
428
+ * This function gives the effects that the code of a visit declares for a verb, as keys (FACTS.EFFECTS, VERB.EFFECTS).
429
+ * The text `"type"` gives the type of the document after each alias. A visit without a kind declares no effect.
430
+ * `check`, `factsOf`, `effectsOf` and `checkLatent` read the declarations with this one function (D27).
431
+ *
432
+ * For `eval`, the answer of the hook `facts` of the machine for the code of the visit wins over the contract of the
433
+ * kind (CAP.OWN-EFFECTS, D118). Thus a language gives the effects of an own entry for another evaluator. Without a
434
+ * machine, or without the field `effects` in the answer, the contract gives the effects.
418
435
  *
419
436
  * @param v - The visit.
420
437
  * @param verb - The verb. The default is `eval`. A kind without a declaration for the verb gives no effect.
421
- * @returns The keys of each declared effect, in the order of the contract.
438
+ * @param machine - The machine with the hook `facts`, when the caller has it.
439
+ * @param name - The name of the evaluator of the visit, when the caller knows it. Else the ladder gives it.
440
+ * @returns The keys of each declared effect, in the order of the declaration.
422
441
  */
423
- export function declaredEffects(v: DocVisit, verb = "eval"): readonly (readonly Key[])[] {
424
- return verbDeclared(v, verb) ?? NO_EFFECTS;
442
+ export function declaredEffects(v: DocVisit, verb = "eval", machine?: Machine, name?: string): readonly (readonly Key[])[] {
443
+ return hookEffects(v, verb, machine, name) ?? verbDeclared(v, verb) ?? NO_EFFECTS;
444
+ }
445
+
446
+ /**
447
+ * This function gives the effects that the hook `facts` gives for the code of a visit, as keys (D118). The result is
448
+ * `undefined` for another verb than `eval`, without the hook, or when the answer has no field `effects`.
449
+ */
450
+ function hookEffects(v: DocVisit, verb: string, machine: Machine | undefined, name: string | undefined): readonly (readonly Key[])[] | undefined {
451
+ if (verb !== "eval" || machine?.facts === undefined || v.kind === undefined) return undefined;
452
+ const code = name ?? answerOf(machine.world, v.declared ? undefined : v.doc.own, v.kind, "eval")?.name;
453
+ const effects = code === undefined ? undefined : machine.facts(v.typePath ?? [], code)?.effects;
454
+ return effects === undefined ? undefined : effectKeys(v, effects);
455
+ }
456
+
457
+ /**
458
+ * This function gives the keys of a declaration of effects at a visit (D27). The text `"type"` gives the type of the
459
+ * document after each alias.
460
+ */
461
+ export function effectKeys(v: DocVisit, effects: "type" | readonly string[]): readonly (readonly Key[])[] {
462
+ if (effects.length === 0) return NO_EFFECTS;
463
+ return effects === "type" ? [v.typePath ?? []] : effects.map((kind) => kind.split("."));
425
464
  }
426
465
 
427
466
  /**
@@ -433,8 +472,7 @@ function verbDeclared(v: DocVisit, verb: string): readonly (readonly Key[])[] |
433
472
  if (v.kind === undefined) return verb === "eval" ? NO_EFFECTS : undefined;
434
473
  const effects = effectsOfVerb(v.kind, verb);
435
474
  if (effects === undefined) return undefined;
436
- if (effects.length === 0) return NO_EFFECTS;
437
- return effects === "type" ? [v.typePath ?? []] : effects.map((kind) => kind.split("."));
475
+ return effectKeys(v, effects);
438
476
  }
439
477
 
440
478
  /**
@@ -637,7 +675,8 @@ const FOLD_NAMES: readonly string[] = /* @__PURE__ */ Object.freeze(["acc", "ite
637
675
 
638
676
  /**
639
677
  * This function gives each equation of the tree contract of a kind, with the names that it sees (D42). An equation of
640
- * `equations`, a `unit` and a `default` see no names. `check` and `factsOf` read the equations with this one function.
678
+ * `equations`, a `unit`, a `default` and a contribution see no names. `check` and `factsOf` read the equations with
679
+ * this one function.
641
680
  */
642
681
  export function equationsOf(kind: Kind): readonly Equation[] {
643
682
  const tree = kind.tree;
@@ -651,7 +690,14 @@ export function equationsOf(kind: Kind): readonly Equation[] {
651
690
  out.push({ label: `tree.attributes.${name}.fold.combine`, term: declared.fold.combine, names: FOLD_NAMES, attribute: name, down: false });
652
691
  }
653
692
  if (declared.default !== undefined) out.push({ label: `tree.attributes.${name}.default`, term: declared.default, names: [], attribute: name, down: false });
693
+ const collect = declared.collect === undefined ? undefined : collectOf(declared.collect);
694
+ if (collect?.ok === true && !collect.names) {
695
+ out.push({ label: `tree.attributes.${name}.collect.unit`, term: collect.unit, names: [], attribute: name, down: false });
696
+ out.push({ label: `tree.attributes.${name}.collect.combine`, term: collect.combine, names: FOLD_NAMES, attribute: name, down: false });
697
+ }
654
698
  }
699
+ // A contribution is not the attribute of its node, thus an own part of its name does not hide it (D122).
700
+ for (const [name, term] of Object.entries(tree.contributes ?? {})) out.push({ label: `tree.contributes.${name}`, term, names: [], attribute: name, down: false, contributes: true });
655
701
  return out;
656
702
  }
657
703
 
@@ -665,6 +711,8 @@ export interface Equation {
665
711
  readonly attribute: string;
666
712
  /** `true` for an equation of `down`: it gives the attribute of a child. */
667
713
  readonly down: boolean;
714
+ /** `true` for an equation of `contributes`: it gives a contribution to a collection (TREE.COLLECT). */
715
+ readonly contributes?: true;
668
716
  }
669
717
 
670
718
  /**
@@ -837,7 +885,7 @@ export function answered(world: World, node: SeenNode, name: string): boolean {
837
885
  if (hasEntry(tree?.equations, name)) return true;
838
886
  if (node.parent === undefined ? downNames(world).has(name) : hasEntry(node.parent.kind.tree?.down, name)) return true;
839
887
  const declared = tree?.attributes !== undefined && Object.hasOwn(tree.attributes, name) ? tree.attributes[name] : undefined;
840
- return declared !== undefined && (declared.copy === true || declared.fold !== undefined || declared.default !== undefined);
888
+ return declared !== undefined && (declared.copy === true || declared.fold !== undefined || declared.collect !== undefined || declared.default !== undefined);
841
889
  }
842
890
 
843
891
  /**
@@ -853,10 +901,10 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
853
901
  state.needs.set(kind, needs);
854
902
  for (const name of Object.keys(kind.tree?.attributes ?? {})) needs.push({ name, why: "the kind declares it", hidden: undefined });
855
903
  const self: SeenNode = { kind, text: kindText, own: undefined, parent: undefined };
856
- for (const { label, term, names, attribute, down } of equationsOf(kind)) {
904
+ for (const { label, term, names, attribute, down, contributes } of equationsOf(kind)) {
857
905
  const inner: CheckOptions = { names, ...(of === undefined ? {} : { declaredType: of }), ...(options.caps === undefined ? {} : { caps: [] }) };
858
906
  const read = (name: string): void => {
859
- needs.push({ name, why: `the equation ${label} reads it`, hidden: down ? undefined : attribute });
907
+ needs.push({ name, why: `the equation ${label} reads it`, hidden: down || contributes === true ? undefined : attribute });
860
908
  };
861
909
  for (const issue of checkAll(machine, term, inner, undefined, state, { node: self, read })) {
862
910
  out.push({ code: issue.code, place: [], reason: `In the equation ${label} of the kind ${kindText}: ${issue.reason}` });
@@ -868,12 +916,13 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
868
916
  /**
869
917
  * This function finds the issues of the contract of a tree kind (TREE.CHECK-RESERVED). An attribute with the name of a
870
918
  * 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).
919
+ * declaration `circular` without a start value or with a bad limit gives `#VALUE!` (TREE.CIRCULAR-CHECK). A declaration
920
+ * `collect` without its form gives `#VALUE!` too (TREE.COLLECT).
872
921
  */
873
922
  function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
874
923
  const out: Issue[] = [];
875
924
  const tree = kind.tree;
876
- for (const table of ["equations", "down", "attributes"] as const) {
925
+ for (const table of ["equations", "down", "attributes", "contributes"] as const) {
877
926
  for (const name of Object.keys(tree?.[table] ?? {})) {
878
927
  const why = Object.hasOwn(STRUCTURAL, name) ? STRUCTURAL[name] : undefined;
879
928
  if (why !== undefined) out.push({ code: "#NAME?", place: [], reason: `The kind ${kindText} gives the reserved attribute tree.${table}.${name}: ${why}.` });
@@ -883,6 +932,8 @@ function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
883
932
  for (const [name, declared] of Object.entries(tree?.attributes ?? {})) {
884
933
  const rule = declared.circular === undefined ? undefined : circularOf(declared.circular);
885
934
  if (rule !== undefined && !rule.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad declaration tree.attributes.${name}.circular. ${rule.reason}` });
935
+ const collect = declared.collect === undefined ? undefined : collectOf(declared.collect);
936
+ if (collect !== undefined && !collect.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad declaration tree.attributes.${name}.collect. ${collect.reason}` });
886
937
  }
887
938
  if (kind.side === "run") {
888
939
  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.` });
@@ -971,6 +1022,9 @@ function checkAll(
971
1022
  // run, and a drive for a runner under an evaluator. A document without code fails before its parts, thus it is not
972
1023
  // in the map.
973
1024
  const waits = options.run === true ? new Map<DocVisit, Waits>() : undefined;
1025
+ // The parts that the code of a runner evaluates itself, without a wait (RUN.DRIVES). They get no wait, as under an
1026
+ // evaluator. Only a runner that declares `drives` is in the map.
1027
+ const driven = new Map<DocVisit, readonly LazyRule[]>();
974
1028
  const drives: Drive[] = [];
975
1029
  const maxDepth = machine.maxDepth ?? 1000;
976
1030
  const anyName = options.names === "any";
@@ -1026,7 +1080,7 @@ function checkAll(
1026
1080
  if (known !== true) push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
1027
1081
  }
1028
1082
  const caps = v.caps;
1029
- const stated = verbDeclared(v, verb);
1083
+ const stated = hookEffects(v, verb, machine, name) ?? verbDeclared(v, verb);
1030
1084
  const declared = stated ?? NO_EFFECTS;
1031
1085
  if (allowed !== undefined && caps !== undefined) {
1032
1086
  for (const kind of declared) {
@@ -1035,7 +1089,9 @@ function checkAll(
1035
1089
  }
1036
1090
  if (waits !== undefined && coded) {
1037
1091
  // The parent of the root is the driver of the run, and it can wait. An evaluator gives its parts no wait.
1038
- const up = v.parent === undefined ? true : waits.get(v.parent);
1092
+ let up = v.parent === undefined ? true : waits.get(v.parent);
1093
+ const rules = v.parent === undefined ? undefined : driven.get(v.parent);
1094
+ if (up !== false && rules !== undefined && v.parent !== undefined && rulesCover(rules, v.parent.kind?.parts, v.parent.doc.value as Json, v.slot)) up = false;
1039
1095
  let chain: Waits = false;
1040
1096
  if (runner && up === false) {
1041
1097
  // The run drives a runner under an evaluator, and only a real wait fails (RUN.DRIVE-NESTED). Its issue goes in
@@ -1047,6 +1103,10 @@ function checkAll(
1047
1103
  chain = up ?? true;
1048
1104
  }
1049
1105
  waits.set(v, chain);
1106
+ if (runner && v.kind !== undefined) {
1107
+ const own = driveRules(machine, v, v.kind, name, answer.rung, verb);
1108
+ if (own !== undefined && own.length > 0) driven.set(v, own);
1109
+ }
1050
1110
  // A runner waits only for an effect that reaches the host (C6). A refused effect gives #CAP! without a wait. A
1051
1111
  // runner without a declaration for the verb can cause each effect, thus it can wait.
1052
1112
  if (typeof chain === "object" && !chain.wait) {