@mark1russell7/coeval 0.0.0-stage → 0.1.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/src/deref.ts ADDED
@@ -0,0 +1,290 @@
1
+ import { type Envelope, type Json, type Key, field, fromJson, isKey, isRecord, typePath } from "./doc.ts";
2
+ import { type World, entriesAsJson, lookupKind } from "./world.ts";
3
+
4
+ /** The brand of a host thing: an object of the host that answers the steps of a path itself. */
5
+ export const THING: unique symbol = Symbol("coeval.thing");
6
+
7
+ /**
8
+ * A host thing: an object that answers the steps of a path itself, for example a node of a live tree. It is the
9
+ * own entry of the ladder for the verb `deref`.
10
+ */
11
+ export interface HostThing {
12
+ readonly [THING]: true;
13
+ /** A short text for a reader, for example `node card`. */
14
+ readonly label: string;
15
+ /**
16
+ * This function answers one step.
17
+ *
18
+ * @param key - The key of the step.
19
+ * @returns What the step reaches, or `undefined`.
20
+ */
21
+ deref(key: Key): Thing | undefined;
22
+ /**
23
+ * This function gives the thing as a value, when a reference ends on it.
24
+ *
25
+ * @returns The value.
26
+ */
27
+ toJson(): Json;
28
+ }
29
+
30
+ /** Something that a path can reach: a JSON value, or a host thing. */
31
+ export type Thing = Json | HostThing;
32
+
33
+ /**
34
+ * This function tells if a thing is a host thing.
35
+ *
36
+ * @param thing - The thing.
37
+ * @returns `true` for a host thing.
38
+ */
39
+ export function isHostThing(thing: Thing | undefined): thing is HostThing {
40
+ return typeof thing === "object" && thing !== null && !Array.isArray(thing) && (thing as Partial<HostThing>)[THING] === true;
41
+ }
42
+
43
+ /** The roots of a context. Each path starts with one of them. */
44
+ export const ROOTS = ["self", "names", "world", "event", "caps"] as const;
45
+
46
+ /** One root of a context. */
47
+ export type Root = (typeof ROOTS)[number];
48
+
49
+ /**
50
+ * The context of a term: a record with five roots. A reference reaches outside its term only through them.
51
+ *
52
+ * - `self`: the node that the term is part of.
53
+ * - `names`: the values that the evaluators around the term bound.
54
+ * - `world`: the registry of kinds.
55
+ * - `event`: the event that started a run.
56
+ * - `caps`: the effects that the term can cause.
57
+ */
58
+ export interface Context {
59
+ readonly self: Thing;
60
+ readonly names: Readonly<Record<string, Json>>;
61
+ readonly world: Thing;
62
+ readonly event: Thing;
63
+ readonly caps: readonly string[];
64
+ }
65
+
66
+ /** The code of a `deref` evaluator: it answers one step of a path on a document of its kind. */
67
+ export type DerefCode = (doc: Envelope, key: Key) => Thing | undefined;
68
+
69
+ /** The result of a step document: what it reached, or the reason of a failure. */
70
+ export type WalkResult = { readonly ok: true; readonly thing: Thing } | { readonly ok: false; readonly reason: string };
71
+
72
+ /** The input of a walker: the code of a step document, for example `nearest`. */
73
+ export interface WalkInput {
74
+ /** The value of the step document. */
75
+ readonly data: Json;
76
+ /** The thing that the path reached before this step. */
77
+ readonly here: Thing;
78
+ readonly context: Context;
79
+ /**
80
+ * This function answers one step on a thing, through the ladder.
81
+ *
82
+ * @param thing - The thing.
83
+ * @param key - The key.
84
+ * @returns What the step reaches, or `undefined`.
85
+ */
86
+ deref(thing: Thing, key: Key): Thing | undefined;
87
+ }
88
+
89
+ /** A walker: the code of the verb `step` of a kind of step documents. */
90
+ export type Walker = (input: WalkInput) => WalkResult;
91
+
92
+ /** What a resolution needs: the world, and the code of the `deref` evaluators and of the walkers. */
93
+ export interface Navigator {
94
+ readonly world: World;
95
+ readonly derefs?: Readonly<Record<string, DerefCode>>;
96
+ readonly walkers?: Readonly<Record<string, Walker>>;
97
+ }
98
+
99
+ /** The way that one hop of a path was answered. */
100
+ export type HopBy = "root" | "own" | "kind" | "default" | "step" | "collapse";
101
+
102
+ /** One hop of a path: the step, what it reached, and who answered it. */
103
+ export interface Hop {
104
+ readonly step: Json;
105
+ readonly thing: Thing;
106
+ readonly by: HopBy;
107
+ }
108
+
109
+ /** The result of a resolution: what the path reached, with each hop, or an error with the hops before it. */
110
+ export type Resolution =
111
+ | { readonly ok: true; readonly thing: Thing; readonly hops: readonly Hop[] }
112
+ | { readonly ok: false; readonly code: "#REF!" | "#CYCLE!" | "#STEP?"; readonly reason: string; readonly hops: readonly Hop[] };
113
+
114
+ function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
115
+ return verbs !== undefined && Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
116
+ }
117
+
118
+ function plainStep(thing: Json, key: Key): Json | undefined {
119
+ if (Array.isArray(thing)) {
120
+ const index = typeof key === "number" ? key : /^\d+$/.test(key) ? Number(key) : -1;
121
+ return index >= 0 ? (thing as readonly Json[])[index] : undefined;
122
+ }
123
+ if (isRecord(thing) && typeof key === "string") return field(thing, key);
124
+ return undefined;
125
+ }
126
+
127
+ /**
128
+ * This function answers one step of a path on a thing, through the ladder of the verb `deref`. A host thing
129
+ * answers for itself (own). A document asks its own entry, then its kind. The default goes into the value: a field
130
+ * of a record, or an item of a list.
131
+ *
132
+ * @param nav - The world and the code of the evaluators.
133
+ * @param thing - The thing that the path reached.
134
+ * @param key - The key of the step.
135
+ * @returns What the step reaches and the rung that answered, or `undefined`.
136
+ */
137
+ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly thing: Thing; readonly by: HopBy } | undefined {
138
+ if (isHostThing(thing)) {
139
+ const reached = thing.deref(key);
140
+ return reached === undefined ? undefined : { thing: reached, by: "own" };
141
+ }
142
+ const envelope = fromJson(thing);
143
+ if (envelope !== undefined) {
144
+ const own = entryOf(envelope.own, "deref");
145
+ const ownCode = own === undefined ? undefined : nav.derefs?.[own];
146
+ if (ownCode !== undefined) {
147
+ const reached = ownCode(envelope, key);
148
+ return reached === undefined ? undefined : { thing: reached, by: "own" };
149
+ }
150
+ const found = lookupKind(nav.world, envelope.type);
151
+ const name = found.ok ? entryOf(found.kind.verbs, "deref") : undefined;
152
+ const code = name === undefined ? undefined : nav.derefs?.[name];
153
+ if (code !== undefined) {
154
+ const reached = code(envelope, key);
155
+ return reached === undefined ? undefined : { thing: reached, by: "kind" };
156
+ }
157
+ const reached = plainStep(envelope.value, key);
158
+ return reached === undefined ? undefined : { thing: reached, by: "default" };
159
+ }
160
+ const reached = plainStep(thing, key);
161
+ return reached === undefined ? undefined : { thing: reached, by: "default" };
162
+ }
163
+
164
+ /**
165
+ * This function tells if a thing is a reference document: an envelope of the type `ref`.
166
+ *
167
+ * @param thing - The thing.
168
+ * @returns `true` for a reference document.
169
+ */
170
+ export function isRef(thing: Thing | undefined): thing is Envelope & { readonly value: readonly Json[] } {
171
+ if (thing === undefined || isHostThing(thing)) return false;
172
+ const envelope = fromJson(thing);
173
+ if (envelope === undefined || !Array.isArray(envelope.value)) return false;
174
+ const path = typePath(envelope.type);
175
+ return path !== undefined && path.length === 1 && path[0] === "ref";
176
+ }
177
+
178
+ /**
179
+ * 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).
181
+ *
182
+ * @param world - The world.
183
+ * @returns The thing.
184
+ */
185
+ export function worldThing(world: World): Json {
186
+ return entriesAsJson(world.kinds);
187
+ }
188
+
189
+ /**
190
+ * This function makes a context. The world comes from the navigator when the call gives none.
191
+ *
192
+ * @param parts - The roots to set.
193
+ * @returns The context.
194
+ */
195
+ export function context(parts: Partial<Context> = {}): Context {
196
+ return {
197
+ self: parts.self ?? null,
198
+ names: parts.names ?? {},
199
+ world: parts.world ?? null,
200
+ event: parts.event ?? null,
201
+ caps: parts.caps ?? [],
202
+ };
203
+ }
204
+
205
+ const MAX_COLLAPSE = 64;
206
+
207
+ /**
208
+ * This function resolves a path from a context, and records each hop.
209
+ *
210
+ * 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
+ * of the verb `deref` on the thing that the path reached. A step document is evaluated by the walker of its kind,
212
+ * with the thing that the path reached. After each hop, a reference document is followed until the path reaches
213
+ * something else (collapse).
214
+ *
215
+ * @param nav - The world and the code of the evaluators.
216
+ * @param ctx - The context.
217
+ * @param path - The steps.
218
+ * @returns What the path reached and each hop, or an error.
219
+ */
220
+ export function resolve(nav: Navigator, ctx: Context, path: readonly Json[]): Resolution {
221
+ const hops: Hop[] = [];
222
+ let collapses = 0;
223
+
224
+ const collapse = (thing: Thing): Resolution => {
225
+ let here = thing;
226
+ while (isRef(here)) {
227
+ collapses += 1;
228
+ if (collapses > MAX_COLLAPSE) return { ok: false, code: "#CYCLE!", reason: "The references form a cycle.", hops };
229
+ const inner = walk(here.value);
230
+ if (!inner.ok) return inner;
231
+ here = inner.thing;
232
+ hops.push({ step: here === null ? null : "collapse", thing: here, by: "collapse" });
233
+ }
234
+ return { ok: true, thing: here, hops };
235
+ };
236
+
237
+ const walk = (steps: readonly Json[]): Resolution => {
238
+ const [root, ...rest] = steps;
239
+ if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
240
+ return { ok: false, code: "#REF!", reason: `A path starts with a root: ${ROOTS.join(", ")}.`, hops };
241
+ }
242
+ let here: Thing = ctx[root as Root] as Thing;
243
+ hops.push({ step: root, thing: here, by: "root" });
244
+ const first = collapse(here);
245
+ if (!first.ok) return first;
246
+ here = first.thing;
247
+ for (const step of rest) {
248
+ if (isKey(step)) {
249
+ const reached = derefStep(nav, here, step);
250
+ if (reached === undefined) return { ok: false, code: "#REF!", reason: `The step ${String(step)} reaches nothing.`, hops };
251
+ hops.push({ step, thing: reached.thing, by: reached.by });
252
+ here = reached.thing;
253
+ } else {
254
+ const envelope = fromJson(step);
255
+ const found = envelope === undefined ? undefined : lookupKind(nav.world, envelope.type);
256
+ const name = found?.ok === true ? entryOf(found.kind.verbs, "step") : undefined;
257
+ const walker = name === undefined ? undefined : nav.walkers?.[name];
258
+ if (envelope === undefined || walker === undefined) {
259
+ return { ok: false, code: "#STEP?", reason: "A step is a key, or a document of a kind with the verb step.", hops };
260
+ }
261
+ const result = walker({
262
+ data: envelope.value,
263
+ here,
264
+ context: ctx,
265
+ deref: (thing, key) => derefStep(nav, thing, key)?.thing,
266
+ });
267
+ if (!result.ok) return { ok: false, code: "#REF!", reason: result.reason, hops };
268
+ hops.push({ step, thing: result.thing, by: "step" });
269
+ here = result.thing;
270
+ }
271
+ const next = collapse(here);
272
+ if (!next.ok) return next;
273
+ here = next.thing;
274
+ }
275
+ return { ok: true, thing: here, hops };
276
+ };
277
+
278
+ if (path.length === 0) return { ok: false, code: "#REF!", reason: "A path has at least one step: its root.", hops };
279
+ return walk(path);
280
+ }
281
+
282
+ /**
283
+ * This function gives the value of a thing: the thing itself for JSON, or the value that a host thing gives.
284
+ *
285
+ * @param thing - The thing.
286
+ * @returns The value.
287
+ */
288
+ export function valueOfThing(thing: Thing): Json {
289
+ return isHostThing(thing) ? thing.toJson() : thing;
290
+ }
package/src/doc.ts ADDED
@@ -0,0 +1,182 @@
1
+ /** A JSON value. */
2
+ export type Json = null | boolean | number | string | readonly Json[] | { readonly [key: string]: Json };
3
+
4
+ /** A JSON record. */
5
+ export type JsonRecord = { readonly [key: string]: Json };
6
+
7
+ /** One step of a path that is a plain key: a name, or an index of a list. */
8
+ export type Key = string | number;
9
+
10
+ /**
11
+ * The type of a document is a reference into the world (the registry of kinds).
12
+ *
13
+ * - A text is a base reference with one key: `"add"`.
14
+ * - A list of keys is a base reference with a path: `["git", "push"]`.
15
+ * - A document of type `ref` is the same reference, expanded. Its path starts at the root `world`.
16
+ *
17
+ * The kernel resolves a base reference itself. The expanded form is necessary only to attach more to the
18
+ * reference. Thus the chain of types stops at a base reference.
19
+ */
20
+ export type TypeRef = string | readonly Key[] | { readonly type: "ref"; readonly value: readonly Json[] };
21
+
22
+ /**
23
+ * A typed document: an envelope with a header and a value. The header names the type of the value. The value is
24
+ * not changed by the envelope at any time. Thus any JSON value can have a type: a number, a list, a text or a record.
25
+ */
26
+ export type Envelope = {
27
+ /** The type: a reference to a kind of the world. */
28
+ readonly type: TypeRef;
29
+ /** The value. */
30
+ readonly value: Json;
31
+ /** The name of a binding. A sequence gives the result of this document to the later steps under this name. */
32
+ readonly name?: string;
33
+ /** The own entries: one evaluator name for each verb. An own entry changes only this document. */
34
+ readonly own?: Readonly<Record<string, string>>;
35
+ };
36
+
37
+ /** The keys that an envelope can have. */
38
+ export const ENVELOPE_KEYS = ["type", "value", "name", "own"] as const;
39
+
40
+ /**
41
+ * This function tells if a value is a JSON record (not an array and not null).
42
+ *
43
+ * @param value - The value to examine.
44
+ * @returns `true` for a record.
45
+ */
46
+ export function isRecord(value: Json | undefined): value is JsonRecord {
47
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48
+ }
49
+
50
+ /**
51
+ * This function reads a field of a record. It reads only the own fields, thus `constructor` is not a field.
52
+ *
53
+ * @param record - The record.
54
+ * @param key - The name of the field.
55
+ * @returns The value of the field, or `undefined` when the record has no such field.
56
+ */
57
+ export function field(record: JsonRecord, key: string): Json | undefined {
58
+ return Object.hasOwn(record, key) ? record[key] : undefined;
59
+ }
60
+
61
+ /**
62
+ * This function tells if a value is a key of a path: a text, or a whole number from 0.
63
+ *
64
+ * @param value - The value to examine.
65
+ * @returns `true` for a key.
66
+ */
67
+ export function isKey(value: Json | undefined): value is Key {
68
+ return typeof value === "string" || (typeof value === "number" && Number.isInteger(value) && value >= 0);
69
+ }
70
+
71
+ /**
72
+ * This function tells if a value is a reference to a type. The reference is a text, a list of keys, or a document
73
+ * of type `ref` whose path starts at the root `world`.
74
+ *
75
+ * @param value - The value to examine.
76
+ * @returns `true` for a reference to a type.
77
+ */
78
+ export function isTypeRef(value: Json | undefined): boolean {
79
+ if (typeof value === "string") return value !== "";
80
+ if (Array.isArray(value)) return value.length > 0 && (value as readonly Json[]).every(isKey);
81
+ if (!isRecord(value)) return false;
82
+ const path = field(value, "value");
83
+ return field(value, "type") === "ref" && Array.isArray(path) && (path as readonly Json[])[0] === "world";
84
+ }
85
+
86
+ /**
87
+ * This function gives the path of a reference to a type, from the root of the world.
88
+ *
89
+ * @param type - The reference.
90
+ * @returns The keys from the root of the world, or `undefined` for a computed step that has no fixed key.
91
+ */
92
+ export function typePath(type: TypeRef): readonly Key[] | undefined {
93
+ if (typeof type === "string") return [type];
94
+ if (Array.isArray(type)) return type as readonly Key[];
95
+ const steps = (type as { readonly value: readonly Json[] }).value.slice(1);
96
+ return steps.every(isKey) ? (steps as readonly Key[]) : undefined;
97
+ }
98
+
99
+ /**
100
+ * This function gives the text of a type for a reader: its keys, joined with a dot.
101
+ *
102
+ * @param type - The reference to the type.
103
+ * @returns The text, for example `git.push`.
104
+ */
105
+ export function typeName(type: TypeRef): string {
106
+ return typePath(type)?.join(".") ?? "(computed)";
107
+ }
108
+
109
+ /**
110
+ * This function tells if a value has the form of an envelope. That form is a record with a reference to a type
111
+ * and a `value`. It has no other keys than the keys of an envelope. A contract decides where an envelope is
112
+ * expected. This function only checks the form.
113
+ *
114
+ * @param value - The value to examine.
115
+ * @returns `true` when the value has the form of an envelope.
116
+ */
117
+ export function isEnvelope(value: Json | undefined): value is JsonRecord & Envelope {
118
+ if (!isRecord(value)) return false;
119
+ if (!isTypeRef(field(value, "type")) || !Object.hasOwn(value, "value")) return false;
120
+ const name = field(value, "name");
121
+ if (name !== undefined && typeof name !== "string") return false;
122
+ const own = field(value, "own");
123
+ if (own !== undefined && !isRecord(own)) return false;
124
+ return Object.keys(value).every((key) => (ENVELOPE_KEYS as readonly string[]).includes(key));
125
+ }
126
+
127
+ /**
128
+ * This function makes an envelope.
129
+ *
130
+ * @param type - The reference to the kind.
131
+ * @param value - The value.
132
+ * @param header - The optional name and own entries.
133
+ * @returns The envelope.
134
+ */
135
+ export function typed(type: TypeRef, value: Json, header: { readonly name?: string; readonly own?: Record<string, string> } = {}): Envelope {
136
+ return { type, value, ...(header.name === undefined ? {} : { name: header.name }), ...(header.own === undefined ? {} : { own: header.own }) };
137
+ }
138
+
139
+ /**
140
+ * This function makes a reference document: a path from a root of the context.
141
+ *
142
+ * @param path - The steps. The first step is a root: `self`, `names`, `world`, `event` or `caps`.
143
+ * @returns The document of type `ref`.
144
+ */
145
+ export function ref(...path: readonly Json[]): Envelope {
146
+ return typed("ref", path);
147
+ }
148
+
149
+ /**
150
+ * This function gives an envelope as a JSON record, for a store or for a view.
151
+ *
152
+ * @param doc - The envelope.
153
+ * @returns The same envelope as JSON.
154
+ */
155
+ export function toJson(doc: Envelope): JsonRecord {
156
+ return {
157
+ type: doc.type as Json,
158
+ value: doc.value,
159
+ ...(doc.name === undefined ? {} : { name: doc.name }),
160
+ ...(doc.own === undefined ? {} : { own: doc.own }),
161
+ };
162
+ }
163
+
164
+ /**
165
+ * This function reads an envelope from JSON. A value without the form of an envelope gives `undefined`.
166
+ *
167
+ * @param value - The JSON value.
168
+ * @returns The envelope, or `undefined`.
169
+ */
170
+ export function fromJson(value: Json | undefined): Envelope | undefined {
171
+ if (!isEnvelope(value)) return undefined;
172
+ const own = field(value, "own");
173
+ const ownEntries: Record<string, string> = {};
174
+ if (isRecord(own)) {
175
+ for (const [verb, name] of Object.entries(own)) if (typeof name === "string") ownEntries[verb] = name;
176
+ }
177
+ const name = field(value, "name");
178
+ return typed(field(value, "type") as TypeRef, field(value, "value") ?? null, {
179
+ ...(typeof name === "string" ? { name } : {}),
180
+ ...(isRecord(own) ? { own: ownEntries } : {}),
181
+ });
182
+ }