@mark1russell7/coeval 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +40 -2
- package/build/index.d.ts +1600 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3378 -0
- package/build/index.js.map +1 -0
- package/package.json +49 -3
- package/src/classify.ts +54 -0
- package/src/context.ts +193 -0
- package/src/deref.ts +401 -0
- package/src/doc.ts +251 -0
- package/src/errors.ts +141 -0
- package/src/evaluate.ts +1069 -0
- package/src/formula.ts +409 -0
- package/src/index.ts +183 -0
- package/src/ladder.ts +141 -0
- package/src/order.ts +111 -0
- package/src/parts.ts +156 -0
- package/src/static.ts +364 -0
- package/src/stdlib.ts +310 -0
- package/src/world.ts +237 -0
package/src/classify.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Envelope, type Json, field, isRecord, typed } from "./doc.ts";
|
|
2
|
+
|
|
3
|
+
/** A shape proposes a kind for each document that has all of its fields. */
|
|
4
|
+
export interface Shape {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly fields: readonly string[];
|
|
7
|
+
readonly kind: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The result of a classification. */
|
|
11
|
+
export interface Classification {
|
|
12
|
+
/** The kind of the one most specific shape. */
|
|
13
|
+
readonly kind?: string;
|
|
14
|
+
/** The names of the most specific shapes. */
|
|
15
|
+
readonly candidates: readonly string[];
|
|
16
|
+
/** `no-match`: no shape applies. `ambiguous`: two or more shapes match equally well. */
|
|
17
|
+
readonly error?: "no-match" | "ambiguous";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isStrictSuperset(larger: readonly string[], smaller: readonly string[]): boolean {
|
|
21
|
+
return larger.length > smaller.length && smaller.every((name) => larger.includes(name));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* This function proposes a kind for a value without a type, from the fields that the value has.
|
|
26
|
+
* A shape applies when the value has each field of the shape. The most specific shapes are the ones that
|
|
27
|
+
* no other applicable shape contains. A host uses a classifier when data comes in, and then tags the data.
|
|
28
|
+
* The ladder does not call a classifier.
|
|
29
|
+
*
|
|
30
|
+
* @param shapes - The shapes to examine.
|
|
31
|
+
* @param value - The value without a type.
|
|
32
|
+
* @returns The proposed kind, or an error with the candidates.
|
|
33
|
+
*/
|
|
34
|
+
export function classify(shapes: readonly Shape[], value: Json): Classification {
|
|
35
|
+
const applicable = isRecord(value)
|
|
36
|
+
? shapes.filter((shape) => shape.fields.every((name) => field(value, name) !== undefined))
|
|
37
|
+
: [];
|
|
38
|
+
const best = applicable.filter((shape) => !applicable.some((other) => isStrictSuperset(other.fields, shape.fields)));
|
|
39
|
+
const candidates = best.map((shape) => shape.name);
|
|
40
|
+
const only = best.length === 1 ? best[0] : undefined;
|
|
41
|
+
if (only !== undefined) return { kind: only.kind, candidates };
|
|
42
|
+
return { candidates, error: best.length === 0 ? "no-match" : "ambiguous" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* This function puts a value in an envelope with a type. The value does not change.
|
|
47
|
+
*
|
|
48
|
+
* @param value - The value.
|
|
49
|
+
* @param kind - The name of the kind.
|
|
50
|
+
* @returns The envelope.
|
|
51
|
+
*/
|
|
52
|
+
export function tag(value: Json, kind: string): Envelope {
|
|
53
|
+
return typed(kind, value);
|
|
54
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { Envelope } from "./doc.ts";
|
|
2
|
+
import type { Side } from "./ladder.ts";
|
|
3
|
+
|
|
4
|
+
/** The parts of a context. A term can reach outside itself only through these parts. */
|
|
5
|
+
export const PARTS = ["self", "names", "world", "event", "caps"] as const;
|
|
6
|
+
|
|
7
|
+
/** One part of a context. */
|
|
8
|
+
export type Part = (typeof PARTS)[number];
|
|
9
|
+
|
|
10
|
+
/** The description of one part of a context. */
|
|
11
|
+
export interface PartInfo {
|
|
12
|
+
readonly part: Part;
|
|
13
|
+
readonly title: string;
|
|
14
|
+
/** What a reference through this part reaches. */
|
|
15
|
+
readonly reaches: string;
|
|
16
|
+
/** The direction in which a reference through this part goes. */
|
|
17
|
+
readonly direction: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The descriptions of the parts, in the order of `PARTS`. */
|
|
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
|
+
},
|
|
28
|
+
{
|
|
29
|
+
part: "names",
|
|
30
|
+
title: "Names",
|
|
31
|
+
reaches: "The values that a binding gives: a let, a parameter, or the result of an earlier step.",
|
|
32
|
+
direction: "Back, to the place where the program binds the name.",
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
part: "world",
|
|
37
|
+
title: "World",
|
|
38
|
+
reaches: "The stored documents and collections, by an absolute ID or by a query.",
|
|
39
|
+
direction: "Out, to the store.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
part: "event",
|
|
43
|
+
title: "Event",
|
|
44
|
+
reaches: "The trigger of a run: the event and its values.",
|
|
45
|
+
direction: "From the past, to the run that the event starts.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
part: "caps",
|
|
49
|
+
title: "Capabilities",
|
|
50
|
+
reaches: "The effects that a term can cause. A part of the term gets the same set or a smaller set.",
|
|
51
|
+
direction: "Down, and only smaller.",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What a construct gives the parts of its body, for one part of the context.
|
|
57
|
+
*
|
|
58
|
+
* - `same`: the parts get the same value.
|
|
59
|
+
* - `extends`: the parts get the value with more names.
|
|
60
|
+
* - `moves`: the parts get a new position.
|
|
61
|
+
* - `narrows`: the parts get a smaller set.
|
|
62
|
+
* - `sets`: the parts get a new value.
|
|
63
|
+
* - `none`: the parts get nothing.
|
|
64
|
+
*/
|
|
65
|
+
export type Give = "same" | "extends" | "moves" | "narrows" | "sets" | "none";
|
|
66
|
+
|
|
67
|
+
/** How a construct uses one part of the context. */
|
|
68
|
+
export interface Use {
|
|
69
|
+
readonly reads: boolean;
|
|
70
|
+
readonly gives: Give;
|
|
71
|
+
readonly note: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A construct of a language, and how it uses each part of the context. */
|
|
75
|
+
export interface Construct {
|
|
76
|
+
readonly id: string;
|
|
77
|
+
readonly title: string;
|
|
78
|
+
readonly side: Side;
|
|
79
|
+
readonly summary: string;
|
|
80
|
+
readonly example: Envelope;
|
|
81
|
+
readonly uses: Readonly<Record<Part, Use>>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const NONE: Use = { reads: false, gives: "none", note: "It does not use this part." };
|
|
85
|
+
|
|
86
|
+
/** Six constructs that show the range of the context. */
|
|
87
|
+
export const CONSTRUCTS: readonly Construct[] = [
|
|
88
|
+
{
|
|
89
|
+
id: "formula",
|
|
90
|
+
title: "Formula",
|
|
91
|
+
side: "derive",
|
|
92
|
+
summary: "A pure formula that gives a value at one position of a structure.",
|
|
93
|
+
example: {
|
|
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
|
+
},
|
|
99
|
+
},
|
|
100
|
+
uses: {
|
|
101
|
+
names: { reads: true, gives: "extends", note: "A let binds names for its body." },
|
|
102
|
+
self: { reads: true, gives: "moves", note: "A move reads the parent. An axis moves self to each target." },
|
|
103
|
+
world: NONE,
|
|
104
|
+
event: NONE,
|
|
105
|
+
caps: { reads: false, gives: "none", note: "A formula causes no effect, thus its parts get no capability." },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "sequence",
|
|
110
|
+
title: "Sequence",
|
|
111
|
+
side: "run",
|
|
112
|
+
summary: "A list of steps. A step can read the result of each earlier step by its name.",
|
|
113
|
+
example: {
|
|
114
|
+
type: "seq",
|
|
115
|
+
value: [
|
|
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 } } },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
uses: {
|
|
121
|
+
names: { reads: true, gives: "extends", note: "The result of each step is a name for the later steps." },
|
|
122
|
+
self: NONE,
|
|
123
|
+
world: { reads: true, gives: "same", note: "A step reads and writes the store through a procedure." },
|
|
124
|
+
event: { reads: false, gives: "same", note: "The steps get the event of the run without a change." },
|
|
125
|
+
caps: { reads: true, gives: "narrows", note: "A nested call gets the same capabilities or fewer." },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "node",
|
|
130
|
+
title: "Tree node",
|
|
131
|
+
side: "derive",
|
|
132
|
+
summary: "A node of a document tree. Its data and its box go down to its children.",
|
|
133
|
+
example: {
|
|
134
|
+
type: "node",
|
|
135
|
+
value: { data: { title: "Notes" }, children: [{ type: "text", value: { type: "ref", value: ["names", "title"] } }] },
|
|
136
|
+
},
|
|
137
|
+
uses: {
|
|
138
|
+
names: { reads: true, gives: "extends", note: "The data of the node becomes a name for its children." },
|
|
139
|
+
self: { reads: true, gives: "moves", note: "Each child becomes self for its own part." },
|
|
140
|
+
world: { reads: true, gives: "same", note: "A node can show stored documents." },
|
|
141
|
+
event: NONE,
|
|
142
|
+
caps: NONE,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "rule",
|
|
147
|
+
title: "Layout rule",
|
|
148
|
+
side: "derive",
|
|
149
|
+
summary: "A rule that gives the box of a child from the box of its parent.",
|
|
150
|
+
example: { type: ["layouts", "stack"], value: { gap: 8 } },
|
|
151
|
+
uses: {
|
|
152
|
+
names: { reads: true, gives: "none", note: "The rule reads its parameters." },
|
|
153
|
+
self: { reads: true, gives: "none", note: "The rule reads the box of the parent and the key of the child." },
|
|
154
|
+
world: NONE,
|
|
155
|
+
event: NONE,
|
|
156
|
+
caps: NONE,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "handler",
|
|
161
|
+
title: "Event handler",
|
|
162
|
+
side: "run",
|
|
163
|
+
summary: "A run that an event starts. It holds a sequence as data until the event comes.",
|
|
164
|
+
example: {
|
|
165
|
+
type: "on",
|
|
166
|
+
value: {
|
|
167
|
+
event: "click",
|
|
168
|
+
run: { type: "seq", value: [{ type: ["card", "toggle"], value: { id: { type: "ref", value: ["event", "target"] } } }] },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
uses: {
|
|
172
|
+
names: { reads: true, gives: "same", note: "The handler reads the names of its place." },
|
|
173
|
+
self: { reads: true, gives: "same", note: "The handler knows the node that got the event." },
|
|
174
|
+
world: { reads: true, gives: "same", note: "The run reads and writes the store." },
|
|
175
|
+
event: { reads: true, gives: "sets", note: "The handler gives the event to its run." },
|
|
176
|
+
caps: { reads: true, gives: "narrows", note: "The run gets only the capabilities of the handler." },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "query",
|
|
181
|
+
title: "Query",
|
|
182
|
+
side: "derive",
|
|
183
|
+
summary: "A selection of stored documents. A storage adapter decides where it runs.",
|
|
184
|
+
example: { type: "query", value: { collection: "cards", match: { board: { type: "ref", value: ["names", "board"] } } } },
|
|
185
|
+
uses: {
|
|
186
|
+
names: { reads: true, gives: "none", note: "The query reads its parameters." },
|
|
187
|
+
self: NONE,
|
|
188
|
+
world: { reads: true, gives: "none", note: "The query reads one collection." },
|
|
189
|
+
event: NONE,
|
|
190
|
+
caps: NONE,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
];
|
package/src/deref.ts
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
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";
|
|
3
|
+
|
|
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;
|
|
9
|
+
|
|
10
|
+
/**
|
|
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
|
|
12
|
+
* own entry of the ladder for the verb `deref`.
|
|
13
|
+
*/
|
|
14
|
+
export interface HostThing {
|
|
15
|
+
readonly [THING]: true;
|
|
16
|
+
/** A short text for a reader, for example `node card`. */
|
|
17
|
+
readonly label: string;
|
|
18
|
+
/**
|
|
19
|
+
* This function answers one step.
|
|
20
|
+
*
|
|
21
|
+
* @param key - The key of the step.
|
|
22
|
+
* @returns What the step reaches, or `undefined` when it reaches nothing.
|
|
23
|
+
*/
|
|
24
|
+
deref(key: Key): Thing | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* This function gives the thing as a value, when a reference ends on it.
|
|
27
|
+
*
|
|
28
|
+
* @returns The value.
|
|
29
|
+
*/
|
|
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;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Something that a path can reach: any value of the host, or a host thing. */
|
|
42
|
+
export type Thing = unknown;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function tells if a thing is a host thing.
|
|
46
|
+
*
|
|
47
|
+
* @param thing - The thing.
|
|
48
|
+
* @returns `true` for a host thing.
|
|
49
|
+
*/
|
|
50
|
+
export function isHostThing(thing: unknown): thing is HostThing {
|
|
51
|
+
return typeof thing === "object" && thing !== null && (thing as Partial<HostThing>)[THING] === true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The roots of a context. Each path starts with one of them. */
|
|
55
|
+
export const ROOTS = ["self", "names", "world", "event", "caps"] as const;
|
|
56
|
+
|
|
57
|
+
/** One root of a context. */
|
|
58
|
+
export type Root = (typeof ROOTS)[number];
|
|
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
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The context of a term: a record with five roots. A reference reaches outside its term only through them.
|
|
68
|
+
*
|
|
69
|
+
* - `self`: the node that the term is part of.
|
|
70
|
+
* - `names`: the values that the evaluators around the term bound.
|
|
71
|
+
* - `world`: the registry of kinds.
|
|
72
|
+
* - `event`: the event that started a run.
|
|
73
|
+
* - `caps`: the effects that the term can cause.
|
|
74
|
+
*/
|
|
75
|
+
export interface Context {
|
|
76
|
+
readonly self: Thing;
|
|
77
|
+
readonly names: Readonly<Record<string, unknown>>;
|
|
78
|
+
readonly world: Thing;
|
|
79
|
+
readonly event: Thing;
|
|
80
|
+
readonly caps: readonly Cap[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The code of a `deref` evaluator: it answers one step of a path on a document of its kind. */
|
|
84
|
+
export type DerefCode = (doc: Envelope, key: Key) => Thing | undefined;
|
|
85
|
+
|
|
86
|
+
/** The result of a step document: what it reached, or the reason of a failure. */
|
|
87
|
+
export type WalkResult = { readonly ok: true; readonly thing: Thing } | { readonly ok: false; readonly reason: string };
|
|
88
|
+
|
|
89
|
+
/** The input of a walker: the code of a step document, for example `nearest`. */
|
|
90
|
+
export interface WalkInput {
|
|
91
|
+
/** The value of the step document. */
|
|
92
|
+
readonly data: Json;
|
|
93
|
+
/** The thing that the path reached before this step. */
|
|
94
|
+
readonly here: Thing;
|
|
95
|
+
readonly context: Context;
|
|
96
|
+
/**
|
|
97
|
+
* This function answers one step on a thing, through the ladder.
|
|
98
|
+
*
|
|
99
|
+
* @param thing - The thing.
|
|
100
|
+
* @param key - The key.
|
|
101
|
+
* @returns What the step reaches, or `undefined`.
|
|
102
|
+
*/
|
|
103
|
+
deref(thing: Thing, key: Key): Thing | undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A walker: the code of the verb `step` of a kind of step documents. */
|
|
107
|
+
export type Walker = (input: WalkInput) => WalkResult;
|
|
108
|
+
|
|
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
|
+
*/
|
|
116
|
+
export interface Navigator {
|
|
117
|
+
readonly world: World;
|
|
118
|
+
readonly derefs?: Readonly<Record<string, DerefCode>>;
|
|
119
|
+
readonly walkers?: Readonly<Record<string, Walker>>;
|
|
120
|
+
readonly plain?: boolean;
|
|
121
|
+
readonly roots?: readonly Root[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The way that one hop of a path was answered. */
|
|
125
|
+
export type HopBy = "root" | "own" | "kind" | "default" | "step" | "collapse";
|
|
126
|
+
|
|
127
|
+
/** One hop of a path: the step, what it reached, and who answered it. */
|
|
128
|
+
export interface Hop {
|
|
129
|
+
readonly step: Json;
|
|
130
|
+
readonly thing: Thing;
|
|
131
|
+
readonly by: HopBy;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The codes of a failed resolution. */
|
|
135
|
+
export type ResolveCode = "#REF!" | "#NAME?" | "#N/A" | "#CYCLE!" | "#STEP?";
|
|
136
|
+
|
|
137
|
+
/** The result of a resolution: what the path reached, with each hop, or an error with the hops before it. */
|
|
138
|
+
export type Resolution =
|
|
139
|
+
| { readonly ok: true; readonly thing: Thing; 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
|
+
}
|
|
147
|
+
|
|
148
|
+
function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
|
|
149
|
+
return verbs !== undefined && Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
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 {
|
|
154
|
+
if (Array.isArray(thing)) {
|
|
155
|
+
if (key === "length") return { found: true, value: thing.length };
|
|
156
|
+
const index = typeof key === "number" ? key : /^\d+$/.test(key) ? Number(key) : -1;
|
|
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;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* This function answers one step of a path on a thing, through the ladder of the verb `deref`. A host thing
|
|
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.
|
|
170
|
+
*
|
|
171
|
+
* @param nav - The world and the code of the evaluators.
|
|
172
|
+
* @param thing - The thing that the path reached.
|
|
173
|
+
* @param key - The key of the step.
|
|
174
|
+
* @returns What the step reaches and the rung that answered, or `undefined` when it reaches nothing.
|
|
175
|
+
*/
|
|
176
|
+
export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly thing: Thing; readonly by: HopBy } | undefined {
|
|
177
|
+
if (isHostThing(thing)) {
|
|
178
|
+
const reached = thing.deref(key);
|
|
179
|
+
return reached === undefined ? undefined : { thing: reached, by: "own" };
|
|
180
|
+
}
|
|
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;
|
|
185
|
+
const ownCode = own === undefined ? undefined : nav.derefs?.[own];
|
|
186
|
+
if (ownCode !== undefined) {
|
|
187
|
+
const reached = ownCode(envelope, key);
|
|
188
|
+
return reached === undefined ? undefined : { thing: reached, by: "own" };
|
|
189
|
+
}
|
|
190
|
+
const found = lookupKind(nav.world, envelope.type);
|
|
191
|
+
const name = found.ok ? entryOf(found.kind.verbs, "deref") : undefined;
|
|
192
|
+
const code = name === undefined ? undefined : nav.derefs?.[name];
|
|
193
|
+
if (code !== undefined) {
|
|
194
|
+
const reached = code(envelope, key);
|
|
195
|
+
return reached === undefined ? undefined : { thing: reached, by: "kind" };
|
|
196
|
+
}
|
|
197
|
+
const reached = plainStep(envelope.value, key);
|
|
198
|
+
return reached === undefined ? undefined : { thing: reached.value, by: "default" };
|
|
199
|
+
}
|
|
200
|
+
const reached = plainStep(thing, key);
|
|
201
|
+
return reached === undefined ? undefined : { thing: reached.value, by: "default" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* This function tells if a thing is a reference document: an envelope of the type `ref` with a list for its path.
|
|
206
|
+
*
|
|
207
|
+
* @param thing - The thing.
|
|
208
|
+
* @returns `true` for a reference document.
|
|
209
|
+
*/
|
|
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);
|
|
213
|
+
return path !== undefined && path.length === 1 && path[0] === "ref";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const worlds = new WeakMap<World, Json>();
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* This function makes the thing of the root `world`: the entries of the world as JSON. An alias is a reference
|
|
220
|
+
* document in it, thus a walk through the world follows each alias (collapse). The thing of a world is made one
|
|
221
|
+
* time.
|
|
222
|
+
*
|
|
223
|
+
* @param world - The world.
|
|
224
|
+
* @returns The thing.
|
|
225
|
+
*/
|
|
226
|
+
export function worldThing(world: World): Json {
|
|
227
|
+
let thing = worlds.get(world);
|
|
228
|
+
if (thing === undefined) {
|
|
229
|
+
thing = entriesAsJson(world.kinds);
|
|
230
|
+
worlds.set(world, thing);
|
|
231
|
+
}
|
|
232
|
+
return thing;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* This function makes a context. The world comes from the navigator when the call gives none.
|
|
237
|
+
*
|
|
238
|
+
* @param parts - The roots to set.
|
|
239
|
+
* @returns The context.
|
|
240
|
+
*/
|
|
241
|
+
export function context(parts: Partial<Context> = {}): Context {
|
|
242
|
+
return {
|
|
243
|
+
self: parts.self ?? null,
|
|
244
|
+
names: parts.names ?? {},
|
|
245
|
+
world: parts.world ?? null,
|
|
246
|
+
event: parts.event ?? null,
|
|
247
|
+
caps: parts.caps ?? [],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
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
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* This function resolves a path from a context, and records each hop.
|
|
290
|
+
*
|
|
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
|
|
292
|
+
* of the verb `deref` on the thing that the path reached. A step document is evaluated by the walker of its kind,
|
|
293
|
+
* with the thing that the path reached. After each hop, a reference document is followed until the path reaches
|
|
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.
|
|
303
|
+
*
|
|
304
|
+
* @param nav - The world and the code of the evaluators.
|
|
305
|
+
* @param ctx - The context.
|
|
306
|
+
* @param path - The steps.
|
|
307
|
+
* @param options - `hops: false` does not record the hops.
|
|
308
|
+
* @returns What the path reached and each hop, or an error.
|
|
309
|
+
*/
|
|
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 });
|
|
318
|
+
let collapses = 0;
|
|
319
|
+
|
|
320
|
+
const collapse = (thing: Thing): Resolution => {
|
|
321
|
+
let here = thing;
|
|
322
|
+
if (nav.plain === true) return { ok: true, thing: here, hops: done };
|
|
323
|
+
while (isRef(here)) {
|
|
324
|
+
collapses += 1;
|
|
325
|
+
if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", "The references form a cycle.");
|
|
326
|
+
const inner = walk(here.value);
|
|
327
|
+
if (!inner.ok) return inner;
|
|
328
|
+
here = inner.thing;
|
|
329
|
+
hops?.push({ step: "collapse", thing: here, by: "collapse" });
|
|
330
|
+
}
|
|
331
|
+
return { ok: true, thing: here, hops: done };
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const walk = (steps: readonly unknown[]): Resolution => {
|
|
335
|
+
const root = steps[0];
|
|
336
|
+
if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
|
|
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(", ")}.`);
|
|
341
|
+
}
|
|
342
|
+
let here: Thing = ctx[root as Root];
|
|
343
|
+
hops?.push({ step: root, thing: here, by: "root" });
|
|
344
|
+
const first = collapse(here);
|
|
345
|
+
if (!first.ok) return first;
|
|
346
|
+
here = first.thing;
|
|
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
|
+
}
|
|
356
|
+
if (isKey(step)) {
|
|
357
|
+
const reached = derefStep(nav, here, step);
|
|
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 });
|
|
363
|
+
here = reached.thing;
|
|
364
|
+
} else {
|
|
365
|
+
const envelope = isEnvelope(step) ? step : undefined;
|
|
366
|
+
const found = envelope === undefined ? undefined : lookupKind(nav.world, envelope.type);
|
|
367
|
+
const name = found?.ok === true ? entryOf(found.kind.verbs, "step") : undefined;
|
|
368
|
+
const walker = name === undefined ? undefined : nav.walkers?.[name];
|
|
369
|
+
if (envelope === undefined || walker === undefined) {
|
|
370
|
+
return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
|
|
371
|
+
}
|
|
372
|
+
const result = walker({
|
|
373
|
+
data: envelope.value,
|
|
374
|
+
here,
|
|
375
|
+
context: ctx,
|
|
376
|
+
deref: (thing, key) => derefStep(nav, thing, key)?.thing,
|
|
377
|
+
});
|
|
378
|
+
if (!result.ok) return failed("#N/A", result.reason);
|
|
379
|
+
hops?.push({ step: envelope as Json, thing: result.thing, by: "step" });
|
|
380
|
+
here = result.thing;
|
|
381
|
+
}
|
|
382
|
+
const next = collapse(here);
|
|
383
|
+
if (!next.ok) return next;
|
|
384
|
+
here = next.thing;
|
|
385
|
+
}
|
|
386
|
+
return { ok: true, thing: here, hops: done };
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
if (path.length === 0) return failed("#REF!", "A path has at least one step: its root.");
|
|
390
|
+
return walk(path);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* This function gives the value of a thing: the value that a host thing gives, or else the thing itself.
|
|
395
|
+
*
|
|
396
|
+
* @param thing - The thing.
|
|
397
|
+
* @returns The value.
|
|
398
|
+
*/
|
|
399
|
+
export function valueOfThing(thing: Thing): unknown {
|
|
400
|
+
return isHostThing(thing) ? thing.toJson() : thing;
|
|
401
|
+
}
|