@mark1russell7/coeval 0.6.1 → 0.6.3

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
@@ -4,7 +4,7 @@ import type { CapsRule, Kind } 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, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
6
6
  import { STD_EVALUATORS } from "./stdlib.ts";
7
- import { type EvalError, type Failed, type Result, Failure, fail, ok, placed, thrown } from "./errors.ts";
7
+ import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
8
8
  import {
9
9
  type AllOptions,
10
10
  type Effect,
@@ -12,6 +12,7 @@ import {
12
12
  type EvalInput,
13
13
  type Machine,
14
14
  type Part,
15
+ type Perform,
15
16
  type RunFn,
16
17
  type RunInput,
17
18
  type RunPart,
@@ -19,6 +20,7 @@ import {
19
20
  intersectCaps,
20
21
  makeContext,
21
22
  permits,
23
+ placeText,
22
24
  } from "./evaluate.ts";
23
25
  import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
24
26
 
@@ -45,6 +47,18 @@ export interface ReferenceOptions {
45
47
  readonly definitions?: Definitions;
46
48
  /** A list that gets each effect, in its order. */
47
49
  readonly effects?: Effect[];
50
+ /** The mock of the host (RUN.MOCK). It answers each effect at once. Without it, an effect gives its payload. */
51
+ readonly perform?: Perform;
52
+ }
53
+
54
+ /** The scope of the addresses of effects (RUN.ADDRESS): the root of the evaluation, or one item of `all`. */
55
+ interface Scope {
56
+ /** The text before each address of the scope. */
57
+ readonly prefix: string;
58
+ /** The number of earlier effects at each place of the scope, by the text of the place. */
59
+ readonly counts: Map<string, number>;
60
+ /** The number of earlier uses of `all` in the scope. */
61
+ alls: number;
48
62
  }
49
63
 
50
64
  /** The state of one evaluation of the reference. */
@@ -53,6 +67,9 @@ interface Env {
53
67
  readonly definitions: Definitions;
54
68
  readonly effects: Effect[];
55
69
  readonly maxDepth: number;
70
+ readonly perform: Perform | undefined;
71
+ /** The scope of the code that evaluates at this time. `all` changes it for each item. */
72
+ scope: Scope;
56
73
  }
57
74
 
58
75
  /** The code that answers a document: a function evaluator, a runner, or a gap of the reference. */
@@ -167,15 +184,52 @@ function resolveFully(machine: Machine, ctx: Context, path: readonly unknown[],
167
184
  return options?.hops === true ? found : { ...found, hops: [] };
168
185
  }
169
186
 
170
- /** This function records an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). */
171
- function record(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Failed | undefined {
187
+ /**
188
+ * This function causes an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). It records the effect with its
189
+ * address (RUN.ADDRESS) and its outcome (RUN.OUTCOME). The mock answers the effect at once (RUN.MOCK). Without a
190
+ * mock, the effect gives its payload (RUN.EVAL).
191
+ */
192
+ function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
172
193
  const keys = typeof kind === "string" ? kind.split(".") : kind;
173
194
  const text = keys.join(".");
174
195
  const machine = env.machine;
175
196
  const allowed = machine.permits === undefined ? permits(ctx.caps, keys) : machine.permits(ctx.caps, keys, ctx);
176
197
  if (!allowed) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
177
- env.effects.push({ index: env.effects.length, kind: text, path: keys, payload, node: -1, at: 0, place, context: ctx });
178
- return undefined;
198
+ const at = placeText(place);
199
+ const count = env.scope.counts.get(at) ?? 0;
200
+ env.scope.counts.set(at, count + 1);
201
+ const effect: { -readonly [K in keyof Effect]: Effect[K] } = {
202
+ index: env.effects.length,
203
+ kind: text,
204
+ path: keys,
205
+ payload,
206
+ node: -1,
207
+ at: 0,
208
+ place,
209
+ context: ctx,
210
+ address: `${env.scope.prefix}${at}#${count}`,
211
+ };
212
+ env.effects.push(effect);
213
+ effect.outcome = answer(env.perform, effect, place);
214
+ return effect.outcome;
215
+ }
216
+
217
+ /**
218
+ * This function gives the answer of the mock to an effect. A throw gives `#CALC!` with the cause, and a `Failure`
219
+ * gives its own error value. A promise gives `#CALC!`, because an evaluation does not wait.
220
+ */
221
+ function answer(perform: Perform | undefined, effect: Effect, place: readonly Key[]): Result {
222
+ if (perform === undefined) return ok(effect.payload);
223
+ let given: unknown;
224
+ try {
225
+ given = perform(effect);
226
+ } catch (error) {
227
+ if (error instanceof Failure) return placed({ ok: false, error: error.error }, place);
228
+ return fail("#CALC!", `The host failed the effect ${effect.kind}: ${describe(error)}`, { place, cause: error });
229
+ }
230
+ if (!(given instanceof Promise)) return ok(given);
231
+ void given.catch(() => undefined);
232
+ return fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place });
179
233
  }
180
234
 
181
235
  /** This function evaluates a document. Each error gets the place of the document where it started (K5). */
@@ -247,7 +301,7 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
247
301
  place: at.place,
248
302
  ...(at.address ? { address: true } : {}),
249
303
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
250
- effect: (kind, payload) => record(env, at.ctx, kind, payload, at.place) ?? ok(payload),
304
+ effect: (kind, payload) => cause(env, at.ctx, kind, payload, at.place),
251
305
  fill,
252
306
  };
253
307
  }
@@ -275,9 +329,9 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
275
329
  place: at.place,
276
330
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
277
331
  effect: function* (kind, payload) {
278
- return record(env, at.ctx, kind, payload, at.place) ?? ok(payload);
332
+ return cause(env, at.ctx, kind, payload, at.place);
279
333
  },
280
- all: (list, options) => together(list, options),
334
+ all: (list, options) => together(env, list, options),
281
335
  fill,
282
336
  };
283
337
  }
@@ -287,14 +341,21 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
287
341
  * goes on until it ends or waits. In an evaluation, a part that waits does not end, thus it keeps its place in the
288
342
  * limit. With `failFast`, the first failure stops the parts that wait, and the later parts do not start
289
343
  * (RUN.FAIL-FAST). When no part failed and a part waits, the steps wait, and the drive gives `#CALC!`.
344
+ *
345
+ * Each part has its own scope of addresses. Its prefix names the use of `all` in the scope around it, and the part
346
+ * (RUN.ADDRESS).
290
347
  */
291
- function* together(list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
348
+ function* together(env: Env, list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
292
349
  const limit = Math.max(1, options.concurrency ?? list.length);
293
350
  const results: (Result | undefined)[] = list.map(() => undefined);
294
351
  const waiting: { readonly steps: Steps; readonly promise: Promise<unknown> }[] = [];
352
+ const outer = env.scope;
353
+ const address = `${outer.prefix}all${outer.alls}`;
354
+ outer.alls += 1;
295
355
  let failed = false;
296
356
  for (let index = 0; index < list.length && !failed && waiting.length < limit; index += 1) {
297
357
  let result: Result | undefined;
358
+ env.scope = { prefix: `${address}.${index}/`, counts: new Map(), alls: 0 };
298
359
  try {
299
360
  const steps = (list[index] as RunPart).evaluate();
300
361
  const step = steps.next();
@@ -302,6 +363,8 @@ function* together(list: readonly RunPart[], options: AllOptions = {}): Generato
302
363
  else waiting.push({ steps, promise: step.value });
303
364
  } catch (error) {
304
365
  result = thrown(error, "a part");
366
+ } finally {
367
+ env.scope = outer;
305
368
  }
306
369
  results[index] = result;
307
370
  if (result !== undefined && options.failFast === true && !result.ok) failed = true;
@@ -325,16 +388,23 @@ function* together(list: readonly RunPart[], options: AllOptions = {}): Generato
325
388
  /**
326
389
  * This function evaluates a document by the rules, without a compilation. It is the reference of the plans: a plan,
327
390
  * a traced plan and the second tier of a language must give its results. It covers evaluations, not runs: an effect
328
- * gives its payload, and a part that waits gives `#CALC!`.
391
+ * gives the answer of the mock or its payload, and a part that waits gives `#CALC!`.
329
392
  *
330
393
  * @param machine - The world and the code of the evaluators.
331
394
  * @param doc - The document.
332
395
  * @param context - The names and the capabilities at the root.
333
- * @param options - The type of a bare root value, the definitions of compiled code, and a list for the effects.
396
+ * @param options - The type of a bare root value, the definitions of compiled code, a list for the effects and a mock.
334
397
  * @returns The result.
335
398
  */
336
399
  export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
337
- const env: Env = { machine, definitions: options.definitions ?? {}, effects: options.effects ?? [], maxDepth: machine.maxDepth ?? 1000 };
400
+ const env: Env = {
401
+ machine,
402
+ definitions: options.definitions ?? {},
403
+ effects: options.effects ?? [],
404
+ maxDepth: machine.maxDepth ?? 1000,
405
+ perform: options.perform,
406
+ scope: { prefix: "", counts: new Map(), alls: 0 },
407
+ };
338
408
  const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
339
409
  try {
340
410
  return evaluateAt(env, { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx });
@@ -366,7 +436,7 @@ interface RefNode {
366
436
  readonly pending: Set<string>;
367
437
  }
368
438
 
369
- const refNodes = new WeakMap<object, RefNode>();
439
+ const refNodes = /* @__PURE__ */ new WeakMap<object, RefNode>();
370
440
 
371
441
  /** The attributes in evaluation in the reference, in the order of their demands (TREE.CYCLE). */
372
442
  const inEvaluation: { readonly node: RefNode; readonly name: string }[] = [];
@@ -461,6 +531,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
461
531
  if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
462
532
  throw new Failure(result.error);
463
533
  },
534
+ has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)),
464
535
  toJson: () => {
465
536
  const result = jsonOfNode(node, self, []);
466
537
  if (result.ok) return result.value;
@@ -484,7 +555,7 @@ export const STD_DEFINITIONS: Definitions = { evaluators: { "tree.node": treeDef
484
555
 
485
556
  /* The definitions of the formula kit: one function evaluator for each construct, written from docs/SPEC.md. */
486
557
 
487
- const CLOSURE: unique symbol = Symbol.for("coeval.closure") as never;
558
+ const CLOSURE: unique symbol = /* @__PURE__ */ Symbol.for("coeval.closure") as never;
488
559
 
489
560
  /** This function sets an own field, also the field `__proto__` (SHAPE.PROTO). */
490
561
  function setField(target: Record<string, unknown>, key: string, value: unknown): void {
@@ -696,7 +767,7 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
696
767
  }
697
768
 
698
769
  /** The mark of a field that the profile leaves out of a record (FORMULA.RECORD). */
699
- const SKIP: unique symbol = Symbol("skip");
770
+ const SKIP: unique symbol = /* @__PURE__ */ Symbol("skip");
700
771
 
701
772
  /** The values of parts in order. The first error stops them, also with `combine` (FORMULA.STEPS, FORMULA.FN). */
702
773
  function valuesOfStrict(parts: readonly Part[]): { readonly values: readonly unknown[] } | Failed {
package/src/replay.ts ADDED
@@ -0,0 +1,159 @@
1
+ import { docEquals } from "./doc.ts";
2
+ import { type Result, Failure, fail } from "./errors.ts";
3
+ import { type Effect, type Journal, type Perform, JOURNAL } from "./evaluate.ts";
4
+
5
+ /*
6
+ * Record and replay (R2 P2, D19). A recorder wraps the function `perform` of a host and keeps a log. The log has each
7
+ * effect with its address, its request and its outcome, and the order in which the waits of each `all` settled. A
8
+ * replayer answers each effect from the log by its address, thus a replay does no host effect. The kernel gives the
9
+ * address and the outcome of each effect. The journal on the function `perform` carries the order of the waits.
10
+ */
11
+
12
+ /** One effect of a log: its address, its request and its outcome. */
13
+ export interface ReplayEntry {
14
+ /** The address of the effect (RUN.ADDRESS), for example `1#0` or `all0.1/1#0`. */
15
+ readonly address: string;
16
+ /** The kind of the effect, as a text with dots. */
17
+ readonly kind: string;
18
+ readonly payload: unknown;
19
+ /** The outcome of the effect (RUN.OUTCOME). It is absent when the recorded run stopped before the answer came. */
20
+ readonly outcome?: Result;
21
+ /** `true` when the host made the run wait for the outcome. */
22
+ readonly waited: boolean;
23
+ }
24
+
25
+ /** An item of an `all` whose wait settled in the recorded run. */
26
+ export interface ReplaySettle {
27
+ /** The address of the `all`, for example `all0` or `all0.1/all0`. */
28
+ readonly all: string;
29
+ /** The index of the item. */
30
+ readonly item: number;
31
+ }
32
+
33
+ /** The log of a run: each effect in the order of the requests, and the order in which the waits of each `all` settled. */
34
+ export interface ReplayLog {
35
+ readonly effects: readonly ReplayEntry[];
36
+ readonly settles: readonly ReplaySettle[];
37
+ }
38
+
39
+ /** A recorder: the function `perform` for a run, and the log of that run (RUN.RECORD). */
40
+ export interface Recorder {
41
+ /** The function to give the run. It gives each effect to the function `perform` of the host, and it records the effect. */
42
+ readonly perform: Perform;
43
+ /**
44
+ * This function gives the log of the run until this time. An effect gets its outcome when the answer comes.
45
+ *
46
+ * @returns The log.
47
+ */
48
+ log(): ReplayLog;
49
+ }
50
+
51
+ /** The options of a replayer. */
52
+ export interface ReplayOptions {
53
+ /**
54
+ * `false`: each answer comes at once, also for an effect that waited in the recorded run. An evaluation needs this
55
+ * value, because it does not wait. The default is `true`: an effect waits when it waited in the recorded run.
56
+ */
57
+ readonly wait?: boolean;
58
+ }
59
+
60
+ /**
61
+ * This function makes a recorder (RUN.RECORD). The recorder gives each effect to the function `perform` of the host.
62
+ * Its log keeps each effect with its address, its kind, its payload and the outcome that the kernel gives it. The log
63
+ * also keeps the order in which the waits of each `all` settle.
64
+ *
65
+ * @param perform - The function of the host. Without it, each effect gives its payload, as in a run without a host.
66
+ * @returns The function `perform` for the run, and the log.
67
+ */
68
+ export function recorder(perform?: Perform): Recorder {
69
+ const seen: { readonly effect: Effect; waited: boolean }[] = [];
70
+ const settles: ReplaySettle[] = [];
71
+ const journal: Journal = {
72
+ settled: (all, item) => {
73
+ settles.push({ all, item });
74
+ },
75
+ };
76
+ const host = (effect: Effect): unknown => {
77
+ const entry = { effect, waited: false };
78
+ seen.push(entry);
79
+ const answer = perform === undefined ? effect.payload : perform(effect);
80
+ if (answer instanceof Promise) entry.waited = true;
81
+ return answer;
82
+ };
83
+ return {
84
+ perform: Object.assign(host, { [JOURNAL]: journal }),
85
+ log: () => ({
86
+ effects: seen.map(({ effect, waited }) => ({
87
+ address: effect.address ?? "",
88
+ kind: effect.kind,
89
+ payload: effect.payload,
90
+ waited,
91
+ ...(effect.outcome === undefined ? {} : { outcome: effect.outcome }),
92
+ })),
93
+ settles: [...settles],
94
+ }),
95
+ };
96
+ }
97
+
98
+ /**
99
+ * This function makes a replayer: a function `perform` that answers each effect from a log, by its address (RUN.REPLAY). A
100
+ * recorded error comes again with its code, its reason, its place and its cause. The waits of each `all` settle in the
101
+ * recorded order. An effect that the log does not have at its address gives `#REPLAY!` (RUN.REPLAY-MISMATCH).
102
+ *
103
+ * A replayer keeps its place in the order of the waits, thus each run needs a new replayer.
104
+ *
105
+ * @param log - The log of a recorder.
106
+ * @param options - `wait: false` for an evaluation.
107
+ * @returns The function `perform` for the replay.
108
+ */
109
+ export function replayer(log: ReplayLog, options: ReplayOptions = {}): Perform {
110
+ const wait = options.wait !== false;
111
+ const entries = new Map<string, ReplayEntry>();
112
+ for (const entry of log.effects) entries.set(entry.address, entry);
113
+ const order = new Map<string, number[]>();
114
+ for (const settle of log.settles) {
115
+ const items = order.get(settle.all);
116
+ if (items === undefined) order.set(settle.all, [settle.item]);
117
+ else items.push(settle.item);
118
+ }
119
+ const journal: Journal = {
120
+ next: (all, waiting) => {
121
+ const items = order.get(all);
122
+ const item = items?.[0];
123
+ if (items === undefined || item === undefined || !waiting.includes(item)) return undefined;
124
+ items.shift();
125
+ return item;
126
+ },
127
+ };
128
+ const perform = (effect: Effect): unknown => {
129
+ const entry = effect.address === undefined ? undefined : entries.get(effect.address);
130
+ const wrong = mismatch(effect, entry);
131
+ if (wrong !== undefined || entry === undefined) throw wrong;
132
+ const outcome = entry.outcome;
133
+ const failure =
134
+ outcome === undefined
135
+ ? replayError(effect, `The log has no outcome for the effect ${effect.kind} at ${effect.address ?? ""}: the recorded run stopped before the answer.`)
136
+ : outcome.ok
137
+ ? undefined
138
+ : new Failure(outcome.error);
139
+ const value = outcome?.ok === true ? outcome.value : undefined;
140
+ if (wait && entry.waited) return failure === undefined ? Promise.resolve(value) : Promise.reject(failure);
141
+ if (failure !== undefined) throw failure;
142
+ return value;
143
+ };
144
+ return Object.assign(perform, { [JOURNAL]: journal });
145
+ }
146
+
147
+ /** This function gives the error of an effect that the log does not have at its address, or `undefined`. */
148
+ function mismatch(effect: Effect, entry: ReplayEntry | undefined): Failure | undefined {
149
+ const at = effect.address ?? "";
150
+ if (entry === undefined) return replayError(effect, `The log has no effect at the address ${at}, and the run asks for the effect ${effect.kind}.`);
151
+ if (entry.kind !== effect.kind) return replayError(effect, `The log has the effect ${entry.kind} at the address ${at}, and the run asks for the effect ${effect.kind}.`);
152
+ if (!docEquals(entry.payload, effect.payload)) return replayError(effect, `The effect ${effect.kind} at the address ${at} has another payload than the log.`);
153
+ return undefined;
154
+ }
155
+
156
+ /** This function gives the exception that carries `#REPLAY!` to the kernel, with the place of the effect. */
157
+ function replayError(effect: Effect, reason: string): Failure {
158
+ return new Failure(fail("#REPLAY!", reason, { place: effect.place }).error);
159
+ }
package/src/same.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { type Key, docEquals, equalWith } from "./doc.ts";
2
+ import type { EvalError, Result } from "./errors.ts";
3
+ import { Held } from "./formula.ts";
4
+
5
+ /**
6
+ * This function tells if two results are the same for an early cutoff (RESULT.SAME). A schedule uses it to stop the
7
+ * work when a part that evaluates again gives the same result. A wrong `true` keeps a stale value, but a wrong `false`
8
+ * only costs work. Thus each rule gives `false` when it is not sure.
9
+ *
10
+ * - Two values compare by the rules of `docEquals`. Lists and plain records compare by their items and fields. Each
11
+ * other value compares with `Object.is`.
12
+ * - Thus `-0` is not `0`, and `NaN` is `NaN`. A date, a map and a closure of `fn` are the same only as themselves.
13
+ * - Two errors are the same when they have the same code, reason and place (RESULT.SAME-ERROR). The cause is not
14
+ * compared. A `Held` value compares its error by the same rule.
15
+ * - A value and an error are not the same.
16
+ *
17
+ * `sameValue` of `agree` uses the same walk, with rules for two tiers (D20). It compares two closures by their
18
+ * parameters, and two errors by their code and place.
19
+ *
20
+ * @param a - The earlier result.
21
+ * @param b - The new result.
22
+ * @param equal - The rule of the host for two values, for example two dates. It answers first, and `undefined` gives the rules above.
23
+ * @returns `true` when the results are the same.
24
+ */
25
+ export function sameResult(a: Result<unknown>, b: Result<unknown>, equal?: (a: unknown, b: unknown) => boolean | undefined): boolean {
26
+ if (a.ok && b.ok) return equalWith(a.value, b.value, equal === undefined ? sameHeld : (x, y) => equal(x, y) ?? sameHeld(x, y));
27
+ if (!a.ok && !b.ok) return sameError(a.error, b.error);
28
+ return false;
29
+ }
30
+
31
+ /** The rule of `sameResult` for held errors. Two `Held` values are the same when their errors are the same. */
32
+ function sameHeld(x: unknown, y: unknown): boolean | undefined {
33
+ if (x instanceof Held || y instanceof Held) return x instanceof Held && y instanceof Held && sameError(x.error, y.error);
34
+ return undefined;
35
+ }
36
+
37
+ /** Two errors are the same when they have the same code, reason and place. An error without a place has no place in both. */
38
+ function sameError(a: EvalError, b: EvalError): boolean {
39
+ if (a.code !== b.code || a.reason !== b.reason) return false;
40
+ if (a.place === undefined || b.place === undefined) return a.place === b.place;
41
+ return docEquals(a.place as readonly Key[], b.place as readonly Key[]);
42
+ }
package/src/static.ts CHANGED
@@ -127,7 +127,7 @@ function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots
127
127
  }
128
128
 
129
129
  /** The names of a walk without names. A walk does not change a set at any time: it makes a new set to add names. */
130
- const NO_NAMES: ReadonlySet<string> = new Set();
130
+ const NO_NAMES: ReadonlySet<string> = /* @__PURE__ */ new Set();
131
131
 
132
132
  /** This function gives the options of a walk without the names of the host. */
133
133
  function withoutNames(options: WalkOptions): WalkOptions {
package/src/stdlib.ts CHANGED
@@ -122,9 +122,9 @@ function dataOf(type: "number" | "string" | "boolean"): Evaluator {
122
122
 
123
123
  /** The code of the evaluators of the standard world, by name. */
124
124
  export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
125
- "data.number": dataOf("number"),
126
- "data.text": dataOf("string"),
127
- "data.bool": dataOf("boolean"),
125
+ "data.number": /* @__PURE__ */ dataOf("number"),
126
+ "data.text": /* @__PURE__ */ dataOf("string"),
127
+ "data.bool": /* @__PURE__ */ dataOf("boolean"),
128
128
  "math.add": ({ parts }) => {
129
129
  const all = allValues(parts.map((part) => part.evaluate()));
130
130
  if ("error" in all) return all.error;
@@ -164,7 +164,7 @@ export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
164
164
  */
165
165
 
166
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;
167
+ const TREE_NODE: unique symbol = /* @__PURE__ */ Symbol.for("coeval.tree.node") as never;
168
168
 
169
169
  /** One tree in one evaluation. It is open until its root gives the JSON of the tree. */
170
170
  interface TreeBuild {
@@ -189,7 +189,7 @@ interface NodeState {
189
189
  /** The memo cell of one attribute: in evaluation, or done with its result. */
190
190
  type Cell = { readonly busy: true } | { readonly busy: false; readonly result: Result };
191
191
 
192
- const BUSY: Cell = Object.freeze({ busy: true });
192
+ const BUSY: Cell = /* @__PURE__ */ Object.freeze({ busy: true });
193
193
 
194
194
  /** One attribute in evaluation. The text names it in the reason of a cycle. */
195
195
  interface Demand {
@@ -324,6 +324,10 @@ function treeThing(parts: readonly Part[], context: Context, type: string, place
324
324
  if (!result.ok) throw new Failure(result.error);
325
325
  return result.value;
326
326
  },
327
+ has(key: Key): boolean {
328
+ // DEREF.HOST-UNDEFINED: an attribute with the value undefined is a field, not a missing step.
329
+ return key === "parent" || key === "type" || parts.some((part) => part.key === String(key));
330
+ },
327
331
  toJson(): unknown {
328
332
  const result = state.json(new Set());
329
333
  if (!result.ok) throw new Failure(result.error);
@@ -392,7 +396,7 @@ export function nearestWalker(options: NearestOptions = {}): Walker {
392
396
  };
393
397
  }
394
398
 
395
- const nearest: Walker = nearestWalker();
399
+ const nearest: Walker = /* @__PURE__ */ nearestWalker();
396
400
 
397
401
  /** The walkers of the standard world, by name. */
398
402
  export const STD_WALKERS: Readonly<Record<string, Walker>> = { "walk.nearest": nearest };