@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/static.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
|
+
import { type Kind, type NamesSource, type ScopeRule, type World, lookupKind } from "./world.ts";
|
|
3
|
+
import { ask } from "./ladder.ts";
|
|
4
|
+
import { type PartSlot, pathMatches, splitParts } from "./parts.ts";
|
|
5
|
+
import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
|
|
6
|
+
import type { ErrorCode } from "./errors.ts";
|
|
7
|
+
import { type Machine, permits } from "./evaluate.ts";
|
|
8
|
+
|
|
9
|
+
/** One document that a walk visits. */
|
|
10
|
+
export interface DocVisit {
|
|
11
|
+
/** The keys from the root document to this document, through the parts. It is the place of an error. */
|
|
12
|
+
readonly place: readonly Key[];
|
|
13
|
+
/** The keys from the root JSON value to this document. A change at this path changes this document. */
|
|
14
|
+
readonly at: readonly Key[];
|
|
15
|
+
/** The document. A bare value has the type that its position declares. */
|
|
16
|
+
readonly doc: Envelope;
|
|
17
|
+
/** `true` when the type came from the position and not from an envelope. */
|
|
18
|
+
readonly declared: boolean;
|
|
19
|
+
/** The kind of the document, when the world has it. */
|
|
20
|
+
readonly kind?: Kind;
|
|
21
|
+
/** The keys of the kind after each alias. */
|
|
22
|
+
readonly typePath?: readonly Key[];
|
|
23
|
+
/** The number of documents around this document. The root has 0. */
|
|
24
|
+
readonly depth: number;
|
|
25
|
+
/** The names that the kinds around this document bind for it. */
|
|
26
|
+
readonly names: ReadonlySet<string>;
|
|
27
|
+
/** The names that a sequence around this document binds only for the parts after it. */
|
|
28
|
+
readonly later: ReadonlySet<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A position that a walk cannot read as a document. */
|
|
32
|
+
export interface WalkProblem {
|
|
33
|
+
readonly place: readonly Key[];
|
|
34
|
+
readonly at: readonly Key[];
|
|
35
|
+
readonly code: "#PART?" | "#VALUE!";
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The options of a walk. */
|
|
40
|
+
export interface WalkOptions {
|
|
41
|
+
/** The type of a bare root value. */
|
|
42
|
+
readonly declaredType?: string;
|
|
43
|
+
/** The names that the host binds at the root. */
|
|
44
|
+
readonly names?: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* This function gets each position that is not a document.
|
|
47
|
+
*
|
|
48
|
+
* @param problem - The position and the reason.
|
|
49
|
+
*/
|
|
50
|
+
readonly problem?: (problem: WalkProblem) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function valueAt(value: unknown, path: readonly Key[]): unknown {
|
|
54
|
+
let here: unknown = value;
|
|
55
|
+
for (const key of path) {
|
|
56
|
+
if (Array.isArray(here)) here = here[Number(key)];
|
|
57
|
+
else if (isRecord(here) && Object.hasOwn(here, String(key))) here = here[String(key)];
|
|
58
|
+
else return undefined;
|
|
59
|
+
}
|
|
60
|
+
return here;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function headerName(item: Json): string | undefined {
|
|
64
|
+
return isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot[]): readonly string[] {
|
|
68
|
+
if ("fixed" in source) return source.fixed;
|
|
69
|
+
if ("keysAt" in source) {
|
|
70
|
+
const at = valueAt(value, source.keysAt);
|
|
71
|
+
return isRecord(at) ? Object.keys(at) : [];
|
|
72
|
+
}
|
|
73
|
+
if ("listAt" in source) {
|
|
74
|
+
const at = valueAt(value, source.listAt);
|
|
75
|
+
return Array.isArray(at) ? (at as readonly unknown[]).filter((item): item is string => typeof item === "string") : [];
|
|
76
|
+
}
|
|
77
|
+
if ("textAt" in source) {
|
|
78
|
+
const at = valueAt(value, source.textAt);
|
|
79
|
+
if (typeof at === "string") return [at];
|
|
80
|
+
return source.otherwise === undefined ? [] : [source.otherwise];
|
|
81
|
+
}
|
|
82
|
+
return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The names that the scope rules of a kind give each part, and the names that only later parts see. */
|
|
86
|
+
function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
|
|
87
|
+
const sees: string[][] = slots.map(() => []);
|
|
88
|
+
const later: string[][] = slots.map(() => []);
|
|
89
|
+
for (const rule of rules ?? []) {
|
|
90
|
+
const covered = slots.map((slot, index) => ({ slot, index })).filter(({ slot }) => rule.in.some((pattern) => pathMatches(slot.path, pattern)));
|
|
91
|
+
if (rule.sequential !== true) {
|
|
92
|
+
const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
|
|
93
|
+
for (const { index } of covered) sees[index]?.push(...names);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if ("headers" in rule.names) {
|
|
97
|
+
covered.forEach(({ index }, position) => {
|
|
98
|
+
const before = covered.slice(0, position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
99
|
+
const after = covered.slice(position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
100
|
+
sees[index]?.push(...before);
|
|
101
|
+
later[index]?.push(...after);
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
|
|
106
|
+
covered.forEach(({ index }, position) => {
|
|
107
|
+
if (position > 0) sees[index]?.push(...names);
|
|
108
|
+
else later[index]?.push(...names);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { sees, later };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* This function visits each document of a document: the root and each part at each depth, by the contracts of the
|
|
116
|
+
* kinds. A part of a kind that the world does not have is not visited. The walk uses no stack of the host for the
|
|
117
|
+
* depth, thus a deep document cannot overflow it.
|
|
118
|
+
*
|
|
119
|
+
* @param world - The world of the kinds.
|
|
120
|
+
* @param doc - The document, as JSON.
|
|
121
|
+
* @param visit - The function that gets each document. It gives `"skip"` to not visit the parts of a document.
|
|
122
|
+
* @param options - The type of a bare root value, the names of the host, and a function for the problems.
|
|
123
|
+
*/
|
|
124
|
+
export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => void | "skip", options: WalkOptions = {}): void {
|
|
125
|
+
interface Item {
|
|
126
|
+
readonly value: unknown;
|
|
127
|
+
readonly place: readonly Key[];
|
|
128
|
+
readonly at: readonly Key[];
|
|
129
|
+
readonly declared: string | undefined;
|
|
130
|
+
readonly depth: number;
|
|
131
|
+
readonly names: ReadonlySet<string>;
|
|
132
|
+
readonly later: ReadonlySet<string>;
|
|
133
|
+
}
|
|
134
|
+
const stack: Item[] = [
|
|
135
|
+
{ value: doc, place: [], at: [], declared: options.declaredType, depth: 0, names: new Set(options.names ?? []), later: new Set() },
|
|
136
|
+
];
|
|
137
|
+
while (stack.length > 0) {
|
|
138
|
+
const item = stack.pop() as Item;
|
|
139
|
+
const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
|
|
140
|
+
if (envelope === undefined && item.declared === undefined) {
|
|
141
|
+
options.problem?.({ place: item.place, at: item.at, code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
|
|
145
|
+
const found = lookupKind(world, doc.type);
|
|
146
|
+
const kind = found.ok ? found.kind : undefined;
|
|
147
|
+
const answer = visit({
|
|
148
|
+
place: item.place,
|
|
149
|
+
at: item.at,
|
|
150
|
+
doc,
|
|
151
|
+
declared: envelope === undefined,
|
|
152
|
+
...(kind === undefined ? {} : { kind, typePath: found.ok ? found.path : [] }),
|
|
153
|
+
depth: item.depth,
|
|
154
|
+
names: item.names,
|
|
155
|
+
later: item.later,
|
|
156
|
+
});
|
|
157
|
+
if (answer === "skip" || kind === undefined) continue;
|
|
158
|
+
const split = splitParts(kind.parts, doc.value);
|
|
159
|
+
if (!split.ok) {
|
|
160
|
+
const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
|
|
161
|
+
options.problem?.({ place: item.place, at: item.at, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
|
|
165
|
+
const base = envelope === undefined ? item.at : [...item.at, "value"];
|
|
166
|
+
for (let index = split.parts.length - 1; index >= 0; index -= 1) {
|
|
167
|
+
const slot = split.parts[index] as PartSlot;
|
|
168
|
+
const extra = sees[index] ?? [];
|
|
169
|
+
const hidden = later[index] ?? [];
|
|
170
|
+
stack.push({
|
|
171
|
+
value: slot.item,
|
|
172
|
+
place: [...item.place, ...slot.path],
|
|
173
|
+
at: [...base, ...slot.path],
|
|
174
|
+
declared: slot.of,
|
|
175
|
+
depth: item.depth + 1,
|
|
176
|
+
names: extra.length === 0 ? item.names : new Set([...item.names, ...extra]),
|
|
177
|
+
later: hidden.length === 0 ? item.later : new Set([...item.later, ...hidden]),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** One reference of a document. */
|
|
184
|
+
export interface RefUse {
|
|
185
|
+
/** The place of the reference document, through the parts. */
|
|
186
|
+
readonly place: readonly Key[];
|
|
187
|
+
/** The keys from the root JSON value to the reference document. */
|
|
188
|
+
readonly at: readonly Key[];
|
|
189
|
+
/** The path of the reference: its root and its steps. */
|
|
190
|
+
readonly path: readonly Json[];
|
|
191
|
+
readonly root: string;
|
|
192
|
+
/** `false` when a kind around the reference binds its first name. A reference to another root is free. */
|
|
193
|
+
readonly free: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* This function finds each reference of a document: each document of the type `ref` that a walk reaches. A
|
|
198
|
+
* reference to a name that a kind around it binds is not free. The others are what the document reads from
|
|
199
|
+
* outside: its dependencies.
|
|
200
|
+
*
|
|
201
|
+
* @param world - The world of the kinds.
|
|
202
|
+
* @param doc - The document, as JSON.
|
|
203
|
+
* @param options - The type of a bare root value. The names of the host do not change the result.
|
|
204
|
+
* @returns The references, in the order of the document.
|
|
205
|
+
*/
|
|
206
|
+
export function refsOf(world: World, doc: unknown, options: WalkOptions = {}): readonly RefUse[] {
|
|
207
|
+
const out: RefUse[] = [];
|
|
208
|
+
// The walk gets no names of the host: a name that the host binds is a dependency, thus its reference is free.
|
|
209
|
+
const { names: _host, ...rest } = options;
|
|
210
|
+
walk(
|
|
211
|
+
world,
|
|
212
|
+
doc,
|
|
213
|
+
(v) => {
|
|
214
|
+
if (!isRef(v.doc)) return undefined;
|
|
215
|
+
const path = v.doc.value as readonly Json[];
|
|
216
|
+
const root = typeof path[0] === "string" ? path[0] : "";
|
|
217
|
+
const first = path[1];
|
|
218
|
+
const bound = root === "names" && isKey(first) && v.names.has(String(first));
|
|
219
|
+
out.push({ place: v.place, at: v.at, path, root, free: !bound });
|
|
220
|
+
return "skip";
|
|
221
|
+
},
|
|
222
|
+
rest,
|
|
223
|
+
);
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setAt(value: unknown, path: readonly Key[], next: unknown): unknown {
|
|
228
|
+
if (path.length === 0) return next;
|
|
229
|
+
const [key, ...rest] = path as [Key, ...Key[]];
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
const copy = [...value];
|
|
232
|
+
copy[Number(key)] = setAt(value[Number(key)], rest, next);
|
|
233
|
+
return copy;
|
|
234
|
+
}
|
|
235
|
+
const record = (isRecord(value) ? value : {}) as Readonly<Record<string, unknown>>;
|
|
236
|
+
const copy: Record<string, unknown> = {};
|
|
237
|
+
for (const name of Object.keys(record)) Object.defineProperty(copy, name, { value: record[name], enumerable: true, writable: true, configurable: true });
|
|
238
|
+
Object.defineProperty(copy, String(key), { value: setAt(record[String(key)], rest, next), enumerable: true, writable: true, configurable: true });
|
|
239
|
+
return copy;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* This function changes the free references of a document. The document is not changed: the function gives a new
|
|
244
|
+
* document that shares each part without a change.
|
|
245
|
+
*
|
|
246
|
+
* @param world - The world of the kinds.
|
|
247
|
+
* @param doc - The document, as JSON.
|
|
248
|
+
* @param change - The function that gives the new document of a reference, or `undefined` to keep it.
|
|
249
|
+
* @param options - The type of a bare root value, and the names that the host binds at the root.
|
|
250
|
+
* @returns The new document.
|
|
251
|
+
*/
|
|
252
|
+
export function mapRefs(world: World, doc: unknown, change: (use: RefUse) => unknown, options: WalkOptions = {}): unknown {
|
|
253
|
+
let out = doc;
|
|
254
|
+
for (const use of refsOf(world, doc, options)) {
|
|
255
|
+
if (!use.free) continue;
|
|
256
|
+
const next = change(use);
|
|
257
|
+
if (next !== undefined) out = setAt(out, use.at, next);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A problem that a check finds before a document runs. */
|
|
263
|
+
export interface Issue {
|
|
264
|
+
readonly code: ErrorCode;
|
|
265
|
+
/** The keys from the root document to the document with the problem. */
|
|
266
|
+
readonly place: readonly Key[];
|
|
267
|
+
readonly reason: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The options of a check. */
|
|
271
|
+
export interface CheckOptions {
|
|
272
|
+
/** The type of a bare root value. */
|
|
273
|
+
readonly declaredType?: string;
|
|
274
|
+
/** The names that the host binds at the root. `"any"` does not check the names. The default is no names. */
|
|
275
|
+
readonly names?: readonly string[] | "any";
|
|
276
|
+
/** The capabilities at the root. Without them, the check does not check the effects. */
|
|
277
|
+
readonly caps?: readonly Cap[];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* This function checks a document before an evaluation, by the contracts of its kinds. It finds the problems
|
|
282
|
+
* that an evaluation finds when it reaches them:
|
|
283
|
+
*
|
|
284
|
+
* - `#KIND?`: a kind that the world does not have.
|
|
285
|
+
* - `#NOEVAL`: no evaluator.
|
|
286
|
+
* - `#VALUE!`: a value without the shape of its kind.
|
|
287
|
+
* - `#PART?`: a value without a type.
|
|
288
|
+
* - `#DEPTH!`: too much nesting.
|
|
289
|
+
* - `#REF!` and `#STEP?`: a bad path.
|
|
290
|
+
* - `#NAME?`: a name that nothing binds.
|
|
291
|
+
* - `#CAP!`: an effect that the capabilities do not permit.
|
|
292
|
+
*
|
|
293
|
+
* @param machine - The world and the code of the evaluators.
|
|
294
|
+
* @param doc - The document, as JSON.
|
|
295
|
+
* @param options - The type of a bare root value, the names of the host and the capabilities.
|
|
296
|
+
* @returns The problems, in the order of the document.
|
|
297
|
+
*/
|
|
298
|
+
export function check(machine: Machine, doc: unknown, options: CheckOptions = {}): readonly Issue[] {
|
|
299
|
+
const issues: Issue[] = [];
|
|
300
|
+
const maxDepth = machine.maxDepth ?? 1000;
|
|
301
|
+
const anyName = options.names === "any";
|
|
302
|
+
const roots: readonly string[] = machine.roots ?? ROOTS;
|
|
303
|
+
const has = (table: Readonly<Record<string, unknown>> | undefined, name: string): boolean => table !== undefined && Object.hasOwn(table, name);
|
|
304
|
+
walk(
|
|
305
|
+
machine.world,
|
|
306
|
+
doc,
|
|
307
|
+
(v) => {
|
|
308
|
+
if (v.depth > maxDepth) {
|
|
309
|
+
issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.` });
|
|
310
|
+
return "skip";
|
|
311
|
+
}
|
|
312
|
+
const header = v.declared ? {} : v.doc;
|
|
313
|
+
const answer = ask(machine.world, header, "eval", v.declared ? { declaredType: typeName(v.doc.type) } : {});
|
|
314
|
+
if (answer.error === "unknown-kind") {
|
|
315
|
+
issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.` });
|
|
316
|
+
return "skip";
|
|
317
|
+
}
|
|
318
|
+
const name = answer.evaluator;
|
|
319
|
+
if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
|
|
320
|
+
issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.` });
|
|
321
|
+
}
|
|
322
|
+
if (options.caps !== undefined && v.kind?.effects !== undefined) {
|
|
323
|
+
const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
|
|
324
|
+
for (const kind of kinds) {
|
|
325
|
+
if (!permits(options.caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: `The capabilities do not permit the effect ${kind.join(".")}.` });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!isRef(v.doc)) return undefined;
|
|
329
|
+
const path = v.doc.value as readonly Json[];
|
|
330
|
+
const root = path[0];
|
|
331
|
+
if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
|
|
332
|
+
issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.` });
|
|
333
|
+
return "skip";
|
|
334
|
+
}
|
|
335
|
+
if (!roots.includes(root)) {
|
|
336
|
+
issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.` });
|
|
337
|
+
}
|
|
338
|
+
const first = path[1];
|
|
339
|
+
if (root === ("names" as Root) && !anyName && isKey(first) && !v.names.has(String(first))) {
|
|
340
|
+
const later = v.later.has(String(first));
|
|
341
|
+
issues.push({
|
|
342
|
+
code: "#NAME?",
|
|
343
|
+
place: v.place,
|
|
344
|
+
reason: later ? `The name ${String(first)} is bound by a later step.` : `Nothing binds the name ${String(first)}.`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
path.slice(1).forEach((step) => {
|
|
348
|
+
if (isKey(step)) return;
|
|
349
|
+
const found = isEnvelope(step) ? lookupKind(machine.world, step.type) : undefined;
|
|
350
|
+
const walker = found?.ok === true && Object.hasOwn(found.kind.verbs, "step") ? found.kind.verbs["step"] : undefined;
|
|
351
|
+
if (walker === undefined || !has(machine.walkers, walker)) {
|
|
352
|
+
issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step." });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return "skip";
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
359
|
+
...(Array.isArray(options.names) ? { names: options.names } : {}),
|
|
360
|
+
problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason }),
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
return issues;
|
|
364
|
+
}
|
package/src/stdlib.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { type Json, type Key, field, isRecord } from "./doc.ts";
|
|
2
|
+
import { type Context, type Evaluator, type Machine, type Part, type Result, type RunPart, type Runner, type Steps, fail, ok } from "./evaluate.ts";
|
|
3
|
+
import { type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
|
|
4
|
+
import type { World } from "./ladder.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The world of the examples: small kinds for numbers, lists, conditions, names, sequences and effects.
|
|
8
|
+
* Each kind names an evaluator for `eval` and a view for `view`. The site draws each document with its view.
|
|
9
|
+
*/
|
|
10
|
+
export const STD_WORLD: World = {
|
|
11
|
+
kinds: {
|
|
12
|
+
number: { verbs: { eval: "data.number", view: "view.value" }, summary: "A number. Its value is the result." },
|
|
13
|
+
text: { verbs: { eval: "data.text", view: "view.value" }, summary: "A text. Its value is the result." },
|
|
14
|
+
bool: { verbs: { eval: "data.bool", view: "view.value" }, summary: "A truth value. Its value is the result." },
|
|
15
|
+
add: {
|
|
16
|
+
verbs: { eval: "math.add", view: "view.op" },
|
|
17
|
+
parts: { shape: "list", of: "number" },
|
|
18
|
+
summary: "The sum of its parts. A bare number in its list gets the type number.",
|
|
19
|
+
},
|
|
20
|
+
multiply: {
|
|
21
|
+
verbs: { eval: "math.multiply", view: "view.op" },
|
|
22
|
+
parts: { shape: "list", of: "number" },
|
|
23
|
+
summary: "The product of its parts.",
|
|
24
|
+
},
|
|
25
|
+
greater: {
|
|
26
|
+
verbs: { eval: "math.greater", view: "view.op" },
|
|
27
|
+
parts: { shape: "list", of: "number" },
|
|
28
|
+
summary: "True when the first part is greater than the second part.",
|
|
29
|
+
},
|
|
30
|
+
list: { verbs: { eval: "list.all", view: "view.op" }, parts: { shape: "list" }, summary: "The results of its parts, as a list." },
|
|
31
|
+
if: {
|
|
32
|
+
verbs: { eval: "control.if", view: "view.op" },
|
|
33
|
+
parts: { shape: "record", fields: ["test", "then", "else"] },
|
|
34
|
+
summary: "It evaluates the test, then only the branch that the test selects.",
|
|
35
|
+
},
|
|
36
|
+
ref: {
|
|
37
|
+
verbs: { eval: "ref.resolve", view: "view.ref" },
|
|
38
|
+
summary: "A path from a root of the context. Each step goes up the ladder of the verb deref.",
|
|
39
|
+
},
|
|
40
|
+
nearest: {
|
|
41
|
+
verbs: { step: "walk.nearest", view: "view.card" },
|
|
42
|
+
summary: "A step of a path: it follows a key again and again, until it reaches a node of a type.",
|
|
43
|
+
},
|
|
44
|
+
panel: {
|
|
45
|
+
verbs: { eval: "tree.node", view: "view.op" },
|
|
46
|
+
parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
|
|
47
|
+
summary: "A node of a tree. Its parts see it as self, and each node links to its parent.",
|
|
48
|
+
},
|
|
49
|
+
box: {
|
|
50
|
+
verbs: { eval: "tree.node", view: "view.op" },
|
|
51
|
+
parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
|
|
52
|
+
summary: "A node of a tree, as panel. A formula in a box can read the parent of the box.",
|
|
53
|
+
},
|
|
54
|
+
math: {
|
|
55
|
+
kinds: {
|
|
56
|
+
sum: { type: "ref", value: ["world", "add"] },
|
|
57
|
+
product: { type: "ref", value: ["world", "multiply"] },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
seq: {
|
|
61
|
+
verbs: { eval: "control.seq", view: "view.seq" },
|
|
62
|
+
parts: { shape: "list" },
|
|
63
|
+
scope: [
|
|
64
|
+
{ names: { headers: true }, in: [["*"]], sequential: true },
|
|
65
|
+
{ names: { fixed: ["last"] }, in: [["*"]], sequential: true },
|
|
66
|
+
],
|
|
67
|
+
summary: "Its steps, in order. The result of a named step is a name for the later steps.",
|
|
68
|
+
},
|
|
69
|
+
pure: {
|
|
70
|
+
verbs: { eval: "control.pure", view: "view.op" },
|
|
71
|
+
parts: { shape: "list" },
|
|
72
|
+
summary: "It evaluates its part without capabilities, thus an effect in it fails.",
|
|
73
|
+
},
|
|
74
|
+
log: {
|
|
75
|
+
verbs: { eval: "effect.log", view: "view.op" },
|
|
76
|
+
parts: { shape: "list", of: "text" },
|
|
77
|
+
side: "run",
|
|
78
|
+
summary: "It writes the result of its part to the log. This is an effect.",
|
|
79
|
+
},
|
|
80
|
+
write: {
|
|
81
|
+
verbs: { eval: "effect.write", view: "view.op" },
|
|
82
|
+
parts: { shape: "record", fields: ["value"] },
|
|
83
|
+
side: "run",
|
|
84
|
+
summary: "It writes the result of its part to the store, at its key. This is an effect.",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
defaults: { view: "view.card" },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function isNumber(value: unknown): value is number {
|
|
91
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** This function evaluates each part, collects the errors, and gives the values when no part failed. */
|
|
95
|
+
function allValues(results: readonly Result[]): { readonly values: readonly unknown[] } | { readonly error: Result } {
|
|
96
|
+
const errors = results.filter((result) => !result.ok);
|
|
97
|
+
const first = errors[0];
|
|
98
|
+
if (first !== undefined && errors.length === 1) return { error: first };
|
|
99
|
+
if (errors.length > 1) return { error: fail("#ARGS", `${errors.length} parts failed.`) };
|
|
100
|
+
return { values: results.map((result) => (result.ok ? result.value : null)) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function numbers(values: readonly unknown[], kind: string): readonly number[] | Result {
|
|
104
|
+
const bad = values.findIndex((value) => !isNumber(value));
|
|
105
|
+
if (bad !== -1) return fail("#VALUE!", `Part ${bad} of ${kind} is not a number.`);
|
|
106
|
+
return values.filter(isNumber);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function dataOf(type: "number" | "string" | "boolean"): Evaluator {
|
|
110
|
+
return ({ data }) =>
|
|
111
|
+
typeof data === type && (type !== "number" || isNumber(data))
|
|
112
|
+
? ok(data)
|
|
113
|
+
: fail("#VALUE!", `The value must be a ${type === "string" ? "text" : type}.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The code of the evaluators of the standard world, by name. */
|
|
117
|
+
export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
|
|
118
|
+
"data.number": dataOf("number"),
|
|
119
|
+
"data.text": dataOf("string"),
|
|
120
|
+
"data.bool": dataOf("boolean"),
|
|
121
|
+
"math.add": ({ parts }) => {
|
|
122
|
+
const all = allValues(parts.map((part) => part.evaluate()));
|
|
123
|
+
if ("error" in all) return all.error;
|
|
124
|
+
const values = numbers(all.values, "add");
|
|
125
|
+
return Array.isArray(values) ? ok(values.reduce((sum, value) => sum + value, 0)) : (values as Result);
|
|
126
|
+
},
|
|
127
|
+
"math.multiply": ({ parts }) => {
|
|
128
|
+
const all = allValues(parts.map((part) => part.evaluate()));
|
|
129
|
+
if ("error" in all) return all.error;
|
|
130
|
+
const values = numbers(all.values, "multiply");
|
|
131
|
+
return Array.isArray(values) ? ok(values.reduce((product, value) => product * value, 1)) : (values as Result);
|
|
132
|
+
},
|
|
133
|
+
"math.greater": ({ parts }) => {
|
|
134
|
+
if (parts.length !== 2) return fail("#VALUE!", "greater needs exactly two parts.");
|
|
135
|
+
const all = allValues(parts.map((part) => part.evaluate()));
|
|
136
|
+
if ("error" in all) return all.error;
|
|
137
|
+
const values = numbers(all.values, "greater");
|
|
138
|
+
if (!Array.isArray(values)) return values as Result;
|
|
139
|
+
const [a, b] = values as readonly number[];
|
|
140
|
+
return ok((a ?? 0) > (b ?? 0));
|
|
141
|
+
},
|
|
142
|
+
"ref.resolve": ({ data, resolve }) => {
|
|
143
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
144
|
+
return fail("#VALUE!", "The value of ref is a path: a list that starts with a root, for example [\"names\", \"a\"].");
|
|
145
|
+
}
|
|
146
|
+
const found = resolve(data as readonly Json[]);
|
|
147
|
+
return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
|
|
148
|
+
},
|
|
149
|
+
"tree.node": (input) => treeNode(input.parts, input.context, input.kind),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* This function evaluates a node of a tree. The node is a host thing. A step `parent` reaches the node that holds
|
|
154
|
+
* it. A step with the name of a part evaluates that part on demand, one time. Each part sees the node as self.
|
|
155
|
+
*/
|
|
156
|
+
function treeNode(parts: readonly Part[], context: Context, type: string): Result {
|
|
157
|
+
const memo = new Map<string, Result>();
|
|
158
|
+
const read = (key: string): Result | undefined => {
|
|
159
|
+
const known = memo.get(key);
|
|
160
|
+
if (known !== undefined) return known;
|
|
161
|
+
const part = parts.find((item) => item.key === key);
|
|
162
|
+
if (part === undefined) return undefined;
|
|
163
|
+
const result = part.evaluate({ ...context, self: me });
|
|
164
|
+
memo.set(key, result);
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
const me: HostThing & { readonly type: string } = {
|
|
168
|
+
[THING]: true,
|
|
169
|
+
type,
|
|
170
|
+
label: `node ${type}`,
|
|
171
|
+
deref(key: Key): Thing | undefined {
|
|
172
|
+
if (key === "parent") return context.self;
|
|
173
|
+
if (key === "type") return type;
|
|
174
|
+
const result = read(String(key));
|
|
175
|
+
return result?.ok === true ? result.value : undefined;
|
|
176
|
+
},
|
|
177
|
+
toJson(): unknown {
|
|
178
|
+
const out: Record<string, unknown> = {};
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
const result = read(part.key);
|
|
181
|
+
out[part.key] = result?.ok === true ? result.value : null;
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
const out: Record<string, unknown> = {};
|
|
187
|
+
for (const part of parts) {
|
|
188
|
+
const result = read(part.key);
|
|
189
|
+
if (result === undefined) continue;
|
|
190
|
+
if (!result.ok) return result;
|
|
191
|
+
out[part.key] = result.value;
|
|
192
|
+
}
|
|
193
|
+
return ok(out);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The options of a walker `nearest`. */
|
|
197
|
+
export interface NearestOptions {
|
|
198
|
+
/**
|
|
199
|
+
* This function tells if a node has the wanted type. The default compares the field `type` of a host thing.
|
|
200
|
+
*
|
|
201
|
+
* @param node - The node that the walk reached.
|
|
202
|
+
* @param wanted - The type that the step document asks for.
|
|
203
|
+
* @returns `true` when the node has the type.
|
|
204
|
+
*/
|
|
205
|
+
readonly matches?: (node: Thing, wanted: Json | undefined) => boolean;
|
|
206
|
+
/** The most hops of one walk. The default is 256. */
|
|
207
|
+
readonly maxHops?: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hostType(node: Thing, wanted: Json | undefined): boolean {
|
|
211
|
+
return isHostThing(node) && (node as { readonly type?: unknown }).type === wanted;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* This function makes a walker `nearest`. The walker follows a key (the default is `parent`) until the node that it
|
|
216
|
+
* reaches has the wanted type. With `self: true`, the node where the walk starts is the first candidate. A node
|
|
217
|
+
* without that key ends the walk with an error.
|
|
218
|
+
*
|
|
219
|
+
* @param options - The test of a type and the most hops.
|
|
220
|
+
* @returns The walker.
|
|
221
|
+
*/
|
|
222
|
+
export function nearestWalker(options: NearestOptions = {}): Walker {
|
|
223
|
+
const matches = options.matches ?? hostType;
|
|
224
|
+
const maxHops = options.maxHops ?? 256;
|
|
225
|
+
return ({ data, here, deref }) => {
|
|
226
|
+
const follow = isRecord(data) && typeof field(data, "follow") === "string" ? String(field(data, "follow")) : "parent";
|
|
227
|
+
const wanted = isRecord(data) ? field(data, "type") : undefined;
|
|
228
|
+
const fromSelf = isRecord(data) && field(data, "self") === true;
|
|
229
|
+
let at: Thing | undefined = here;
|
|
230
|
+
for (let hops = 0; hops < maxHops; hops += 1) {
|
|
231
|
+
if (hops > 0 || !fromSelf) at = at === undefined || at === null ? undefined : deref(at, follow);
|
|
232
|
+
if (at === undefined || at === null) return { ok: false, reason: `No node of the type ${String(wanted)} is on the way.` };
|
|
233
|
+
if (matches(at, wanted)) return { ok: true, thing: at };
|
|
234
|
+
}
|
|
235
|
+
return { ok: false, reason: "The walk is too long." };
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const nearest: Walker = nearestWalker();
|
|
240
|
+
|
|
241
|
+
/** The walkers of the standard world, by name. */
|
|
242
|
+
export const STD_WALKERS: Readonly<Record<string, Walker>> = { "walk.nearest": nearest };
|
|
243
|
+
|
|
244
|
+
/** This function evaluates parts one after the other, and stops at the first error. */
|
|
245
|
+
function* inOrder(parts: readonly RunPart[]): Generator<Promise<unknown>, { readonly values: readonly unknown[] } | { readonly error: Result }, unknown> {
|
|
246
|
+
const values: unknown[] = [];
|
|
247
|
+
for (const part of parts) {
|
|
248
|
+
const result = yield* part.evaluate();
|
|
249
|
+
if (!result.ok) return { error: result };
|
|
250
|
+
values.push(result.value);
|
|
251
|
+
}
|
|
252
|
+
return { values };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The runners of the standard world, by name: the kinds that can hold effects. Each one evaluates its parts in
|
|
257
|
+
* order. In a run, an effect ends before the next step starts.
|
|
258
|
+
*/
|
|
259
|
+
export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
260
|
+
"control.seq": function* ({ parts, context }): Steps {
|
|
261
|
+
let names: Record<string, unknown> = { ...context.names };
|
|
262
|
+
let last: unknown = null;
|
|
263
|
+
for (const part of parts) {
|
|
264
|
+
const result = yield* part.evaluate({ ...context, names });
|
|
265
|
+
if (!result.ok) return result;
|
|
266
|
+
names = { ...names, last: result.value };
|
|
267
|
+
if (part.name !== undefined) names = { ...names, [part.name]: result.value };
|
|
268
|
+
last = result.value;
|
|
269
|
+
}
|
|
270
|
+
return ok(last);
|
|
271
|
+
},
|
|
272
|
+
"control.pure": function* ({ parts, context }) {
|
|
273
|
+
const only = parts[0];
|
|
274
|
+
if (only === undefined) return fail("#VALUE!", "pure needs one part.");
|
|
275
|
+
return yield* only.evaluate({ ...context, caps: [] });
|
|
276
|
+
},
|
|
277
|
+
"control.if": function* ({ parts }) {
|
|
278
|
+
const test = parts.find((part) => part.key === "test");
|
|
279
|
+
if (test === undefined) return fail("#VALUE!", "if needs a test.");
|
|
280
|
+
const chosen = yield* test.evaluate();
|
|
281
|
+
if (!chosen.ok) return chosen;
|
|
282
|
+
if (typeof chosen.value !== "boolean") return fail("#VALUE!", "The test of if must give a truth value.");
|
|
283
|
+
const branch = parts.find((part) => part.key === (chosen.value === true ? "then" : "else"));
|
|
284
|
+
return branch === undefined ? ok(null) : yield* branch.evaluate();
|
|
285
|
+
},
|
|
286
|
+
"list.all": function* ({ parts }) {
|
|
287
|
+
const all = yield* inOrder(parts);
|
|
288
|
+
return "error" in all ? all.error : ok(all.values);
|
|
289
|
+
},
|
|
290
|
+
"effect.log": function* ({ parts, effect }) {
|
|
291
|
+
const all = yield* inOrder(parts);
|
|
292
|
+
if ("error" in all) return all.error;
|
|
293
|
+
return yield* effect("log", all.values.length === 1 ? (all.values[0] ?? null) : all.values);
|
|
294
|
+
},
|
|
295
|
+
"effect.write": function* ({ parts, data, effect }) {
|
|
296
|
+
const key = isRecord(data) ? field(data, "key") : undefined;
|
|
297
|
+
if (typeof key !== "string") return fail("#VALUE!", "write needs a text key.");
|
|
298
|
+
const value = parts.find((part) => part.key === "value");
|
|
299
|
+
if (value === undefined) return fail("#VALUE!", "write needs a value.");
|
|
300
|
+
const result = yield* value.evaluate();
|
|
301
|
+
if (!result.ok) return result;
|
|
302
|
+
return yield* effect("write", { key, value: result.value });
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** The machine of the examples: the standard world, its evaluators, its runners and its walkers. */
|
|
307
|
+
export const STD_MACHINE: Machine = { world: STD_WORLD, evaluators: STD_EVALUATORS, runners: STD_RUNNERS, walkers: STD_WALKERS };
|
|
308
|
+
|
|
309
|
+
/** The capabilities of the examples at the root: the two effects of the standard world. */
|
|
310
|
+
export const STD_CAPS: readonly string[] = ["log", "write"];
|