@mark1russell7/coeval 0.6.3 → 0.7.1
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 +63 -5
- package/build/index.d.ts +1771 -254
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5809 -511
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +15 -3
- package/src/codec.ts +185 -0
- package/src/conform.ts +1068 -0
- package/src/deref.ts +284 -47
- package/src/durable.ts +238 -0
- package/src/effects.ts +110 -0
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +480 -113
- package/src/facts.ts +252 -0
- package/src/formula.ts +450 -42
- package/src/graph.ts +399 -0
- package/src/index.ts +46 -0
- package/src/ladder.ts +40 -1
- package/src/merge.ts +91 -4
- package/src/parts.ts +26 -11
- package/src/providers.ts +286 -0
- package/src/reads.ts +105 -0
- package/src/reference.ts +23 -9
- package/src/schedule.ts +436 -0
- package/src/static.ts +266 -29
- package/src/stdlib.ts +24 -7
- package/src/syntax.ts +846 -0
- package/src/types.ts +768 -0
- package/src/typing.ts +390 -0
- package/src/world.ts +63 -9
package/src/formula.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { type Json, type Key, isRecord } from "./doc.ts";
|
|
2
|
-
import type { Kind, WorldEntry } from "./world.ts";
|
|
3
|
-
import { type Context, type Resolution, valueOfThing } from "./deref.ts";
|
|
1
|
+
import { type Json, type Key, docEquals, isKey, isRecord } from "./doc.ts";
|
|
2
|
+
import type { Kind, KindFacts, LazyRule, WorldEntry } from "./world.ts";
|
|
3
|
+
import { type CompiledPath, type Context, type Resolution, compilePath, valueOfThing } from "./deref.ts";
|
|
4
4
|
import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
|
|
5
|
-
import type
|
|
5
|
+
import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled } from "./evaluate.ts";
|
|
6
6
|
import { type MachinePart, mergeMachines } from "./merge.ts";
|
|
7
|
+
import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, stepType, typeText } from "./types.ts";
|
|
8
|
+
import type { TypeInput, TypeRule } from "./typing.ts";
|
|
9
|
+
import { decodeDomain, domainTypeOf } from "./codec.ts";
|
|
7
10
|
|
|
8
11
|
/** An operation of a formula language: a function of the values of its arguments. */
|
|
9
12
|
export type OpFn = (...args: never[]) => unknown;
|
|
@@ -14,15 +17,19 @@ export type OpFn = (...args: never[]) => unknown;
|
|
|
14
17
|
*/
|
|
15
18
|
export type LazyOp = (args: readonly (() => Result)[]) => Result;
|
|
16
19
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The code of an extension: it gives the value of the data of an extension document, in the context of the document.
|
|
22
|
+
* It also gets the domains of the machine, for the codecs of `ext.domain` (D40).
|
|
23
|
+
*/
|
|
24
|
+
export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<string, Domain>>) => Result;
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* The rules of one formula language: its operations, and the rules that differ between languages.
|
|
22
28
|
*
|
|
23
29
|
* - `ops`: the operations of the namespace `ops`, by their name (the keys after `ops`, with dots between them).
|
|
24
30
|
* - `lazy`: the operations that evaluate their arguments themselves.
|
|
25
|
-
* - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined
|
|
31
|
+
* - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined`, `ext.number` and
|
|
32
|
+
* `ext.domain`.
|
|
26
33
|
* - `truthy`: the truth of a value for `if`, `and` and `or`. `undefined` is an error `#VALUE!`. The default
|
|
27
34
|
* accepts only `true` and `false`.
|
|
28
35
|
* - `otherwise`: the value of an `if` without `else`. The default is `null`. A profile with this key gives its value,
|
|
@@ -33,6 +40,14 @@ export type ExtFn = (data: Json, context: Context) => Result;
|
|
|
33
40
|
* - `check`: a check of the value of an operation, for example "a number must be finite".
|
|
34
41
|
* - `combine`: the error of two or more failed parts of an operation, a `record` or a `list`. With it, each part
|
|
35
42
|
* evaluates, and one failed part gives its own error. Without it, the first error stops the evaluation.
|
|
43
|
+
* - `pure`: the pure operations, for `factsOf` (D24). `true` names each operation that is not volatile. The default
|
|
44
|
+
* names no operation.
|
|
45
|
+
* - `volatile`: the operations whose results can change while their arguments stay the same, for example a clock.
|
|
46
|
+
* - `sigs`: the signatures of the operations, by their name, for a check of types (TYPE.SIGNATURE). A list gives
|
|
47
|
+
* overloads. An operation without a signature takes and gives `unknown` (TYPE.GRADUAL).
|
|
48
|
+
*
|
|
49
|
+
* A check of types trusts the signatures. Each operation must give a member of the result of each signature that
|
|
50
|
+
* its arguments match. The hook `check` must keep each result in that type.
|
|
36
51
|
*/
|
|
37
52
|
export interface FormulaProfile {
|
|
38
53
|
readonly ops?: Readonly<Record<string, OpFn>> | ((name: string) => OpFn | undefined);
|
|
@@ -45,6 +60,23 @@ export interface FormulaProfile {
|
|
|
45
60
|
readonly missing?: (value: unknown) => boolean;
|
|
46
61
|
readonly check?: (value: unknown, op: string) => Result | undefined;
|
|
47
62
|
readonly combine?: (errors: readonly EvalError[]) => EvalError;
|
|
63
|
+
readonly pure?: true | readonly string[];
|
|
64
|
+
readonly volatile?: readonly string[];
|
|
65
|
+
readonly sigs?: Readonly<Record<string, Signature | readonly Signature[]>> | ((name: string) => Signature | readonly Signature[] | undefined);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter of
|
|
70
|
+
* the type `unknown` at the end of `params` is optional. A later version can add a key, for example the type that a
|
|
71
|
+
* true result proves.
|
|
72
|
+
*/
|
|
73
|
+
export interface Signature {
|
|
74
|
+
/** The types of the parameters, in order. */
|
|
75
|
+
readonly params: readonly ValueType[];
|
|
76
|
+
/** The type of each argument after the parameters. Without it, the operation takes no more arguments. */
|
|
77
|
+
readonly rest?: ValueType;
|
|
78
|
+
/** The type of the result. */
|
|
79
|
+
readonly to: ValueType;
|
|
48
80
|
}
|
|
49
81
|
|
|
50
82
|
/** The mark of a field that the profile leaves out of a record. */
|
|
@@ -70,7 +102,10 @@ export function isClosure(value: unknown): value is Closure {
|
|
|
70
102
|
return typeof value === "function" && (value as Partial<Closure>)[CLOSURE] !== undefined;
|
|
71
103
|
}
|
|
72
104
|
|
|
73
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* The standard extensions: the values that JSON cannot write. `ext.domain` reads a member of a domain through the
|
|
107
|
+
* codec of the domain (EXT.DOMAIN, D40).
|
|
108
|
+
*/
|
|
74
109
|
export const STD_EXT: Readonly<Record<string, ExtFn>> = {
|
|
75
110
|
undefined: () => ok(undefined),
|
|
76
111
|
number: (data) => {
|
|
@@ -80,6 +115,7 @@ export const STD_EXT: Readonly<Record<string, ExtFn>> = {
|
|
|
80
115
|
if (data === "-0") return ok(-0);
|
|
81
116
|
return fail("#VALUE!", "The value of ext.number is NaN, Infinity, -Infinity or -0.");
|
|
82
117
|
},
|
|
118
|
+
domain: (data, _context, domains) => decodeDomain(data, domains),
|
|
83
119
|
};
|
|
84
120
|
|
|
85
121
|
/** The truth of a value in the rules of JavaScript: `0`, `""`, `null`, `undefined` and `NaN` are false. */
|
|
@@ -131,48 +167,354 @@ export function formulaKinds(): Readonly<Record<string, WorldEntry>> {
|
|
|
131
167
|
const term = { shape: "term", of: "lit" } as const;
|
|
132
168
|
const terms = { shape: "list", of: "lit" } as const;
|
|
133
169
|
const kinds: Record<string, Kind | WorldEntry> = {
|
|
134
|
-
lit: { verbs: { eval: "formula.lit" }, summary: LIT_SUMMARY },
|
|
135
|
-
ref: { verbs: { eval: "formula.ref" }, summary: "A path from a root of the context. Its result is what the path reaches." },
|
|
136
|
-
record: { verbs: { eval: "formula.record" }, parts: { shape: "map", of: "lit" }, summary: "A record: each field is a term." },
|
|
137
|
-
list: { verbs: { eval: "formula.list" }, parts: terms, summary: "A list: each item is a term." },
|
|
170
|
+
lit: { verbs: { eval: "formula.lit" }, pure: true, summary: LIT_SUMMARY },
|
|
171
|
+
ref: { verbs: { eval: "formula.ref" }, pure: true, summary: "A path from a root of the context. Its result is what the path reaches." },
|
|
172
|
+
record: { verbs: { eval: "formula.record" }, parts: { shape: "map", of: "lit" }, pure: true, summary: "A record: each field is a term." },
|
|
173
|
+
list: { verbs: { eval: "formula.list" }, parts: terms, pure: true, summary: "A list: each item is a term." },
|
|
138
174
|
let: {
|
|
139
175
|
verbs: { eval: "formula.let" },
|
|
140
176
|
parts: { shape: "record", fields: { bind: { shape: "map", of: "lit" }, body: term }, required: ["bind", "body"] },
|
|
141
177
|
scope: [{ names: { keysAt: ["bind"] }, in: [["body"]] }],
|
|
178
|
+
pure: true,
|
|
142
179
|
summary: "Names for the body. Each binding sees the names around the let, not the other bindings.",
|
|
143
180
|
},
|
|
144
181
|
fn: {
|
|
145
182
|
verbs: { eval: "formula.fn" },
|
|
146
183
|
parts: { shape: "record", fields: { params: { shape: "list", item: { shape: "text" }, distinct: true }, body: term }, required: ["params", "body"] },
|
|
147
184
|
scope: [{ names: { listAt: ["params"] }, in: [["body"]] }],
|
|
185
|
+
// The body evaluates only when something calls the function.
|
|
186
|
+
pure: true,
|
|
187
|
+
lazy: [{ in: [["body"]] }],
|
|
148
188
|
summary: "A function: its parameters are names for its body.",
|
|
149
189
|
},
|
|
150
190
|
call: {
|
|
151
191
|
verbs: { eval: "formula.call" },
|
|
152
192
|
parts: { shape: "record", fields: { fn: term, args: terms }, required: ["fn"] },
|
|
193
|
+
// A host function in a name is a value. When it is volatile, the host marks its read (FACTS.VOLATILE).
|
|
194
|
+
pure: true,
|
|
153
195
|
summary: "It calls a function with the values of its arguments.",
|
|
154
196
|
},
|
|
155
197
|
if: {
|
|
156
198
|
verbs: { eval: "formula.if" },
|
|
157
199
|
parts: { shape: "record", fields: { test: term, then: term, else: term }, required: ["test", "then"] },
|
|
200
|
+
pure: true,
|
|
201
|
+
lazy: [{ in: [["then"], ["else"]] }],
|
|
158
202
|
summary: "It evaluates the test, then only the branch that the test selects.",
|
|
159
203
|
},
|
|
160
|
-
and: { verbs: { eval: "formula.and" }, parts: terms, summary: "True when each term is true. It stops at the first false term." },
|
|
161
|
-
or: { verbs: { eval: "formula.or" }, parts: terms, summary: "True when one term is true. It stops at the first true term." },
|
|
204
|
+
and: { verbs: { eval: "formula.and" }, parts: terms, pure: true, lazy: [{ after: 1 }], summary: "True when each term is true. It stops at the first false term." },
|
|
205
|
+
or: { verbs: { eval: "formula.or" }, parts: terms, pure: true, lazy: [{ after: 1 }], summary: "True when one term is true. It stops at the first true term." },
|
|
162
206
|
ops: { kinds: {}, fallback: { verbs: { eval: "formula.op" }, parts: terms, summary: "An operation of the language, applied to the values of its terms." } },
|
|
163
207
|
ext: { kinds: {}, fallback: { verbs: { eval: "formula.ext" }, summary: "An extension of the language. Its value is data." } },
|
|
164
208
|
};
|
|
165
209
|
return kinds as Readonly<Record<string, WorldEntry>>;
|
|
166
210
|
}
|
|
167
211
|
|
|
212
|
+
/** This function gives the type of a value of a profile: the value itself as `one`. `undefined` and a function give `unknown`. */
|
|
213
|
+
function valueType(value: unknown): ValueType {
|
|
214
|
+
return value === undefined || typeof value === "function" ? "unknown" : { one: [value as Json] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** This function tells if a value has the form of a signature: a record with `params` as a list, and `to`. */
|
|
218
|
+
function isSignature(value: unknown): value is Signature {
|
|
219
|
+
return typeof value === "object" && value !== null && Array.isArray((value as { readonly params?: unknown }).params) && Object.hasOwn(value, "to");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** This function gives the text of a signature, for the reason of an issue. */
|
|
223
|
+
function signatureText(sig: Signature): string {
|
|
224
|
+
const params = sig.params.map(typeText);
|
|
225
|
+
if (sig.rest !== undefined) params.push(`...${typeText(sig.rest)}`);
|
|
226
|
+
return `(${params.join(", ")}) => ${typeText(sig.to)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function argumentsText(count: number): string {
|
|
230
|
+
return `${count} argument${count === 1 ? "" : "s"}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** This function gives the text of a list of names for a reason, for example `a, b and c`. */
|
|
234
|
+
function listText(names: readonly string[]): string {
|
|
235
|
+
return names.length < 2 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1] as string}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* This function tells if a signature takes a count of arguments. A parameter of the type `unknown` at the end is
|
|
240
|
+
* optional, because a missing argument is `undefined`.
|
|
241
|
+
*
|
|
242
|
+
* @returns `undefined` when the number is correct, or else the text of the numbers that the signature takes.
|
|
243
|
+
*/
|
|
244
|
+
function arityOf(sig: Signature, count: number, domains: Machine["domains"]): string | undefined {
|
|
245
|
+
let least = sig.params.length;
|
|
246
|
+
while (least > 0 && isUnknown(sig.params[least - 1], domains)) least -= 1;
|
|
247
|
+
const most = sig.rest === undefined ? sig.params.length : Number.POSITIVE_INFINITY;
|
|
248
|
+
if (count >= least && count <= most) return undefined;
|
|
249
|
+
if (most === Number.POSITIVE_INFINITY) return `at least ${argumentsText(least)}`;
|
|
250
|
+
return least === most ? argumentsText(most) : `${least} to ${argumentsText(most)}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** This function gives the parameters of a document of `fn`: the texts of its list `params`. */
|
|
254
|
+
function paramsOf(value: unknown): readonly string[] {
|
|
255
|
+
const params = isRecord(value) ? value["params"] : undefined;
|
|
256
|
+
return Array.isArray(params) ? params.filter((param): param is string => typeof param === "string") : [];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* This function makes the type rules of the formula kit for one profile (TYPE.SYNTH, D25). Each evaluator of the kit
|
|
261
|
+
* holds its rule. The rules read the rules of the profile that change a type: `truthy`, `logic`, `otherwise`, `omit`
|
|
262
|
+
* and `sigs`.
|
|
263
|
+
*/
|
|
264
|
+
function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule>> {
|
|
265
|
+
// Only the default truth accepts nothing but a truth value. A truth of the profile accepts each value.
|
|
266
|
+
const truth: ValueType | undefined = profile.truthy === undefined ? "bool" : undefined;
|
|
267
|
+
const logicValue = profile.logic === "value";
|
|
268
|
+
const otherwise = Object.hasOwn(profile, "otherwise") ? valueType(profile.otherwise) : "null";
|
|
269
|
+
const lookup = (name: string): readonly unknown[] | undefined => {
|
|
270
|
+
const sigs = profile.sigs;
|
|
271
|
+
if (sigs === undefined) return undefined;
|
|
272
|
+
let found: unknown;
|
|
273
|
+
if (typeof sigs === "function") {
|
|
274
|
+
try {
|
|
275
|
+
found = sigs(name);
|
|
276
|
+
} catch {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
found = Object.hasOwn(sigs, name) ? sigs[name] : undefined;
|
|
281
|
+
}
|
|
282
|
+
if (found === undefined) return undefined;
|
|
283
|
+
return Array.isArray(found) ? found : [found];
|
|
284
|
+
};
|
|
285
|
+
const nameOf = (input: TypeInput): string => input.typePath.slice(1).join(".");
|
|
286
|
+
/** The signatures of an operation. Each type of a signature is a contract, thus the check examines it one time. */
|
|
287
|
+
const signatures = (input: TypeInput): { readonly sigs: readonly Signature[]; readonly bad: boolean } | undefined => {
|
|
288
|
+
const name = nameOf(input);
|
|
289
|
+
const raw = lookup(name);
|
|
290
|
+
if (raw === undefined) return undefined;
|
|
291
|
+
const sigs: Signature[] = [];
|
|
292
|
+
for (const sig of raw) {
|
|
293
|
+
if (!isSignature(sig)) continue;
|
|
294
|
+
sig.params.forEach((param, index) => input.contract(param, `parameter ${index + 1} of a signature of ops.${name}`));
|
|
295
|
+
if (sig.rest !== undefined) input.contract(sig.rest, `rest parameter of a signature of ops.${name}`);
|
|
296
|
+
input.contract(sig.to, `result of a signature of ops.${name}`);
|
|
297
|
+
sigs.push(sig);
|
|
298
|
+
}
|
|
299
|
+
return { sigs, bad: sigs.length !== raw.length };
|
|
300
|
+
};
|
|
301
|
+
const logic = (word: "and" | "or"): TypeRule => ({
|
|
302
|
+
part: (_input, path) => (truth === undefined ? {} : { expect: truth, what: `term ${Number(path[0]) + 1} of ${word}` }),
|
|
303
|
+
synth: (input) => {
|
|
304
|
+
if (!logicValue) return "bool";
|
|
305
|
+
// With the logic "value", the result is the term that decided, or the last term.
|
|
306
|
+
if (input.paths.length === 0) return { one: [word === "and"] };
|
|
307
|
+
return input.paths.reduce<ValueType>((type, path) => join(type, input.typeOf(path), input.machine.domains), "never");
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"formula.lit": { synth: (input) => valueType(input.doc.value) },
|
|
313
|
+
|
|
314
|
+
"formula.ref": {
|
|
315
|
+
synth: (input, report) => {
|
|
316
|
+
const path = input.doc.value;
|
|
317
|
+
// An address gives a thing, not a value. The check gives #VALUE! for a path that is not a list.
|
|
318
|
+
if (input.address || !Array.isArray(path) || path.length === 0) return "unknown";
|
|
319
|
+
const domains = input.machine.domains;
|
|
320
|
+
const plain = input.machine.plain === true;
|
|
321
|
+
let type: ValueType;
|
|
322
|
+
let start: number;
|
|
323
|
+
if (path[0] === "names") {
|
|
324
|
+
const name = path[1];
|
|
325
|
+
// The record of the names holds the error of a failed binding, thus the path ["names"] has no type. A record
|
|
326
|
+
// of names with only the keys of a header is an envelope, and a path into it asks the ladder.
|
|
327
|
+
if (!isKey(name) || (!plain && HEADER_KEYS.has(String(name)))) return "unknown";
|
|
328
|
+
type = input.nameType(String(name)) ?? "unknown";
|
|
329
|
+
start = 2;
|
|
330
|
+
} else {
|
|
331
|
+
type = input.rootType(String(path[0]));
|
|
332
|
+
start = 1;
|
|
333
|
+
}
|
|
334
|
+
for (let index = start; ; index += 1) {
|
|
335
|
+
// A path follows a reference document that it reaches, and it asks the ladder for a step into an envelope.
|
|
336
|
+
if (isUnknown(type, domains) || (!plain && holdsEnvelope(type, domains))) return "unknown";
|
|
337
|
+
if (index >= path.length) return type;
|
|
338
|
+
const step = path[index];
|
|
339
|
+
// A step document gives what its walker reaches. It has no type yet.
|
|
340
|
+
if (!isKey(step)) return "unknown";
|
|
341
|
+
const next = stepType(type, step, domains, input.contract);
|
|
342
|
+
if ("missing" in next) {
|
|
343
|
+
const fields = next.missing.length === 0 ? "" : ` Its fields are ${listText(next.missing)}.`;
|
|
344
|
+
report({ reason: `The value at ${path.slice(0, index).map(String).join(".")} has no field ${String(step)}.${fields}` });
|
|
345
|
+
return "unknown";
|
|
346
|
+
}
|
|
347
|
+
type = next.type;
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
"formula.record": {
|
|
353
|
+
part: (input, path) => {
|
|
354
|
+
const want = fieldOf(input.expected, String(path[0]), input.machine.domains);
|
|
355
|
+
return want === undefined ? {} : { expect: want, what: `field ${String(path[0])} of the record` };
|
|
356
|
+
},
|
|
357
|
+
synth: (input) => {
|
|
358
|
+
const fields: Record<string, ValueType> = {};
|
|
359
|
+
for (const path of input.paths) setOwn(fields, String(path[0]), input.typeOf(path));
|
|
360
|
+
const keys = Object.keys(fields);
|
|
361
|
+
// A profile with omit can leave out each field, thus each field is optional.
|
|
362
|
+
return { record: fields, ...(profile.omit !== undefined && keys.length > 0 ? { optional: keys } : {}), closed: true };
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
"formula.list": {
|
|
367
|
+
part: (input, path) => {
|
|
368
|
+
const want = itemOf(input.expected, input.machine.domains);
|
|
369
|
+
return want === undefined ? {} : { expect: want, what: `item ${Number(path[0]) + 1} of the list` };
|
|
370
|
+
},
|
|
371
|
+
synth: (input) => ({ list: input.paths.reduce<ValueType>((type, path) => join(type, input.typeOf(path), input.machine.domains), "never") }),
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
"formula.let": {
|
|
375
|
+
part: (input, path) => {
|
|
376
|
+
if (path[0] !== "body") return {};
|
|
377
|
+
// The scope rule keysAt puts the bindings first, thus their types are known here.
|
|
378
|
+
const names: Record<string, ValueType> = {};
|
|
379
|
+
for (const each of input.paths) if (each[0] === "bind" && each.length === 2) setOwn(names, String(each[1]), input.typeOf(each));
|
|
380
|
+
return { names, ...(isUnknown(input.expected, input.machine.domains) ? {} : { expect: input.expected, what: "body of let" }) };
|
|
381
|
+
},
|
|
382
|
+
synth: (input) => input.typeOf(["body"]),
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
"formula.fn": {
|
|
386
|
+
part: (input, path) => {
|
|
387
|
+
if (path[0] !== "body") return {};
|
|
388
|
+
// An expected function type gives the types of the parameters. Without it, each parameter is unknown.
|
|
389
|
+
const want = fnOf(input.expected, input.machine.domains);
|
|
390
|
+
const names: Record<string, ValueType> = {};
|
|
391
|
+
paramsOf(input.doc.value).forEach((name, index) => setOwn(names, name, want?.params[index] ?? "unknown"));
|
|
392
|
+
return { names, ...(want === undefined || isUnknown(want.to, input.machine.domains) ? {} : { expect: want.to, what: "body of fn" }) };
|
|
393
|
+
},
|
|
394
|
+
synth: (input) => {
|
|
395
|
+
const want = fnOf(input.expected, input.machine.domains);
|
|
396
|
+
return { fn: paramsOf(input.doc.value).map((_name, index) => want?.params[index] ?? "unknown"), to: input.typeOf(["body"]) };
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
"formula.call": {
|
|
401
|
+
synth: (input, report) => {
|
|
402
|
+
const domains = input.machine.domains;
|
|
403
|
+
const target = input.typeOf(["fn"]);
|
|
404
|
+
if (isUnknown(target, domains)) return "unknown";
|
|
405
|
+
const want = fnOf(target, domains);
|
|
406
|
+
if (want === undefined) {
|
|
407
|
+
if (callable(target, domains)) return "unknown";
|
|
408
|
+
report({ path: ["fn"], reason: `The fn of call has the type ${typeText(target)}. It must give a function.`, found: target });
|
|
409
|
+
return "unknown";
|
|
410
|
+
}
|
|
411
|
+
const args = input.paths.filter((path) => path[0] === "args");
|
|
412
|
+
for (const path of args) {
|
|
413
|
+
const index = Number(path[1]);
|
|
414
|
+
const param = want.params[index];
|
|
415
|
+
// The function does not read an argument after its parameters.
|
|
416
|
+
if (param === undefined) continue;
|
|
417
|
+
const found = input.typeOf(path);
|
|
418
|
+
if (!fits(found, param, domains)) {
|
|
419
|
+
report({ path, reason: `The argument ${index + 1} of the call has the type ${typeText(found)}. The function expects the type ${typeText(param)}.`, expected: param, found });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const missing = want.params.findIndex((param, index) => index >= args.length && !isUnknown(param, domains));
|
|
423
|
+
if (missing !== -1) {
|
|
424
|
+
const param = want.params[missing] as ValueType;
|
|
425
|
+
report({ reason: `The call gives ${argumentsText(args.length)}. The function expects the type ${typeText(param)} for the argument ${missing + 1}.`, expected: param });
|
|
426
|
+
}
|
|
427
|
+
return want.to;
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
"formula.if": {
|
|
432
|
+
part: (input, path) => {
|
|
433
|
+
if (path[0] === "test") return truth === undefined ? {} : { expect: truth, what: "test of if" };
|
|
434
|
+
return isUnknown(input.expected, input.machine.domains) ? {} : { expect: input.expected, what: `${String(path[0])} branch of if` };
|
|
435
|
+
},
|
|
436
|
+
synth: (input) => {
|
|
437
|
+
const value = input.doc.value;
|
|
438
|
+
const other = isRecord(value) && Object.hasOwn(value, "else") ? input.typeOf(["else"]) : otherwise;
|
|
439
|
+
return join(input.typeOf(["then"]), other, input.machine.domains);
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
"formula.and": logic("and"),
|
|
444
|
+
"formula.or": logic("or"),
|
|
445
|
+
|
|
446
|
+
"formula.op": {
|
|
447
|
+
part: (input, path) => {
|
|
448
|
+
const read = signatures(input);
|
|
449
|
+
// Only one signature gives the types of the arguments before their check. Overloads select after it.
|
|
450
|
+
if (read === undefined || read.sigs.length !== 1) return {};
|
|
451
|
+
const sig = read.sigs[0] as Signature;
|
|
452
|
+
const index = Number(path[0]);
|
|
453
|
+
const want = index < sig.params.length ? sig.params[index] : sig.rest;
|
|
454
|
+
return want === undefined ? {} : { expect: want, what: `argument ${index + 1} of ops.${nameOf(input)}` };
|
|
455
|
+
},
|
|
456
|
+
synth: (input, report) => {
|
|
457
|
+
const read = signatures(input);
|
|
458
|
+
if (read === undefined) return "unknown";
|
|
459
|
+
const name = nameOf(input);
|
|
460
|
+
const domains = input.machine.domains;
|
|
461
|
+
if (read.bad) report({ reason: `A signature of ops.${name} is not a record with params and to. The check does not read it.` });
|
|
462
|
+
const { sigs } = read;
|
|
463
|
+
if (sigs.length === 0) return "unknown";
|
|
464
|
+
const args = input.paths.map((path) => input.typeOf(path));
|
|
465
|
+
if (sigs.length === 1) {
|
|
466
|
+
const sig = sigs[0] as Signature;
|
|
467
|
+
const takes = arityOf(sig, args.length, domains);
|
|
468
|
+
if (takes !== undefined) report({ reason: `The operation ops.${name} takes ${takes}. It gets ${args.length}.` });
|
|
469
|
+
return sig.to;
|
|
470
|
+
}
|
|
471
|
+
const paramAt = (sig: Signature, index: number): ValueType => sig.params[index] ?? sig.rest ?? "unknown";
|
|
472
|
+
const fitting = sigs.filter((sig) => arityOf(sig, args.length, domains) === undefined && args.every((arg, index) => fits(arg, paramAt(sig, index), domains)));
|
|
473
|
+
if (fitting.length === 0) {
|
|
474
|
+
const given = args.map(typeText).join(", ");
|
|
475
|
+
report({ reason: `No signature of ops.${name} fits the arguments (${given}). The signatures are ${sigs.map(signatureText).join(" and ")}.` });
|
|
476
|
+
return "unknown";
|
|
477
|
+
}
|
|
478
|
+
const first = fitting[0] as Signature;
|
|
479
|
+
// Precise arguments are members of the first signature that fits. An argument with unknown can match a later one.
|
|
480
|
+
if (fitting.length === 1 || args.every((arg) => isPrecise(arg, domains))) return first.to;
|
|
481
|
+
return fitting.every((sig) => docEquals(sig.to, first.to)) ? first.to : "unknown";
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
"formula.ext": {
|
|
486
|
+
synth: (input) => {
|
|
487
|
+
const name = nameOf(input);
|
|
488
|
+
const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
|
|
489
|
+
if (own) return "unknown";
|
|
490
|
+
if (name === "number") return "number";
|
|
491
|
+
return name === "domain" ? domainTypeOf(input.doc.value, input.machine.domains) : "unknown";
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
168
497
|
/**
|
|
169
498
|
* This function makes the evaluators of the formula kit for one language. Each evaluator compiles its document
|
|
170
|
-
* one time into code.
|
|
499
|
+
* one time into code. Each evaluator also holds its type rule, for a check of types (TYPE.SYNTH).
|
|
171
500
|
*
|
|
172
501
|
* @param profile - The operations and the rules of the language.
|
|
173
502
|
* @returns The evaluators, by their name.
|
|
174
503
|
*/
|
|
175
504
|
export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record<string, Evaluator>> {
|
|
505
|
+
const evaluators = formulaCode(profile);
|
|
506
|
+
const types = formulaTypes(profile);
|
|
507
|
+
const out: Record<string, Evaluator> = {};
|
|
508
|
+
for (const name of Object.keys(evaluators)) {
|
|
509
|
+
const evaluator = evaluators[name] as CompiledEvaluator;
|
|
510
|
+
const type = Object.hasOwn(types, name) ? types[name] : undefined;
|
|
511
|
+
out[name] = type === undefined ? evaluator : { compile: evaluator.compile, type };
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** This function makes the code of the evaluators of the formula kit, without their type rules. */
|
|
517
|
+
function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledEvaluator>> {
|
|
176
518
|
const truthy = profile.truthy ?? strictTruthy;
|
|
177
519
|
const otherwise = ok(Object.hasOwn(profile, "otherwise") ? profile.otherwise : null);
|
|
178
520
|
const logicValue = profile.logic === "value";
|
|
@@ -203,13 +545,54 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
203
545
|
return { ok: false, error: errors.length === 1 ? (errors[0] as EvalError) : combine(errors) };
|
|
204
546
|
};
|
|
205
547
|
|
|
548
|
+
/*
|
|
549
|
+
* The code of a reference (PLAN.LEAN). The helpers below serve each reference of the language. Each code keeps only
|
|
550
|
+
* what its own reference needs. A reference with a static path keeps its compiled path, its first name and its mark
|
|
551
|
+
* `address`. It does not keep its document, its split or the functions of the other form.
|
|
552
|
+
*/
|
|
553
|
+
|
|
554
|
+
/** This function gives the result of a resolution of a reference. An address gives what the path reaches: a host thing stays a thing. */
|
|
555
|
+
const give = (found: Resolution, address: boolean): Result => {
|
|
556
|
+
if (!found.ok) return fail(found.code, found.reason);
|
|
557
|
+
if (address) return ok(found.thing);
|
|
558
|
+
const reached = valueOfThing(found.thing);
|
|
559
|
+
if (reached instanceof Held) return { ok: false, error: reached.error };
|
|
560
|
+
if (missing !== undefined && missing(reached)) return fail("#N/A", "The path reaches no value.");
|
|
561
|
+
return ok(reached);
|
|
562
|
+
};
|
|
563
|
+
/** This function gives the error of a name whose binding failed, or `undefined` when the binding holds no error. */
|
|
564
|
+
const held = (ctx: Context, rt: Runtime, head: string | undefined): Failed | undefined => {
|
|
565
|
+
if (head === undefined || !Object.hasOwn(ctx.names, head)) return undefined;
|
|
566
|
+
const bound = ctx.names[head];
|
|
567
|
+
if (!(bound instanceof Held)) return undefined;
|
|
568
|
+
// The result comes from the binding of the name, thus the runtime resolves the name for the list of reads (READS.RECORD).
|
|
569
|
+
rt.resolve(ctx, ["names", head]);
|
|
570
|
+
return { ok: false, error: bound.error };
|
|
571
|
+
};
|
|
572
|
+
/** This function gives the code of a reference with a static path. A path without a first name does not test a binding. */
|
|
573
|
+
const staticRef = (compiledPath: CompiledPath, head: string | undefined, address: boolean): Code =>
|
|
574
|
+
head === undefined
|
|
575
|
+
? (ctx, rt) => give(resolveCompiled(rt, ctx, compiledPath), address)
|
|
576
|
+
: (ctx, rt) => held(ctx, rt, head) ?? give(resolveCompiled(rt, ctx, compiledPath), address);
|
|
577
|
+
/**
|
|
578
|
+
* This function gives the code of a reference whose step documents are parts, when the kind ref has a deep shape.
|
|
579
|
+
* The steps evaluate first, in order, thus the error of a step comes before the error of a binding (FORMULA.STEPS).
|
|
580
|
+
*/
|
|
581
|
+
const stepsRef = (parts: readonly CompiledPart[], fill: CompileInput["fill"], head: string | undefined, address: boolean): Code => {
|
|
582
|
+
return (ctx, rt) => {
|
|
583
|
+
const steps = valuesOf(parts, ctx, rt);
|
|
584
|
+
if (!Array.isArray(steps)) return steps;
|
|
585
|
+
return held(ctx, rt, head) ?? give(rt.resolve(ctx, fill(steps) as readonly unknown[]), address);
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
|
|
206
589
|
return {
|
|
207
590
|
"formula.lit": compiled(({ doc }) => {
|
|
208
591
|
const result = ok(doc.value);
|
|
209
592
|
return () => result;
|
|
210
593
|
}),
|
|
211
594
|
|
|
212
|
-
"formula.ref": compiled(({ doc, parts, fill, address }) => {
|
|
595
|
+
"formula.ref": compiled(({ doc, parts, fill, address, machine }) => {
|
|
213
596
|
const path = doc.value;
|
|
214
597
|
if (!Array.isArray(path) || path.length === 0) {
|
|
215
598
|
const bad = fail("#VALUE!", 'The value of ref is a path: a list that starts with a root, for example ["names", "a"].');
|
|
@@ -217,28 +600,9 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
217
600
|
}
|
|
218
601
|
// A path through a name whose binding failed gives the error of the binding.
|
|
219
602
|
const head = path[0] === "names" && typeof path[1] === "string" ? path[1] : undefined;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (address === true) return ok(found.thing);
|
|
224
|
-
const reached = valueOfThing(found.thing);
|
|
225
|
-
if (reached instanceof Held) return { ok: false, error: reached.error };
|
|
226
|
-
if (missing !== undefined && missing(reached)) return fail("#N/A", "The path reaches no value.");
|
|
227
|
-
return ok(reached);
|
|
228
|
-
};
|
|
229
|
-
const held = (ctx: Context): Failed | undefined => {
|
|
230
|
-
if (head === undefined || !Object.hasOwn(ctx.names, head)) return undefined;
|
|
231
|
-
const bound = ctx.names[head];
|
|
232
|
-
return bound instanceof Held ? { ok: false, error: bound.error } : undefined;
|
|
233
|
-
};
|
|
234
|
-
if (parts.length === 0) return (ctx, rt) => held(ctx) ?? give(rt.resolve(ctx, path));
|
|
235
|
-
// The step documents of a path are its parts when the kind ref has a deep shape. They evaluate first, in order,
|
|
236
|
-
// thus the error of a step comes before the error of a binding that the path reads (FORMULA.STEPS).
|
|
237
|
-
return (ctx, rt) => {
|
|
238
|
-
const steps = valuesOf(parts, ctx, rt);
|
|
239
|
-
if (!Array.isArray(steps)) return steps;
|
|
240
|
-
return held(ctx) ?? give(rt.resolve(ctx, fill(steps) as readonly unknown[]));
|
|
241
|
-
};
|
|
603
|
+
// A path without parts is static, thus it compiles one time for the machine of the plan (PLAN.PATH).
|
|
604
|
+
if (parts.length === 0) return staticRef(compilePath(machine, path), head, address === true);
|
|
605
|
+
return stepsRef(parts, fill, head, address === true);
|
|
242
606
|
}),
|
|
243
607
|
|
|
244
608
|
"formula.step": compiled(({ doc, parts, fill }) => {
|
|
@@ -312,6 +676,8 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
312
676
|
const names = params as readonly string[];
|
|
313
677
|
const body = partsOf(value)["body"] as CompiledPart;
|
|
314
678
|
return (ctx, rt) => {
|
|
679
|
+
// A closure keeps the context for each later use, thus the list of reads gets each root (READS.KEPT).
|
|
680
|
+
rt.keepsContext?.();
|
|
315
681
|
const closure = (...args: readonly unknown[]): unknown => {
|
|
316
682
|
const scope: Record<string, unknown> = { ...ctx.names };
|
|
317
683
|
names.forEach((name, index) => setOwn(scope, name, args[index]));
|
|
@@ -456,7 +822,7 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
456
822
|
};
|
|
457
823
|
}),
|
|
458
824
|
|
|
459
|
-
"formula.ext": compiled(({ typePath, doc }) => {
|
|
825
|
+
"formula.ext": compiled(({ typePath, doc, machine }) => {
|
|
460
826
|
const name = typePath.slice(1).join(".");
|
|
461
827
|
const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name) ? profile.ext[name] : undefined;
|
|
462
828
|
const handler = own ?? (Object.hasOwn(STD_EXT, name) ? STD_EXT[name] : undefined);
|
|
@@ -466,7 +832,7 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
466
832
|
}
|
|
467
833
|
return (ctx) => {
|
|
468
834
|
try {
|
|
469
|
-
return handler(doc.value, ctx);
|
|
835
|
+
return handler(doc.value, ctx, machine.domains);
|
|
470
836
|
} catch (error) {
|
|
471
837
|
return thrown(error, `the extension ${name}`);
|
|
472
838
|
}
|
|
@@ -507,21 +873,63 @@ export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[
|
|
|
507
873
|
};
|
|
508
874
|
}
|
|
509
875
|
|
|
876
|
+
/** The facts of an extension of the profile. It gets the context, thus it can read outside `resolve`. */
|
|
877
|
+
const NOT_PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: false });
|
|
878
|
+
|
|
879
|
+
/** The facts of a standard extension. */
|
|
880
|
+
const PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: true });
|
|
881
|
+
|
|
882
|
+
/** The lazy rule of a lazy operation: it covers each argument. */
|
|
883
|
+
const EACH_PART: readonly LazyRule[] = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Object.freeze({})]);
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* This function makes the hook `facts` of the formula kit for one profile (FACTS.PROFILE, D24). It answers for the
|
|
887
|
+
* operations and the extensions, because one fallback kind covers each of them.
|
|
888
|
+
*
|
|
889
|
+
* - An operation of `lazy` has a lazy rule for each argument.
|
|
890
|
+
* - An operation is pure when the profile names it in `pure`, or when `pure` is `true`. A volatile operation is not pure.
|
|
891
|
+
* - A standard extension is pure. An extension of the profile gets the context, thus it is not pure.
|
|
892
|
+
*
|
|
893
|
+
* @param profile - The operations, the extensions and their facts.
|
|
894
|
+
* @returns The hook: the facts of a name, or `undefined` for the facts of the contract.
|
|
895
|
+
*/
|
|
896
|
+
export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[], code: string) => KindFacts | undefined {
|
|
897
|
+
const volatile = new Set(profile.volatile ?? []);
|
|
898
|
+
const pure = profile.pure === true ? undefined : new Set(profile.pure ?? []);
|
|
899
|
+
const known = formulaKnown(profile);
|
|
900
|
+
return (path, code) => {
|
|
901
|
+
if (code !== "formula.op" && code !== "formula.ext") return undefined;
|
|
902
|
+
const name = path.slice(1).join(".");
|
|
903
|
+
if (code === "formula.ext") return profile.ext !== undefined && Object.hasOwn(profile.ext, name) ? NOT_PURE : Object.hasOwn(STD_EXT, name) ? PURE : undefined;
|
|
904
|
+
// An operation that the language does not have gives #NAME?. The kit gives it no facts.
|
|
905
|
+
if (known(path, code) !== true) return undefined;
|
|
906
|
+
const isVolatile = volatile.has(name);
|
|
907
|
+
const lazy = profile.lazy !== undefined && Object.hasOwn(profile.lazy, name);
|
|
908
|
+
return { pure: !isVolatile && (pure === undefined || pure.has(name)), volatile: isVolatile, ...(lazy ? { lazy: EACH_PART } : {}) };
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
510
912
|
/**
|
|
511
913
|
* This function makes a machine for a formula language. It merges the kit and the parts that the language adds with
|
|
512
914
|
* `mergeMachines`. Thus a kind `ops.*` or a namespace `ops` of the language extends the namespace of the kit, and
|
|
513
915
|
* the fallback of the kit stays. The hook `known` of the kit comes from the profile (`formulaKnown`). A hook `known`
|
|
514
|
-
* of the language answers after it.
|
|
916
|
+
* of the language answers after it. The hook `facts` of the kit comes from the profile too (`formulaFacts`).
|
|
515
917
|
*
|
|
516
918
|
* A kind or an evaluator of the language that differs from the entry of the kit with its name is a conflict. The
|
|
517
919
|
* language names the path in `replaces` to replace the entry, for example `["world", "kinds", "ref"]` or
|
|
518
920
|
* `["evaluators", "formula.op"]`. Then the hook of the kit does not answer for a replaced evaluator.
|
|
519
921
|
*
|
|
922
|
+
* The kit gives the domains of the kernel (`STD_DOMAINS`). Thus a domain of the language with the name `number` is a
|
|
923
|
+
* conflict. The language names `["domains", "number"]` in `replaces` to replace it.
|
|
924
|
+
*
|
|
520
925
|
* @param profile - The operations and the rules of the language.
|
|
521
926
|
* @param more - The parts of a machine that the language adds, with the paths of the entries that it replaces.
|
|
522
927
|
* @returns The machine.
|
|
523
928
|
* @throws {MergeError} The language and the kit have a conflict.
|
|
524
929
|
*/
|
|
525
930
|
export function formulaMachine(profile: FormulaProfile = {}, more: MachinePart = {}): Machine {
|
|
526
|
-
return mergeMachines(
|
|
931
|
+
return mergeMachines(
|
|
932
|
+
{ world: { kinds: formulaKinds() }, evaluators: formulaEvaluators(profile), domains: STD_DOMAINS, known: formulaKnown(profile), facts: formulaFacts(profile) },
|
|
933
|
+
more,
|
|
934
|
+
);
|
|
527
935
|
}
|