@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/README.md +36 -7
- package/build/index.d.ts +1767 -303
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4989 -417
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +56 -55
- package/src/conform.ts +1068 -0
- package/src/deref.ts +298 -50
- package/src/doc.ts +23 -2
- package/src/durable.ts +238 -0
- package/src/effects.ts +110 -0
- package/src/errors.ts +3 -1
- package/src/evaluate.ts +656 -124
- package/src/facts.ts +252 -0
- package/src/formula.ts +433 -36
- package/src/graph.ts +399 -0
- package/src/index.ts +34 -1
- package/src/ladder.ts +40 -1
- package/src/merge.ts +91 -4
- package/src/parts.ts +26 -11
- package/src/providers.ts +286 -0
- package/src/reads.ts +105 -0
- package/src/reference.ts +101 -18
- package/src/replay.ts +159 -0
- package/src/same.ts +42 -0
- package/src/schedule.ts +436 -0
- package/src/static.ts +266 -29
- package/src/stdlib.ts +28 -7
- package/src/types.ts +741 -0
- package/src/typing.ts +390 -0
- package/src/world.ts +63 -9
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
|
|
156
|
-
|
|
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):
|
|
259
|
+
function plainStep(thing: unknown, key: Key): unknown {
|
|
161
260
|
if (Array.isArray(thing)) {
|
|
162
|
-
if (key === "length") return
|
|
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 ?
|
|
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) ?
|
|
267
|
+
return Object.hasOwn(thing, name) ? (thing as Readonly<Record<string, unknown>>)[name] : NOTHING;
|
|
169
268
|
}
|
|
170
|
-
return
|
|
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
|
|
176
|
-
*
|
|
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
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
const reached =
|
|
195
|
-
return reached === undefined ? undefined : { thing: reached, by:
|
|
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 ===
|
|
308
|
+
return reached === NOTHING ? undefined : { thing: reached, by: "default" };
|
|
206
309
|
}
|
|
207
310
|
const reached = plainStep(thing, key);
|
|
208
|
-
return reached ===
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
316
|
-
* with the thing that the path reached. After each hop, a reference document is
|
|
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
|
-
|
|
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
|
|
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
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
const
|
|
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`
|
|
234
|
-
*
|
|
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.
|