@mark1russell7/coeval 0.1.0 → 0.2.1
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 +41 -0
- package/build/index.d.ts +835 -148
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1911 -383
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +25 -21
- package/src/deref.ts +178 -58
- package/src/doc.ts +86 -17
- package/src/errors.ts +141 -0
- package/src/evaluate.ts +797 -281
- package/src/formula.ts +409 -0
- package/src/index.ts +72 -7
- package/src/parts.ts +140 -27
- package/src/static.ts +364 -0
- package/src/stdlib.ts +60 -33
- package/src/world.ts +96 -22
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
|
|
5
|
-
"description": "coeval:
|
|
4
|
+
"version": "0.2.1",
|
|
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 = ["
|
|
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: "
|
|
94
|
-
value: {
|
|
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
|
-
|
|
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
|
|
113
|
-
{ type: "store
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
5
|
-
|
|
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():
|
|
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:
|
|
31
|
-
export type Thing =
|
|
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:
|
|
40
|
-
return typeof thing === "object" && thing !== null &&
|
|
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,
|
|
77
|
+
readonly names: Readonly<Record<string, unknown>>;
|
|
61
78
|
readonly world: Thing;
|
|
62
79
|
readonly event: Thing;
|
|
63
|
-
readonly caps: readonly
|
|
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,22 @@ 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
|
-
/**
|
|
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
|
+
* - `adopt`: the thing of each value that a path reaches. A host gives its own values a host thing with this
|
|
116
|
+
* function, for example a value whose fields read through signals. A host thing is not followed as a reference.
|
|
117
|
+
*/
|
|
93
118
|
export interface Navigator {
|
|
94
119
|
readonly world: World;
|
|
95
120
|
readonly derefs?: Readonly<Record<string, DerefCode>>;
|
|
96
121
|
readonly walkers?: Readonly<Record<string, Walker>>;
|
|
122
|
+
readonly plain?: boolean;
|
|
123
|
+
readonly roots?: readonly Root[];
|
|
124
|
+
readonly adopt?: (value: unknown) => Thing;
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
/** The way that one hop of a path was answered. */
|
|
@@ -106,42 +134,57 @@ export interface Hop {
|
|
|
106
134
|
readonly by: HopBy;
|
|
107
135
|
}
|
|
108
136
|
|
|
137
|
+
/** The codes of a failed resolution. */
|
|
138
|
+
export type ResolveCode = "#REF!" | "#NAME?" | "#N/A" | "#CYCLE!" | "#STEP?";
|
|
139
|
+
|
|
109
140
|
/** The result of a resolution: what the path reached, with each hop, or an error with the hops before it. */
|
|
110
141
|
export type Resolution =
|
|
111
142
|
| { readonly ok: true; readonly thing: Thing; readonly hops: readonly Hop[] }
|
|
112
|
-
| { readonly ok: false; readonly code:
|
|
143
|
+
| { readonly ok: false; readonly code: ResolveCode; readonly reason: string; readonly hops: readonly Hop[] };
|
|
144
|
+
|
|
145
|
+
/** The options of a resolution. */
|
|
146
|
+
export interface ResolveOptions {
|
|
147
|
+
/** `false`: do not record the hops. The default is `true`. */
|
|
148
|
+
readonly hops?: boolean;
|
|
149
|
+
}
|
|
113
150
|
|
|
114
151
|
function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
|
|
115
152
|
return verbs !== undefined && Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
|
|
116
153
|
}
|
|
117
154
|
|
|
118
|
-
|
|
155
|
+
/** The own fields of a value: the items and the length of a list, and the own properties of an object. */
|
|
156
|
+
function plainStep(thing: unknown, key: Key): { readonly found: true; readonly value: unknown } | undefined {
|
|
119
157
|
if (Array.isArray(thing)) {
|
|
158
|
+
if (key === "length") return { found: true, value: thing.length };
|
|
120
159
|
const index = typeof key === "number" ? key : /^\d+$/.test(key) ? Number(key) : -1;
|
|
121
|
-
return index >= 0 ? (thing as readonly
|
|
160
|
+
return index >= 0 && index < thing.length ? { found: true, value: (thing as readonly unknown[])[index] } : undefined;
|
|
161
|
+
}
|
|
162
|
+
if (typeof thing === "object" && thing !== null) {
|
|
163
|
+
const name = String(key);
|
|
164
|
+
return Object.hasOwn(thing, name) ? { found: true, value: (thing as Readonly<Record<string, unknown>>)[name] } : undefined;
|
|
122
165
|
}
|
|
123
|
-
if (isRecord(thing) && typeof key === "string") return field(thing, key);
|
|
124
166
|
return undefined;
|
|
125
167
|
}
|
|
126
168
|
|
|
127
169
|
/**
|
|
128
170
|
* 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:
|
|
130
|
-
* of
|
|
171
|
+
* answers for itself (own). A document asks its own entry, then its kind. The default goes into the value: an own
|
|
172
|
+
* field of an object, or an item of a list.
|
|
131
173
|
*
|
|
132
174
|
* @param nav - The world and the code of the evaluators.
|
|
133
175
|
* @param thing - The thing that the path reached.
|
|
134
176
|
* @param key - The key of the step.
|
|
135
|
-
* @returns What the step reaches and the rung that answered, or `undefined
|
|
177
|
+
* @returns What the step reaches and the rung that answered, or `undefined` when it reaches nothing.
|
|
136
178
|
*/
|
|
137
179
|
export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly thing: Thing; readonly by: HopBy } | undefined {
|
|
138
180
|
if (isHostThing(thing)) {
|
|
139
181
|
const reached = thing.deref(key);
|
|
140
182
|
return reached === undefined ? undefined : { thing: reached, by: "own" };
|
|
141
183
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
184
|
+
if (nav.plain !== true && isEnvelope(thing)) {
|
|
185
|
+
const envelope = thing as JsonRecord & Envelope;
|
|
186
|
+
const ownEntries = field(envelope, "own");
|
|
187
|
+
const own = isRecord(ownEntries) ? entryOf(ownEntries as Readonly<Record<string, string>>, "deref") : undefined;
|
|
145
188
|
const ownCode = own === undefined ? undefined : nav.derefs?.[own];
|
|
146
189
|
if (ownCode !== undefined) {
|
|
147
190
|
const reached = ownCode(envelope, key);
|
|
@@ -155,35 +198,41 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
|
|
|
155
198
|
return reached === undefined ? undefined : { thing: reached, by: "kind" };
|
|
156
199
|
}
|
|
157
200
|
const reached = plainStep(envelope.value, key);
|
|
158
|
-
return reached === undefined ? undefined : { thing: reached, by: "default" };
|
|
201
|
+
return reached === undefined ? undefined : { thing: reached.value, by: "default" };
|
|
159
202
|
}
|
|
160
203
|
const reached = plainStep(thing, key);
|
|
161
|
-
return reached === undefined ? undefined : { thing: reached, by: "default" };
|
|
204
|
+
return reached === undefined ? undefined : { thing: reached.value, by: "default" };
|
|
162
205
|
}
|
|
163
206
|
|
|
164
207
|
/**
|
|
165
|
-
* This function tells if a thing is a reference document: an envelope of the type `ref
|
|
208
|
+
* This function tells if a thing is a reference document: an envelope of the type `ref` with a list for its path.
|
|
166
209
|
*
|
|
167
210
|
* @param thing - The thing.
|
|
168
211
|
* @returns `true` for a reference document.
|
|
169
212
|
*/
|
|
170
|
-
export function isRef(thing:
|
|
171
|
-
if (thing
|
|
172
|
-
const
|
|
173
|
-
if (envelope === undefined || !Array.isArray(envelope.value)) return false;
|
|
174
|
-
const path = typePath(envelope.type);
|
|
213
|
+
export function isRef(thing: unknown): thing is Envelope & { readonly value: readonly Json[] } {
|
|
214
|
+
if (!isEnvelope(thing) || !Array.isArray(thing.value)) return false;
|
|
215
|
+
const path = typePath(thing.type as TypeRef);
|
|
175
216
|
return path !== undefined && path.length === 1 && path[0] === "ref";
|
|
176
217
|
}
|
|
177
218
|
|
|
219
|
+
const worlds = new WeakMap<World, Json>();
|
|
220
|
+
|
|
178
221
|
/**
|
|
179
222
|
* 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).
|
|
223
|
+
* document in it, thus a walk through the world follows each alias (collapse). The thing of a world is made one
|
|
224
|
+
* time.
|
|
181
225
|
*
|
|
182
226
|
* @param world - The world.
|
|
183
227
|
* @returns The thing.
|
|
184
228
|
*/
|
|
185
229
|
export function worldThing(world: World): Json {
|
|
186
|
-
|
|
230
|
+
let thing = worlds.get(world);
|
|
231
|
+
if (thing === undefined) {
|
|
232
|
+
thing = entriesAsJson(world.kinds);
|
|
233
|
+
worlds.set(world, thing);
|
|
234
|
+
}
|
|
235
|
+
return thing;
|
|
187
236
|
}
|
|
188
237
|
|
|
189
238
|
/**
|
|
@@ -203,6 +252,42 @@ export function context(parts: Partial<Context> = {}): Context {
|
|
|
203
252
|
}
|
|
204
253
|
|
|
205
254
|
const MAX_COLLAPSE = 64;
|
|
255
|
+
const NO_HOPS: readonly Hop[] = Object.freeze([]);
|
|
256
|
+
|
|
257
|
+
/** A value that can be a reference document: a record with the type `ref`. The full check comes after. */
|
|
258
|
+
function mayBeRef(thing: unknown): boolean {
|
|
259
|
+
return typeof thing === "object" && thing !== null && (thing as { readonly type?: unknown }).type === "ref";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* This function resolves the common path without a record of hops: a root, then keys, through plain values. It
|
|
264
|
+
* gives `undefined` when the path needs the full algorithm. Examples are a host thing, an envelope, a reference to
|
|
265
|
+
* follow, a step document and a failure (for its exact reason).
|
|
266
|
+
*/
|
|
267
|
+
function resolveQuick(nav: Navigator, ctx: Context, path: readonly unknown[]): Resolution | undefined {
|
|
268
|
+
const root = path[0];
|
|
269
|
+
if (root !== "names" && root !== "self" && root !== "event") return undefined;
|
|
270
|
+
if (nav.roots !== undefined && !(nav.roots as readonly string[]).includes(root)) return undefined;
|
|
271
|
+
if (nav.adopt !== undefined) return undefined;
|
|
272
|
+
let here: unknown = ctx[root];
|
|
273
|
+
const plain = nav.plain === true;
|
|
274
|
+
for (let index = 1; index < path.length; index += 1) {
|
|
275
|
+
if (typeof here !== "object" || here === null) return undefined;
|
|
276
|
+
if (!plain && mayBeRef(here)) return undefined;
|
|
277
|
+
if ((here as Partial<HostThing>)[THING] === true) return undefined;
|
|
278
|
+
const step = path[index];
|
|
279
|
+
if (Array.isArray(here)) {
|
|
280
|
+
if (typeof step !== "number" || !Number.isInteger(step) || step < 0 || step >= here.length) return undefined;
|
|
281
|
+
here = (here as readonly unknown[])[step];
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (typeof step !== "string" || !Object.hasOwn(here, step)) return undefined;
|
|
285
|
+
if (!plain && isEnvelope(here)) return undefined;
|
|
286
|
+
here = (here as Readonly<Record<string, unknown>>)[step];
|
|
287
|
+
}
|
|
288
|
+
if (!plain && mayBeRef(here)) return undefined;
|
|
289
|
+
return { ok: true, thing: here, hops: NO_HOPS };
|
|
290
|
+
}
|
|
206
291
|
|
|
207
292
|
/**
|
|
208
293
|
* This function resolves a path from a context, and records each hop.
|
|
@@ -210,81 +295,116 @@ const MAX_COLLAPSE = 64;
|
|
|
210
295
|
* 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
296
|
* of the verb `deref` on the thing that the path reached. A step document is evaluated by the walker of its kind,
|
|
212
297
|
* with the thing that the path reached. After each hop, a reference document is followed until the path reaches
|
|
213
|
-
* something else (collapse).
|
|
298
|
+
* something else (collapse), except in a plain navigation.
|
|
299
|
+
*
|
|
300
|
+
* The codes of a failure:
|
|
301
|
+
*
|
|
302
|
+
* - `#REF!`: the path has no root, or a root that is not open here.
|
|
303
|
+
* - `#NAME?`: the root `names` has no such name.
|
|
304
|
+
* - `#N/A`: a step reaches nothing.
|
|
305
|
+
* - `#STEP?`: a step is not a key and not a step document.
|
|
306
|
+
* - `#CYCLE!`: the references form a cycle.
|
|
214
307
|
*
|
|
215
308
|
* @param nav - The world and the code of the evaluators.
|
|
216
309
|
* @param ctx - The context.
|
|
217
310
|
* @param path - The steps.
|
|
311
|
+
* @param options - `hops: false` does not record the hops.
|
|
218
312
|
* @returns What the path reached and each hop, or an error.
|
|
219
313
|
*/
|
|
220
|
-
export function resolve(nav: Navigator, ctx: Context, path: readonly
|
|
221
|
-
|
|
314
|
+
export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[], options: ResolveOptions = {}): Resolution {
|
|
315
|
+
if (options.hops === false) {
|
|
316
|
+
const quick = resolveQuick(nav, ctx, path);
|
|
317
|
+
if (quick !== undefined) return quick;
|
|
318
|
+
}
|
|
319
|
+
const hops: Hop[] | undefined = options.hops === false ? undefined : [];
|
|
320
|
+
const done = hops ?? NO_HOPS;
|
|
321
|
+
const failed = (code: ResolveCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
|
|
222
322
|
let collapses = 0;
|
|
323
|
+
const adopt = nav.adopt ?? ((value: unknown): Thing => value);
|
|
223
324
|
|
|
224
325
|
const collapse = (thing: Thing): Resolution => {
|
|
225
326
|
let here = thing;
|
|
327
|
+
if (nav.plain === true) return { ok: true, thing: here, hops: done };
|
|
226
328
|
while (isRef(here)) {
|
|
227
329
|
collapses += 1;
|
|
228
|
-
if (collapses > MAX_COLLAPSE) return
|
|
330
|
+
if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", "The references form a cycle.");
|
|
229
331
|
const inner = walk(here.value);
|
|
230
332
|
if (!inner.ok) return inner;
|
|
231
|
-
here = inner.thing;
|
|
232
|
-
hops
|
|
333
|
+
here = adopt(inner.thing);
|
|
334
|
+
hops?.push({ step: "collapse", thing: here, by: "collapse" });
|
|
233
335
|
}
|
|
234
|
-
return { ok: true, thing: here, hops };
|
|
336
|
+
return { ok: true, thing: here, hops: done };
|
|
235
337
|
};
|
|
236
338
|
|
|
237
|
-
const walk = (steps: readonly
|
|
238
|
-
const
|
|
339
|
+
const walk = (steps: readonly unknown[]): Resolution => {
|
|
340
|
+
const root = steps[0];
|
|
239
341
|
if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
|
|
240
|
-
return
|
|
342
|
+
return failed("#REF!", `A path starts with a root: ${ROOTS.join(", ")}.`);
|
|
343
|
+
}
|
|
344
|
+
if (nav.roots !== undefined && !(nav.roots as readonly string[]).includes(root)) {
|
|
345
|
+
return failed("#REF!", `The root ${root} is not open here. The open roots are ${nav.roots.join(", ")}.`);
|
|
241
346
|
}
|
|
242
|
-
let here: Thing = ctx[root as Root]
|
|
243
|
-
hops
|
|
347
|
+
let here: Thing = adopt(ctx[root as Root]);
|
|
348
|
+
hops?.push({ step: root, thing: here, by: "root" });
|
|
244
349
|
const first = collapse(here);
|
|
245
350
|
if (!first.ok) return first;
|
|
246
351
|
here = first.thing;
|
|
247
|
-
for (
|
|
352
|
+
for (let index = 1; index < steps.length; index += 1) {
|
|
353
|
+
const step = steps[index];
|
|
354
|
+
if (isHostThing(here) && here.resolve !== undefined && steps.slice(index).every(isKey)) {
|
|
355
|
+
const keys = steps.slice(index) as readonly Key[];
|
|
356
|
+
const reached = here.resolve(keys);
|
|
357
|
+
if (reached === undefined) return failed("#N/A", `The path ${keys.join(".")} reaches nothing.`);
|
|
358
|
+
const thing = adopt(reached.thing);
|
|
359
|
+
hops?.push({ step: keys as readonly Json[], thing, by: "own" });
|
|
360
|
+
return collapse(thing);
|
|
361
|
+
}
|
|
248
362
|
if (isKey(step)) {
|
|
249
363
|
const reached = derefStep(nav, here, step);
|
|
250
|
-
if (reached === undefined)
|
|
251
|
-
|
|
252
|
-
|
|
364
|
+
if (reached === undefined) {
|
|
365
|
+
if (root === "names" && index === 1) return failed("#NAME?", `The context has no name ${String(step)}.`);
|
|
366
|
+
return failed("#N/A", `The step ${String(step)} reaches nothing.`);
|
|
367
|
+
}
|
|
368
|
+
here = adopt(reached.thing);
|
|
369
|
+
hops?.push({ step, thing: here, by: reached.by });
|
|
253
370
|
} else {
|
|
254
|
-
const envelope =
|
|
371
|
+
const envelope = isEnvelope(step) ? step : undefined;
|
|
255
372
|
const found = envelope === undefined ? undefined : lookupKind(nav.world, envelope.type);
|
|
256
373
|
const name = found?.ok === true ? entryOf(found.kind.verbs, "step") : undefined;
|
|
257
374
|
const walker = name === undefined ? undefined : nav.walkers?.[name];
|
|
258
375
|
if (envelope === undefined || walker === undefined) {
|
|
259
|
-
return
|
|
376
|
+
return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
|
|
260
377
|
}
|
|
261
378
|
const result = walker({
|
|
262
379
|
data: envelope.value,
|
|
263
380
|
here,
|
|
264
381
|
context: ctx,
|
|
265
|
-
deref: (thing, key) =>
|
|
382
|
+
deref: (thing, key) => {
|
|
383
|
+
const reached = derefStep(nav, thing, key);
|
|
384
|
+
return reached === undefined ? undefined : adopt(reached.thing);
|
|
385
|
+
},
|
|
266
386
|
});
|
|
267
|
-
if (!result.ok) return
|
|
268
|
-
|
|
269
|
-
here
|
|
387
|
+
if (!result.ok) return failed("#N/A", result.reason);
|
|
388
|
+
here = adopt(result.thing);
|
|
389
|
+
hops?.push({ step: envelope as Json, thing: here, by: "step" });
|
|
270
390
|
}
|
|
271
391
|
const next = collapse(here);
|
|
272
392
|
if (!next.ok) return next;
|
|
273
393
|
here = next.thing;
|
|
274
394
|
}
|
|
275
|
-
return { ok: true, thing: here, hops };
|
|
395
|
+
return { ok: true, thing: here, hops: done };
|
|
276
396
|
};
|
|
277
397
|
|
|
278
|
-
if (path.length === 0) return
|
|
398
|
+
if (path.length === 0) return failed("#REF!", "A path has at least one step: its root.");
|
|
279
399
|
return walk(path);
|
|
280
400
|
}
|
|
281
401
|
|
|
282
402
|
/**
|
|
283
|
-
* This function gives the value of a thing: the
|
|
403
|
+
* This function gives the value of a thing: the value that a host thing gives, or else the thing itself.
|
|
284
404
|
*
|
|
285
405
|
* @param thing - The thing.
|
|
286
406
|
* @returns The value.
|
|
287
407
|
*/
|
|
288
|
-
export function valueOfThing(thing: Thing):
|
|
408
|
+
export function valueOfThing(thing: Thing): unknown {
|
|
289
409
|
return isHostThing(thing) ? thing.toJson() : thing;
|
|
290
410
|
}
|