@mark1russell7/coeval 0.1.0 → 0.2.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,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.1.0",
5
- "description": "coeval: a base for declarative languages. Documents in envelopes, a ladder that finds the evaluator, sealed parts, references as paths from a context, and a traced evaluation.",
4
+ "version": "0.2.0",
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",
8
8
  "exports": {
package/src/context.ts CHANGED
@@ -2,7 +2,7 @@ import type { Envelope } from "./doc.ts";
2
2
  import type { Side } from "./ladder.ts";
3
3
 
4
4
  /** The parts of a context. A term can reach outside itself only through these parts. */
5
- export const PARTS = ["names", "focus", "world", "event", "caps"] as const;
5
+ export const PARTS = ["self", "names", "world", "event", "caps"] as const;
6
6
 
7
7
  /** One part of a context. */
8
8
  export type Part = (typeof PARTS)[number];
@@ -19,18 +19,19 @@ export interface PartInfo {
19
19
 
20
20
  /** The descriptions of the parts, in the order of `PARTS`. */
21
21
  export const PART_INFO: readonly PartInfo[] = [
22
+ {
23
+ part: "self",
24
+ title: "Self",
25
+ reaches: "A position in a structure: the node itself, its parent, its children or its siblings.",
26
+ direction: "Any direction that a move permits: up, down or to the side.",
27
+ },
22
28
  {
23
29
  part: "names",
24
30
  title: "Names",
25
31
  reaches: "The values that a binding gives: a let, a parameter, or the result of an earlier step.",
26
32
  direction: "Back, to the place where the program binds the name.",
27
33
  },
28
- {
29
- part: "focus",
30
- title: "Focus",
31
- reaches: "A position in a structure: the node itself, its parent, its children or its siblings.",
32
- direction: "Any direction that a move permits: up, down or to the side.",
33
- },
34
+
34
35
  {
35
36
  part: "world",
36
37
  title: "World",
@@ -90,12 +91,15 @@ export const CONSTRUCTS: readonly Construct[] = [
90
91
  side: "derive",
91
92
  summary: "A pure formula that gives a value at one position of a structure.",
92
93
  example: {
93
- type: "formula",
94
- value: { let: { gap: 8 }, body: { op: "add", args: [{ ref: "width", at: "parent" }, { var: "gap" }] } },
94
+ type: "let",
95
+ value: {
96
+ bind: { gap: 8 },
97
+ body: { type: ["ops", "add"], value: [{ type: "ref", value: ["self", "parent", "width"] }, { type: "ref", value: ["names", "gap"] }] },
98
+ },
95
99
  },
96
100
  uses: {
97
101
  names: { reads: true, gives: "extends", note: "A let binds names for its body." },
98
- focus: { reads: true, gives: "moves", note: "A move reads the parent. An axis moves the focus to each target." },
102
+ self: { reads: true, gives: "moves", note: "A move reads the parent. An axis moves self to each target." },
99
103
  world: NONE,
100
104
  event: NONE,
101
105
  caps: { reads: false, gives: "none", note: "A formula causes no effect, thus its parts get no capability." },
@@ -109,13 +113,13 @@ export const CONSTRUCTS: readonly Construct[] = [
109
113
  example: {
110
114
  type: "seq",
111
115
  value: [
112
- { type: "store.find", value: { match: { board: "b1" } }, name: "cards" },
113
- { type: "store.patch", value: { ids: { type: "ref", value: "cards.ids" }, set: { done: true } } },
116
+ { type: ["store", "find"], value: { match: { board: "b1" } }, name: "cards" },
117
+ { type: ["store", "patch"], value: { ids: { type: "ref", value: ["names", "cards", "ids"] }, set: { done: true } } },
114
118
  ],
115
119
  },
116
120
  uses: {
117
121
  names: { reads: true, gives: "extends", note: "The result of each step is a name for the later steps." },
118
- focus: NONE,
122
+ self: NONE,
119
123
  world: { reads: true, gives: "same", note: "A step reads and writes the store through a procedure." },
120
124
  event: { reads: false, gives: "same", note: "The steps get the event of the run without a change." },
121
125
  caps: { reads: true, gives: "narrows", note: "A nested call gets the same capabilities or fewer." },
@@ -128,11 +132,11 @@ export const CONSTRUCTS: readonly Construct[] = [
128
132
  summary: "A node of a document tree. Its data and its box go down to its children.",
129
133
  example: {
130
134
  type: "node",
131
- value: { data: { title: "Notes" }, children: [{ type: "text", value: { type: "ref", value: "title" } }] },
135
+ value: { data: { title: "Notes" }, children: [{ type: "text", value: { type: "ref", value: ["names", "title"] } }] },
132
136
  },
133
137
  uses: {
134
138
  names: { reads: true, gives: "extends", note: "The data of the node becomes a name for its children." },
135
- focus: { reads: true, gives: "moves", note: "Each child becomes the focus of its own part." },
139
+ self: { reads: true, gives: "moves", note: "Each child becomes self for its own part." },
136
140
  world: { reads: true, gives: "same", note: "A node can show stored documents." },
137
141
  event: NONE,
138
142
  caps: NONE,
@@ -143,10 +147,10 @@ export const CONSTRUCTS: readonly Construct[] = [
143
147
  title: "Layout rule",
144
148
  side: "derive",
145
149
  summary: "A rule that gives the box of a child from the box of its parent.",
146
- example: { type: "rule", value: { use: "stack", with: { gap: 8 } } },
150
+ example: { type: ["layouts", "stack"], value: { gap: 8 } },
147
151
  uses: {
148
152
  names: { reads: true, gives: "none", note: "The rule reads its parameters." },
149
- focus: { reads: true, gives: "none", note: "The rule reads the box of the parent and the key of the child." },
153
+ self: { reads: true, gives: "none", note: "The rule reads the box of the parent and the key of the child." },
150
154
  world: NONE,
151
155
  event: NONE,
152
156
  caps: NONE,
@@ -161,12 +165,12 @@ export const CONSTRUCTS: readonly Construct[] = [
161
165
  type: "on",
162
166
  value: {
163
167
  event: "click",
164
- run: { type: "seq", value: [{ type: "card.toggle", value: { id: { type: "ref", value: "event.target" } } }] },
168
+ run: { type: "seq", value: [{ type: ["card", "toggle"], value: { id: { type: "ref", value: ["event", "target"] } } }] },
165
169
  },
166
170
  },
167
171
  uses: {
168
172
  names: { reads: true, gives: "same", note: "The handler reads the names of its place." },
169
- focus: { reads: true, gives: "same", note: "The handler knows the node that got the event." },
173
+ self: { reads: true, gives: "same", note: "The handler knows the node that got the event." },
170
174
  world: { reads: true, gives: "same", note: "The run reads and writes the store." },
171
175
  event: { reads: true, gives: "sets", note: "The handler gives the event to its run." },
172
176
  caps: { reads: true, gives: "narrows", note: "The run gets only the capabilities of the handler." },
@@ -177,10 +181,10 @@ export const CONSTRUCTS: readonly Construct[] = [
177
181
  title: "Query",
178
182
  side: "derive",
179
183
  summary: "A selection of stored documents. A storage adapter decides where it runs.",
180
- example: { type: "query", value: { collection: "cards", match: { board: { type: "ref", value: "board" } } } },
184
+ example: { type: "query", value: { collection: "cards", match: { board: { type: "ref", value: ["names", "board"] } } } },
181
185
  uses: {
182
186
  names: { reads: true, gives: "none", note: "The query reads its parameters." },
183
- focus: NONE,
187
+ self: NONE,
184
188
  world: { reads: true, gives: "none", note: "The query reads one collection." },
185
189
  event: NONE,
186
190
  caps: NONE,
package/src/deref.ts CHANGED
@@ -1,8 +1,11 @@
1
- import { type Envelope, type Json, type Key, field, fromJson, isKey, isRecord, typePath } from "./doc.ts";
1
+ import { type Envelope, type Json, type JsonRecord, type Key, type TypeRef, field, isEnvelope, isKey, isRecord, typePath } from "./doc.ts";
2
2
  import { type World, entriesAsJson, lookupKind } from "./world.ts";
3
3
 
4
- /** The brand of a host thing: an object of the host that answers the steps of a path itself. */
5
- export const THING: unique symbol = Symbol("coeval.thing");
4
+ /**
5
+ * The brand of a host thing: an object of the host that answers the steps of a path itself. The brand is a
6
+ * global symbol, thus two copies of the kernel in one program know the things of each other.
7
+ */
8
+ export const THING: unique symbol = Symbol.for("coeval.thing") as never;
6
9
 
7
10
  /**
8
11
  * A host thing: an object that answers the steps of a path itself, for example a node of a live tree. It is the
@@ -16,7 +19,7 @@ export interface HostThing {
16
19
  * This function answers one step.
17
20
  *
18
21
  * @param key - The key of the step.
19
- * @returns What the step reaches, or `undefined`.
22
+ * @returns What the step reaches, or `undefined` when it reaches nothing.
20
23
  */
21
24
  deref(key: Key): Thing | undefined;
22
25
  /**
@@ -24,11 +27,19 @@ export interface HostThing {
24
27
  *
25
28
  * @returns The value.
26
29
  */
27
- toJson(): Json;
30
+ toJson(): unknown;
31
+ /**
32
+ * This optional function answers all the steps of a path that are keys, in one call. A store uses it to read a
33
+ * path without one call for each step.
34
+ *
35
+ * @param keys - The keys.
36
+ * @returns What the keys reach, or `undefined` when they reach nothing.
37
+ */
38
+ resolve?(keys: readonly Key[]): { readonly thing: Thing } | undefined;
28
39
  }
29
40
 
30
- /** Something that a path can reach: a JSON value, or a host thing. */
31
- export type Thing = Json | HostThing;
41
+ /** Something that a path can reach: any value of the host, or a host thing. */
42
+ export type Thing = unknown;
32
43
 
33
44
  /**
34
45
  * This function tells if a thing is a host thing.
@@ -36,8 +47,8 @@ export type Thing = Json | HostThing;
36
47
  * @param thing - The thing.
37
48
  * @returns `true` for a host thing.
38
49
  */
39
- export function isHostThing(thing: Thing | undefined): thing is HostThing {
40
- return typeof thing === "object" && thing !== null && !Array.isArray(thing) && (thing as Partial<HostThing>)[THING] === true;
50
+ export function isHostThing(thing: unknown): thing is HostThing {
51
+ return typeof thing === "object" && thing !== null && (thing as Partial<HostThing>)[THING] === true;
41
52
  }
42
53
 
43
54
  /** The roots of a context. Each path starts with one of them. */
@@ -46,6 +57,12 @@ export const ROOTS = ["self", "names", "world", "event", "caps"] as const;
46
57
  /** One root of a context. */
47
58
  export type Root = (typeof ROOTS)[number];
48
59
 
60
+ /**
61
+ * A capability: the name of a kind of effect, or its keys. A capability permits its kind and each kind under it.
62
+ * Keys are necessary when a key has a dot in it.
63
+ */
64
+ export type Cap = string | readonly Key[];
65
+
49
66
  /**
50
67
  * The context of a term: a record with five roots. A reference reaches outside its term only through them.
51
68
  *
@@ -57,10 +74,10 @@ export type Root = (typeof ROOTS)[number];
57
74
  */
58
75
  export interface Context {
59
76
  readonly self: Thing;
60
- readonly names: Readonly<Record<string, Json>>;
77
+ readonly names: Readonly<Record<string, unknown>>;
61
78
  readonly world: Thing;
62
79
  readonly event: Thing;
63
- readonly caps: readonly string[];
80
+ readonly caps: readonly Cap[];
64
81
  }
65
82
 
66
83
  /** The code of a `deref` evaluator: it answers one step of a path on a document of its kind. */
@@ -89,11 +106,19 @@ export interface WalkInput {
89
106
  /** A walker: the code of the verb `step` of a kind of step documents. */
90
107
  export type Walker = (input: WalkInput) => WalkResult;
91
108
 
92
- /** What a resolution needs: the world, and the code of the `deref` evaluators and of the walkers. */
109
+ /**
110
+ * What a resolution needs: the world, and the code of the `deref` evaluators and of the walkers.
111
+ *
112
+ * - `plain`: `true` when the values are data. Then a step does not ask the kind of an envelope, and a reference
113
+ * that a step reaches stays a value (no collapse).
114
+ * - `roots`: the roots that a path can start at. The default is each root.
115
+ */
93
116
  export interface Navigator {
94
117
  readonly world: World;
95
118
  readonly derefs?: Readonly<Record<string, DerefCode>>;
96
119
  readonly walkers?: Readonly<Record<string, Walker>>;
120
+ readonly plain?: boolean;
121
+ readonly roots?: readonly Root[];
97
122
  }
98
123
 
99
124
  /** The way that one hop of a path was answered. */
@@ -106,42 +131,57 @@ export interface Hop {
106
131
  readonly by: HopBy;
107
132
  }
108
133
 
134
+ /** The codes of a failed resolution. */
135
+ export type ResolveCode = "#REF!" | "#NAME?" | "#N/A" | "#CYCLE!" | "#STEP?";
136
+
109
137
  /** The result of a resolution: what the path reached, with each hop, or an error with the hops before it. */
110
138
  export type Resolution =
111
139
  | { readonly ok: true; readonly thing: Thing; readonly hops: readonly Hop[] }
112
- | { readonly ok: false; readonly code: "#REF!" | "#CYCLE!" | "#STEP?"; readonly reason: string; readonly hops: readonly Hop[] };
140
+ | { readonly ok: false; readonly code: ResolveCode; readonly reason: string; readonly hops: readonly Hop[] };
141
+
142
+ /** The options of a resolution. */
143
+ export interface ResolveOptions {
144
+ /** `false`: do not record the hops. The default is `true`. */
145
+ readonly hops?: boolean;
146
+ }
113
147
 
114
148
  function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
115
149
  return verbs !== undefined && Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
116
150
  }
117
151
 
118
- function plainStep(thing: Json, key: Key): Json | undefined {
152
+ /** The own fields of a value: the items and the length of a list, and the own properties of an object. */
153
+ function plainStep(thing: unknown, key: Key): { readonly found: true; readonly value: unknown } | undefined {
119
154
  if (Array.isArray(thing)) {
155
+ if (key === "length") return { found: true, value: thing.length };
120
156
  const index = typeof key === "number" ? key : /^\d+$/.test(key) ? Number(key) : -1;
121
- return index >= 0 ? (thing as readonly Json[])[index] : undefined;
157
+ return index >= 0 && index < thing.length ? { found: true, value: (thing as readonly unknown[])[index] } : undefined;
158
+ }
159
+ if (typeof thing === "object" && thing !== null) {
160
+ const name = String(key);
161
+ return Object.hasOwn(thing, name) ? { found: true, value: (thing as Readonly<Record<string, unknown>>)[name] } : undefined;
122
162
  }
123
- if (isRecord(thing) && typeof key === "string") return field(thing, key);
124
163
  return undefined;
125
164
  }
126
165
 
127
166
  /**
128
167
  * This function answers one step of a path on a thing, through the ladder of the verb `deref`. A host thing
129
- * answers for itself (own). A document asks its own entry, then its kind. The default goes into the value: a field
130
- * of a record, or an item of a list.
168
+ * answers for itself (own). A document asks its own entry, then its kind. The default goes into the value: an own
169
+ * field of an object, or an item of a list.
131
170
  *
132
171
  * @param nav - The world and the code of the evaluators.
133
172
  * @param thing - The thing that the path reached.
134
173
  * @param key - The key of the step.
135
- * @returns What the step reaches and the rung that answered, or `undefined`.
174
+ * @returns What the step reaches and the rung that answered, or `undefined` when it reaches nothing.
136
175
  */
137
176
  export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly thing: Thing; readonly by: HopBy } | undefined {
138
177
  if (isHostThing(thing)) {
139
178
  const reached = thing.deref(key);
140
179
  return reached === undefined ? undefined : { thing: reached, by: "own" };
141
180
  }
142
- const envelope = fromJson(thing);
143
- if (envelope !== undefined) {
144
- const own = entryOf(envelope.own, "deref");
181
+ if (nav.plain !== true && isEnvelope(thing)) {
182
+ const envelope = thing as JsonRecord & Envelope;
183
+ const ownEntries = field(envelope, "own");
184
+ const own = isRecord(ownEntries) ? entryOf(ownEntries as Readonly<Record<string, string>>, "deref") : undefined;
145
185
  const ownCode = own === undefined ? undefined : nav.derefs?.[own];
146
186
  if (ownCode !== undefined) {
147
187
  const reached = ownCode(envelope, key);
@@ -155,35 +195,41 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
155
195
  return reached === undefined ? undefined : { thing: reached, by: "kind" };
156
196
  }
157
197
  const reached = plainStep(envelope.value, key);
158
- return reached === undefined ? undefined : { thing: reached, by: "default" };
198
+ return reached === undefined ? undefined : { thing: reached.value, by: "default" };
159
199
  }
160
200
  const reached = plainStep(thing, key);
161
- return reached === undefined ? undefined : { thing: reached, by: "default" };
201
+ return reached === undefined ? undefined : { thing: reached.value, by: "default" };
162
202
  }
163
203
 
164
204
  /**
165
- * This function tells if a thing is a reference document: an envelope of the type `ref`.
205
+ * This function tells if a thing is a reference document: an envelope of the type `ref` with a list for its path.
166
206
  *
167
207
  * @param thing - The thing.
168
208
  * @returns `true` for a reference document.
169
209
  */
170
- export function isRef(thing: Thing | undefined): thing is Envelope & { readonly value: readonly Json[] } {
171
- if (thing === undefined || isHostThing(thing)) return false;
172
- const envelope = fromJson(thing);
173
- if (envelope === undefined || !Array.isArray(envelope.value)) return false;
174
- const path = typePath(envelope.type);
210
+ export function isRef(thing: unknown): thing is Envelope & { readonly value: readonly Json[] } {
211
+ if (!isEnvelope(thing) || !Array.isArray(thing.value)) return false;
212
+ const path = typePath(thing.type as TypeRef);
175
213
  return path !== undefined && path.length === 1 && path[0] === "ref";
176
214
  }
177
215
 
216
+ const worlds = new WeakMap<World, Json>();
217
+
178
218
  /**
179
219
  * This function makes the thing of the root `world`: the entries of the world as JSON. An alias is a reference
180
- * document in it, thus a walk through the world follows each alias (collapse).
220
+ * document in it, thus a walk through the world follows each alias (collapse). The thing of a world is made one
221
+ * time.
181
222
  *
182
223
  * @param world - The world.
183
224
  * @returns The thing.
184
225
  */
185
226
  export function worldThing(world: World): Json {
186
- return entriesAsJson(world.kinds);
227
+ let thing = worlds.get(world);
228
+ if (thing === undefined) {
229
+ thing = entriesAsJson(world.kinds);
230
+ worlds.set(world, thing);
231
+ }
232
+ return thing;
187
233
  }
188
234
 
189
235
  /**
@@ -203,6 +249,41 @@ export function context(parts: Partial<Context> = {}): Context {
203
249
  }
204
250
 
205
251
  const MAX_COLLAPSE = 64;
252
+ const NO_HOPS: readonly Hop[] = Object.freeze([]);
253
+
254
+ /** A value that can be a reference document: a record with the type `ref`. The full check comes after. */
255
+ function mayBeRef(thing: unknown): boolean {
256
+ return typeof thing === "object" && thing !== null && (thing as { readonly type?: unknown }).type === "ref";
257
+ }
258
+
259
+ /**
260
+ * This function resolves the common path without a record of hops: a root, then keys, through plain values. It
261
+ * gives `undefined` when the path needs the full algorithm. Examples are a host thing, an envelope, a reference to
262
+ * follow, a step document and a failure (for its exact reason).
263
+ */
264
+ function resolveQuick(nav: Navigator, ctx: Context, path: readonly unknown[]): Resolution | undefined {
265
+ const root = path[0];
266
+ if (root !== "names" && root !== "self" && root !== "event") return undefined;
267
+ if (nav.roots !== undefined && !(nav.roots as readonly string[]).includes(root)) return undefined;
268
+ let here: unknown = ctx[root];
269
+ const plain = nav.plain === true;
270
+ for (let index = 1; index < path.length; index += 1) {
271
+ if (typeof here !== "object" || here === null) return undefined;
272
+ if (!plain && mayBeRef(here)) return undefined;
273
+ if ((here as Partial<HostThing>)[THING] === true) return undefined;
274
+ const step = path[index];
275
+ if (Array.isArray(here)) {
276
+ if (typeof step !== "number" || !Number.isInteger(step) || step < 0 || step >= here.length) return undefined;
277
+ here = (here as readonly unknown[])[step];
278
+ continue;
279
+ }
280
+ if (typeof step !== "string" || !Object.hasOwn(here, step)) return undefined;
281
+ if (!plain && isEnvelope(here)) return undefined;
282
+ here = (here as Readonly<Record<string, unknown>>)[step];
283
+ }
284
+ if (!plain && mayBeRef(here)) return undefined;
285
+ return { ok: true, thing: here, hops: NO_HOPS };
286
+ }
206
287
 
207
288
  /**
208
289
  * This function resolves a path from a context, and records each hop.
@@ -210,53 +291,83 @@ const MAX_COLLAPSE = 64;
210
291
  * 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
211
292
  * of the verb `deref` on the thing that the path reached. A step document is evaluated by the walker of its kind,
212
293
  * with the thing that the path reached. After each hop, a reference document is followed until the path reaches
213
- * something else (collapse).
294
+ * something else (collapse), except in a plain navigation.
295
+ *
296
+ * The codes of a failure:
297
+ *
298
+ * - `#REF!`: the path has no root, or a root that is not open here.
299
+ * - `#NAME?`: the root `names` has no such name.
300
+ * - `#N/A`: a step reaches nothing.
301
+ * - `#STEP?`: a step is not a key and not a step document.
302
+ * - `#CYCLE!`: the references form a cycle.
214
303
  *
215
304
  * @param nav - The world and the code of the evaluators.
216
305
  * @param ctx - The context.
217
306
  * @param path - The steps.
307
+ * @param options - `hops: false` does not record the hops.
218
308
  * @returns What the path reached and each hop, or an error.
219
309
  */
220
- export function resolve(nav: Navigator, ctx: Context, path: readonly Json[]): Resolution {
221
- const hops: Hop[] = [];
310
+ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[], options: ResolveOptions = {}): Resolution {
311
+ if (options.hops === false) {
312
+ const quick = resolveQuick(nav, ctx, path);
313
+ if (quick !== undefined) return quick;
314
+ }
315
+ const hops: Hop[] | undefined = options.hops === false ? undefined : [];
316
+ const done = hops ?? NO_HOPS;
317
+ const failed = (code: ResolveCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
222
318
  let collapses = 0;
223
319
 
224
320
  const collapse = (thing: Thing): Resolution => {
225
321
  let here = thing;
322
+ if (nav.plain === true) return { ok: true, thing: here, hops: done };
226
323
  while (isRef(here)) {
227
324
  collapses += 1;
228
- if (collapses > MAX_COLLAPSE) return { ok: false, code: "#CYCLE!", reason: "The references form a cycle.", hops };
325
+ if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", "The references form a cycle.");
229
326
  const inner = walk(here.value);
230
327
  if (!inner.ok) return inner;
231
328
  here = inner.thing;
232
- hops.push({ step: here === null ? null : "collapse", thing: here, by: "collapse" });
329
+ hops?.push({ step: "collapse", thing: here, by: "collapse" });
233
330
  }
234
- return { ok: true, thing: here, hops };
331
+ return { ok: true, thing: here, hops: done };
235
332
  };
236
333
 
237
- const walk = (steps: readonly Json[]): Resolution => {
238
- const [root, ...rest] = steps;
334
+ const walk = (steps: readonly unknown[]): Resolution => {
335
+ const root = steps[0];
239
336
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
240
- return { ok: false, code: "#REF!", reason: `A path starts with a root: ${ROOTS.join(", ")}.`, hops };
337
+ return failed("#REF!", `A path starts with a root: ${ROOTS.join(", ")}.`);
338
+ }
339
+ if (nav.roots !== undefined && !(nav.roots as readonly string[]).includes(root)) {
340
+ return failed("#REF!", `The root ${root} is not open here. The open roots are ${nav.roots.join(", ")}.`);
241
341
  }
242
- let here: Thing = ctx[root as Root] as Thing;
243
- hops.push({ step: root, thing: here, by: "root" });
342
+ let here: Thing = ctx[root as Root];
343
+ hops?.push({ step: root, thing: here, by: "root" });
244
344
  const first = collapse(here);
245
345
  if (!first.ok) return first;
246
346
  here = first.thing;
247
- for (const step of rest) {
347
+ for (let index = 1; index < steps.length; index += 1) {
348
+ const step = steps[index];
349
+ if (isHostThing(here) && here.resolve !== undefined && steps.slice(index).every(isKey)) {
350
+ const keys = steps.slice(index) as readonly Key[];
351
+ const reached = here.resolve(keys);
352
+ if (reached === undefined) return failed("#N/A", `The path ${keys.join(".")} reaches nothing.`);
353
+ hops?.push({ step: keys as readonly Json[], thing: reached.thing, by: "own" });
354
+ return collapse(reached.thing);
355
+ }
248
356
  if (isKey(step)) {
249
357
  const reached = derefStep(nav, here, step);
250
- if (reached === undefined) return { ok: false, code: "#REF!", reason: `The step ${String(step)} reaches nothing.`, hops };
251
- hops.push({ step, thing: reached.thing, by: reached.by });
358
+ if (reached === undefined) {
359
+ if (root === "names" && index === 1) return failed("#NAME?", `The context has no name ${String(step)}.`);
360
+ return failed("#N/A", `The step ${String(step)} reaches nothing.`);
361
+ }
362
+ hops?.push({ step, thing: reached.thing, by: reached.by });
252
363
  here = reached.thing;
253
364
  } else {
254
- const envelope = fromJson(step);
365
+ const envelope = isEnvelope(step) ? step : undefined;
255
366
  const found = envelope === undefined ? undefined : lookupKind(nav.world, envelope.type);
256
367
  const name = found?.ok === true ? entryOf(found.kind.verbs, "step") : undefined;
257
368
  const walker = name === undefined ? undefined : nav.walkers?.[name];
258
369
  if (envelope === undefined || walker === undefined) {
259
- return { ok: false, code: "#STEP?", reason: "A step is a key, or a document of a kind with the verb step.", hops };
370
+ return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
260
371
  }
261
372
  const result = walker({
262
373
  data: envelope.value,
@@ -264,27 +375,27 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly Json[]): Re
264
375
  context: ctx,
265
376
  deref: (thing, key) => derefStep(nav, thing, key)?.thing,
266
377
  });
267
- if (!result.ok) return { ok: false, code: "#REF!", reason: result.reason, hops };
268
- hops.push({ step, thing: result.thing, by: "step" });
378
+ if (!result.ok) return failed("#N/A", result.reason);
379
+ hops?.push({ step: envelope as Json, thing: result.thing, by: "step" });
269
380
  here = result.thing;
270
381
  }
271
382
  const next = collapse(here);
272
383
  if (!next.ok) return next;
273
384
  here = next.thing;
274
385
  }
275
- return { ok: true, thing: here, hops };
386
+ return { ok: true, thing: here, hops: done };
276
387
  };
277
388
 
278
- if (path.length === 0) return { ok: false, code: "#REF!", reason: "A path has at least one step: its root.", hops };
389
+ if (path.length === 0) return failed("#REF!", "A path has at least one step: its root.");
279
390
  return walk(path);
280
391
  }
281
392
 
282
393
  /**
283
- * This function gives the value of a thing: the thing itself for JSON, or the value that a host thing gives.
394
+ * This function gives the value of a thing: the value that a host thing gives, or else the thing itself.
284
395
  *
285
396
  * @param thing - The thing.
286
397
  * @returns The value.
287
398
  */
288
- export function valueOfThing(thing: Thing): Json {
399
+ export function valueOfThing(thing: Thing): unknown {
289
400
  return isHostThing(thing) ? thing.toJson() : thing;
290
401
  }