@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/src/stdlib.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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, fail, ok } from "./evaluate.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
3
|
import { type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
|
|
4
4
|
import type { World } from "./ladder.ts";
|
|
5
5
|
|
|
@@ -60,6 +60,10 @@ export const STD_WORLD: World = {
|
|
|
60
60
|
seq: {
|
|
61
61
|
verbs: { eval: "control.seq", view: "view.seq" },
|
|
62
62
|
parts: { shape: "list" },
|
|
63
|
+
scope: [
|
|
64
|
+
{ names: { headers: true }, in: [["*"]], sequential: true },
|
|
65
|
+
{ names: { fixed: ["last"] }, in: [["*"]], sequential: true },
|
|
66
|
+
],
|
|
63
67
|
summary: "Its steps, in order. The result of a named step is a name for the later steps.",
|
|
64
68
|
},
|
|
65
69
|
pure: {
|
|
@@ -83,12 +87,12 @@ export const STD_WORLD: World = {
|
|
|
83
87
|
defaults: { view: "view.card" },
|
|
84
88
|
};
|
|
85
89
|
|
|
86
|
-
function isNumber(value:
|
|
90
|
+
function isNumber(value: unknown): value is number {
|
|
87
91
|
return typeof value === "number" && Number.isFinite(value);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
/** This function evaluates each part, collects the errors, and gives the values when no part failed. */
|
|
91
|
-
function allValues(results: readonly Result[]): { readonly values: readonly
|
|
95
|
+
function allValues(results: readonly Result[]): { readonly values: readonly unknown[] } | { readonly error: Result } {
|
|
92
96
|
const errors = results.filter((result) => !result.ok);
|
|
93
97
|
const first = errors[0];
|
|
94
98
|
if (first !== undefined && errors.length === 1) return { error: first };
|
|
@@ -96,7 +100,7 @@ function allValues(results: readonly Result[]): { readonly values: readonly Json
|
|
|
96
100
|
return { values: results.map((result) => (result.ok ? result.value : null)) };
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
function numbers(values: readonly
|
|
103
|
+
function numbers(values: readonly unknown[], kind: string): readonly number[] | Result {
|
|
100
104
|
const bad = values.findIndex((value) => !isNumber(value));
|
|
101
105
|
if (bad !== -1) return fail("#VALUE!", `Part ${bad} of ${kind} is not a number.`);
|
|
102
106
|
return values.filter(isNumber);
|
|
@@ -139,11 +143,8 @@ export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
|
|
|
139
143
|
if (!Array.isArray(data) || data.length === 0) {
|
|
140
144
|
return fail("#VALUE!", "The value of ref is a path: a list that starts with a root, for example [\"names\", \"a\"].");
|
|
141
145
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
if (found.ok) return ok(valueOfThing(found.thing));
|
|
145
|
-
if (path[0] === "names" && found.hops.length === 1) return fail("#NAME?", `The context has no name ${String(path[1])}.`);
|
|
146
|
-
return fail(found.code, found.reason);
|
|
146
|
+
const found = resolve(data as readonly Json[]);
|
|
147
|
+
return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
|
|
147
148
|
},
|
|
148
149
|
"tree.node": (input) => treeNode(input.parts, input.context, input.kind),
|
|
149
150
|
};
|
|
@@ -173,8 +174,8 @@ function treeNode(parts: readonly Part[], context: Context, type: string): Resul
|
|
|
173
174
|
const result = read(String(key));
|
|
174
175
|
return result?.ok === true ? result.value : undefined;
|
|
175
176
|
},
|
|
176
|
-
toJson():
|
|
177
|
-
const out: Record<string,
|
|
177
|
+
toJson(): unknown {
|
|
178
|
+
const out: Record<string, unknown> = {};
|
|
178
179
|
for (const part of parts) {
|
|
179
180
|
const result = read(part.key);
|
|
180
181
|
out[part.key] = result?.ok === true ? result.value : null;
|
|
@@ -182,7 +183,7 @@ function treeNode(parts: readonly Part[], context: Context, type: string): Resul
|
|
|
182
183
|
return out;
|
|
183
184
|
},
|
|
184
185
|
};
|
|
185
|
-
const out: Record<string,
|
|
186
|
+
const out: Record<string, unknown> = {};
|
|
186
187
|
for (const part of parts) {
|
|
187
188
|
const result = read(part.key);
|
|
188
189
|
if (result === undefined) continue;
|
|
@@ -192,31 +193,57 @@ function treeNode(parts: readonly Part[], context: Context, type: string): Resul
|
|
|
192
193
|
return ok(out);
|
|
193
194
|
}
|
|
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
|
+
|
|
195
214
|
/**
|
|
196
|
-
*
|
|
197
|
-
* wanted type. With `self: true`, the node where the walk starts is the first candidate. A node
|
|
198
|
-
* ends the walk with an error.
|
|
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.
|
|
199
221
|
*/
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
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();
|
|
213
240
|
|
|
214
241
|
/** The walkers of the standard world, by name. */
|
|
215
242
|
export const STD_WALKERS: Readonly<Record<string, Walker>> = { "walk.nearest": nearest };
|
|
216
243
|
|
|
217
244
|
/** This function evaluates parts one after the other, and stops at the first error. */
|
|
218
|
-
function* inOrder(parts: readonly RunPart[]): Generator<Promise<
|
|
219
|
-
const values:
|
|
245
|
+
function* inOrder(parts: readonly RunPart[]): Generator<Promise<unknown>, { readonly values: readonly unknown[] } | { readonly error: Result }, unknown> {
|
|
246
|
+
const values: unknown[] = [];
|
|
220
247
|
for (const part of parts) {
|
|
221
248
|
const result = yield* part.evaluate();
|
|
222
249
|
if (!result.ok) return { error: result };
|
|
@@ -230,9 +257,9 @@ function* inOrder(parts: readonly RunPart[]): Generator<Promise<Result>, { reado
|
|
|
230
257
|
* order. In a run, an effect ends before the next step starts.
|
|
231
258
|
*/
|
|
232
259
|
export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
233
|
-
"control.seq": function* ({ parts, context }) {
|
|
234
|
-
let names: Record<string,
|
|
235
|
-
let last:
|
|
260
|
+
"control.seq": function* ({ parts, context }): Steps {
|
|
261
|
+
let names: Record<string, unknown> = { ...context.names };
|
|
262
|
+
let last: unknown = null;
|
|
236
263
|
for (const part of parts) {
|
|
237
264
|
const result = yield* part.evaluate({ ...context, names });
|
|
238
265
|
if (!result.ok) return result;
|
package/src/world.ts
CHANGED
|
@@ -4,33 +4,84 @@ import { type Envelope, type Json, type Key, type TypeRef, field, isRecord, type
|
|
|
4
4
|
export type Side = "derive" | "run";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Where the parts of a value are. A part is a typed document that the evaluator gets
|
|
7
|
+
* Where the parts of a value are: the shape of the value. A part is a typed document that the evaluator gets
|
|
8
|
+
* sealed. `of` is the type of a bare part: a part without an envelope gets this type from its position.
|
|
8
9
|
*
|
|
9
10
|
* - `none`: the value is data. It has no parts.
|
|
10
|
-
* - `
|
|
11
|
-
* - `
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
11
|
+
* - `term`: the value itself is one part.
|
|
12
|
+
* - `list`: the value is a list. Each item has the shape `item` (the default is a part).
|
|
13
|
+
* - `map`: the value is a record. Each field has the shape `item` (the default is a part).
|
|
14
|
+
* - `record`: the value is a record. Each named field has its shape. A list of names makes each named field a
|
|
15
|
+
* part. The other fields have the shape `rest` (the default is data). `required` names the fields that must be.
|
|
16
|
+
* - `deep`: each envelope at any depth of the value is a part, and the rest is data.
|
|
14
17
|
*/
|
|
15
|
-
export type
|
|
18
|
+
export type Shape =
|
|
16
19
|
| { readonly shape: "none" }
|
|
17
|
-
| { readonly shape: "
|
|
18
|
-
| { readonly shape: "
|
|
20
|
+
| { readonly shape: "term"; readonly of?: string }
|
|
21
|
+
| { readonly shape: "list"; readonly of?: string; readonly item?: Shape }
|
|
22
|
+
| { readonly shape: "map"; readonly of?: string; readonly item?: Shape }
|
|
23
|
+
| {
|
|
24
|
+
readonly shape: "record";
|
|
25
|
+
readonly fields: readonly string[] | Readonly<Record<string, Shape>>;
|
|
26
|
+
readonly of?: string;
|
|
27
|
+
readonly required?: readonly string[];
|
|
28
|
+
readonly rest?: Shape;
|
|
29
|
+
}
|
|
30
|
+
| { readonly shape: "deep"; readonly of?: string };
|
|
31
|
+
|
|
32
|
+
/** The former name of `Shape`. */
|
|
33
|
+
export type PartsSpec = Shape;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where the names of a scope rule come from.
|
|
37
|
+
*
|
|
38
|
+
* - `fixed`: the names in the list.
|
|
39
|
+
* - `keysAt`: the keys of the record at this path of the value, for example the bindings of a `let`.
|
|
40
|
+
* - `listAt`: the texts of the list at this path of the value, for example the parameters of a `fn`.
|
|
41
|
+
* - `textAt`: the text at this path of the value, or `otherwise` when the value has no text there.
|
|
42
|
+
* - `headers`: the binding name (`name`) of each part that the rule covers.
|
|
43
|
+
*/
|
|
44
|
+
export type NamesSource =
|
|
45
|
+
| { readonly fixed: readonly string[] }
|
|
46
|
+
| { readonly keysAt: readonly Key[] }
|
|
47
|
+
| { readonly listAt: readonly Key[] }
|
|
48
|
+
| { readonly textAt: readonly Key[]; readonly otherwise?: string }
|
|
49
|
+
| { readonly headers: true };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A scope rule of a kind: names that the kind binds, and the parts that see them. The static kit reads the rules,
|
|
53
|
+
* and the evaluator of the kind binds the same names when it evaluates the parts.
|
|
54
|
+
*/
|
|
55
|
+
export interface ScopeRule {
|
|
56
|
+
readonly names: NamesSource;
|
|
57
|
+
/** The parts that see the names, as patterns of their paths in the value. The key `"*"` matches each key. */
|
|
58
|
+
readonly in: readonly (readonly Key[])[];
|
|
59
|
+
/** `true`: a part sees only the names of the parts before it, as in a sequence. */
|
|
60
|
+
readonly sequential?: boolean;
|
|
61
|
+
}
|
|
19
62
|
|
|
20
63
|
/** A kind gives the name of an evaluator for each verb, and the contract of its value. */
|
|
21
64
|
export interface Kind {
|
|
22
65
|
readonly verbs: Readonly<Record<string, string>>;
|
|
23
66
|
/** Where the parts of the value are. The default is `none`. */
|
|
24
|
-
readonly parts?:
|
|
67
|
+
readonly parts?: Shape;
|
|
68
|
+
/** The names that the kind binds for its parts. */
|
|
69
|
+
readonly scope?: readonly ScopeRule[];
|
|
25
70
|
/** The side of the kind. A kind of the run side causes effects. The default is `derive`. */
|
|
26
71
|
readonly side?: Side;
|
|
72
|
+
/**
|
|
73
|
+
* The effects that a document of this kind can cause. It is a list of the kinds of the effects. The text
|
|
74
|
+
* `"type"` means that the kind of the effect is the type of the document itself, as for a call.
|
|
75
|
+
*/
|
|
76
|
+
readonly effects?: "type" | readonly string[];
|
|
27
77
|
/** One sentence that tells what the kind is. */
|
|
28
78
|
readonly summary?: string;
|
|
29
79
|
}
|
|
30
80
|
|
|
31
|
-
/** A namespace: a group of entries under one key of the world. */
|
|
81
|
+
/** A namespace: a group of entries under one key of the world. `fallback` is the kind of each other key in it. */
|
|
32
82
|
export interface Namespace {
|
|
33
83
|
readonly kinds: Readonly<Record<string, WorldEntry>>;
|
|
84
|
+
readonly fallback?: Kind;
|
|
34
85
|
}
|
|
35
86
|
|
|
36
87
|
/** An alias: a reference to another entry of the world. The lookup follows it (collapse). */
|
|
@@ -70,6 +121,16 @@ export function isAlias(entry: WorldEntry | undefined): entry is Alias {
|
|
|
70
121
|
return entry !== undefined && (entry as { type?: unknown }).type === "ref" && Array.isArray((entry as { value?: unknown }).value);
|
|
71
122
|
}
|
|
72
123
|
|
|
124
|
+
/**
|
|
125
|
+
* This function tells if an entry of the world is a namespace.
|
|
126
|
+
*
|
|
127
|
+
* @param entry - The entry.
|
|
128
|
+
* @returns `true` for a namespace.
|
|
129
|
+
*/
|
|
130
|
+
export function isNamespace(entry: WorldEntry | undefined): entry is Namespace {
|
|
131
|
+
return entry !== undefined && !isKind(entry) && !isAlias(entry) && Object.hasOwn(entry, "kinds");
|
|
132
|
+
}
|
|
133
|
+
|
|
73
134
|
function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): WorldEntry | undefined {
|
|
74
135
|
const name = String(key);
|
|
75
136
|
return Object.hasOwn(entries, name) ? entries[name] : undefined;
|
|
@@ -77,7 +138,8 @@ function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): World
|
|
|
77
138
|
|
|
78
139
|
/**
|
|
79
140
|
* This function finds the kind that a type names. It walks the path from the root of the world, one key at a
|
|
80
|
-
* time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup.
|
|
141
|
+
* time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup. In a
|
|
142
|
+
* namespace with a fallback, a key that the namespace does not have gets the fallback kind.
|
|
81
143
|
*
|
|
82
144
|
* @param world - The world.
|
|
83
145
|
* @param type - The reference to the type.
|
|
@@ -90,12 +152,16 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
|
|
|
90
152
|
let aliases = 0;
|
|
91
153
|
for (;;) {
|
|
92
154
|
let entries = world.kinds;
|
|
155
|
+
let fallback: Kind | undefined;
|
|
93
156
|
let entry: WorldEntry | undefined;
|
|
94
157
|
let followed = false;
|
|
95
158
|
for (let index = 0; index < path.length; index += 1) {
|
|
96
159
|
const key = path[index] as Key;
|
|
97
160
|
entry = entryAt(entries, key);
|
|
98
|
-
if (entry === undefined)
|
|
161
|
+
if (entry === undefined) {
|
|
162
|
+
if (fallback !== undefined) return { ok: true, kind: fallback, path, aliases };
|
|
163
|
+
return { ok: false, reason: "unknown", path };
|
|
164
|
+
}
|
|
99
165
|
if (isAlias(entry)) {
|
|
100
166
|
aliases += 1;
|
|
101
167
|
if (aliases > 32) return { ok: false, reason: "cycle", path };
|
|
@@ -108,8 +174,9 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
|
|
|
108
174
|
break;
|
|
109
175
|
}
|
|
110
176
|
if (index < path.length - 1) {
|
|
111
|
-
if (
|
|
112
|
-
entries =
|
|
177
|
+
if (!isNamespace(entry)) return { ok: false, reason: "not-a-kind", path };
|
|
178
|
+
entries = entry.kinds;
|
|
179
|
+
fallback = entry.fallback;
|
|
113
180
|
}
|
|
114
181
|
}
|
|
115
182
|
if (followed) continue;
|
|
@@ -135,6 +202,16 @@ export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Js
|
|
|
135
202
|
return out;
|
|
136
203
|
}
|
|
137
204
|
|
|
205
|
+
function describeShape(shape: Shape | undefined): Json {
|
|
206
|
+
if (shape === undefined || shape.shape === "none") return "none";
|
|
207
|
+
if (shape.shape === "term") return `a part${shape.of === undefined ? "" : ` of ${shape.of}`}`;
|
|
208
|
+
if (shape.shape === "deep") return "each document in the value";
|
|
209
|
+
if (shape.shape === "list") return `a list${shape.of === undefined ? "" : ` of ${shape.of}`}`;
|
|
210
|
+
if (shape.shape === "map") return `a record of parts${shape.of === undefined ? "" : ` of ${shape.of}`}`;
|
|
211
|
+
const names = Array.isArray(shape.fields) ? shape.fields : Object.keys(shape.fields);
|
|
212
|
+
return `the fields ${names.join(", ")}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
138
215
|
/**
|
|
139
216
|
* This function makes the document of a kind, so that a view can draw it as each other document.
|
|
140
217
|
*
|
|
@@ -143,13 +220,10 @@ export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Js
|
|
|
143
220
|
* @returns The document of type `kind`.
|
|
144
221
|
*/
|
|
145
222
|
export function kindDocument(name: string, kind: Kind): Envelope {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
? `a list${kind.parts.of === undefined ? "" : ` of ${kind.parts.of}`}`
|
|
151
|
-
: `the fields ${kind.parts.fields.join(", ")}`;
|
|
152
|
-
return { type: "kind", value: { name, verbs: { ...kind.verbs }, parts, side: kind.side ?? "derive", summary: kind.summary ?? "" } };
|
|
223
|
+
return {
|
|
224
|
+
type: "kind",
|
|
225
|
+
value: { name, verbs: { ...kind.verbs }, parts: describeShape(kind.parts), side: kind.side ?? "derive", summary: kind.summary ?? "" },
|
|
226
|
+
};
|
|
153
227
|
}
|
|
154
228
|
|
|
155
229
|
/**
|
|
@@ -158,6 +232,6 @@ export function kindDocument(name: string, kind: Kind): Envelope {
|
|
|
158
232
|
* @param value - The value.
|
|
159
233
|
* @returns `true` when the value has verbs.
|
|
160
234
|
*/
|
|
161
|
-
export function looksLikeKind(value:
|
|
235
|
+
export function looksLikeKind(value: unknown): boolean {
|
|
162
236
|
return isRecord(value) && isRecord(field(value, "verbs"));
|
|
163
237
|
}
|