@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/README.md +41 -0
- package/build/index.d.ts +832 -148
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1898 -376
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +25 -21
- package/src/deref.ts +165 -54
- 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.
|
|
5
|
-
"description": "coeval:
|
|
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 = ["
|
|
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,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
|
-
/**
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
130
|
-
* of
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
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:
|
|
171
|
-
if (thing
|
|
172
|
-
const
|
|
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
|
-
|
|
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
|
|
221
|
-
|
|
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
|
|
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
|
|
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
|
|
238
|
-
const
|
|
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
|
|
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]
|
|
243
|
-
hops
|
|
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 (
|
|
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)
|
|
251
|
-
|
|
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 =
|
|
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
|
|
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
|
|
268
|
-
hops
|
|
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
|
|
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
|
|
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):
|
|
399
|
+
export function valueOfThing(thing: Thing): unknown {
|
|
289
400
|
return isHostThing(thing) ? thing.toJson() : thing;
|
|
290
401
|
}
|