@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/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 { Code, CompileInput, CompiledEvaluator, CompiledPart, Evaluator, Machine } from "./evaluate.ts";
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
- /** The code of an extension: it gives the value of the data of an extension document, in the context of the document. */
18
- export type ExtFn = (data: Json, context: Context) => Result;
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` and `ext.number`.
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
- /** The standard extensions: the values that JSON cannot write. */
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
- const give = (found: Resolution): Result => {
221
- if (!found.ok) return fail(found.code, found.reason);
222
- // An address gives what the path reaches: a host thing stays a thing.
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({ world: { kinds: formulaKinds() }, evaluators: formulaEvaluators(profile), known: formulaKnown(profile) }, more);
931
+ return mergeMachines(
932
+ { world: { kinds: formulaKinds() }, evaluators: formulaEvaluators(profile), domains: STD_DOMAINS, known: formulaKnown(profile), facts: formulaFacts(profile) },
933
+ more,
934
+ );
527
935
  }