@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.20.1",
4
+ "version": "0.22.0",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
package/src/conform.ts CHANGED
@@ -214,6 +214,8 @@ interface Sample {
214
214
  readonly otherSplit: SplitOk;
215
215
  /** `true` for each part that a lazy rule covers. */
216
216
  readonly lazy: readonly boolean[];
217
+ /** `true` for each part that a rule `drives` covers: the runner can evaluate it with its code (D119). */
218
+ readonly driven: readonly boolean[];
217
219
  /** The names that the scope rules give each part. */
218
220
  readonly names: readonly (readonly string[])[];
219
221
  }
@@ -282,6 +284,8 @@ interface Contract {
282
284
  readonly declared: readonly Cap[];
283
285
  readonly pure: boolean;
284
286
  readonly lazy: readonly LazyRule[] | undefined;
287
+ /** The parts that the runner evaluates with its code, without a wait (RUN.DRIVES). */
288
+ readonly drives: readonly LazyRule[] | undefined;
285
289
  /** `true` when the facts describe the verb: they describe only `eval`. */
286
290
  readonly describes: boolean;
287
291
  /** `true` when the contract declares the effects of the verb: for `eval`, and for a verb in `verbEffects`. */
@@ -315,6 +319,8 @@ class Shot implements Runtime {
315
319
  readonly context: Context,
316
320
  /** The key of each part, for the reasons. */
317
321
  readonly keys: readonly string[],
322
+ /** `true` for each part that the runner can evaluate with its code, by the rules `drives` (D119). */
323
+ private readonly driven: readonly boolean[],
318
324
  private readonly perform: Perform | undefined,
319
325
  /** The resolutions of an earlier evaluation. Each resolution with the same path gets the same answer (C4). */
320
326
  private readonly replay: readonly Asked[] | undefined,
@@ -334,8 +340,8 @@ class Shot implements Runtime {
334
340
  this.calls.push({ part, via, context: ctx, later: this.afterwards });
335
341
  const key = this.keys[part] ?? String(part);
336
342
  if (via === "run" && !this.runner) this.problems.push(`The code of an evaluator starts the steps of the part ${key}. Only a runner waits.`);
337
- if (via === "code" && this.runner && this.running) {
338
- this.problems.push(`The runner evaluates the part ${key} with its code, and that code cannot wait. A runner evaluates a part with its steps (K65).`);
343
+ if (via === "code" && this.runner && this.running && this.driven[part] !== true) {
344
+ this.problems.push(`The runner evaluates the part ${key} with its code, and that code cannot wait. A runner evaluates a part with its steps, or its contract declares the part in drives (D119).`);
339
345
  }
340
346
  return this.scenario.results[part] ?? NO_RESULT;
341
347
  }
@@ -498,13 +504,15 @@ function typesNaming(world: World, name: string, verb: string): readonly TypeRef
498
504
  function contractOf(env: Env, kind: Kind, typePath: readonly Key[], caps: readonly Cap[] | undefined): Contract {
499
505
  const facts = env.machine.facts?.(typePath, env.name);
500
506
  // A verb without a declaration keeps the effects of eval for the capabilities of the probes, as before D49.
501
- const own = effectsOfVerb(kind, env.verb);
507
+ // For eval, the effects of the answer of the hook win over the contract, as in check (D118).
508
+ const own = (env.verb === "eval" ? facts?.effects : undefined) ?? effectsOfVerb(kind, env.verb);
502
509
  const effects = own ?? kind.effects;
503
510
  const declared: readonly Cap[] = effects === undefined ? [] : effects === "type" ? [typePath] : effects;
504
511
  return {
505
512
  declared,
506
513
  pure: (facts?.pure ?? kind.pure) === true,
507
514
  lazy: facts?.lazy ?? kind.lazy,
515
+ drives: env.verb === "eval" ? (facts?.drives ?? kind.drives) : undefined,
508
516
  describes: env.verb === "eval",
509
517
  declares: own !== undefined,
510
518
  caps: caps ?? [...declared, "probe"],
@@ -637,6 +645,7 @@ function samplesOf(type: TypeRef, typePath: readonly Key[], kind: Kind, contract
637
645
  other: { type, value: otherValue },
638
646
  otherSplit,
639
647
  lazy: split.parts.map((_slot, index) => contract.lazy?.some((rule) => covers(rule, split.parts, index)) === true),
648
+ driven: split.parts.map((_slot, index) => contract.drives?.some((rule) => covers(rule, split.parts, index)) === true),
640
649
  names: scopesOf(kind.scope, value, split.parts).sees,
641
650
  });
642
651
  }
@@ -769,7 +778,7 @@ function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sampl
769
778
  /** This function evaluates a probe document one time in a new shot. */
770
779
  function shoot(env: Env, instance: Instance, sample: Sample, scenario: Scenario, more: { readonly context?: Context; readonly replay?: readonly Asked[] } = {}): Shot {
771
780
  const context = more.context ?? rootContext(env.machine, makeContext(env.names, scenario.caps));
772
- const shot = new Shot(env.machine, env.runner, scenario, context, sample.split.parts.map((slot) => slot.key), env.perform, more.replay);
781
+ const shot = new Shot(env.machine, env.runner, scenario, context, sample.split.parts.map((slot) => slot.key), sample.driven, env.perform, more.replay);
773
782
  instance(shot);
774
783
  return shot;
775
784
  }
package/src/deref.ts CHANGED
@@ -2,6 +2,7 @@ import { type Envelope, type Json, type JsonRecord, type Key, type TypeRef, fiel
2
2
  import { type Kind, type World, entriesAsJson, lookupKind } from "./world.ts";
3
3
  import { answerOf } from "./ladder.ts";
4
4
  import type { ErrorCode } from "./errors.ts";
5
+ import type { ValueType } from "./types.ts";
5
6
 
6
7
  /**
7
8
  * The brand of a host thing: an object of the host that answers the steps of a path itself. The brand is a
@@ -169,8 +170,37 @@ export interface WalkInput {
169
170
  readonly visit?: (key: Key, thing: Thing) => void;
170
171
  }
171
172
 
172
- /** A walker: the code of the verb `step` of a kind of step documents. */
173
- export type Walker = (input: WalkInput) => WalkResult;
173
+ /** The input of the type rule of a walker: the data of the step document, and the type of the thing before the step. */
174
+ export interface WalkTypeInput {
175
+ /** The value of the step document. A step document with parts has no type rule, thus this value holds no part. */
176
+ readonly data: Json;
177
+ /** The type of the thing that the path reached before this step. */
178
+ readonly here: ValueType;
179
+ }
180
+
181
+ /**
182
+ * A walker: the code of the verb `step` of a kind of step documents. A plain function is a walker.
183
+ *
184
+ * A walker can hold a type rule in its field `type` (TYPE.WALKER, D121). The rule gives the type of each thing that
185
+ * the walker reaches from a thing of the type `here`. It must be sound, as the rule of an evaluator. A walker
186
+ * without a rule reaches the type `unknown`. A host gives the rule with `Object.assign(walker, { type })`.
187
+ */
188
+ export interface Walker {
189
+ /**
190
+ * This function answers the step document on the thing that the path reached.
191
+ *
192
+ * @param input - The data of the step document, the thing before the step and the ladder.
193
+ * @returns What the step reached, or the reason of a failure.
194
+ */
195
+ (input: WalkInput): WalkResult;
196
+ /**
197
+ * This optional function gives the type of what the walker reaches.
198
+ *
199
+ * @param input - The data of the step document and the type before the step.
200
+ * @returns The type of each thing that the walker reaches.
201
+ */
202
+ readonly type?: (input: WalkTypeInput) => ValueType;
203
+ }
174
204
 
175
205
  /**
176
206
  * What a resolution needs: the world, and the code of the `deref` evaluators and of the walkers.
package/src/effects.ts CHANGED
@@ -46,7 +46,7 @@ function reaching(machine: Machine, doc: unknown, options: EffectsOptions): read
46
46
  doc,
47
47
  (v) => {
48
48
  const caps = v.caps ?? EACH;
49
- for (const kind of declaredEffects(v)) if (rule(caps, kind)) out.push({ kind, visit: v });
49
+ for (const kind of declaredEffects(v, "eval", machine)) if (rule(caps, kind)) out.push({ kind, visit: v });
50
50
  return undefined;
51
51
  },
52
52
  { caps: options.caps ?? EACH, ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }) },
@@ -65,7 +65,9 @@ function reaching(machine: Machine, doc: unknown, options: EffectsOptions): read
65
65
  *
66
66
  * The result covers each effect that an evaluation or a run gives the host (CAP.EFFECTS-SOUND). A procedure value keeps
67
67
  * the capabilities of the document that made it, thus the effects of its body count at the body (CAP.LATENT). The
68
- * contract describes the code of its kind. The code of an own entry for another evaluator is outside the result.
68
+ * contract describes the code of its kind. The contract does not describe the code of an own entry for another
69
+ * evaluator. The answer of the hook `facts` for that code gives its effects (CAP.OWN-EFFECTS, D118). Without an
70
+ * answer, its effects are outside the result.
69
71
  *
70
72
  * @param machine - The world and its contracts, and the hook `permits`.
71
73
  * @param doc - The document, as JSON.
package/src/evaluate.ts CHANGED
@@ -1792,6 +1792,8 @@ function pureCode(c: Compiler, envelope: Envelope | undefined, kind: Kind, typeP
1792
1792
  } catch {
1793
1793
  return false;
1794
1794
  }
1795
+ // The effects of the answer of the hook make the code not pure, also for an own entry (D118).
1796
+ if (answer?.effects !== undefined && answer.effects.length > 0) return false;
1795
1797
  // The contract of the kind describes the code of its own name, or an own entry that names the same code (D24).
1796
1798
  const described = c.verb === "eval" && (envelope === undefined || quick?.rung !== "own" || name === answerOf(c.machine.world, undefined, kind, c.verb)?.name);
1797
1799
  const pure = described ? (answer?.pure ?? kind.pure) : answer?.pure;
package/src/facts.ts CHANGED
@@ -4,7 +4,7 @@ import { answerOf } from "./ladder.ts";
4
4
  import { pathMatches, splitParts } from "./parts.ts";
5
5
  import { type Root, ROOTS, isRef } from "./deref.ts";
6
6
  import type { Machine } from "./evaluate.ts";
7
- import { type DocVisit, type WalkOptions, declaredEffects, equationsOf, walk } from "./static.ts";
7
+ import { type DocVisit, type WalkOptions, declaredEffects, effectKeys, equationsOf, walk } from "./static.ts";
8
8
 
9
9
  /**
10
10
  * One read of a document: a free reference, with its marks (FACTS.READS). A plan resolves the path of the reference
@@ -272,7 +272,8 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
272
272
  }
273
273
  // check and effectsOf read the declared effects with the same function (D27). The verb of the facts selects the
274
274
  // declaration: effects for eval, and the entry of verbEffects for another verb (D49, D111).
275
- const declared = declaredEffects(v, verb);
275
+ // The answer of the hook for the code of eval wins over the contract, also for an own entry (D118).
276
+ const declared = verb === "eval" && answer?.effects !== undefined ? effectKeys(v, answer.effects) : declaredEffects(v, verb);
276
277
  // The field side gives the side of eval. For another verb, a declared effect needs a runner, as in check, thus
277
278
  // the run side (FACTS.VERB-SIDE).
278
279
  const runs = verb === "eval" ? kind.side === "run" : declared.length > 0;
package/src/formula.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { type Json, type Key, docEquals, isKey, isRecord } from "./doc.ts";
1
+ import { type Json, type Key, docEquals, isKey, isRecord, typeName } from "./doc.ts";
2
+ import { partsOf as docParts } from "./parts.ts";
2
3
  import type { Kind, KindFacts, LazyRule, WorldEntry } from "./world.ts";
3
- import { type CompiledPath, type Context, type Resolution, compilePath, valueOfThing } from "./deref.ts";
4
+ import { type CompiledPath, type Context, type Resolution, compilePath, valueOfThing, walkerOf } from "./deref.ts";
4
5
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
5
6
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled, totalCode } from "./evaluate.ts";
6
7
  import { type MachinePart, mergeMachines } from "./merge.ts";
@@ -114,6 +115,12 @@ export function isClosure(value: unknown): value is Closure {
114
115
  return typeof value === "function" && (value as Partial<Closure>)[CLOSURE] !== undefined;
115
116
  }
116
117
 
118
+ /**
119
+ * The runtime of an evaluation with the clock of `call` (FORMULA.CALL-DEPTH, D125): the count of the applications of
120
+ * closures that run at this time. The first application adds the field, thus an evaluation without one has no field.
121
+ */
122
+ type Clocked = Runtime & { applying?: number };
123
+
117
124
  /**
118
125
  * The standard extensions: the values that JSON cannot write. `ext.domain` reads a member of a domain through the
119
126
  * codec of the domain (EXT.DOMAIN, D40).
@@ -278,6 +285,29 @@ function paramsOf(value: unknown): readonly string[] {
278
285
  return Array.isArray(params) ? params.filter((param): param is string => typeof param === "string") : [];
279
286
  }
280
287
 
288
+ /** This function gives the text of a step of a path: a key, or the kind of a step document in parentheses. */
289
+ function stepText(step: unknown): string {
290
+ if (isKey(step)) return String(step);
291
+ return isRecord(step) ? `(${typeName(step["type"] as Parameters<typeof typeName>[0])})` : "(step)";
292
+ }
293
+
294
+ /**
295
+ * This function gives the type of what a step document reaches (TYPE.WALKER, D121). The type rule of its walker gives
296
+ * it. A step document with parts gets its data in the run, thus its type is `unknown`, as for a walker without a rule.
297
+ *
298
+ * @param input - The input of the rule of the reference.
299
+ * @param step - The step document.
300
+ * @param here - The type before the step.
301
+ * @returns The type of what the step reaches.
302
+ */
303
+ function walkType(input: TypeInput, step: unknown, here: ValueType): ValueType {
304
+ const rule = walkerOf(input.machine, step)?.type;
305
+ if (rule === undefined) return "unknown";
306
+ const read = docParts(input.machine.world, step);
307
+ if (!read.ok || !read.split.ok || read.split.parts.length > 0) return "unknown";
308
+ return input.contract(rule({ data: read.doc.value, here }), `type of the step ${typeName(read.doc.type)}`);
309
+ }
310
+
281
311
  /**
282
312
  * This function makes the type rules of the formula kit for one profile (TYPE.SYNTH, D25). Each evaluator of the kit
283
313
  * holds its rule. The rules read the rules of the profile that change a type: `truthy`, `truthType`, `logic`,
@@ -395,12 +425,14 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
395
425
  if (isUnknown(type, domains) || (!plain && holdsEnvelope(type, domains))) return "unknown";
396
426
  if (index >= path.length) return type;
397
427
  const step = path[index];
398
- // A step document gives what its walker reaches. It has no type yet.
399
- if (!isKey(step)) return "unknown";
428
+ if (!isKey(step)) {
429
+ type = walkType(input, step, type);
430
+ continue;
431
+ }
400
432
  const next = stepType(type, step, domains, input.contract);
401
433
  if ("missing" in next) {
402
434
  const fields = next.missing.length === 0 ? "" : ` Its fields are ${listText(next.missing)}.`;
403
- report({ reason: `The value at ${path.slice(0, index).map(String).join(".")} has no field ${String(step)}.${fields}` });
435
+ report({ reason: `The value at ${path.slice(0, index).map(stepText).join(".")} has no field ${String(step)}.${fields}` });
404
436
  return "unknown";
405
437
  }
406
438
  type = next.type;
@@ -899,15 +931,26 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
899
931
  }
900
932
  return known;
901
933
  };
934
+ // The clock of call permits a quarter of the depth limit, because an application nests its body (D125).
935
+ const limit = Math.ceil((machine.maxDepth ?? 1000) / 4);
902
936
  return (ctx, rt) => {
903
937
  // A closure keeps the context for each later use, thus the list of reads gets each path that it can read (READS.CLOSURE).
904
938
  rt.keepsContext?.(ctx, reads);
939
+ const clock = rt as Clocked;
905
940
  const closure = (...args: readonly unknown[]): unknown => {
906
- const scope: Record<string, unknown> = { ...ctx.names };
907
- names.forEach((name, index) => setOwn(scope, name, args[index]));
908
- const result = body.code({ ...ctx, names: scope }, rt);
909
- if (!result.ok) throw new Failure(result.error);
910
- return result.value;
941
+ // Each application counts one level on the runtime of its evaluation, and the count goes down after it (FORMULA.CALL-DEPTH).
942
+ const level = (clock.applying ?? 0) + 1;
943
+ if (level > limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
944
+ clock.applying = level;
945
+ try {
946
+ const scope: Record<string, unknown> = { ...ctx.names };
947
+ names.forEach((name, index) => setOwn(scope, name, args[index]));
948
+ const result = body.code({ ...ctx, names: scope }, rt);
949
+ if (!result.ok) throw new Failure(result.error);
950
+ return result.value;
951
+ } finally {
952
+ clock.applying = level - 1;
953
+ }
911
954
  };
912
955
  Object.defineProperty(closure, CLOSURE, { value: { params: names } });
913
956
  return ok(closure);
package/src/index.ts CHANGED
@@ -36,10 +36,15 @@ export {
36
36
  } from "./errors.ts";
37
37
  export {
38
38
  type Alias,
39
+ type AtArgs,
40
+ type AtEquation,
41
+ type AtRule,
39
42
  type AttributeDeclaration,
40
43
  type CapsRule,
41
44
  type CircularDeclaration,
42
45
  type CircularRule,
46
+ type CollectDeclaration,
47
+ type CollectRule,
43
48
  type Kind,
44
49
  type KindFacts,
45
50
  type LazyRule,
@@ -55,7 +60,11 @@ export {
55
60
  type World,
56
61
  type WorldEntry,
57
62
  CIRCULAR_LIMIT,
63
+ argsKey,
64
+ atArgsOf,
65
+ atRuleOf,
58
66
  circularOf,
67
+ collectOf,
59
68
  effectsOfVerb,
60
69
  entriesAsJson,
61
70
  equationType,
@@ -108,6 +117,7 @@ export {
108
117
  type Thing,
109
118
  type WalkInput,
110
119
  type WalkResult,
120
+ type WalkTypeInput,
111
121
  type Walker,
112
122
  ROOTS,
113
123
  THING,
package/src/instances.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Json, type Key, field, isKey, isRecord, typeName } from "./doc.ts";
2
- import { type Kind, equationType } from "./world.ts";
2
+ import { type CollectRule, type Kind, argsKey, collectOf, equationType } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
4
  import { isRef, walkerOf } from "./deref.ts";
5
5
  import type { Machine } from "./evaluate.ts";
@@ -7,13 +7,16 @@ import { type Above, codeFactsOf, partLazy } from "./facts.ts";
7
7
  import { type NodeGraph, type GraphEdge, cycles } from "./graph.ts";
8
8
  import { STD_WALKERS } from "./stdlib.ts";
9
9
  import {
10
+ type AtStep,
10
11
  type CheckOptions,
11
12
  type DocVisit,
12
13
  type Issue,
13
14
  type SeenNode,
14
15
  REF_CODE,
15
16
  TREE_NODE,
17
+ atStepOf,
16
18
  downNames,
19
+ isAtStep,
17
20
  isStructural,
18
21
  keepsSelf,
19
22
  selvesOf,
@@ -39,6 +42,8 @@ export interface AttributeVertex {
39
42
  readonly type: string;
40
43
  /** `true` when the kind of the node declares the attribute `circular` (TREE.CIRCULAR-CHECK). Else the field is absent. */
41
44
  readonly circular?: true;
45
+ /** The arguments of an instance of an attribute with parameters (TREE.AT-CHECK). Else the field is absent. */
46
+ readonly args?: Readonly<Record<string, Json>>;
42
47
  }
43
48
 
44
49
  /** One read of an attribute by the code of another attribute (D107). */
@@ -220,6 +225,20 @@ function vertexOf(b: Builder, node: NodeInfo, attribute: string): string {
220
225
  return id;
221
226
  }
222
227
 
228
+ /**
229
+ * This function gives the vertex of an instance of an attribute with parameters (TREE.AT-CHECK, D124). Its name is the
230
+ * name of a plain vertex with the key of the arguments after the name of the attribute.
231
+ */
232
+ function atVertexOf(b: Builder, node: NodeInfo, at: AtStep & { readonly kind: "exact" }): string {
233
+ const id = `${attributeVertexId(node.visit.place, at.attr).slice(0, -1)},${at.key}]`;
234
+ if (!b.vertices.has(id)) {
235
+ b.vertices.set(id, { id, place: node.visit.place, attribute: at.attr, type: node.text, args: at.names });
236
+ b.nodes.set(id, node);
237
+ b.work.push(id);
238
+ }
239
+ return id;
240
+ }
241
+
223
242
  function edge(b: Builder, from: string | undefined, r: Reader, lazy = false): void {
224
243
  b.edges.push({ from, to: r.to, lazy: r.lazy || lazy, place: r.place });
225
244
  b.ats.push(r.at);
@@ -310,6 +329,15 @@ function follow(b: Builder, start: Value, path: readonly Json[], from: number, r
310
329
  }
311
330
  continue;
312
331
  }
332
+ // A step tree.at with literal arguments reads one instance (TREE.AT-CHECK). A step that fails gives no read.
333
+ if (isAtStep(b.machine, step)) {
334
+ const at = atStepOf(node.kind, node.text, step.value);
335
+ if (at.kind === "unknown") edge(b, undefined, r);
336
+ if (at.kind !== "exact") return;
337
+ edge(b, atVertexOf(b, node, at), r);
338
+ here = "unknown";
339
+ continue;
340
+ }
313
341
  // The standard walker nearest depends only on the structure (R3 P5). Each other walker can read each attribute.
314
342
  if (walkerOf(b.machine, step) !== STD_WALKERS["walk.nearest"]) {
315
343
  edge(b, undefined, r);
@@ -361,10 +389,11 @@ function termRefs(b: Builder, kind: Kind, label: string, term: Json): readonly T
361
389
  return out;
362
390
  }
363
391
 
364
- /** The names of the equation of a rung, before the run. */
392
+ /** The names of the equation of a rung, before the run. The parameters of an attribute hold JSON data (TREE.AT). */
365
393
  interface TermNames {
366
394
  readonly child?: NodeInfo;
367
395
  readonly prev?: Value;
396
+ readonly params?: readonly string[];
368
397
  }
369
398
 
370
399
  /** This function adds the reads of an equation of a rung: self is a node, and the names of the rung have their values. */
@@ -386,7 +415,10 @@ function termReads(b: Builder, refs: readonly TermRef[], self: NodeInfo, names:
386
415
  // A name that a kind inside the equation binds holds a value of the run. The others are the names of the rung.
387
416
  let start: Value;
388
417
  if (ref.bound.has(name)) start = "unknown";
389
- else if (names?.child !== undefined && name === "child") start = { node: names.child };
418
+ else if (names?.params !== undefined) {
419
+ if (!names.params.includes(name)) continue;
420
+ start = "data";
421
+ } else if (names?.child !== undefined && name === "child") start = { node: names.child };
390
422
  else if (names?.prev !== undefined && name === "prev") start = names.prev;
391
423
  else if (name === "index") start = "data";
392
424
  else if (name === "acc" || name === "item") start = "unknown";
@@ -412,9 +444,16 @@ function prevOf(parent: NodeInfo, child: NodeInfo): Value {
412
444
  */
413
445
  function rungReads(b: Builder, id: string): void {
414
446
  const node = b.nodes.get(id) as NodeInfo;
415
- const key = (b.vertices.get(id) as AttributeVertex).attribute;
447
+ const vertex = b.vertices.get(id) as AttributeVertex;
448
+ const key = vertex.attribute;
416
449
  const tree = node.kind.tree;
417
450
  const r: Reader = { to: id, lazy: false, place: node.visit.place, at: node.visit.at };
451
+ // An instance of an attribute with parameters has one rung: its equation in tree.at (TREE.AT).
452
+ if (vertex.args !== undefined) {
453
+ const at = atStepOf(node.kind, node.text, { attr: key, args: vertex.args });
454
+ if (at.kind === "exact") termReads(b, termRefs(b, node.kind, `tree.at.${key}`, at.equation), node, { params: at.params }, r);
455
+ return;
456
+ }
418
457
  const equation = hasOwnEntry(tree?.equations, key) ? tree?.equations?.[key] : undefined;
419
458
  if (equation !== undefined) {
420
459
  termReads(b, termRefs(b, node.kind, `tree.equations.${key}`, equation), node, undefined, r);
@@ -460,9 +499,58 @@ function rungReads(b: Builder, id: string): void {
460
499
  termReads(b, termRefs(b, node.kind, `tree.attributes.${key}.fold.combine`, fold.combine), node, undefined, combined ? later : { ...later, lazy: true });
461
500
  return;
462
501
  }
502
+ const collect = declared?.collect === undefined ? undefined : collectOf(declared.collect);
503
+ if (collect?.ok === true) {
504
+ collectReads(b, node, key, collect, later);
505
+ return;
506
+ }
463
507
  if (declared?.default !== undefined) termReads(b, termRefs(b, node.kind, `tree.attributes.${key}.default`, declared.default), node, undefined, later);
464
508
  }
465
509
 
510
+ /** This function gives the list children of a node before the run. A node without a rung for it has no children. */
511
+ function childrenOf(b: Builder, at: NodeInfo): Value {
512
+ const part = at.parts.get("children");
513
+ if (part !== undefined) return valueOf(part);
514
+ const tree = at.kind.tree;
515
+ if (hasOwnEntry(tree?.equations, "children") || hasOwnEntry(tree?.attributes, "children")) return "unknown";
516
+ if (at.up === undefined) return downNames(b.machine.world).has("children") ? "unknown" : "data";
517
+ return at.up !== null && hasOwnEntry(at.up.kind.tree?.down, "children") ? "unknown" : "data";
518
+ }
519
+
520
+ /**
521
+ * This function adds the reads of a collection (TREE.COLLECT, D122), in the order of the run. Each node of the subtree
522
+ * gives the reads of its contribution and of its list `children`. A list that only the run knows gives an edge with an
523
+ * unknown source. The registry of names reads only the lists `children`.
524
+ */
525
+ function collectReads(b: Builder, node: NodeInfo, key: string, rule: CollectRule & { readonly ok: true }, later: Reader): void {
526
+ if (!rule.names) termReads(b, termRefs(b, node.kind, `tree.attributes.${key}.collect.unit`, rule.unit), node, undefined, later);
527
+ let combined = false;
528
+ const done = new Set<NodeInfo>();
529
+ const stack: NodeInfo[] = [node];
530
+ while (stack.length > 0) {
531
+ const at = stack.pop() as NodeInfo;
532
+ if (done.has(at)) continue;
533
+ done.add(at);
534
+ const term = rule.names || !hasOwnEntry(at.kind.tree?.contributes, key) ? undefined : at.kind.tree?.contributes?.[key];
535
+ if (term !== undefined) {
536
+ termReads(b, termRefs(b, at.kind, `tree.contributes.${key}`, term), at, undefined, later);
537
+ combined = true;
538
+ }
539
+ edge(b, vertexOf(b, at, "children"), later);
540
+ const children = childrenOf(b, at);
541
+ if (children === "unknown") edge(b, undefined, later);
542
+ else if (typeof children === "object" && "list" in children) {
543
+ const items = itemsOf(children.list);
544
+ for (let index = items.length - 1; index >= 0; index -= 1) {
545
+ const child = items[index]?.node;
546
+ if (child !== undefined && child.up === at) stack.push(child);
547
+ }
548
+ }
549
+ }
550
+ // The combine evaluates once for each contribution, thus without a known contribution its reads are lazy.
551
+ if (!rule.names) termReads(b, termRefs(b, node.kind, `tree.attributes.${key}.collect.combine`, rule.combine), node, undefined, combined ? later : { ...later, lazy: true });
552
+ }
553
+
466
554
  /** The vertices and the edges of a document, and the roots of the demand of the run: the own parts of each tree root. */
467
555
  interface Built {
468
556
  readonly b: Builder;
@@ -585,7 +673,8 @@ function allCircular(b: Builder, ids: readonly string[]): boolean {
585
673
  }
586
674
 
587
675
  function vertexText(vertex: AttributeVertex): string {
588
- return `${vertex.attribute} of ${vertex.type} at [${vertex.place.join(", ")}]`;
676
+ const name = vertex.args === undefined ? vertex.attribute : `${vertex.attribute} with ${String(argsKey(vertex.args))}`;
677
+ return `${name} of ${vertex.type} at [${vertex.place.join(", ")}]`;
589
678
  }
590
679
 
591
680
  /**
package/src/parts.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
2
- import { type Kind, type NamesSource, type ScopeRule, type Shape, type World, lookupKind } from "./world.ts";
2
+ import { type Kind, type LazyRule, type NamesSource, type ScopeRule, type Shape, type World, lookupKind } from "./world.ts";
3
3
  import type { ValueType } from "./types.ts";
4
4
 
5
5
  /** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
@@ -269,6 +269,35 @@ export function pathMatches(path: readonly Key[], pattern: readonly Key[]): bool
269
269
  return true;
270
270
  }
271
271
 
272
+ /**
273
+ * This function tells if a list of rules in the form of the lazy rules covers a part (D24, D119). A rule without
274
+ * `in` covers each part. With `after: n`, a rule covers only the parts after the first `n` parts that it matches.
275
+ *
276
+ * @param rules - The rules.
277
+ * @param shape - The shape of the parts of the parent.
278
+ * @param value - The value of the parent.
279
+ * @param slot - The path of the part in the value.
280
+ * @returns `true` when a rule covers the part.
281
+ */
282
+ export function rulesCover(rules: readonly LazyRule[], shape: Shape | undefined, value: Json, slot: readonly Key[]): boolean {
283
+ for (const rule of rules) {
284
+ if (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(slot, pattern))) continue;
285
+ const after = rule.after ?? 0;
286
+ if (after <= 0) return true;
287
+ const split = splitParts(shape, value);
288
+ let count = 0;
289
+ for (const part of split.ok ? split.parts : []) {
290
+ if (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(part.path, pattern))) continue;
291
+ if (part.path.length === slot.length && pathMatches(part.path, slot)) {
292
+ if (count >= after) return true;
293
+ break;
294
+ }
295
+ count += 1;
296
+ }
297
+ }
298
+ return false;
299
+ }
300
+
272
301
  function valueAt(value: unknown, path: readonly Key[]): unknown {
273
302
  let here: unknown = value;
274
303
  for (const key of path) {