@mark1russell7/coeval 0.16.0 → 0.18.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.16.0",
4
+ "version": "0.18.0",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
@@ -15,7 +15,8 @@
15
15
  "files": [
16
16
  "build",
17
17
  "src",
18
- "!src/**/*.test.ts"
18
+ "!src/**/*.test.ts",
19
+ "!src/**/*.test-support.ts"
19
20
  ],
20
21
  "sideEffects": false,
21
22
  "license": "MIT",
package/src/agree.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { type Key, docEquals, equalWith } from "./doc.ts";
2
- import type { Context } from "./deref.ts";
2
+ import { type Context, isHostThing } from "./deref.ts";
3
3
  import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
4
- import { type Effect, type Journal, type Machine, type Perform, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
4
+ import { type Effect, type Machine, type Perform, type Read, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
5
5
  import { Held, isClosure } from "./formula.ts";
6
6
  import { type Definitions, reference, referenceRun } from "./reference.ts";
7
- import { type ReplaySettle, recorder, replayer } from "./replay.ts";
7
+ import { type ReplaySettle, recorder, relay, replayer } from "./replay.ts";
8
8
 
9
9
  /**
10
10
  * A tier that `agree` compares with the plan.
@@ -91,7 +91,16 @@ const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.
91
91
 
92
92
  /** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
93
93
  export type Agreement =
94
- | { readonly ok: true; readonly result: Result; readonly effects: readonly Effect[] }
94
+ | {
95
+ readonly ok: true;
96
+ readonly result: Result;
97
+ readonly effects: readonly Effect[];
98
+ /**
99
+ * The free reads of the plan that `agreeRun` compared with the reference run (RUN.AGREES-READS). It is absent
100
+ * when no read compared, for example in `agree`.
101
+ */
102
+ readonly reads?: readonly Read[];
103
+ }
95
104
  | {
96
105
  readonly ok: false;
97
106
  readonly tier: Tier;
@@ -340,12 +349,9 @@ interface Watched {
340
349
  function watched(perform: Perform): Watched {
341
350
  const effects: Effect[] = [];
342
351
  const settles: ReplaySettle[] = [];
343
- const inner = (perform as { readonly [JOURNAL]?: Journal })[JOURNAL];
344
- const settled = (all: string, item: number): void => {
352
+ const journal = relay(perform, (all, item) => {
345
353
  settles.push({ all, item });
346
- inner?.settled?.(all, item);
347
- };
348
- const journal: Journal = inner?.next === undefined ? { settled } : { settled, next: (all, waiting) => inner.next?.(all, waiting) };
354
+ });
349
355
  const host = (effect: Effect): unknown => {
350
356
  effects.push(effect);
351
357
  return perform(effect);
@@ -367,6 +373,87 @@ function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]):
367
373
  return undefined;
368
374
  }
369
375
 
376
+ /** The roots beside `self` that a value without static reads keeps whole (READS.KEPT). */
377
+ const KEPT_ROOTS: readonly string[] = /* @__PURE__ */ Object.freeze(["names", "world", "event", "caps"]);
378
+
379
+ /**
380
+ * This function tells if the plan does not know the reads of a run. A value without static reads keeps each root
381
+ * (READS.KEPT), thus the list holds a kept read of each whole root.
382
+ */
383
+ function readsUnknown(reads: readonly Read[]): boolean {
384
+ return KEPT_ROOTS.every((root) => reads.some((read) => read.kept === true && read.path.length === 1 && read.path[0] === root));
385
+ }
386
+
387
+ /**
388
+ * This function gives the reads that `agreeRun` compares: the free reads of the resolutions (RUN.AGREES-READS). A
389
+ * kept read is the bound of the plan for the later reads of a value, and the reference has no such bound.
390
+ */
391
+ function resolutions(reads: readonly Read[]): readonly Read[] {
392
+ return reads.filter((read) => read.free && read.kept !== true);
393
+ }
394
+
395
+ function pathText(path: readonly unknown[] | undefined): string {
396
+ if (path === undefined) return "no read";
397
+ try {
398
+ return JSON.stringify(path);
399
+ } catch {
400
+ return path.map(String).join(", ");
401
+ }
402
+ }
403
+
404
+ /**
405
+ * This function gives the rule of a comparison of reads. The rule of the caller answers first. Each tier makes its
406
+ * own nodes of a tree and its own adopted things. Thus two host things are the same when they have the same label,
407
+ * and they are nodes or have the same JSON object.
408
+ */
409
+ function sameThing(equal: AgreeOptions["equal"]): (a: unknown, b: unknown) => boolean | undefined {
410
+ return (a, b) => {
411
+ const told = equal?.(a, b);
412
+ if (told !== undefined) return told;
413
+ if (!isHostThing(a) || !isHostThing(b)) return undefined;
414
+ if (a.label !== b.label) return false;
415
+ if (a.label.startsWith("node ")) return true;
416
+ try {
417
+ return Object.is(a.toJson(), b.toJson());
418
+ } catch {
419
+ return false;
420
+ }
421
+ };
422
+ }
423
+
424
+ /** This function gives the difference of what two reads reached, or `undefined` when they agree. */
425
+ function reachedDiffers(a: Result, b: Result, equal: (a: unknown, b: unknown) => boolean | undefined, strict: boolean): string | undefined {
426
+ if (a.ok && b.ok) return sameValue(a.value, b.value, equal) ? undefined : "The read reaches another value in the tier.";
427
+ if (!a.ok && !b.ok) {
428
+ if (a.error.code !== b.error.code) return `The read gives ${a.error.code} in the plan, and ${b.error.code} in the tier.`;
429
+ return strict && a.error.reason !== b.error.reason ? `The read gives ${a.error.code} with another reason in the tier: "${b.error.reason}" for "${a.error.reason}".` : undefined;
430
+ }
431
+ if (!b.ok) return `The read reaches a value in the plan, and ${b.error.code} in the tier.`;
432
+ return `The read gives ${(a as { readonly error: EvalError }).error.code} in the plan, and a value in the tier.`;
433
+ }
434
+
435
+ /**
436
+ * This function gives the first difference of the reads of the plan and of a tier, or `undefined` when they agree
437
+ * (RUN.AGREES-READS). Only the free reads of the resolutions compare, in their order. When the plan does not know its
438
+ * reads, the function compares nothing.
439
+ */
440
+ function readsDiffer(plan: readonly Read[], tier: readonly Read[], options: AgreeOptions): string | undefined {
441
+ if (readsUnknown(plan)) return undefined;
442
+ const a = resolutions(plan);
443
+ const b = resolutions(tier);
444
+ const equal = sameThing(options.equal);
445
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
446
+ const x = a[index];
447
+ const y = b[index];
448
+ if (x === undefined || y === undefined || !sameValue(x.path, y.path, equal)) {
449
+ return `The free read ${index} is ${pathText(x?.path)} in the plan, and ${pathText(y?.path)} in the tier.`;
450
+ }
451
+ const why = reachedDiffers(x.reached, y.reached, equal, options.strict === true);
452
+ if (why !== undefined) return `The free read ${index} of ${pathText(x.path)} differs. ${why}`;
453
+ }
454
+ return undefined;
455
+ }
456
+
370
457
  /**
371
458
  * This function compares the run of a document in a plan with its run in other tiers (RUN.AGREES). First the plan
372
459
  * starts the run with a recorder around the host. Then each tier replays the log of that run. These tiers are the
@@ -375,6 +462,13 @@ function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]):
375
462
  * The rules of `agree` compare the results and the effects, with their addresses and outcomes. A tier must also
376
463
  * settle the waits of each `all` in the order of the plan, thus a replay keeps the order of the waits.
377
464
  *
465
+ * The reference run must also give the free reads of the plan, in their order (RUN.AGREES-READS). Another replay of
466
+ * the plan records these reads. A kept read does not compare. When the plan keeps each root, its reads are not known,
467
+ * and no read compares.
468
+ *
469
+ * One replayer of the log serves each tier and the replay for the reads (RUN.AGREES-ONE-REPLAYER). The replayer keeps
470
+ * no state of a run (RUN.REPLAY-RUNS), thus each tier replays the same log as with a replayer of its own.
471
+ *
378
472
  * @param machine - The world, the evaluators and the runners.
379
473
  * @param doc - The document.
380
474
  * @param context - The names and the capabilities at the root.
@@ -396,15 +490,22 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
396
490
  const result = await plan.run(context, { perform: first.perform });
397
491
  const saw = observe?.();
398
492
  const log = recording.log();
493
+ // The reads of the plan come from another replay, thus the run of the plan above keeps its folds (PLAN.FOLD).
494
+ let planReads: readonly Read[] | undefined;
495
+ let compared: readonly Read[] | undefined;
496
+ // The kernel tells the journal the count of the waits that settled, thus one replayer serves each tier (RUN.AGREES-ONE-REPLAYER).
497
+ const replay = replayer(log);
399
498
 
400
499
  for (const tier of options.tiers ?? DEFAULT_TIERS) {
401
500
  // A run has no tier with fixed names (PLAN.FIXED).
402
501
  if (tier === "fixed") continue;
403
- const replay = watched(replayer(log));
404
- const host = { perform: replay.perform };
502
+ const tierReplay = watched(replay);
503
+ const host = { perform: tierReplay.perform };
405
504
  let other: Result;
505
+ let tierReads: Read[] | undefined;
406
506
  if (tier === "reference") {
407
- other = await referenceRun(machine, doc, context, { ...declared, ...host, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
507
+ tierReads = [];
508
+ other = await referenceRun(machine, doc, context, { ...declared, ...host, reads: tierReads, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
408
509
  } else if (tier === "traced") {
409
510
  other = (await run(machine, doc, context, { ...declared, ...host })).result;
410
511
  } else if (tier === "folds") {
@@ -416,12 +517,23 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
416
517
  const compareCalls = observe !== undefined && tier !== "folds";
417
518
  const calls = observe?.();
418
519
  if (overflowed(result) || overflowed(other)) continue;
419
- const why =
520
+ let why =
420
521
  resultDiffers(result, other, options) ??
421
- effectsDiffer(first.effects, replay.effects, options) ??
422
- settlesDiffer(first.settles, replay.settles) ??
522
+ effectsDiffer(first.effects, tierReplay.effects, options) ??
523
+ settlesDiffer(first.settles, tierReplay.settles) ??
423
524
  (compareCalls && !sameValue(saw, calls, options.equal) ? "The host saw other calls in the tier." : undefined);
525
+ if (why === undefined && tierReads !== undefined) {
526
+ if (planReads === undefined) {
527
+ const reads: Read[] = [];
528
+ await plan.run(context, { perform: replay, reads });
529
+ // The calls of this replay are not a tier, thus the next tier compares only its own calls.
530
+ observe?.();
531
+ planReads = reads;
532
+ }
533
+ why = readsDiffer(planReads, tierReads, options);
534
+ if (why === undefined && !readsUnknown(planReads)) compared = resolutions(planReads);
535
+ }
424
536
  if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
425
537
  }
426
- return { ok: true, result, effects: first.effects };
538
+ return compared === undefined ? { ok: true, result, effects: first.effects } : { ok: true, result, effects: first.effects, reads: compared };
427
539
  }
package/src/conform.ts CHANGED
@@ -32,6 +32,8 @@ import { type PartSlot, type Split, pathMatches, scopesOf, splitParts } from "./
32
32
  import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
33
33
  import { STD_EVALUATORS } from "./stdlib.ts";
34
34
  import { sameValue } from "./agree.ts";
35
+ import { declaredTruthOf } from "./formula.ts";
36
+ import { falseCovers, membersOf, typeIssues, typeText } from "./types.ts";
35
37
  import { type Random, random } from "./generate.ts";
36
38
  import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
37
39
 
@@ -56,8 +58,12 @@ import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effe
56
58
  * - `C8`: compiled code gives the result of code that compiles again at the time of the evaluation.
57
59
  * - `local`: the code keeps no state from one call to the next.
58
60
  * - `agree`: compiled code gives the result of its definition for the reference interpreter.
61
+ * - `truth`: the own `truthy` of a formula profile gives `true` or `false` for each member of its `truthType` (D86).
62
+ * Only `formula.if`, `formula.and` and `formula.or` of the formula kit get this check.
63
+ * - `cover`: each sample of a covered domain of the machine is in one domain of each cover of the domain (D94). Each
64
+ * evaluator and each runner gets this check, because `fits` trusts the covers for each program.
59
65
  */
60
- export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree";
66
+ export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth" | "cover";
61
67
 
62
68
  /** The probe of a finding: the document, the result of each probe part, the capabilities and the handler. */
63
69
  export interface ConformProbe {
@@ -132,7 +138,7 @@ export interface ConformOptions {
132
138
  }
133
139
 
134
140
  /** The order of the obligations in the list of findings. */
135
- const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree"];
141
+ const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth", "cover"];
136
142
 
137
143
  /** A function in the results of the probe parts. It gives the count of its arguments. */
138
144
  const PROBE_FN = (...args: readonly unknown[]): number => args.length;
@@ -1025,6 +1031,56 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
1025
1031
  }
1026
1032
  }
1027
1033
 
1034
+ /**
1035
+ * This function checks the declared truth of a profile (D86). The function `truthy` must give `true` or `false` for
1036
+ * each member of `truthType`, and it must not throw. The members come from the lists `values` of the domains of the
1037
+ * type. A type without such a list gets its members from the results of the palette and from the samples of the
1038
+ * domains (D99).
1039
+ */
1040
+ function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformProbe): void {
1041
+ const truth = declaredTruthOf(code);
1042
+ if (truth === undefined) return;
1043
+ const domains = env.machine.domains;
1044
+ // The check gives an issue for a bad type, and it then needs nothing of a test (D81).
1045
+ if (typeIssues(truth.truthType, domains).length > 0) return;
1046
+ const samples = env.results.flatMap((result) => (result.ok ? [result.value] : []));
1047
+ const of = (value: unknown): string => `the member ${valueText(value)} of the truth type ${typeText(truth.truthType)}`;
1048
+ for (const value of membersOf(truth.truthType, domains, samples)) {
1049
+ let given: unknown;
1050
+ try {
1051
+ given = truth.truthy(value);
1052
+ } catch (error) {
1053
+ report(env, "truth", `The function truthy of the profile throws for ${of(value)}: ${describe(error)}`, () => probe(value));
1054
+ return;
1055
+ }
1056
+ if (typeof given === "boolean") continue;
1057
+ const what = given === undefined ? "no truth" : valueText(given);
1058
+ report(env, "truth", `The function truthy of the profile gives ${what} for ${of(value)}. A truth is true or false.`, () => probe(value));
1059
+ return;
1060
+ }
1061
+ }
1062
+
1063
+ /**
1064
+ * This function checks the covers of the domains of the machine from samples (D94). Each sample of a covered domain
1065
+ * must be in one domain of each valid cover. The samples of a domain come from its list `values`. A domain without
1066
+ * that list gets its members from the palette and from the samples of the domains, as for the check `truth`.
1067
+ */
1068
+ function checkCovers(env: Env, probe: (value: unknown) => ConformProbe): void {
1069
+ const domains = env.machine.domains;
1070
+ if (domains === undefined) return;
1071
+ const palette = env.results.flatMap((result) => (result.ok ? [result.value] : []));
1072
+ const samples: Record<string, readonly unknown[]> = {};
1073
+ for (const [name, domain] of Object.entries(domains)) if (domain.covers !== undefined) samples[name] = membersOf(name, domains, palette);
1074
+ const found = falseCovers(domains, samples)[0];
1075
+ if (found === undefined) return;
1076
+ report(
1077
+ env,
1078
+ "cover",
1079
+ `The member ${valueText(found.value)} of the domain ${found.domain} is in no domain of its cover ${found.cover.join(", ")}. The check of types trusts the cover, thus it is unsound for ${found.domain}.`,
1080
+ () => probe(found.value),
1081
+ );
1082
+ }
1083
+
1028
1084
  /**
1029
1085
  * This function examines one evaluator or runner of a machine against the obligations of host code (K92, D36). It
1030
1086
  * starts the code directly, without the kernel, with probe parts and a probe runtime. It generates probe documents from
@@ -1032,8 +1088,11 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
1032
1088
  *
1033
1089
  * The checks follow the obligations C1 to C8 of `docs/SEMANTICS.md`. Two more checks follow the premises of the
1034
1090
  * simulation of the plan. The check `local` finds state that stays from one call to the next. The check `agree` compares
1035
- * compiled code with its definition. The
1036
- * finding of an obligation names the first probe that breaks it. An empty list means that each probe conforms.
1091
+ * compiled code with its definition. The finding of an obligation names the first probe that breaks it. An empty list
1092
+ * means that each probe conforms.
1093
+ *
1094
+ * The check `truth` tests the declared truth of a formula profile (D86). The check `cover` tests the covers of the
1095
+ * domains of the machine from samples (D94).
1037
1096
  *
1038
1097
  * @param machine - The machine with the code and the world of the kinds.
1039
1098
  * @param name - The name of the evaluator or of the runner. A runner wins over an evaluator of the same name.
@@ -1064,13 +1123,24 @@ export function conforms(machine: Machine, name: string, options: ConformOptions
1064
1123
  found: new Map(),
1065
1124
  };
1066
1125
  const types = options.types ?? typesNaming(machine.world, name, verb);
1126
+ let first: Sample | undefined;
1067
1127
  if (types.length === 0) throw new Error(`No kind of the world names ${name} for the verb ${verb}. Give the types of the probe documents in the option types.`);
1068
1128
  for (const type of types) {
1069
1129
  const found = lookupKind(machine.world, type);
1070
1130
  if (!found.ok) throw new Error(`The world has no kind for the type ${typeName(type)}.`);
1071
1131
  const contract = contractOf(env, found.kind, found.path, options.caps);
1072
1132
  const extra = own(options.values, typeName(type)) ?? own(options.values, found.path.join(".")) ?? [];
1073
- for (const sample of samplesOf(type, found.path, found.kind, contract, extra)) examine(env, sample, contract);
1133
+ for (const sample of samplesOf(type, found.path, found.kind, contract, extra)) {
1134
+ first ??= sample;
1135
+ examine(env, sample, contract);
1136
+ }
1074
1137
  }
1138
+ // A probe of the truth or of a cover gives the value to each part of the first probe document.
1139
+ const probeWith = (value: unknown): ConformProbe =>
1140
+ first === undefined
1141
+ ? { type: typeName(types[0] as TypeRef), doc: { type: types[0] as TypeRef, value: null }, results: {}, waits: [], caps: NO_CAPS, running: runner }
1142
+ : probeOf(first, { results: first.split.parts.map(() => ok(value)), waits: [], caps: NO_CAPS, running: runner });
1143
+ checkTruth(env, code, probeWith);
1144
+ checkCovers(env, probeWith);
1075
1145
  return ORDER.filter((obligation) => env.found.has(obligation)).map((obligation) => env.found.get(obligation) as Finding);
1076
1146
  }
package/src/deref.ts CHANGED
@@ -101,8 +101,31 @@ export interface Context {
101
101
  readonly caps: readonly Cap[];
102
102
  }
103
103
 
104
- /** The code of a `deref` evaluator: it answers one step of a path on a document of its kind. */
105
- export type DerefCode = (doc: Envelope, key: Key) => Thing | undefined;
104
+ /**
105
+ * The code of a `deref` evaluator: it answers one step of a path on a document of its kind. A field that holds
106
+ * `undefined` also gives `undefined`. The optional function `has` of the code tells the two cases apart
107
+ * (DEREF.KIND-UNDEFINED). A host gives the code a `has` with `Object.assign(code, { has })`.
108
+ */
109
+ export interface DerefCode {
110
+ /**
111
+ * This function answers one step on a document.
112
+ *
113
+ * @param doc - The document.
114
+ * @param key - The key of the step.
115
+ * @returns What the step reaches, or `undefined` when it reaches nothing.
116
+ */
117
+ (doc: Envelope, key: Key): Thing | undefined;
118
+ /**
119
+ * This optional function tells if a document has a field for a key. The kernel asks it only when the code gives
120
+ * `undefined`. Then `true` tells that the field holds `undefined`. A code without `has` reads `undefined` as a
121
+ * missing field, as a host thing does (DEREF.HOST-UNDEFINED).
122
+ *
123
+ * @param doc - The document.
124
+ * @param key - The key of the step.
125
+ * @returns `true` when the document has the field.
126
+ */
127
+ readonly has?: (doc: Envelope, key: Key) => boolean;
128
+ }
106
129
 
107
130
  /**
108
131
  * The result of a step document: what it reached, or the reason of a failure. A failure can give its code, for
@@ -118,13 +141,24 @@ export interface WalkInput {
118
141
  readonly here: Thing;
119
142
  readonly context: Context;
120
143
  /**
121
- * This function answers one step on a thing, through the ladder.
144
+ * This function answers one step on a thing, through the ladder. A field that holds `undefined` also gives
145
+ * `undefined`. The function `has` tells the two cases apart (DEREF.WALKER-UNDEFINED).
122
146
  *
123
147
  * @param thing - The thing.
124
148
  * @param key - The key.
125
149
  * @returns What the step reaches, or `undefined`.
126
150
  */
127
151
  deref(thing: Thing, key: Key): Thing | undefined;
152
+ /**
153
+ * This optional function tells if a step on a thing reaches a field, through the ladder. A walker needs it only
154
+ * when `deref` gives `undefined`. Then `true` tells that the field holds `undefined` (DEREF.WALKER-UNDEFINED).
155
+ * Each resolution of the kernel gives it. The function asks the ladder again, thus a host thing answers the step again.
156
+ *
157
+ * @param thing - The thing.
158
+ * @param key - The key.
159
+ * @returns `true` when the step reaches a field, also a field that holds `undefined`.
160
+ */
161
+ has?(thing: Thing, key: Key): boolean;
128
162
  /**
129
163
  * This optional function records one hop inside the walk (HOP.EACH-STEP). The resolution gives it only when it
130
164
  * records hops. The hop of the whole walk comes after these hops, as before.
@@ -318,8 +352,10 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
318
352
  // A navigator without the code of a deref needs no lookup of the kind: no rung can answer.
319
353
  const answer = derefs === undefined ? undefined : answerOf(nav.world, ownEntries(envelope), kindFor(nav.world, envelope.type), "deref", (name) => codeIn(derefs, name));
320
354
  if (answer !== undefined) {
321
- const reached = (derefs as Readonly<Record<string, DerefCode>>)[answer.name]?.(envelope, key);
322
- return reached === undefined ? undefined : { thing: reached, by: answer.rung };
355
+ const code = (derefs as Readonly<Record<string, DerefCode>>)[answer.name];
356
+ const reached = code?.(envelope, key);
357
+ // DEREF.KIND-UNDEFINED: the `has` of the code tells a field that holds undefined from a missing field.
358
+ return reached !== undefined || code?.has?.(envelope, key) === true ? { thing: reached, by: answer.rung } : undefined;
323
359
  }
324
360
  const reached = plainStep(envelope.value, key);
325
361
  return reached === NOTHING ? undefined : { thing: reached, by: "default" };
@@ -596,6 +632,38 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
596
632
  return resolveFully(nav, ctx, path, options.hops === false ? undefined : [], undefined);
597
633
  }
598
634
 
635
+ /** The functions `deref` and `has` of the input of a walker, for one navigator. */
636
+ interface WalkSteps {
637
+ readonly nav: Navigator;
638
+ readonly deref: (thing: Thing, key: Key) => Thing | undefined;
639
+ readonly has: (thing: Thing, key: Key) => boolean;
640
+ }
641
+
642
+ /** The functions of the last navigator that gave a walker its input. A walk with the same navigator makes no new functions. */
643
+ let lastSteps: WalkSteps | undefined;
644
+
645
+ /**
646
+ * This function gives the functions `deref` and `has` of the input of a walker. They depend only on the navigator,
647
+ * thus the function keeps the functions of the last navigator (K170).
648
+ */
649
+ function walkSteps(nav: Navigator): WalkSteps {
650
+ if (lastSteps !== undefined && lastSteps.nav === nav) return lastSteps;
651
+ const adopt = nav.adopt;
652
+ lastSteps = {
653
+ nav,
654
+ deref:
655
+ adopt === undefined
656
+ ? (thing, key) => derefStep(nav, thing, key)?.thing
657
+ : (thing, key) => {
658
+ const reached = derefStep(nav, thing, key);
659
+ return reached === undefined ? undefined : adopt(reached.thing);
660
+ },
661
+ // DEREF.WALKER-UNDEFINED: the same ladder tells a field that holds undefined from a missing field.
662
+ has: (thing, key) => derefStep(nav, thing, key) !== undefined,
663
+ };
664
+ return lastSteps;
665
+ }
666
+
599
667
  /**
600
668
  * This function resolves a path with the full algorithm of `resolve`.
601
669
  *
@@ -668,15 +736,12 @@ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], ho
668
736
  const walker = given === undefined ? walkerOf(nav, step) : (given ?? undefined);
669
737
  if (walker === undefined) return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
670
738
  const envelope = step as Envelope;
671
- const deref = (thing: Thing, key: Key): Thing | undefined => {
672
- const reached = derefStep(nav, thing, key);
673
- return reached === undefined ? undefined : adopt(reached.thing);
674
- };
739
+ const { deref, has } = walkSteps(nav);
675
740
  // A resolution without a record of hops gives no `visit`, thus a walker records nothing (HOP.EACH-STEP).
676
741
  const result = walker(
677
742
  hops === undefined
678
- ? { data: envelope.value, here, context: ctx, deref }
679
- : { data: envelope.value, here, context: ctx, deref, visit: (key, thing) => hops.push({ step: key, thing, by: "step" }) },
743
+ ? { data: envelope.value, here, context: ctx, deref, has }
744
+ : { data: envelope.value, here, context: ctx, deref, has, visit: (key, thing) => hops.push({ step: key, thing, by: "step" }) },
680
745
  );
681
746
  if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
682
747
  here = adopt(result.thing);