@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/world.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { type Envelope, type Json, type Key, type TypeRef, field, isRecord, typePath } from "./doc.ts";
|
|
2
|
+
|
|
3
|
+
/** The two sides of evaluation. `derive` is pure. `run` causes effects in order. */
|
|
4
|
+
export type Side = "derive" | "run";
|
|
5
|
+
|
|
6
|
+
/**
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* - `none`: the value is data. It has no parts.
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
export type Shape =
|
|
19
|
+
| { readonly shape: "none" }
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A kind gives the name of an evaluator for each verb, and the contract of its value. */
|
|
64
|
+
export interface Kind {
|
|
65
|
+
readonly verbs: Readonly<Record<string, string>>;
|
|
66
|
+
/** Where the parts of the value are. The default is `none`. */
|
|
67
|
+
readonly parts?: Shape;
|
|
68
|
+
/** The names that the kind binds for its parts. */
|
|
69
|
+
readonly scope?: readonly ScopeRule[];
|
|
70
|
+
/** The side of the kind. A kind of the run side causes effects. The default is `derive`. */
|
|
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[];
|
|
77
|
+
/** One sentence that tells what the kind is. */
|
|
78
|
+
readonly summary?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A namespace: a group of entries under one key of the world. `fallback` is the kind of each other key in it. */
|
|
82
|
+
export interface Namespace {
|
|
83
|
+
readonly kinds: Readonly<Record<string, WorldEntry>>;
|
|
84
|
+
readonly fallback?: Kind;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** An alias: a reference to another entry of the world. The lookup follows it (collapse). */
|
|
88
|
+
export type Alias = { readonly type: "ref"; readonly value: readonly Json[] };
|
|
89
|
+
|
|
90
|
+
/** One entry of the world: a kind, a namespace, or an alias. */
|
|
91
|
+
export type WorldEntry = Kind | Namespace | Alias;
|
|
92
|
+
|
|
93
|
+
/** A world holds the kinds and the defaults that answer the questions. */
|
|
94
|
+
export interface World {
|
|
95
|
+
readonly kinds: Readonly<Record<string, WorldEntry>>;
|
|
96
|
+
readonly defaults: Readonly<Record<string, string>>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The result of a lookup of a type in the world. */
|
|
100
|
+
export type Lookup =
|
|
101
|
+
| { readonly ok: true; readonly kind: Kind; readonly path: readonly Key[]; readonly aliases: number }
|
|
102
|
+
| { readonly ok: false; readonly reason: "unknown" | "cycle" | "not-a-kind" | "computed"; readonly path: readonly Key[] };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* This function tells if an entry of the world is a kind.
|
|
106
|
+
*
|
|
107
|
+
* @param entry - The entry.
|
|
108
|
+
* @returns `true` for a kind.
|
|
109
|
+
*/
|
|
110
|
+
export function isKind(entry: WorldEntry | undefined): entry is Kind {
|
|
111
|
+
return entry !== undefined && Object.hasOwn(entry, "verbs");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* This function tells if an entry of the world is an alias.
|
|
116
|
+
*
|
|
117
|
+
* @param entry - The entry.
|
|
118
|
+
* @returns `true` for an alias.
|
|
119
|
+
*/
|
|
120
|
+
export function isAlias(entry: WorldEntry | undefined): entry is Alias {
|
|
121
|
+
return entry !== undefined && (entry as { type?: unknown }).type === "ref" && Array.isArray((entry as { value?: unknown }).value);
|
|
122
|
+
}
|
|
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
|
+
|
|
134
|
+
function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): WorldEntry | undefined {
|
|
135
|
+
const name = String(key);
|
|
136
|
+
return Object.hasOwn(entries, name) ? entries[name] : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* This function finds the kind that a type names. It walks the path from the root of the world, one key at a
|
|
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.
|
|
143
|
+
*
|
|
144
|
+
* @param world - The world.
|
|
145
|
+
* @param type - The reference to the type.
|
|
146
|
+
* @returns The kind and the path of the kind after each alias, or the reason of a failure.
|
|
147
|
+
*/
|
|
148
|
+
export function lookupKind(world: World, type: TypeRef): Lookup {
|
|
149
|
+
const start = typePath(type);
|
|
150
|
+
if (start === undefined) return { ok: false, reason: "computed", path: [] };
|
|
151
|
+
let path: readonly Key[] = start;
|
|
152
|
+
let aliases = 0;
|
|
153
|
+
for (;;) {
|
|
154
|
+
let entries = world.kinds;
|
|
155
|
+
let fallback: Kind | undefined;
|
|
156
|
+
let entry: WorldEntry | undefined;
|
|
157
|
+
let followed = false;
|
|
158
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
159
|
+
const key = path[index] as Key;
|
|
160
|
+
entry = entryAt(entries, key);
|
|
161
|
+
if (entry === undefined) {
|
|
162
|
+
if (fallback !== undefined) return { ok: true, kind: fallback, path, aliases };
|
|
163
|
+
return { ok: false, reason: "unknown", path };
|
|
164
|
+
}
|
|
165
|
+
if (isAlias(entry)) {
|
|
166
|
+
aliases += 1;
|
|
167
|
+
if (aliases > 32) return { ok: false, reason: "cycle", path };
|
|
168
|
+
const target = entry.value.slice(1);
|
|
169
|
+
if (entry.value[0] !== "world" || !target.every((item) => typeof item === "string" || typeof item === "number")) {
|
|
170
|
+
return { ok: false, reason: "not-a-kind", path };
|
|
171
|
+
}
|
|
172
|
+
path = [...(target as readonly Key[]), ...path.slice(index + 1)];
|
|
173
|
+
followed = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
if (index < path.length - 1) {
|
|
177
|
+
if (!isNamespace(entry)) return { ok: false, reason: "not-a-kind", path };
|
|
178
|
+
entries = entry.kinds;
|
|
179
|
+
fallback = entry.fallback;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (followed) continue;
|
|
183
|
+
if (!isKind(entry)) return { ok: false, reason: "not-a-kind", path };
|
|
184
|
+
return { ok: true, kind: entry, path, aliases };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* This function gives the entries of a world as a JSON record, so that a reference can walk it. A kind is a
|
|
190
|
+
* record with its verbs, an alias is a reference document, and a namespace is a record of its entries.
|
|
191
|
+
*
|
|
192
|
+
* @param entries - The entries.
|
|
193
|
+
* @returns The JSON form.
|
|
194
|
+
*/
|
|
195
|
+
export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Json {
|
|
196
|
+
const out: Record<string, Json> = {};
|
|
197
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
198
|
+
if (isAlias(entry)) out[key] = { type: "ref", value: entry.value };
|
|
199
|
+
else if (isKind(entry)) out[key] = JSON.parse(JSON.stringify(entry)) as Json;
|
|
200
|
+
else out[key] = entriesAsJson((entry as Namespace).kinds);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
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
|
+
|
|
215
|
+
/**
|
|
216
|
+
* This function makes the document of a kind, so that a view can draw it as each other document.
|
|
217
|
+
*
|
|
218
|
+
* @param name - The name of the kind.
|
|
219
|
+
* @param kind - The kind.
|
|
220
|
+
* @returns The document of type `kind`.
|
|
221
|
+
*/
|
|
222
|
+
export function kindDocument(name: string, kind: Kind): Envelope {
|
|
223
|
+
return {
|
|
224
|
+
type: "kind",
|
|
225
|
+
value: { name, verbs: { ...kind.verbs }, parts: describeShape(kind.parts), side: kind.side ?? "derive", summary: kind.summary ?? "" },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* This function tells if a JSON value is the record of a kind.
|
|
231
|
+
*
|
|
232
|
+
* @param value - The value.
|
|
233
|
+
* @returns `true` when the value has verbs.
|
|
234
|
+
*/
|
|
235
|
+
export function looksLikeKind(value: unknown): boolean {
|
|
236
|
+
return isRecord(value) && isRecord(field(value, "verbs"));
|
|
237
|
+
}
|