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