@mark1russell7/coeval 0.6.2 → 0.7.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/deref.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type Envelope, type Json, type JsonRecord, type Key, type TypeRef, field, isEnvelope, isKey, isRecord, typePath } from "./doc.ts";
2
- import { type World, entriesAsJson, lookupKind } from "./world.ts";
2
+ import { type Kind, type World, entriesAsJson, lookupKind } from "./world.ts";
3
+ import { answerOf } from "./ladder.ts";
3
4
  import type { ErrorCode } from "./errors.ts";
4
5
 
5
6
  /**
@@ -17,12 +18,22 @@ export interface HostThing {
17
18
  /** A short text for a reader, for example `node card`. */
18
19
  readonly label: string;
19
20
  /**
20
- * This function answers one step.
21
+ * This function answers one step. A field that holds `undefined` also gives `undefined`. The function `has` tells
22
+ * the two cases apart (DEREF.HOST-UNDEFINED).
21
23
  *
22
24
  * @param key - The key of the step.
23
25
  * @returns What the step reaches, or `undefined` when it reaches nothing.
24
26
  */
25
27
  deref(key: Key): Thing | undefined;
28
+ /**
29
+ * This optional function tells if the thing has a field for a key. The kernel asks it only when `deref` gives
30
+ * `undefined`. Then `true` tells that the field holds `undefined`. A thing without `has` reads `undefined` as a
31
+ * missing field.
32
+ *
33
+ * @param key - The key of the step.
34
+ * @returns `true` when the thing has the field.
35
+ */
36
+ has?(key: Key): boolean;
26
37
  /**
27
38
  * This function gives the thing as a value, when a reference ends on it.
28
39
  *
@@ -34,7 +45,7 @@ export interface HostThing {
34
45
  * path without one call for each step.
35
46
  *
36
47
  * @param keys - The keys.
37
- * @returns What the keys reach, or `undefined` when they reach nothing.
48
+ * @returns What the keys reach, or `undefined` when they reach nothing. `{ thing: undefined }` tells that the keys reach a field that holds `undefined`.
38
49
  */
39
50
  resolve?(keys: readonly Key[]): { readonly thing: Thing } | undefined;
40
51
  }
@@ -152,28 +163,126 @@ export interface ResolveOptions {
152
163
  readonly hops?: boolean;
153
164
  }
154
165
 
155
- function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
156
- return verbs !== undefined && Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
166
+ /** This function gives the own entries of an envelope, when it has a record of them. */
167
+ function ownEntries(envelope: Envelope): Readonly<Record<string, string>> | undefined {
168
+ const own = field(envelope as JsonRecord, "own");
169
+ return isRecord(own) ? (own as Readonly<Record<string, string>>) : undefined;
170
+ }
171
+
172
+ /** One node of the memo of the kinds of a world: the kind of the keys up to this node, and the longer keys. */
173
+ interface KindNode {
174
+ /** The kind of the keys, `null` for a type without a kind, or `undefined` before the first lookup. */
175
+ kind?: Kind | null;
176
+ next?: Map<Key, KindNode>;
177
+ }
178
+
179
+ /** The memo of the kinds of one world, by the keys of a type. */
180
+ interface KindMemo {
181
+ first: Map<Key, KindNode>;
182
+ /** The number of kinds in the memo. */
183
+ size: number;
184
+ }
185
+
186
+ /** The most kinds in the memo of one world. A larger memo starts again, thus the types of the data cannot fill the memory. */
187
+ const MAX_KINDS = 1000;
188
+
189
+ const kindMemos = /* @__PURE__ */ new WeakMap<World, KindMemo>();
190
+
191
+ /**
192
+ * This function gives the kind of a type for the ladder: `null` when the world does not have the kind. The result
193
+ * depends only on the world and on the keys of the type, thus a memo for each world keeps it (PLAN.KIND-MEMO). A
194
+ * world does not change after a plan or a resolution uses it, as for `worldThing`.
195
+ */
196
+ function kindFor(world: World, type: TypeRef): Kind | null {
197
+ const keys = typeof type === "string" ? undefined : typePath(type);
198
+ // A computed type has no keys, and `lookupKind` gives it no kind.
199
+ if (typeof type !== "string" && (keys === undefined || keys.length === 0)) {
200
+ const found = lookupKind(world, type);
201
+ return found.ok ? found.kind : null;
202
+ }
203
+ let memo = kindMemos.get(world);
204
+ if (memo === undefined || memo.size >= MAX_KINDS) {
205
+ memo = { first: new Map(), size: 0 };
206
+ kindMemos.set(world, memo);
207
+ }
208
+ const node = keys === undefined ? nodeAt(memo.first, type as string) : nodeOf(memo.first, keys);
209
+ if (node.kind === undefined) {
210
+ const found = lookupKind(world, type);
211
+ node.kind = found.ok ? found.kind : null;
212
+ memo.size += 1;
213
+ }
214
+ return node.kind;
215
+ }
216
+
217
+ /** This function gives the node of one key in a level of the memo, and makes it when the level does not have it. */
218
+ function nodeAt(level: Map<Key, KindNode>, key: Key): KindNode {
219
+ let node = level.get(key);
220
+ if (node === undefined) {
221
+ node = {};
222
+ level.set(key, node);
223
+ }
224
+ return node;
225
+ }
226
+
227
+ /** This function gives the node of the keys of a type in the memo. */
228
+ function nodeOf(first: Map<Key, KindNode>, keys: readonly Key[]): KindNode {
229
+ let node = nodeAt(first, keys[0] as Key);
230
+ for (let index = 1; index < keys.length; index += 1) node = nodeAt((node.next ??= new Map()), keys[index] as Key);
231
+ return node;
157
232
  }
158
233
 
234
+ /** This function tells if a table of code has the name. An entry of a document can hold another value than a text. */
235
+ function codeIn(table: Readonly<Record<string, unknown>>, name: unknown): boolean {
236
+ return typeof name === "string" && Object.hasOwn(table, name);
237
+ }
238
+
239
+ /**
240
+ * This function finds the walker of a step document through the ladder of the verb `step` (VERB.LADDER). The rungs
241
+ * are the own entry, then the kind, then the default of the world. A rung answers only with a walker that the
242
+ * navigator has. A resolution and `check` use this function, thus they agree on `#STEP?`.
243
+ *
244
+ * @param nav - The world and the walkers.
245
+ * @param step - A step of a path that is not a key.
246
+ * @returns The walker, or `undefined` when no rung answers.
247
+ */
248
+ export function walkerOf(nav: Navigator, step: unknown): Walker | undefined {
249
+ const walkers = nav.walkers;
250
+ if (walkers === undefined || !isEnvelope(step)) return undefined;
251
+ const answer = answerOf(nav.world, ownEntries(step), kindFor(nav.world, step.type), "step", (name) => codeIn(walkers, name));
252
+ return answer === undefined ? undefined : walkers[answer.name];
253
+ }
254
+
255
+ /** The answer of `plainStep` for a field that the value does not have. A field can hold `undefined`. */
256
+ const NOTHING: unique symbol = /* @__PURE__ */ Symbol("nothing") as never;
257
+
159
258
  /** The own fields of a value: the items and the length of a list, and the own properties of an object. */
160
- function plainStep(thing: unknown, key: Key): { readonly found: true; readonly value: unknown } | undefined {
259
+ function plainStep(thing: unknown, key: Key): unknown {
161
260
  if (Array.isArray(thing)) {
162
- if (key === "length") return { found: true, value: thing.length };
261
+ if (key === "length") return thing.length;
163
262
  const index = typeof key === "number" ? key : /^\d+$/.test(key) ? Number(key) : -1;
164
- return index >= 0 && index < thing.length ? { found: true, value: (thing as readonly unknown[])[index] } : undefined;
263
+ return index >= 0 && index < thing.length ? (thing as readonly unknown[])[index] : NOTHING;
165
264
  }
166
265
  if (typeof thing === "object" && thing !== null) {
167
266
  const name = String(key);
168
- return Object.hasOwn(thing, name) ? { found: true, value: (thing as Readonly<Record<string, unknown>>)[name] } : undefined;
267
+ return Object.hasOwn(thing, name) ? (thing as Readonly<Record<string, unknown>>)[name] : NOTHING;
169
268
  }
170
- return undefined;
269
+ return NOTHING;
270
+ }
271
+
272
+ /**
273
+ * This function tells if a value can be an envelope by its field `type`. An envelope has its own field `type`, thus
274
+ * a value without that field is not an envelope, and the full test is not necessary.
275
+ */
276
+ function mayBeEnvelope(thing: unknown): boolean {
277
+ return typeof thing === "object" && thing !== null && (thing as { readonly type?: unknown }).type !== undefined;
171
278
  }
172
279
 
173
280
  /**
174
281
  * This function answers one step of a path on a thing, through the ladder of the verb `deref`. A host thing
175
- * answers for itself (own). A document asks its own entry, then its kind. The default goes into the value: an own
176
- * field of an object, or an item of a list.
282
+ * answers for itself (own). A document asks its own entry, then its kind, then the default of the world
283
+ * (VERB.LADDER). A rung answers only with a `deref` that the navigator has. When no rung answers, the step goes into
284
+ * the value: an own field of an object, or an item of a list. A value without a type is data, thus the step goes
285
+ * into it at once.
177
286
  *
178
287
  * @param nav - The world and the code of the evaluators.
179
288
  * @param thing - The thing that the path reached.
@@ -183,29 +292,23 @@ function plainStep(thing: unknown, key: Key): { readonly found: true; readonly v
183
292
  export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly thing: Thing; readonly by: HopBy } | undefined {
184
293
  if (isHostThing(thing)) {
185
294
  const reached = thing.deref(key);
186
- return reached === undefined ? undefined : { thing: reached, by: "own" };
295
+ // DEREF.HOST-UNDEFINED: `has` tells a field that holds undefined from a missing field.
296
+ return reached !== undefined || thing.has?.(key) === true ? { thing: reached, by: "own" } : undefined;
187
297
  }
188
- if (nav.plain !== true && isEnvelope(thing)) {
298
+ if (nav.plain !== true && mayBeEnvelope(thing) && isEnvelope(thing)) {
189
299
  const envelope = thing as JsonRecord & Envelope;
190
- const ownEntries = field(envelope, "own");
191
- const own = isRecord(ownEntries) ? entryOf(ownEntries as Readonly<Record<string, string>>, "deref") : undefined;
192
- const ownCode = own === undefined ? undefined : nav.derefs?.[own];
193
- if (ownCode !== undefined) {
194
- const reached = ownCode(envelope, key);
195
- return reached === undefined ? undefined : { thing: reached, by: "own" };
196
- }
197
- const found = lookupKind(nav.world, envelope.type);
198
- const name = found.ok ? entryOf(found.kind.verbs, "deref") : undefined;
199
- const code = name === undefined ? undefined : nav.derefs?.[name];
200
- if (code !== undefined) {
201
- const reached = code(envelope, key);
202
- return reached === undefined ? undefined : { thing: reached, by: "kind" };
300
+ const derefs = nav.derefs;
301
+ // A navigator without the code of a deref needs no lookup of the kind: no rung can answer.
302
+ const answer = derefs === undefined ? undefined : answerOf(nav.world, ownEntries(envelope), kindFor(nav.world, envelope.type), "deref", (name) => codeIn(derefs, name));
303
+ if (answer !== undefined) {
304
+ const reached = (derefs as Readonly<Record<string, DerefCode>>)[answer.name]?.(envelope, key);
305
+ return reached === undefined ? undefined : { thing: reached, by: answer.rung };
203
306
  }
204
307
  const reached = plainStep(envelope.value, key);
205
- return reached === undefined ? undefined : { thing: reached.value, by: "default" };
308
+ return reached === NOTHING ? undefined : { thing: reached, by: "default" };
206
309
  }
207
310
  const reached = plainStep(thing, key);
208
- return reached === undefined ? undefined : { thing: reached.value, by: "default" };
311
+ return reached === NOTHING ? undefined : { thing: reached, by: "default" };
209
312
  }
210
313
 
211
314
  /**
@@ -215,6 +318,10 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
215
318
  * @returns `true` for a reference document.
216
319
  */
217
320
  export function isRef(thing: unknown): thing is Envelope & { readonly value: readonly Json[] } {
321
+ if (typeof thing !== "object" || thing === null) return false;
322
+ // A text type other than `ref` names another kind, thus the full test of the envelope is not necessary.
323
+ const type = (thing as { readonly type?: unknown }).type;
324
+ if (type === undefined || (typeof type === "string" && type !== "ref")) return false;
218
325
  if (!isEnvelope(thing) || !Array.isArray(thing.value)) return false;
219
326
  const path = typePath(thing.type as TypeRef);
220
327
  return path !== undefined && path.length === 1 && path[0] === "ref";
@@ -269,6 +376,11 @@ function keysFrom(path: readonly unknown[], start: number): boolean {
269
376
  return true;
270
377
  }
271
378
 
379
+ /** This function tells if a root has a value of its own for the short path: each root except `caps`. */
380
+ function quickRoot(root: unknown): root is "names" | "self" | "event" | "world" {
381
+ return root === "names" || root === "self" || root === "event" || root === "world";
382
+ }
383
+
272
384
  /**
273
385
  * This function resolves the common path without a record of hops: a root, then keys. Plain values answer the
274
386
  * keys, or a host thing answers all of them in one call. It gives `undefined` when the path needs the full
@@ -277,17 +389,31 @@ function keysFrom(path: readonly unknown[], start: number): boolean {
277
389
  */
278
390
  function resolveQuick(nav: Navigator, ctx: Context, path: readonly unknown[]): Resolution | undefined {
279
391
  const root = path[0];
280
- if (root !== "names" && root !== "self" && root !== "event" && root !== "world") return undefined;
392
+ if (!quickRoot(root)) return undefined;
281
393
  if (nav.roots !== undefined && !(nav.roots as readonly string[]).includes(root)) return undefined;
282
394
  if (nav.adopt !== undefined) return undefined;
283
- let here: unknown = ctx[root];
284
- const plain = nav.plain === true;
395
+ return walkQuick(ctx[root], path, nav.plain === true, false);
396
+ }
397
+
398
+ /**
399
+ * This function walks the steps of the short path from the value of the root (DEREF.QUICK). `resolveQuick` and each
400
+ * compiled path use it, thus the two cannot disagree. It gives `undefined` when the path needs the full algorithm.
401
+ *
402
+ * @param start - The value of the root.
403
+ * @param path - The steps. The walk starts at the index 1.
404
+ * @param plain - `true` for a plain navigation.
405
+ * @param keyed - `true` when each step after the root is a key. The walk then does not test the steps again.
406
+ */
407
+ function walkQuick(start: unknown, path: readonly unknown[], plain: boolean, keyed: boolean): Resolution | undefined {
408
+ let here = start;
285
409
  for (let index = 1; index < path.length; index += 1) {
286
410
  if (typeof here !== "object" || here === null) return undefined;
287
- if (!plain && mayBeRef(here)) return undefined;
411
+ // One read of the field `type` finds a reference document. Without the field, the value is not an envelope.
412
+ const type = plain ? undefined : (here as { readonly type?: unknown }).type;
413
+ if (type === "ref") return undefined;
288
414
  if ((here as Partial<HostThing>)[THING] === true) {
289
415
  const thing = here as HostThing;
290
- if (thing.resolve === undefined || !keysFrom(path, index)) return undefined;
416
+ if (thing.resolve === undefined || !(keyed || keysFrom(path, index))) return undefined;
291
417
  const keys = path.slice(index) as readonly Key[];
292
418
  const reached = thing.resolve(keys);
293
419
  if (reached === undefined) return { ok: false, code: "#N/A", reason: `The path ${keys.join(".")} reaches nothing.`, hops: NO_HOPS };
@@ -301,20 +427,132 @@ function resolveQuick(nav: Navigator, ctx: Context, path: readonly unknown[]): R
301
427
  continue;
302
428
  }
303
429
  if (typeof step !== "string" || !Object.hasOwn(here, step)) return undefined;
304
- if (!plain && isEnvelope(here)) return undefined;
430
+ if (type !== undefined && isEnvelope(here)) return undefined;
305
431
  here = (here as Readonly<Record<string, unknown>>)[step];
306
432
  }
307
433
  if (!plain && mayBeRef(here)) return undefined;
308
434
  return { ok: true, thing: here, hops: NO_HOPS };
309
435
  }
310
436
 
437
+ /**
438
+ * A compiled path: one static path for one navigator (PLAN.PATH). `compilePath` reads the root, the open roots and
439
+ * the steps one time. It asks the ladder of the verb `step` for each step document one time. Each resolution then
440
+ * examines only the values of the context.
441
+ */
442
+ export interface CompiledPath {
443
+ /** The steps of the path, as the document gives them. The list of reads records these steps. */
444
+ readonly path: readonly unknown[];
445
+ /** The navigator of the compilation. A runtime with another navigator resolves the steps again. */
446
+ readonly nav: Navigator;
447
+ /**
448
+ * This function resolves the path from a context, without a record of hops.
449
+ *
450
+ * @param ctx - The context.
451
+ * @returns The same resolution as `resolve(nav, ctx, path, { hops: false })`.
452
+ */
453
+ resolve(ctx: Context): Resolution;
454
+ }
455
+
456
+ /** The walker of each step document of a path, by its index: `null` when no rung answers, `undefined` for a key. */
457
+ type KnownWalkers = readonly (Walker | null | undefined)[];
458
+
459
+ /** The root whose value starts the short path of a compiled path. `FULL` takes the full algorithm at once. */
460
+ const FULL = 0;
461
+ const FROM_NAMES = 1;
462
+ const FROM_SELF = 2;
463
+ const FROM_EVENT = 3;
464
+ const FROM_WORLD = 4;
465
+ type From = typeof FULL | typeof FROM_NAMES | typeof FROM_SELF | typeof FROM_EVENT | typeof FROM_WORLD;
466
+
467
+ /**
468
+ * A compiled path as an object with fields (PLAN.LEAN). The code of `resolve` is on the class, thus each compiled path
469
+ * keeps only its fields and makes no closure.
470
+ */
471
+ class StaticPath implements CompiledPath {
472
+ // The constructor writes each field one time, thus each field stays constant for V8, as the variable of a closure.
473
+ declare readonly path: readonly unknown[];
474
+ declare readonly nav: Navigator;
475
+ /** The walker of each step document, from the ladder of `step`, or `undefined` for a path of keys. */
476
+ declare private readonly known: KnownWalkers | undefined;
477
+ /** The root that starts the short path, or `FULL`. */
478
+ declare private readonly from: From;
479
+ declare private readonly plain: boolean;
480
+
481
+ constructor(path: readonly unknown[], nav: Navigator, known: KnownWalkers | undefined, from: From, plain: boolean) {
482
+ this.path = path;
483
+ this.nav = nav;
484
+ this.known = known;
485
+ this.from = from;
486
+ this.plain = plain;
487
+ }
488
+
489
+ resolve(ctx: Context): Resolution {
490
+ const from = this.from;
491
+ if (from !== FULL) {
492
+ const start = from === FROM_NAMES ? ctx.names : from === FROM_WORLD ? ctx.world : from === FROM_SELF ? ctx.self : ctx.event;
493
+ const quick = walkQuick(start, this.path, this.plain, true);
494
+ if (quick !== undefined) return quick;
495
+ }
496
+ return resolveFully(this.nav, ctx, this.path, undefined, this.known);
497
+ }
498
+ }
499
+
500
+ /**
501
+ * This function compiles one static path for one navigator. The resolver gives the same thing, error and reason as
502
+ * `resolve` without hops (PLAN.PATH-SAME). It keeps no value of a context.
503
+ *
504
+ * - Some paths take the full algorithm at once, because the short path cannot answer them. A path from `caps` or from
505
+ * a closed root is one. A path with a step document and each path of a navigator with `adopt` are others.
506
+ * - Each other path walks the short path from its root, and it takes the full algorithm when the values need it.
507
+ * - The walker of each step document comes from the ladder one time, here. The navigator and the step document do
508
+ * not change, thus the answer does not change either.
509
+ *
510
+ * @param nav - The navigator. A plan compiles its paths for its machine.
511
+ * @param path - The steps. The first step is a root.
512
+ * @returns The compiled path.
513
+ */
514
+ export function compilePath(nav: Navigator, path: readonly unknown[]): CompiledPath {
515
+ const keyed = keysFrom(path, 1);
516
+ const known = keyed ? undefined : walkersOf(nav, path);
517
+ return new StaticPath(path, nav, known, startOf(nav, path, keyed), nav.plain === true);
518
+ }
519
+
520
+ /** This function gives the root that starts the short path of a compiled path, or `FULL`. */
521
+ function startOf(nav: Navigator, path: readonly unknown[], keyed: boolean): From {
522
+ const root = path[0];
523
+ // The short path of `resolveQuick` refuses each of these paths before it reads a value, thus it is not necessary.
524
+ if (!keyed || !quickRoot(root) || nav.adopt !== undefined) return FULL;
525
+ if (nav.roots !== undefined && !(nav.roots as readonly string[]).includes(root)) return FULL;
526
+ return root === "names" ? FROM_NAMES : root === "self" ? FROM_SELF : root === "event" ? FROM_EVENT : FROM_WORLD;
527
+ }
528
+
529
+ /**
530
+ * This function asks the ladder of the verb `step` for each step document of a path. A step whose lookup throws
531
+ * stays unknown, thus the full algorithm asks the ladder again for it.
532
+ */
533
+ function walkersOf(nav: Navigator, path: readonly unknown[]): KnownWalkers | undefined {
534
+ // Without walkers, `walkerOf` gives `undefined` at once.
535
+ if (nav.walkers === undefined) return undefined;
536
+ const out = new Array<Walker | null | undefined>(path.length);
537
+ for (let index = 1; index < path.length; index += 1) {
538
+ const step = path[index];
539
+ if (isKey(step)) continue;
540
+ try {
541
+ out[index] = walkerOf(nav, step) ?? null;
542
+ } catch {
543
+ // The full algorithm asks the ladder again, and it gives the exception there.
544
+ }
545
+ }
546
+ return out;
547
+ }
548
+
311
549
  /**
312
550
  * This function resolves a path from a context, and records each hop.
313
551
  *
314
552
  * The first step is a root of the context. Each later step is a key or a step document. A key goes up the ladder
315
- * of the verb `deref` on the thing that the path reached. A step document is evaluated by the walker of its kind,
316
- * with the thing that the path reached. After each hop, a reference document is followed until the path reaches
317
- * something else (collapse), except in a plain navigation.
553
+ * of the verb `deref` on the thing that the path reached. A step document is evaluated by the walker that the
554
+ * ladder of the verb `step` gives, with the thing that the path reached. After each hop, a reference document is
555
+ * followed until the path reaches something else (collapse), except in a plain navigation.
318
556
  *
319
557
  * The codes of a failure:
320
558
  *
@@ -335,7 +573,20 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
335
573
  const quick = resolveQuick(nav, ctx, path);
336
574
  if (quick !== undefined) return quick;
337
575
  }
338
- const hops: Hop[] | undefined = options.hops === false ? undefined : [];
576
+ return resolveFully(nav, ctx, path, options.hops === false ? undefined : [], undefined);
577
+ }
578
+
579
+ /**
580
+ * This function resolves a path with the full algorithm of `resolve`.
581
+ *
582
+ * @param nav - The world and the code of the evaluators.
583
+ * @param ctx - The context.
584
+ * @param path - The steps.
585
+ * @param hops - The list that gets each hop, or `undefined` for no record.
586
+ * @param known - The walker of each step document of the path, from `compilePath`. A reference that a step reaches has its own steps, and the ladder answers them.
587
+ * @returns What the path reached and each hop, or an error.
588
+ */
589
+ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], hops: Hop[] | undefined, known: KnownWalkers | undefined): Resolution {
339
590
  const done = hops ?? NO_HOPS;
340
591
  const failed = (code: ResolveCode | ErrorCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
341
592
  let collapses = 0;
@@ -347,7 +598,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
347
598
  while (isRef(here)) {
348
599
  collapses += 1;
349
600
  if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", "The references form a cycle.");
350
- const inner = walk(here.value);
601
+ const inner = walk(here.value, undefined);
351
602
  if (!inner.ok) return inner;
352
603
  here = adopt(inner.thing);
353
604
  hops?.push({ step: "collapse", thing: here, by: "collapse" });
@@ -355,7 +606,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
355
606
  return { ok: true, thing: here, hops: done };
356
607
  };
357
608
 
358
- const walk = (steps: readonly unknown[]): Resolution => {
609
+ const walk = (steps: readonly unknown[], walkers: KnownWalkers | undefined): Resolution => {
359
610
  const root = steps[0];
360
611
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
361
612
  return failed("#REF!", `A path starts with a root: ${ROOTS.join(", ")}.`);
@@ -370,7 +621,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
370
621
  here = first.thing;
371
622
  for (let index = 1; index < steps.length; index += 1) {
372
623
  const step = steps[index];
373
- if (isHostThing(here) && here.resolve !== undefined && steps.slice(index).every(isKey)) {
624
+ if (isHostThing(here) && here.resolve !== undefined && keysFrom(steps, index)) {
374
625
  const keys = steps.slice(index) as readonly Key[];
375
626
  const reached = here.resolve(keys);
376
627
  if (reached === undefined) return failed("#N/A", `The path ${keys.join(".")} reaches nothing.`);
@@ -387,13 +638,10 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
387
638
  here = adopt(reached.thing);
388
639
  hops?.push({ step, thing: here, by: reached.by });
389
640
  } else {
390
- const envelope = isEnvelope(step) ? step : undefined;
391
- const found = envelope === undefined ? undefined : lookupKind(nav.world, envelope.type);
392
- const name = found?.ok === true ? entryOf(found.kind.verbs, "step") : undefined;
393
- const walker = name === undefined ? undefined : nav.walkers?.[name];
394
- if (envelope === undefined || walker === undefined) {
395
- return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
396
- }
641
+ const given = walkers?.[index];
642
+ const walker = given === undefined ? walkerOf(nav, step) : (given ?? undefined);
643
+ if (walker === undefined) return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
644
+ const envelope = step as Envelope;
397
645
  const result = walker({
398
646
  data: envelope.value,
399
647
  here,
@@ -415,7 +663,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
415
663
  };
416
664
 
417
665
  if (path.length === 0) return failed("#REF!", "A path has at least one step: its root.");
418
- return walk(path);
666
+ return walk(path, known);
419
667
  }
420
668
 
421
669
  /**
package/src/doc.ts CHANGED
@@ -230,20 +230,41 @@ export function fromJson(value: unknown): Envelope | undefined {
230
230
  * with this function does not keep a stale host value.
231
231
  *
232
232
  * The function does not use the stack for the depth, thus a deep document cannot overflow it. A cycle of lists or
233
- * records does not make it loop. `sameValue` of `agree` uses the same rules, and it adds rules for closures and for
234
- * a rule of the caller.
233
+ * records does not make it loop. `sameValue` of `agree` and `sameResult` use the same walk, and each adds its rules
234
+ * for other values (D20).
235
235
  *
236
236
  * @param a - The first value.
237
237
  * @param b - The second value.
238
238
  * @returns `true` when the values are equal.
239
239
  */
240
240
  export function docEquals(a: unknown, b: unknown): boolean {
241
+ return equalWith(a, b, undefined);
242
+ }
243
+
244
+ /** A rule for two values that are not the same by `Object.is`: `true`, `false`, or `undefined` for the next rule. */
245
+ export type SameRule = (a: unknown, b: unknown) => boolean | undefined;
246
+
247
+ /**
248
+ * This function is the one walk of `docEquals`, `sameValue` and `sameResult` (D20). It compares two values by the
249
+ * rules of `docEquals`. A rule of the caller answers first for each pair that is not the same by `Object.is`.
250
+ *
251
+ * @param a - The first value.
252
+ * @param b - The second value.
253
+ * @param rule - The rule of the caller. Its `undefined` gives the rules of `docEquals`.
254
+ * @returns `true` when the values are equal.
255
+ */
256
+ export function equalWith(a: unknown, b: unknown, rule: SameRule | undefined): boolean {
241
257
  const stack: [unknown, unknown][] = [[a, b]];
242
258
  // The pairs of containers that the comparison already visits. It is made at the first pair of containers.
243
259
  let seen: Map<object, Set<object>> | undefined;
244
260
  while (stack.length > 0) {
245
261
  const [x, y] = stack.pop() as [unknown, unknown];
246
262
  if (Object.is(x, y)) continue;
263
+ const told = rule?.(x, y);
264
+ if (told !== undefined) {
265
+ if (told) continue;
266
+ return false;
267
+ }
247
268
  const list = Array.isArray(x);
248
269
  if (list !== Array.isArray(y)) return false;
249
270
  // An object that is not a list and not a plain record is a host value: Object.is above compared it.