@mark1russell7/coeval 0.6.3 → 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
  /**
@@ -162,28 +163,126 @@ export interface ResolveOptions {
162
163
  readonly hops?: boolean;
163
164
  }
164
165
 
165
- function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
166
- 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;
167
170
  }
168
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;
232
+ }
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
+
169
258
  /** The own fields of a value: the items and the length of a list, and the own properties of an object. */
170
- function plainStep(thing: unknown, key: Key): { readonly found: true; readonly value: unknown } | undefined {
259
+ function plainStep(thing: unknown, key: Key): unknown {
171
260
  if (Array.isArray(thing)) {
172
- if (key === "length") return { found: true, value: thing.length };
261
+ if (key === "length") return thing.length;
173
262
  const index = typeof key === "number" ? key : /^\d+$/.test(key) ? Number(key) : -1;
174
- 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;
175
264
  }
176
265
  if (typeof thing === "object" && thing !== null) {
177
266
  const name = String(key);
178
- 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;
179
268
  }
180
- 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;
181
278
  }
182
279
 
183
280
  /**
184
281
  * This function answers one step of a path on a thing, through the ladder of the verb `deref`. A host thing
185
- * answers for itself (own). A document asks its own entry, then its kind. The default goes into the value: an own
186
- * 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.
187
286
  *
188
287
  * @param nav - The world and the code of the evaluators.
189
288
  * @param thing - The thing that the path reached.
@@ -196,27 +295,20 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
196
295
  // DEREF.HOST-UNDEFINED: `has` tells a field that holds undefined from a missing field.
197
296
  return reached !== undefined || thing.has?.(key) === true ? { thing: reached, by: "own" } : undefined;
198
297
  }
199
- if (nav.plain !== true && isEnvelope(thing)) {
298
+ if (nav.plain !== true && mayBeEnvelope(thing) && isEnvelope(thing)) {
200
299
  const envelope = thing as JsonRecord & Envelope;
201
- const ownEntries = field(envelope, "own");
202
- const own = isRecord(ownEntries) ? entryOf(ownEntries as Readonly<Record<string, string>>, "deref") : undefined;
203
- const ownCode = own === undefined ? undefined : nav.derefs?.[own];
204
- if (ownCode !== undefined) {
205
- const reached = ownCode(envelope, key);
206
- return reached === undefined ? undefined : { thing: reached, by: "own" };
207
- }
208
- const found = lookupKind(nav.world, envelope.type);
209
- const name = found.ok ? entryOf(found.kind.verbs, "deref") : undefined;
210
- const code = name === undefined ? undefined : nav.derefs?.[name];
211
- if (code !== undefined) {
212
- const reached = code(envelope, key);
213
- 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 };
214
306
  }
215
307
  const reached = plainStep(envelope.value, key);
216
- return reached === undefined ? undefined : { thing: reached.value, by: "default" };
308
+ return reached === NOTHING ? undefined : { thing: reached, by: "default" };
217
309
  }
218
310
  const reached = plainStep(thing, key);
219
- return reached === undefined ? undefined : { thing: reached.value, by: "default" };
311
+ return reached === NOTHING ? undefined : { thing: reached, by: "default" };
220
312
  }
221
313
 
222
314
  /**
@@ -226,6 +318,10 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
226
318
  * @returns `true` for a reference document.
227
319
  */
228
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;
229
325
  if (!isEnvelope(thing) || !Array.isArray(thing.value)) return false;
230
326
  const path = typePath(thing.type as TypeRef);
231
327
  return path !== undefined && path.length === 1 && path[0] === "ref";
@@ -280,6 +376,11 @@ function keysFrom(path: readonly unknown[], start: number): boolean {
280
376
  return true;
281
377
  }
282
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
+
283
384
  /**
284
385
  * This function resolves the common path without a record of hops: a root, then keys. Plain values answer the
285
386
  * keys, or a host thing answers all of them in one call. It gives `undefined` when the path needs the full
@@ -288,17 +389,31 @@ function keysFrom(path: readonly unknown[], start: number): boolean {
288
389
  */
289
390
  function resolveQuick(nav: Navigator, ctx: Context, path: readonly unknown[]): Resolution | undefined {
290
391
  const root = path[0];
291
- if (root !== "names" && root !== "self" && root !== "event" && root !== "world") return undefined;
392
+ if (!quickRoot(root)) return undefined;
292
393
  if (nav.roots !== undefined && !(nav.roots as readonly string[]).includes(root)) return undefined;
293
394
  if (nav.adopt !== undefined) return undefined;
294
- let here: unknown = ctx[root];
295
- 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;
296
409
  for (let index = 1; index < path.length; index += 1) {
297
410
  if (typeof here !== "object" || here === null) return undefined;
298
- 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;
299
414
  if ((here as Partial<HostThing>)[THING] === true) {
300
415
  const thing = here as HostThing;
301
- if (thing.resolve === undefined || !keysFrom(path, index)) return undefined;
416
+ if (thing.resolve === undefined || !(keyed || keysFrom(path, index))) return undefined;
302
417
  const keys = path.slice(index) as readonly Key[];
303
418
  const reached = thing.resolve(keys);
304
419
  if (reached === undefined) return { ok: false, code: "#N/A", reason: `The path ${keys.join(".")} reaches nothing.`, hops: NO_HOPS };
@@ -312,20 +427,132 @@ function resolveQuick(nav: Navigator, ctx: Context, path: readonly unknown[]): R
312
427
  continue;
313
428
  }
314
429
  if (typeof step !== "string" || !Object.hasOwn(here, step)) return undefined;
315
- if (!plain && isEnvelope(here)) return undefined;
430
+ if (type !== undefined && isEnvelope(here)) return undefined;
316
431
  here = (here as Readonly<Record<string, unknown>>)[step];
317
432
  }
318
433
  if (!plain && mayBeRef(here)) return undefined;
319
434
  return { ok: true, thing: here, hops: NO_HOPS };
320
435
  }
321
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
+
322
549
  /**
323
550
  * This function resolves a path from a context, and records each hop.
324
551
  *
325
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
326
- * of the verb `deref` on the thing that the path reached. A step document is evaluated by the walker of its kind,
327
- * with the thing that the path reached. After each hop, a reference document is followed until the path reaches
328
- * 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.
329
556
  *
330
557
  * The codes of a failure:
331
558
  *
@@ -346,7 +573,20 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
346
573
  const quick = resolveQuick(nav, ctx, path);
347
574
  if (quick !== undefined) return quick;
348
575
  }
349
- 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 {
350
590
  const done = hops ?? NO_HOPS;
351
591
  const failed = (code: ResolveCode | ErrorCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
352
592
  let collapses = 0;
@@ -358,7 +598,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
358
598
  while (isRef(here)) {
359
599
  collapses += 1;
360
600
  if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", "The references form a cycle.");
361
- const inner = walk(here.value);
601
+ const inner = walk(here.value, undefined);
362
602
  if (!inner.ok) return inner;
363
603
  here = adopt(inner.thing);
364
604
  hops?.push({ step: "collapse", thing: here, by: "collapse" });
@@ -366,7 +606,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
366
606
  return { ok: true, thing: here, hops: done };
367
607
  };
368
608
 
369
- const walk = (steps: readonly unknown[]): Resolution => {
609
+ const walk = (steps: readonly unknown[], walkers: KnownWalkers | undefined): Resolution => {
370
610
  const root = steps[0];
371
611
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
372
612
  return failed("#REF!", `A path starts with a root: ${ROOTS.join(", ")}.`);
@@ -381,7 +621,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
381
621
  here = first.thing;
382
622
  for (let index = 1; index < steps.length; index += 1) {
383
623
  const step = steps[index];
384
- if (isHostThing(here) && here.resolve !== undefined && steps.slice(index).every(isKey)) {
624
+ if (isHostThing(here) && here.resolve !== undefined && keysFrom(steps, index)) {
385
625
  const keys = steps.slice(index) as readonly Key[];
386
626
  const reached = here.resolve(keys);
387
627
  if (reached === undefined) return failed("#N/A", `The path ${keys.join(".")} reaches nothing.`);
@@ -398,13 +638,10 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
398
638
  here = adopt(reached.thing);
399
639
  hops?.push({ step, thing: here, by: reached.by });
400
640
  } else {
401
- const envelope = isEnvelope(step) ? step : undefined;
402
- const found = envelope === undefined ? undefined : lookupKind(nav.world, envelope.type);
403
- const name = found?.ok === true ? entryOf(found.kind.verbs, "step") : undefined;
404
- const walker = name === undefined ? undefined : nav.walkers?.[name];
405
- if (envelope === undefined || walker === undefined) {
406
- return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
407
- }
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;
408
645
  const result = walker({
409
646
  data: envelope.value,
410
647
  here,
@@ -426,7 +663,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
426
663
  };
427
664
 
428
665
  if (path.length === 0) return failed("#REF!", "A path has at least one step: its root.");
429
- return walk(path);
666
+ return walk(path, known);
430
667
  }
431
668
 
432
669
  /**