@mark1russell7/coeval 0.4.2 → 0.5.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/reference.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type Envelope, type Json, type Key, isEnvelope, isRecord, typeName } from "./doc.ts";
2
2
  import { ask } from "./ladder.ts";
3
- import { type CapsRule, type Kind, lookupKind } from "./world.ts";
4
- import { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
5
- import { type Cap, type Context, type Resolution, type ResolveOptions, resolve, valueOfThing, worldThing } from "./deref.ts";
3
+ import type { CapsRule, Kind } from "./world.ts";
4
+ import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
5
+ import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
6
+ import { STD_EVALUATORS } from "./stdlib.ts";
6
7
  import { type EvalError, type Failed, type Result, Failure, fail, ok, placed, thrown } from "./errors.ts";
7
8
  import {
8
9
  type AllOptions,
@@ -82,7 +83,10 @@ function own<T>(table: Readonly<Record<string, T>> | undefined, name: string): T
82
83
  return table !== undefined && Object.hasOwn(table, name) ? table[name] : undefined;
83
84
  }
84
85
 
85
- /** This function finds the code of an evaluator name. A runner wins over an evaluator of the same name. */
86
+ /**
87
+ * This function finds the code of an evaluator name. A runner wins over an evaluator of the same name. A standard
88
+ * evaluator gets the definition of the reference, unless the options give another definition (REF.TREE).
89
+ */
86
90
  function choose(env: Env, name: string): Chosen | undefined {
87
91
  const runner = own(env.machine.runners, name);
88
92
  if (runner !== undefined) {
@@ -92,7 +96,8 @@ function choose(env: Env, name: string): Chosen | undefined {
92
96
  }
93
97
  const evaluator = own(env.machine.evaluators, name);
94
98
  if (evaluator === undefined) return undefined;
95
- const defined = own(env.definitions.evaluators, name);
99
+ const standard = evaluator === own(STD_EVALUATORS, name) ? own(STD_DEFINITIONS.evaluators, name) : undefined;
100
+ const defined = own(env.definitions.evaluators, name) ?? standard;
96
101
  if (defined !== undefined) return { evaluator: defined };
97
102
  return typeof evaluator === "function" ? { evaluator } : { gap: `The reference has no definition of the compiled evaluator ${name}.` };
98
103
  }
@@ -104,25 +109,20 @@ function choose(env: Env, name: string): Chosen | undefined {
104
109
  */
105
110
  function examine(env: Env, at: At): Seen | Failed {
106
111
  const { place, declared } = at;
107
- const envelope = isEnvelope(at.value) ? (at.value as Envelope) : undefined;
108
- if (envelope === undefined && declared === undefined) return fail("#PART?", "The value has no envelope, and its position declares no type.", { place });
112
+ const world = env.machine.world;
113
+ // The lookup of the kind and the split of the value are definitions of the model (D15): partsOf gives both.
114
+ const read = partsOf(world, at.value, declared);
115
+ if (!read.ok) return fail(read.code, read.reason, { place });
109
116
  if (at.depth > env.maxDepth) return fail("#DEPTH!", `The nesting is deeper than ${env.maxDepth} documents.`, { place });
110
- const doc: Envelope = envelope ?? { type: declared as string, value: at.value as Json };
117
+ const { doc, kind, split } = read;
111
118
  const kindText = typeName(doc.type);
112
- const world = env.machine.world;
113
- const answer = ask(world, envelope ?? {}, "eval", declared === undefined ? {} : { declaredType: declared });
119
+ const answer = ask(world, read.declared ? {} : doc, "eval", declared === undefined ? {} : { declaredType: declared });
114
120
  if (answer.error === "unknown-kind") return fail("#KIND?", `The world has no kind ${kindText}.`, { place });
115
121
  const name = answer.evaluator;
116
122
  const chosen = name === undefined ? undefined : choose(env, name);
117
123
  if (name === undefined || chosen === undefined) return fail("#NOEVAL", `No rung gives an evaluator for ${kindText}.`, { place });
118
- const found = lookupKind(world, doc.type);
119
- const kind = found.ok ? found.kind : undefined;
120
- const split = splitParts(kind?.parts, doc.value);
121
- if (!split.ok) {
122
- const where = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
123
- return fail("#VALUE!", `${split.reason}${where} The kind is ${kindText}.`, { place });
124
- }
125
- return { doc, kindText, typePath: found.ok ? found.path : [], kind, name, chosen, split };
124
+ if (!split.ok) return fail("#VALUE!", split.reason, { place });
125
+ return { doc, kindText, typePath: read.typePath ?? [], kind, name, chosen, split };
126
126
  }
127
127
 
128
128
  /** This function gives the capabilities that the rules of a kind give one part (D14, CAP.CONTRACT, CAP.ONLY). */
@@ -343,6 +343,145 @@ export function reference(machine: Machine, doc: unknown, context: Context = mak
343
343
  }
344
344
  }
345
345
 
346
+ /*
347
+ * The definition of the decorated tree (TREE.*), written from docs/SPEC.md. It shares no code with `tree.node` of
348
+ * stdlib.ts: its nodes, its memo, its marks for attributes in evaluation and its JSON are its own.
349
+ */
350
+
351
+ /** One tree of the reference in one evaluation. */
352
+ interface RefTree {
353
+ open: boolean;
354
+ }
355
+
356
+ /** A node of the reference. Its host thing is the key of the table `refNodes`. */
357
+ interface RefNode {
358
+ readonly tree: RefTree;
359
+ readonly kind: string;
360
+ readonly place: readonly Key[];
361
+ readonly parts: readonly Part[];
362
+ readonly ctx: Context;
363
+ /** The results of the attributes that ended. */
364
+ readonly values: Map<string, Result>;
365
+ /** The attributes in evaluation. */
366
+ readonly pending: Set<string>;
367
+ }
368
+
369
+ const refNodes = new WeakMap<object, RefNode>();
370
+
371
+ /** The attributes in evaluation in the reference, in the order of their demands (TREE.CYCLE). */
372
+ const inEvaluation: { readonly node: RefNode; readonly name: string }[] = [];
373
+
374
+ function nameOfAttribute(node: RefNode, name: string): string {
375
+ return `${name} of ${node.kind} at [${node.place.join(", ")}]`;
376
+ }
377
+
378
+ function refNodeOf(thing: unknown): RefNode | undefined {
379
+ return typeof thing === "object" && thing !== null ? refNodes.get(thing) : undefined;
380
+ }
381
+
382
+ /** TREE.ONCE, TREE.MEMO and TREE.CYCLE: the result of one attribute, or `undefined` when the node has no such part. */
383
+ function attributeOf(node: RefNode, self: HostThing, name: string): Result | undefined {
384
+ const done = node.values.get(name);
385
+ if (done !== undefined) return done;
386
+ if (node.pending.has(name)) {
387
+ const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
388
+ const steps = from < 0 ? [nameOfAttribute(node, name)] : inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
389
+ return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
390
+ }
391
+ const part = node.parts.find((item) => item.key === name);
392
+ if (part === undefined) return undefined;
393
+ node.pending.add(name);
394
+ inEvaluation.push({ node, name });
395
+ try {
396
+ const result = part.evaluate({ ...node.ctx, self });
397
+ node.values.set(name, result);
398
+ return result;
399
+ } finally {
400
+ node.pending.delete(name);
401
+ inEvaluation.pop();
402
+ }
403
+ }
404
+
405
+ /** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
406
+ function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
407
+ const record: Record<string, unknown> = {};
408
+ for (const part of node.parts) {
409
+ const result = attributeOf(node, self, part.key) as Result;
410
+ if (!result.ok) return result;
411
+ const value = plainValue(result.value, [...open, self]);
412
+ if (!value.ok) return value;
413
+ setField(record, part.key, value.value);
414
+ }
415
+ return ok(record);
416
+ }
417
+
418
+ /** This function gives a copy of a value with the JSON of each node in it. */
419
+ function plainValue(value: unknown, open: readonly object[]): Result {
420
+ const node = refNodeOf(value);
421
+ if (node !== undefined) {
422
+ if (open.includes(value as object)) return fail("#CYCLE!", `The JSON of the node ${node.kind} at [${node.place.join(", ")}] holds the node itself.`);
423
+ return jsonOfNode(node, value as HostThing, open);
424
+ }
425
+ if (typeof value !== "object" || value === null || isHostThing(value) || open.includes(value)) return ok(value);
426
+ if (Array.isArray(value)) {
427
+ const items: unknown[] = [];
428
+ for (const item of value) {
429
+ const plain = plainValue(item, [...open, value]);
430
+ if (!plain.ok) return plain;
431
+ items.push(plain.value);
432
+ }
433
+ return ok(items);
434
+ }
435
+ if (!isRecord(value)) return ok(value);
436
+ const copy: Record<string, unknown> = {};
437
+ for (const key of Object.keys(value)) {
438
+ const plain = plainValue(value[key], [...open, value]);
439
+ if (!plain.ok) return plain;
440
+ setField(copy, key, plain.value);
441
+ }
442
+ return ok(copy);
443
+ }
444
+
445
+ /**
446
+ * TREE.LAZY, TREE.PARENT, TREE.SIBLING and TREE.ERROR: a node of an open tree gives its thing, and the root of a new
447
+ * tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error.
448
+ */
449
+ const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
450
+ const outer = refNodeOf(context.self);
451
+ const nested = outer !== undefined && outer.tree.open;
452
+ const node: RefNode = { tree: nested ? outer.tree : { open: true }, kind, place, parts, ctx: context, values: new Map(), pending: new Set() };
453
+ const self: HostThing & { readonly type: string } = {
454
+ [THING]: true,
455
+ type: kind,
456
+ label: `node ${kind}`,
457
+ deref: (key): Thing | undefined => {
458
+ if (key === "parent") return context.self;
459
+ if (key === "type") return kind;
460
+ const result = attributeOf(node, self, String(key));
461
+ if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
462
+ throw new Failure(result.error);
463
+ },
464
+ toJson: () => {
465
+ const result = jsonOfNode(node, self, []);
466
+ if (result.ok) return result.value;
467
+ throw new Failure(result.error);
468
+ },
469
+ };
470
+ refNodes.set(self, node);
471
+ if (nested) return ok(self);
472
+ try {
473
+ return jsonOfNode(node, self, []);
474
+ } finally {
475
+ node.tree.open = false;
476
+ }
477
+ };
478
+
479
+ /**
480
+ * The definitions of the reference for the evaluators of the standard machine that need their own reading. The
481
+ * reference uses them for the standard evaluators, also without the option `definitions`. It holds `tree.node`.
482
+ */
483
+ export const STD_DEFINITIONS: Definitions = { evaluators: { "tree.node": treeDefinition } };
484
+
346
485
  /* The definitions of the formula kit: one function evaluator for each construct, written from docs/SPEC.md. */
347
486
 
348
487
  const CLOSURE: unique symbol = Symbol.for("coeval.closure") as never;
package/src/static.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
2
  import { type Kind, type NamesSource, type ScopeRule, type World, lookupKind } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
- import { type PartSlot, pathMatches, splitParts } from "./parts.ts";
4
+ import { type PartSlot, partsOf, pathMatches } from "./parts.ts";
5
5
  import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
6
6
  import type { ErrorCode } from "./errors.ts";
7
7
  import { type Machine, partCaps, permits } from "./evaluate.ts";
@@ -234,37 +234,32 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
234
234
  ];
235
235
  while (stack.length > 0) {
236
236
  const item = stack.pop() as Item;
237
- const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
238
- if (envelope === undefined && item.declared === undefined) {
239
- options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
237
+ const read = partsOf(world, item.value, item.declared);
238
+ if (!read.ok) {
239
+ options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: read.code, reason: read.reason });
240
240
  continue;
241
241
  }
242
- const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
243
- const found = lookupKind(world, doc.type);
244
- const kind = found.ok ? found.kind : undefined;
242
+ const { doc, kind, split } = read;
245
243
  const here = new Visit(
246
244
  item.place,
247
245
  item.at,
248
246
  doc,
249
- envelope === undefined,
247
+ read.declared,
250
248
  item.depth,
251
249
  item.names,
252
250
  item.later,
253
251
  item.slot,
254
252
  item.parent,
255
253
  kind,
256
- found.ok ? found.path : undefined,
254
+ read.typePath,
257
255
  item.address ? true : undefined,
258
256
  item.caps,
259
257
  );
260
258
  const answer = visit(here);
261
259
  if (answer === "skip" || kind === undefined) continue;
262
- const split = splitParts(kind.parts, doc.value);
263
- const base: Trail | undefined = envelope === undefined ? item.at : { up: item.at, keys: ["value"] };
260
+ const base: Trail | undefined = read.declared ? item.at : { up: item.at, keys: ["value"] };
264
261
  if (!split.ok) {
265
- const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
266
- const where = keysOf({ up: base, keys: split.at });
267
- options.problem?.({ place: keysOf(item.place), at: where, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
262
+ options.problem?.({ place: keysOf(item.place), at: keysOf({ up: base, keys: split.at }), code: "#VALUE!", reason: split.reason });
268
263
  continue;
269
264
  }
270
265
  const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
@@ -424,7 +419,8 @@ function capReason(v: DocVisit, kind: readonly Key[]): string {
424
419
  * - `#PART?`: a value without a type.
425
420
  * - `#DEPTH!`: too much nesting.
426
421
  * - `#REF!` and `#STEP?`: a bad path.
427
- * - `#NAME?`: a name that nothing binds.
422
+ * - `#NAME?`: a name that nothing binds. With the hook `known` of the machine, also a name that the language does not
423
+ * have, for example an operation.
428
424
  * - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
429
425
  *
430
426
  * @param machine - The world and the code of the evaluators.
@@ -455,6 +451,10 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
455
451
  const name = answer.evaluator;
456
452
  if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
457
453
  issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
454
+ } else if (machine.known !== undefined) {
455
+ // The evaluator gives #NAME? in a run for a name that the language does not have, for example an operation.
456
+ const known = machine.known(v.typePath ?? [], name);
457
+ if (known !== true) issues.push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
458
458
  }
459
459
  const caps = v.caps;
460
460
  if (caps !== undefined && v.kind?.effects !== undefined) {
@@ -474,7 +474,8 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
474
474
  const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
475
475
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
476
476
  issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
477
- return "skip";
477
+ // The step documents of the path are parts, and they evaluate before the path, thus the walk visits them.
478
+ return undefined;
478
479
  }
479
480
  if (!roots.includes(root)) {
480
481
  issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });
package/src/stdlib.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type Json, type Key, field, isRecord } from "./doc.ts";
2
- import { type Context, type Evaluator, type Machine, type Part, type Result, type RunPart, type Runner, type Steps, fail, ok } from "./evaluate.ts";
2
+ import { type Context, type EvalInput, type Evaluator, type Machine, type Part, type Result, type RunPart, type Runner, type Steps, fail, ok } from "./evaluate.ts";
3
+ import { Failure } from "./errors.ts";
3
4
  import { type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
4
5
  import type { World } from "./ladder.ts";
5
6
 
@@ -44,12 +45,15 @@ export const STD_WORLD: World = {
44
45
  panel: {
45
46
  verbs: { eval: "tree.node", view: "view.op" },
46
47
  parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
47
- summary: "A node of a tree. Its parts see it as self, and each node links to its parent.",
48
+ // An attribute evaluates on demand, thus in an order that is not the order of the document. It causes no effect.
49
+ caps: [{ rule: "mask" }],
50
+ summary: "A node of a tree. Each part is an attribute that evaluates on demand, one time, with the node as self.",
48
51
  },
49
52
  box: {
50
53
  verbs: { eval: "tree.node", view: "view.op" },
51
54
  parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
52
- summary: "A node of a tree, as panel. A formula in a box can read the parent of the box.",
55
+ caps: [{ rule: "mask" }],
56
+ summary: "A node of a tree, as panel. A formula in a box can read its parent and its siblings.",
53
57
  },
54
58
  math: {
55
59
  kinds: {
@@ -149,51 +153,200 @@ export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
149
153
  const found = resolve(data as readonly Json[]);
150
154
  return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
151
155
  },
152
- "tree.node": (input) => treeNode(input.parts, input.context, input.kind),
156
+ "tree.node": (input) => treeNode(input),
153
157
  };
154
158
 
159
+ /*
160
+ * The decorated tree (TREE.*, R3 P1). Each node is a host thing, and each part of a node is an attribute. An attribute
161
+ * evaluates on demand, with the node as self. One memo cell for each node and attribute keeps its result for one
162
+ * evaluation. The structure comes first: a node inside an attribute of an open tree gives its thing and evaluates
163
+ * nothing. The root gives the JSON of the tree. Thus a reference to a sibling reads the finished list of children.
164
+ */
165
+
166
+ /** The brand of a node of the decorated tree. Its value is the state that the kit reads. */
167
+ const TREE_NODE: unique symbol = Symbol.for("coeval.tree.node") as never;
168
+
169
+ /** One tree in one evaluation. It is open until its root gives the JSON of the tree. */
170
+ interface TreeBuild {
171
+ open: boolean;
172
+ }
173
+
174
+ /** The state of a node behind its brand. */
175
+ interface NodeState {
176
+ readonly build: TreeBuild;
177
+ readonly type: string;
178
+ readonly place: readonly Key[];
179
+ /**
180
+ * This function gives the JSON of the node: each attribute in the order of the parts, with the JSON of each node in
181
+ * its value.
182
+ *
183
+ * @param path - The nodes and the containers whose JSON is in progress. A node on it gives `#CYCLE!`.
184
+ * @returns The JSON, or the first error.
185
+ */
186
+ json(path: Set<object>): Result;
187
+ }
188
+
189
+ /** The memo cell of one attribute: in evaluation, or done with its result. */
190
+ type Cell = { readonly busy: true } | { readonly busy: false; readonly result: Result };
191
+
192
+ const BUSY: Cell = Object.freeze({ busy: true });
193
+
194
+ /** One attribute in evaluation. The text names it in the reason of a cycle. */
195
+ interface Demand {
196
+ readonly cells: Map<string, Cell>;
197
+ readonly key: string;
198
+ readonly text: string;
199
+ }
200
+
201
+ /** The attributes in evaluation, in the order of their demands. An evaluation does not wait, thus one list serves. */
202
+ const demands: Demand[] = [];
203
+
204
+ function attributeText(key: string, type: string, place: readonly Key[]): string {
205
+ return `${key} of ${type} at [${place.join(", ")}]`;
206
+ }
207
+
208
+ /** This function gives the `#CYCLE!` of a demand that reached an attribute in evaluation, with the chain of demands. */
209
+ function cycleAt(cells: Map<string, Cell>, key: string, text: string): Result {
210
+ const first = demands.findIndex((demand) => demand.cells === cells && demand.key === key);
211
+ const chain = first === -1 ? [text] : demands.slice(first).map((demand) => demand.text);
212
+ chain.push(text);
213
+ return fail("#CYCLE!", `The attributes form a cycle: ${chain.join(" -> ")}.`);
214
+ }
215
+
216
+ function stateOf(thing: unknown): NodeState | undefined {
217
+ return typeof thing === "object" && thing !== null ? (thing as { readonly [TREE_NODE]?: NodeState })[TREE_NODE] : undefined;
218
+ }
219
+
220
+ /** This function sets an own field, also the field `__proto__` (SHAPE.PROTO). */
221
+ function putField(target: Record<string, unknown>, key: string, value: unknown): void {
222
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
223
+ }
224
+
225
+ /**
226
+ * This function gives a value with the JSON of each node of a tree in it. A list or a plain record is new only when
227
+ * it holds a node. A container that holds itself stays as it is.
228
+ */
229
+ function plainOf(value: unknown, path: Set<object>): Result {
230
+ const state = stateOf(value);
231
+ if (state !== undefined) {
232
+ if (path.has(value as object)) return fail("#CYCLE!", `The JSON of the node ${state.type} at [${state.place.join(", ")}] holds the node itself.`);
233
+ return state.json(path);
234
+ }
235
+ if (typeof value !== "object" || value === null || path.has(value) || isHostThing(value)) return ok(value);
236
+ if (!Array.isArray(value) && !isRecord(value)) return ok(value);
237
+ path.add(value);
238
+ try {
239
+ if (Array.isArray(value)) {
240
+ let list: unknown[] | undefined;
241
+ for (let index = 0; index < value.length; index += 1) {
242
+ const item = plainOf(value[index], path);
243
+ if (!item.ok) return item;
244
+ if (item.value !== value[index]) (list ??= [...(value as readonly unknown[])])[index] = item.value;
245
+ }
246
+ return ok(list ?? value);
247
+ }
248
+ const record = value as Readonly<Record<string, unknown>>;
249
+ let out: Record<string, unknown> | undefined;
250
+ for (const key of Object.keys(record)) {
251
+ const item = plainOf(record[key], path);
252
+ if (!item.ok) return item;
253
+ if (item.value === record[key]) continue;
254
+ if (out === undefined) {
255
+ out = {};
256
+ for (const other of Object.keys(record)) putField(out, other, record[other]);
257
+ }
258
+ putField(out, key, item.value);
259
+ }
260
+ return ok(out ?? value);
261
+ } finally {
262
+ path.delete(value);
263
+ }
264
+ }
265
+
155
266
  /**
156
- * This function evaluates a node of a tree. The node is a host thing. A step `parent` reaches the node that holds
157
- * it. A step with the name of a part evaluates that part on demand, one time. Each part sees the node as self.
267
+ * This function makes a node of the decorated tree. A step `parent` gives the self of the context of the node, and a
268
+ * step `type` gives its type. A step with the key of a part reads that attribute. A failed attribute throws `Failure`
269
+ * with its own error, thus a reference to it gives that error.
158
270
  */
159
- function treeNode(parts: readonly Part[], context: Context, type: string): Result {
160
- const memo = new Map<string, Result>();
271
+ function treeThing(parts: readonly Part[], context: Context, type: string, place: readonly Key[], build: TreeBuild): HostThing {
272
+ const cells = new Map<string, Cell>();
161
273
  const read = (key: string): Result | undefined => {
162
- const known = memo.get(key);
163
- if (known !== undefined) return known;
274
+ const cell = cells.get(key);
275
+ if (cell !== undefined) return cell.busy ? cycleAt(cells, key, attributeText(key, type, place)) : cell.result;
164
276
  const part = parts.find((item) => item.key === key);
165
277
  if (part === undefined) return undefined;
166
- const result = part.evaluate({ ...context, self: me });
167
- memo.set(key, result);
278
+ cells.set(key, BUSY);
279
+ demands.push({ cells, key, text: attributeText(key, type, place) });
280
+ let result: Result;
281
+ try {
282
+ result = part.evaluate({ ...context, self: node });
283
+ } catch (error) {
284
+ // An exception of the host, for example an overflow of the stack, leaves the attribute without a result.
285
+ cells.delete(key);
286
+ throw error;
287
+ } finally {
288
+ demands.pop();
289
+ }
290
+ cells.set(key, { busy: false, result });
168
291
  return result;
169
292
  };
170
- const me: HostThing & { readonly type: string } = {
293
+ const state: NodeState = {
294
+ build,
295
+ type,
296
+ place,
297
+ json(path) {
298
+ path.add(node);
299
+ try {
300
+ const out: Record<string, unknown> = {};
301
+ for (const part of parts) {
302
+ const result = read(part.key) as Result;
303
+ if (!result.ok) return result;
304
+ const value = plainOf(result.value, path);
305
+ if (!value.ok) return value;
306
+ putField(out, part.key, value.value);
307
+ }
308
+ return ok(out);
309
+ } finally {
310
+ path.delete(node);
311
+ }
312
+ },
313
+ };
314
+ const node: HostThing & { readonly type: string; readonly [TREE_NODE]: NodeState } = {
171
315
  [THING]: true,
316
+ [TREE_NODE]: state,
172
317
  type,
173
318
  label: `node ${type}`,
174
319
  deref(key: Key): Thing | undefined {
175
320
  if (key === "parent") return context.self;
176
321
  if (key === "type") return type;
177
322
  const result = read(String(key));
178
- return result?.ok === true ? result.value : undefined;
323
+ if (result === undefined) return undefined;
324
+ if (!result.ok) throw new Failure(result.error);
325
+ return result.value;
179
326
  },
180
327
  toJson(): unknown {
181
- const out: Record<string, unknown> = {};
182
- for (const part of parts) {
183
- const result = read(part.key);
184
- out[part.key] = result?.ok === true ? result.value : null;
185
- }
186
- return out;
328
+ const result = state.json(new Set());
329
+ if (!result.ok) throw new Failure(result.error);
330
+ return result.value;
187
331
  },
188
332
  };
189
- const out: Record<string, unknown> = {};
190
- for (const part of parts) {
191
- const result = read(part.key);
192
- if (result === undefined) continue;
193
- if (!result.ok) return result;
194
- out[part.key] = result.value;
333
+ return node;
334
+ }
335
+
336
+ /**
337
+ * This function evaluates a node of the decorated tree. Inside an attribute of an open tree, the node gives its host
338
+ * thing, and no attribute evaluates. Else the node is the root of a new tree, and it gives the JSON of the tree.
339
+ */
340
+ function treeNode({ parts, context, kind, place }: EvalInput): Result {
341
+ const above = stateOf(context.self);
342
+ if (above !== undefined && above.build.open) return ok(treeThing(parts, context, kind, place, above.build));
343
+ const build: TreeBuild = { open: true };
344
+ const root = treeThing(parts, context, kind, place, build);
345
+ try {
346
+ return (stateOf(root) as NodeState).json(new Set());
347
+ } finally {
348
+ build.open = false;
195
349
  }
196
- return ok(out);
197
350
  }
198
351
 
199
352
  /** The options of a walker `nearest`. */