@mark1russell7/coeval 0.21.0 → 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.21.0",
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/formula.ts CHANGED
@@ -115,6 +115,12 @@ export function isClosure(value: unknown): value is Closure {
115
115
  return typeof value === "function" && (value as Partial<Closure>)[CLOSURE] !== undefined;
116
116
  }
117
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
+
118
124
  /**
119
125
  * The standard extensions: the values that JSON cannot write. `ext.domain` reads a member of a domain through the
120
126
  * codec of the domain (EXT.DOMAIN, D40).
@@ -925,15 +931,26 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
925
931
  }
926
932
  return known;
927
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);
928
936
  return (ctx, rt) => {
929
937
  // A closure keeps the context for each later use, thus the list of reads gets each path that it can read (READS.CLOSURE).
930
938
  rt.keepsContext?.(ctx, reads);
939
+ const clock = rt as Clocked;
931
940
  const closure = (...args: readonly unknown[]): unknown => {
932
- const scope: Record<string, unknown> = { ...ctx.names };
933
- names.forEach((name, index) => setOwn(scope, name, args[index]));
934
- const result = body.code({ ...ctx, names: scope }, rt);
935
- if (!result.ok) throw new Failure(result.error);
936
- 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
+ }
937
954
  };
938
955
  Object.defineProperty(closure, CLOSURE, { value: { params: names } });
939
956
  return ok(closure);
package/src/index.ts CHANGED
@@ -36,6 +36,9 @@ 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,
@@ -57,6 +60,9 @@ export {
57
60
  type World,
58
61
  type WorldEntry,
59
62
  CIRCULAR_LIMIT,
63
+ argsKey,
64
+ atArgsOf,
65
+ atRuleOf,
60
66
  circularOf,
61
67
  collectOf,
62
68
  effectsOfVerb,
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 CollectRule, type Kind, collectOf, 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);
@@ -634,7 +673,8 @@ function allCircular(b: Builder, ids: readonly string[]): boolean {
634
673
  }
635
674
 
636
675
  function vertexText(vertex: AttributeVertex): string {
637
- 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(", ")}]`;
638
678
  }
639
679
 
640
680
  /**
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, collectOf, 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
  }
@@ -1069,6 +1072,53 @@ function attributeOf(node: RefNode, name: string): Result | undefined {
1069
1072
  return result;
1070
1073
  }
1071
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
+
1072
1122
  /** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
1073
1123
  function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
1074
1124
  const record: Record<string, unknown> = {};
@@ -1157,6 +1207,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1157
1207
  };
1158
1208
  node.thing = self;
1159
1209
  refNodes.set(self, node);
1210
+ AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
1160
1211
  if (nested) return ok(self);
1161
1212
  try {
1162
1213
  return jsonOfNode(node, self, []);
@@ -1285,13 +1336,22 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
1285
1336
  }
1286
1337
  const names = params as readonly string[];
1287
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);
1288
1342
  const closure = (...args: readonly unknown[]): unknown => {
1289
- const scope: Record<string, unknown> = {};
1290
- for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
1291
- names.forEach((name, index) => setField(scope, name, args[index]));
1292
- const result = body.evaluate({ ...context, names: scope });
1293
- if (!result.ok) throw new Failure(result.error);
1294
- 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
+ }
1295
1355
  };
1296
1356
  Object.defineProperty(closure, CLOSURE, { value: { params: names } });
1297
1357
  return ok(closure);
package/src/static.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type Envelope, type Json, type Key, isKey, isRecord, typeName } from "./doc.ts";
2
- import { type Kind, type LazyRule, type World, type WorldEntry, circularOf, collectOf, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
1
+ import { type Envelope, type Json, type Key, field, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
+ import { type Kind, type LazyRule, type World, type WorldEntry, atArgsOf, atRuleOf, circularOf, collectOf, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
3
3
  import { answerOf, ask } from "./ladder.ts";
4
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";
@@ -8,6 +8,7 @@ import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
8
8
  import type { ValueType } from "./types.ts";
9
9
  import { type RootTypes, type TypeNode, type Typed, machineTypeIssues, typePass } from "./typing.ts";
10
10
  import { cycleIssues } from "./instances.ts";
11
+ import { STD_WALKERS } from "./stdlib.ts";
11
12
 
12
13
  /** One document that a walk visits. */
13
14
  export interface DocVisit {
@@ -698,6 +699,11 @@ export function equationsOf(kind: Kind): readonly Equation[] {
698
699
  }
699
700
  // A contribution is not the attribute of its node, thus an own part of its name does not hide it (D122).
700
701
  for (const [name, term] of Object.entries(tree.contributes ?? {})) out.push({ label: `tree.contributes.${name}`, term, names: [], attribute: name, down: false, contributes: true });
702
+ // An equation of an attribute with parameters sees each parameter as a name (TREE.AT, D124).
703
+ for (const [name, declared] of Object.entries(tree.at ?? {})) {
704
+ const rule = atRuleOf(declared);
705
+ if (rule.ok) out.push({ label: `tree.at.${name}`, term: rule.equation, names: rule.params, attribute: name, down: false, at: true });
706
+ }
701
707
  return out;
702
708
  }
703
709
 
@@ -713,6 +719,8 @@ export interface Equation {
713
719
  readonly down: boolean;
714
720
  /** `true` for an equation of `contributes`: it gives a contribution to a collection (TREE.COLLECT). */
715
721
  readonly contributes?: true;
722
+ /** `true` for an equation of `at`: it gives an attribute with parameters (TREE.AT). */
723
+ readonly at?: true;
716
724
  }
717
725
 
718
726
  /**
@@ -901,10 +909,10 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
901
909
  state.needs.set(kind, needs);
902
910
  for (const name of Object.keys(kind.tree?.attributes ?? {})) needs.push({ name, why: "the kind declares it", hidden: undefined });
903
911
  const self: SeenNode = { kind, text: kindText, own: undefined, parent: undefined };
904
- for (const { label, term, names, attribute, down, contributes } of equationsOf(kind)) {
912
+ for (const { label, term, names, attribute, down, contributes, at } of equationsOf(kind)) {
905
913
  const inner: CheckOptions = { names, ...(of === undefined ? {} : { declaredType: of }), ...(options.caps === undefined ? {} : { caps: [] }) };
906
914
  const read = (name: string): void => {
907
- needs.push({ name, why: `the equation ${label} reads it`, hidden: down || contributes === true ? undefined : attribute });
915
+ needs.push({ name, why: `the equation ${label} reads it`, hidden: down || contributes === true || at === true ? undefined : attribute });
908
916
  };
909
917
  for (const issue of checkAll(machine, term, inner, undefined, state, { node: self, read })) {
910
918
  out.push({ code: issue.code, place: [], reason: `In the equation ${label} of the kind ${kindText}: ${issue.reason}` });
@@ -917,12 +925,12 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
917
925
  * This function finds the issues of the contract of a tree kind (TREE.CHECK-RESERVED). An attribute with the name of a
918
926
  * structural step gets no read. A tree kind of the run side contradicts the evaluation on demand of its attributes. A
919
927
  * 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).
928
+ * `collect` without its form gives `#VALUE!` too (TREE.COLLECT). So does an entry of `at` without its form (TREE.AT-CHECK).
921
929
  */
922
930
  function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
923
931
  const out: Issue[] = [];
924
932
  const tree = kind.tree;
925
- for (const table of ["equations", "down", "attributes", "contributes"] as const) {
933
+ for (const table of ["equations", "down", "attributes", "contributes", "at"] as const) {
926
934
  for (const name of Object.keys(tree?.[table] ?? {})) {
927
935
  const why = Object.hasOwn(STRUCTURAL, name) ? STRUCTURAL[name] : undefined;
928
936
  if (why !== undefined) out.push({ code: "#NAME?", place: [], reason: `The kind ${kindText} gives the reserved attribute tree.${table}.${name}: ${why}.` });
@@ -935,6 +943,10 @@ function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
935
943
  const collect = declared.collect === undefined ? undefined : collectOf(declared.collect);
936
944
  if (collect !== undefined && !collect.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad declaration tree.attributes.${name}.collect. ${collect.reason}` });
937
945
  }
946
+ for (const [name, declared] of Object.entries(tree?.at ?? {})) {
947
+ const rule = atRuleOf(declared);
948
+ if (!rule.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad entry tree.at.${name}. ${rule.reason}` });
949
+ }
938
950
  if (kind.side === "run") {
939
951
  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.` });
940
952
  }
@@ -966,9 +978,14 @@ function nodeIssues(machine: Machine, node: SeenNode, state: TreeState, v: DocVi
966
978
  * This function examines a reference from the root `self` (TREE.CHECK-UNKNOWN). The steps `parent` go up the known
967
979
  * nodes. The first other key names an attribute of the node that they reach. When no rung answers the attribute, the
968
980
  * function gives the index of the step and the reason. In an equation, a read of the node of the equation goes to the
969
- * needs of its kind.
981
+ * needs of its kind. A step `tree.at` with literal arguments gets the checks of TREE.AT-CHECK.
970
982
  */
971
- function readIssue(machine: Machine, self: SeenNode | undefined, path: readonly Json[], rootSelf: RootSelf | undefined): { readonly index: number; readonly reason: string } | undefined {
983
+ function readIssue(
984
+ machine: Machine,
985
+ self: SeenNode | undefined,
986
+ path: readonly Json[],
987
+ rootSelf: RootSelf | undefined,
988
+ ): { readonly index: number; readonly reason: string; readonly code?: ErrorCode } | undefined {
972
989
  let node = self;
973
990
  let index = 1;
974
991
  while (node !== undefined && index < path.length && path[index] === "parent") {
@@ -976,6 +993,10 @@ function readIssue(machine: Machine, self: SeenNode | undefined, path: readonly
976
993
  index += 1;
977
994
  }
978
995
  const step = path[index];
996
+ if (node !== undefined && isAtStep(machine, step)) {
997
+ const at = atStepOf(node.kind, node.text, step.value);
998
+ return at.kind === "issue" ? { index, reason: at.reason, code: at.code } : undefined;
999
+ }
979
1000
  if (node === undefined || !isKey(step) || Object.hasOwn(STRUCTURAL, String(step))) return undefined;
980
1001
  const name = String(step);
981
1002
  if (node.own === undefined) {
@@ -985,6 +1006,46 @@ function readIssue(machine: Machine, self: SeenNode | undefined, path: readonly
985
1006
  return answered(machine.world, node, name) ? undefined : { index, reason: `No rung gives the attribute ${name} of the kind ${node.text}.` };
986
1007
  }
987
1008
 
1009
+ /** This function tells if a step of a path is a step `tree.at` of the standard walker (TREE.AT, D124). */
1010
+ export function isAtStep(machine: Machine, step: unknown): step is Envelope {
1011
+ return isEnvelope(step) && walkerOf(machine, step) === STD_WALKERS["walk.tree.at"];
1012
+ }
1013
+
1014
+ /**
1015
+ * A step `tree.at` of a known kind before the run (TREE.AT-CHECK, D124). An exact step names an instance. The run of a
1016
+ * step with an issue fails at the step. Only the run knows a step with a document in its data, or a bad entry.
1017
+ */
1018
+ export type AtStep =
1019
+ | { readonly kind: "exact"; readonly attr: string; readonly key: string; readonly equation: Json; readonly params: readonly string[]; readonly names: Readonly<Record<string, Json>> }
1020
+ | { readonly kind: "issue"; readonly code: ErrorCode; readonly reason: string }
1021
+ | { readonly kind: "unknown" };
1022
+
1023
+ /**
1024
+ * This function examines the data of a step `tree.at` on a node of a known kind (TREE.AT-CHECK, D124). `check` and the
1025
+ * graph of attribute instances read the step with this one function.
1026
+ */
1027
+ export function atStepOf(kind: Kind, kindText: string, data: unknown): AtStep {
1028
+ if (holdsDocument(data)) return { kind: "unknown" };
1029
+ const attr = isRecord(data) ? field(data, "attr") : undefined;
1030
+ if (typeof attr !== "string") return { kind: "issue", code: "#VALUE!", reason: "The step tree.at needs a text attr." };
1031
+ const table = kind.tree?.at;
1032
+ const declared = table !== undefined && Object.hasOwn(table, attr) ? table[attr] : undefined;
1033
+ if (declared === undefined) return { kind: "issue", code: "#N/A", reason: `No rung gives the attribute ${attr} with parameters of the kind ${kindText}.` };
1034
+ const rule = atRuleOf(declared);
1035
+ // The issue of a bad entry is an issue of the kind (contractIssues).
1036
+ if (!rule.ok) return { kind: "unknown" };
1037
+ const given = atArgsOf(rule.params, isRecord(data) && Object.hasOwn(data, "args") ? field(data, "args") : {});
1038
+ if (!given.ok) return { kind: "issue", code: "#VALUE!", reason: `The step tree.at of ${attr} of the kind ${kindText} has bad arguments. ${given.reason}` };
1039
+ return { kind: "exact", attr, key: given.key, equation: rule.equation, params: rule.params, names: given.names };
1040
+ }
1041
+
1042
+ /** This function tells if a value holds a document. A document in the data of a step can be a part, thus only the run knows its value. */
1043
+ function holdsDocument(value: unknown): boolean {
1044
+ if (isEnvelope(value)) return true;
1045
+ if (Array.isArray(value)) return value.some(holdsDocument);
1046
+ return isRecord(value) && Object.values(value).some(holdsDocument);
1047
+ }
1048
+
988
1049
  /** The self of the root of a walk for the checks of attributes: the node of an equation, and the function for its reads. */
989
1050
  interface RootSelf {
990
1051
  readonly node: SeenNode;
@@ -1149,7 +1210,7 @@ function checkAll(
1149
1210
  // Only the standard code of a reference reads the attribute, thus a header that changes the code hides the read.
1150
1211
  if (attributes && root === "self" && v.address !== true && name === REF_CODE) {
1151
1212
  const issue = readIssue(machine, self, path, rootSelf);
1152
- if (issue !== undefined) push({ code: "#N/A", place: v.place, reason: issue.reason, at: stepAt(issue.index) });
1213
+ if (issue !== undefined) push({ code: issue.code ?? "#N/A", place: v.place, reason: issue.reason, at: stepAt(issue.index) });
1153
1214
  }
1154
1215
  path.forEach((step, index) => {
1155
1216
  if (index === 0 || isKey(step)) return;
package/src/stdlib.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  import { Failure } from "./errors.ts";
19
19
  import { type Cap, type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
20
20
  import type { World } from "./ladder.ts";
21
- import { type AttributeDeclaration, type CircularRule, type TreeContract, circularOf, collectOf, equationType, lookupKind } from "./world.ts";
21
+ import { type AttributeDeclaration, type CircularRule, type TreeContract, atArgsOf, atRuleOf, circularOf, collectOf, equationType, lookupKind } from "./world.ts";
22
22
  import { STD_DOMAINS } from "./types.ts";
23
23
  import type { SyntaxProfile } from "./syntax.ts";
24
24
 
@@ -74,6 +74,11 @@ export const STD_WORLD: World = {
74
74
  pure: true,
75
75
  summary: "A step of a path: it goes up to the nearest registry of names, and it gives the node with the name (TREE.NAMED).",
76
76
  },
77
+ at: {
78
+ verbs: { step: "walk.tree.at", view: "view.card" },
79
+ pure: true,
80
+ summary: "A step of a path: it reads an attribute with parameters of the node before it, with the arguments of the step (TREE.AT).",
81
+ },
77
82
  },
78
83
  },
79
84
  panel: {
@@ -177,6 +182,7 @@ export const WORLD_SYNTAX: SyntaxProfile = /* @__PURE__ */ Object.freeze({
177
182
  { name: "box", kind: "box", args: "named", fields: ["width", "height", "children"] },
178
183
  { name: "nearest", kind: "nearest", args: "named", fields: ["type", "follow", "self"] },
179
184
  { name: "tree.named", kind: "tree.named", args: "named", fields: ["name", "in"] },
185
+ { name: "tree.at", kind: "tree.at", args: "named", fields: ["attr", "args"] },
180
186
  // An operator document with fewer terms than its operator takes, for example add(5) (K163, D65).
181
187
  { name: "add", kind: "add" },
182
188
  { name: "multiply", kind: "multiply" },
@@ -343,6 +349,10 @@ export interface AttributeAnswer {
343
349
  * A new plan gives new arrays. Thus a host keeps its cells by the identity of `place` and by `attribute`.
344
350
  * A host that keeps its cells by the keys of `place` can give the answer of another node or a stale answer.
345
351
  *
352
+ * An attribute with parameters gets one cell for each key of its arguments (TREE.AT-CELL, D124). Its `place` is a
353
+ * second array with the same keys, one for each key of a node. Its `attribute` is the JSON text of the name and the
354
+ * arguments, for example `["height",{"width":200}]`.
355
+ *
346
356
  * The function gives the answer of `compute`, or a kept answer. A host with signals gives a computed signal: a change
347
357
  * of its store then makes only the attributes that read the store compute again. Each demand gives a new `compute`, and
348
358
  * a cell that computes again uses the last one.
@@ -916,6 +926,80 @@ function collected(root: HostThing, key: string, declared: unknown): Result {
916
926
  return acc;
917
927
  }
918
928
 
929
+ /** A function from the name of an attribute and the arguments to the result, or `undefined` when no rung answers (D124). */
930
+ export type AtReader = (attr: string, args: unknown) => Result | undefined;
931
+
932
+ /**
933
+ * The host things that answer the step `tree.at` with their own function (D124). A node of the reference is in it, and
934
+ * its fields do not change. The walker reads the state of a node of the kit.
935
+ */
936
+ export const AT_READERS: WeakMap<object, AtReader> = /* @__PURE__ */ new WeakMap();
937
+
938
+ /** The memo of the instances of the attributes with parameters of each node, without the cells of a host (TREE.AT). */
939
+ const atMemos = /* @__PURE__ */ new WeakMap<NodeState, Map<string, Cell>>();
940
+
941
+ /** The second key of each key of a node, for the cells of the attributes with parameters (TREE.AT-CELL). */
942
+ const atPlaces = /* @__PURE__ */ new WeakMap<readonly Key[], readonly Key[]>();
943
+
944
+ /**
945
+ * This function gives an instance of an attribute with parameters of a node (TREE.AT, D124). The equation in
946
+ * `tree.at` of the kind of the node evaluates with the arguments as names. The memo keeps one result for each key of
947
+ * the arguments, and a demand of an instance in evaluation gives `#CYCLE!`. The function is outside the code of
948
+ * `read`, thus a node without such attributes does no new work.
949
+ */
950
+ function atRead(n: NodeState, attr: string, args: unknown): Result | undefined {
951
+ const declared: unknown = entryOf(n.tree?.at, attr);
952
+ if (declared === undefined) return undefined;
953
+ const rule = atRuleOf(declared);
954
+ if (!rule.ok) return fail("#VALUE!", `The attribute ${attributeText(attr, n.type, n.place)} has a bad entry tree.at. ${rule.reason}`);
955
+ const given = atArgsOf(rule.params, args);
956
+ if (!given.ok) return fail("#VALUE!", `The step tree.at of ${attributeText(attr, n.type, n.place)} has bad arguments. ${given.reason}`);
957
+ // An open fixed point does not use the cells of the host (D113).
958
+ const host = openPoint === undefined ? n.host : undefined;
959
+ const place = n.host === undefined ? n.place : keptAt(atPlaces, n.place, () => Object.freeze([...n.place]));
960
+ let marks: Map<string, Cell> | undefined = n.host === undefined ? atMemos.get(n) : keptAt(marksByPlace, place, () => new Map<string, Cell>());
961
+ if (marks === undefined) {
962
+ marks = new Map();
963
+ atMemos.set(n, marks);
964
+ }
965
+ const key = `[${JSON.stringify(attr)},${given.key}]`;
966
+ const text = attributeText(`${attr} with ${given.key}`, n.type, n.place);
967
+ const cell = marks.get(key);
968
+ if (cell !== undefined) return cell.busy ? cycleAt(marks, key, text) : cell.result;
969
+ const demand: Demand = { cells: marks, key, text };
970
+ const loop = openPoint;
971
+ const compute = (): AttributeAnswer => ({ result: n.equation(rule.equation, given.names), by: "kind" });
972
+ marks.set(key, BUSY);
973
+ demands.push(demand);
974
+ let thrown: unknown = NOT_THROWN;
975
+ let answer: AttributeAnswer;
976
+ try {
977
+ answer =
978
+ host === undefined
979
+ ? compute()
980
+ : host(place, key, () => {
981
+ try {
982
+ return compute();
983
+ } catch (error) {
984
+ thrown = error;
985
+ throw error;
986
+ }
987
+ });
988
+ } catch (error) {
989
+ marks.delete(key);
990
+ // An exception of the kit or of the stack goes on. Another exception of the cell is a cycle of the host (D108).
991
+ if (host === undefined || error === thrown) throw error;
992
+ const chain = demands.map((item) => item.text).join(" -> ");
993
+ answer = { result: fail("#CYCLE!", `The cells of the host found a cycle in the demands: ${chain}.`, { cause: error }), by: undefined };
994
+ } finally {
995
+ demands.pop();
996
+ }
997
+ // A result that read an approximation is not final. With the cells of a host, the cell is the memo.
998
+ if (n.host !== undefined || loop?.tentative.has(demand) === true) marks.delete(key);
999
+ else marks.set(key, { busy: false, result: answer.result, by: answer.by });
1000
+ return answer.result;
1001
+ }
1002
+
919
1003
  /**
920
1004
  * This function makes a node of the decorated tree. A step `parent` gives the self of the context of the node, and a
921
1005
  * step `type` gives its type. Another step reads an attribute through its ladder (TREE.LADDER). The rungs are a part of
@@ -1256,8 +1340,25 @@ const named: Walker = ({ data, here, deref }) => {
1256
1340
  return { ok: false, reason: `No registry ${registry} is on the way.` };
1257
1341
  };
1258
1342
 
1343
+ /**
1344
+ * The walker of the step `tree.at` (TREE.AT, D124). The field `attr` names an attribute with parameters, and the
1345
+ * field `args` gives the arguments. The thing before the step is a node. A failed instance throws its own error.
1346
+ */
1347
+ const at: Walker = ({ data, here }) => {
1348
+ const attr = isRecord(data) ? field(data, "attr") : undefined;
1349
+ if (typeof attr !== "string") return { ok: false, code: "#VALUE!", reason: "The step tree.at needs a text attr." };
1350
+ const args = isRecord(data) && Object.hasOwn(data, "args") ? field(data, "args") : {};
1351
+ const state = stateOf(here);
1352
+ const reader = state === undefined && typeof here === "object" && here !== null ? AT_READERS.get(here) : undefined;
1353
+ if (state === undefined && reader === undefined) return { ok: false, reason: `The step tree.at reads ${attr} of a thing that is not a node of a tree.` };
1354
+ const result = state === undefined ? (reader as AtReader)(attr, args) : atRead(state, attr, args);
1355
+ if (result === undefined) return { ok: false, reason: `No rung gives the attribute ${attr} with parameters.` };
1356
+ if (!result.ok) throw new Failure(result.error);
1357
+ return { ok: true, thing: result.value as Thing };
1358
+ };
1359
+
1259
1360
  /** The walkers of the standard world, by name. */
1260
- export const STD_WALKERS: Readonly<Record<string, Walker>> = { "walk.nearest": nearest, "walk.named": named };
1361
+ export const STD_WALKERS: Readonly<Record<string, Walker>> = { "walk.nearest": nearest, "walk.named": named, "walk.tree.at": at };
1261
1362
 
1262
1363
  /** This function evaluates parts one after the other, and stops at the first error. */
1263
1364
  function* inOrder(parts: readonly RunPart[]): Generator<Promise<unknown>, { readonly values: readonly unknown[] } | { readonly error: Result }, unknown> {