@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/README.md +6 -4
- package/build/index.d.ts +94 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +495 -118
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +8 -2
- package/src/doc.ts +26 -8
- package/src/errors.ts +4 -2
- package/src/evaluate.ts +27 -20
- package/src/formula.ts +45 -3
- package/src/generate.ts +37 -8
- package/src/index.ts +3 -2
- package/src/parts.ts +59 -3
- package/src/reference.ts +157 -18
- package/src/static.ts +17 -16
- package/src/stdlib.ts +180 -27
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 {
|
|
4
|
-
import { type PartSlot, type Split,
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
108
|
-
|
|
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
|
|
117
|
+
const { doc, kind, split } = read;
|
|
111
118
|
const kindText = typeName(doc.type);
|
|
112
|
-
const
|
|
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
|
-
|
|
119
|
-
|
|
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,
|
|
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
|
|
238
|
-
if (
|
|
239
|
-
options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
157
|
-
*
|
|
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
|
|
160
|
-
const
|
|
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
|
|
163
|
-
if (
|
|
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
|
-
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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`. */
|