@mark1russell7/coeval 0.4.1 → 0.4.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
package/src/agree.ts ADDED
@@ -0,0 +1,237 @@
1
+ import { type Key, docEquals, isRecord } from "./doc.ts";
2
+ import type { Context } from "./deref.ts";
3
+ import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
4
+ import { type Effect, type Machine, evaluate, makeContext, prepare } from "./evaluate.ts";
5
+ import { Held, isClosure } from "./formula.ts";
6
+ import { type Definitions, reference } from "./reference.ts";
7
+
8
+ /**
9
+ * A tier that `agree` compares with the plan.
10
+ *
11
+ * - `reference`: the reference interpreter, which compiles nothing.
12
+ * - `traced`: the traced evaluation, `evaluate`.
13
+ * - `again`: a second evaluation of the same plan.
14
+ * - `other`: the second tier of a language, from the option `other`.
15
+ */
16
+ export type Tier = "reference" | "traced" | "again" | "other";
17
+
18
+ /** What a second tier gives: its result, and its effects when it records them. */
19
+ export interface TierResult {
20
+ readonly result: Result;
21
+ readonly effects?: readonly Effect[];
22
+ }
23
+
24
+ /** The options of `agree`. */
25
+ export interface AgreeOptions {
26
+ /** The type of a bare root value. */
27
+ readonly declaredType?: string;
28
+ /** The definitional code of the compiled evaluators of the machine, for the reference. */
29
+ readonly definitions?: Definitions;
30
+ /** The tiers to compare with the plan. The default is the reference, the traced evaluation and a second evaluation. */
31
+ readonly tiers?: readonly Exclude<Tier, "other">[];
32
+ /**
33
+ * This function gives the result of the second tier of a language, for example a compiler to loops.
34
+ *
35
+ * @param doc - The document.
36
+ * @param context - The context.
37
+ * @returns The result, or the result with the effects.
38
+ */
39
+ readonly other?: (doc: unknown, context: Context) => Result | TierResult;
40
+ /**
41
+ * This function compares two values that the rules of `sameValue` do not know, for example two dates.
42
+ *
43
+ * @param a - The value of the plan.
44
+ * @param b - The value of the other tier.
45
+ * @returns `true` or `false`, or `undefined` to use the rules of `sameValue`.
46
+ */
47
+ readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
48
+ /** `true`: two errors must also have the same reason, and each error must have a place. */
49
+ readonly strict?: boolean;
50
+ /**
51
+ * This function gives what the host saw since its last call, for example the calls of an operation. `agree` uses
52
+ * it before the plan and after each tier, and compares the results. Thus a tier that evaluates a part that the plan
53
+ * does not evaluate fails.
54
+ *
55
+ * @returns What the host saw.
56
+ */
57
+ readonly observe?: () => unknown;
58
+ }
59
+
60
+ /** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
61
+ export type Agreement =
62
+ | { readonly ok: true; readonly result: Result; readonly effects: readonly Effect[] }
63
+ | {
64
+ readonly ok: false;
65
+ readonly tier: Tier;
66
+ /** The result of the plan. */
67
+ readonly plan: Result;
68
+ /** The result of the tier that disagrees. */
69
+ readonly other: Result;
70
+ /** One sentence that tells the difference. */
71
+ readonly reason: string;
72
+ };
73
+
74
+ /**
75
+ * This function tells if two values are the same for `agree`. Lists and plain records compare item by item and field
76
+ * by field, and the order of the fields does not matter. Each other leaf compares with `Object.is`, thus `-0` is not
77
+ * `0`, and `NaN` is `NaN`. The identity of a list or of a plain record is not compared.
78
+ *
79
+ * Two closures of `fn` are the same when they have the same parameters. Two `Held` values are the same when their
80
+ * errors have the same code and place. Each other object is the same only as itself, unless the rule of the caller
81
+ * tells otherwise.
82
+ *
83
+ * `docEquals` uses the same rules for lists, plain records and the other objects. It has no rules for closures, for
84
+ * `Held` values and for a rule of the caller. Thus two closures are equal there only when they are the same function.
85
+ *
86
+ * @param a - The first value.
87
+ * @param b - The second value.
88
+ * @param equal - The rule of the caller for other values. It answers first, and `undefined` gives the rules above.
89
+ * @returns `true` when the values are the same.
90
+ */
91
+ export function sameValue(a: unknown, b: unknown, equal?: (a: unknown, b: unknown) => boolean | undefined): boolean {
92
+ const stack: (readonly [unknown, unknown])[] = [[a, b]];
93
+ const seen = new Map<object, Set<object>>();
94
+ while (stack.length > 0) {
95
+ const [x, y] = stack.pop() as readonly [unknown, unknown];
96
+ if (Object.is(x, y)) continue;
97
+ const told = equal?.(x, y);
98
+ if (told !== undefined) {
99
+ if (told) continue;
100
+ return false;
101
+ }
102
+ if (typeof x !== "object" && typeof x !== "function") return false;
103
+ if (typeof y !== "object" && typeof y !== "function") return false;
104
+ if (x === null || y === null) return false;
105
+ // A pair that the comparison already visits is the same, unless another pair shows a difference.
106
+ const pairs = seen.get(x) ?? new Set<object>();
107
+ if (pairs.has(y)) continue;
108
+ pairs.add(y);
109
+ seen.set(x, pairs);
110
+ if (isClosure(x) || isClosure(y)) {
111
+ if (!(isClosure(x) && isClosure(y))) return false;
112
+ stack.push([closureParams(x), closureParams(y)]);
113
+ continue;
114
+ }
115
+ if (x instanceof Held || y instanceof Held) {
116
+ if (!(x instanceof Held && y instanceof Held) || errorDiffers(x.error, y.error, false) !== undefined) return false;
117
+ continue;
118
+ }
119
+ if (Array.isArray(x) || Array.isArray(y)) {
120
+ if (!Array.isArray(x) || !Array.isArray(y) || x.length !== y.length) return false;
121
+ for (let index = 0; index < x.length; index += 1) stack.push([x[index], y[index]]);
122
+ continue;
123
+ }
124
+ if (!isRecord(x) || !isRecord(y)) return false;
125
+ const keys = Object.keys(x);
126
+ if (keys.length !== Object.keys(y).length) return false;
127
+ for (const key of keys) {
128
+ if (!Object.hasOwn(y, key)) return false;
129
+ stack.push([x[key], y[key]]);
130
+ }
131
+ }
132
+ return true;
133
+ }
134
+
135
+ const CLOSURE = Symbol.for("coeval.closure");
136
+
137
+ function closureParams(fn: object): unknown {
138
+ return (fn as { readonly [CLOSURE]?: { readonly params: readonly string[] } })[CLOSURE]?.params;
139
+ }
140
+
141
+ function codeAndPlace(error: EvalError): string {
142
+ return error.place === undefined ? error.code : `${error.code} at [${error.place.join(", ")}]`;
143
+ }
144
+
145
+ /** This function gives the difference of two errors, or `undefined` when they agree. */
146
+ function errorDiffers(a: EvalError, b: EvalError, strict: boolean): string | undefined {
147
+ if (a.code !== b.code) return `The plan gives ${codeAndPlace(a)}, and the tier gives ${codeAndPlace(b)}.`;
148
+ if (strict && (a.place === undefined || b.place === undefined)) return `An error ${a.code} has no place.`;
149
+ if (a.place !== undefined && b.place !== undefined && !docEquals(a.place as readonly Key[], b.place as readonly Key[])) {
150
+ return `The error ${a.code} has the place [${a.place.join(", ")}] in the plan, and [${b.place.join(", ")}] in the tier.`;
151
+ }
152
+ if (strict && a.reason !== b.reason) return `The error ${a.code} has another reason in the tier: "${b.reason}" for "${a.reason}".`;
153
+ return undefined;
154
+ }
155
+
156
+ /** A host stack overflow is the one permitted difference (VALUE.DEPTH): the tiers use different numbers of frames. */
157
+ function overflowed(result: Result): boolean {
158
+ return !result.ok && result.error.code === "#DEPTH!" && isStackOverflow(result.error.cause);
159
+ }
160
+
161
+ /** This function gives the difference of two results, or `undefined` when they agree. */
162
+ function resultDiffers(a: Result, b: Result, options: AgreeOptions): string | undefined {
163
+ if (a.ok && b.ok) return sameValue(a.value, b.value, options.equal) ? undefined : "The plan and the tier give different values.";
164
+ if (!a.ok && !b.ok) return errorDiffers(a.error, b.error, options.strict === true);
165
+ return a.ok ? `The plan gives a value, and the tier gives ${codeAndPlace((b as { readonly error: EvalError }).error)}.` : `The plan gives ${codeAndPlace(a.error)}, and the tier gives a value.`;
166
+ }
167
+
168
+ /** This function gives the difference of two lists of effects, or `undefined` when they agree (EFFECT.ORDER). */
169
+ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: AgreeOptions): string | undefined {
170
+ if (a.length !== b.length) return `The plan causes ${a.length} effects, and the tier causes ${b.length}.`;
171
+ for (let index = 0; index < a.length; index += 1) {
172
+ const x = a[index] as Effect;
173
+ const y = b[index] as Effect;
174
+ if (x.kind !== y.kind || !docEquals(x.place as readonly Key[], y.place as readonly Key[]) || !sameValue(x.payload, y.payload, options.equal)) {
175
+ return `The effect ${index} differs: ${x.kind} at [${x.place.join(", ")}] in the plan, and ${y.kind} at [${y.place.join(", ")}] in the tier.`;
176
+ }
177
+ }
178
+ return undefined;
179
+ }
180
+
181
+ /**
182
+ * This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
183
+ * results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
184
+ * both have one. The effects must have the same kinds, places and payloads, in the same order. A language gives its
185
+ * own tier with the option `other`.
186
+ *
187
+ * @param machine - The world and the code of the evaluators.
188
+ * @param doc - The document.
189
+ * @param context - The names and the capabilities at the root.
190
+ * @param options - The tiers, the definitions for the reference, the rules of the comparison and the second tier.
191
+ * @returns The result of the plan, or the first tier that disagrees and the reason.
192
+ */
193
+ export function agree(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeOptions = {}): Agreement {
194
+ const declared = options.declaredType === undefined ? {} : { declaredType: options.declaredType };
195
+ const observe = options.observe;
196
+ observe?.();
197
+ const plan = prepare(machine, doc, declared);
198
+ const effects: Effect[] = [];
199
+ const result = plan.evaluate(context, { effects });
200
+ const saw = observe?.();
201
+
202
+ const differs = (other: Result, otherEffects: readonly Effect[] | undefined): string | undefined => {
203
+ if (overflowed(result) || overflowed(other)) return undefined;
204
+ const why = resultDiffers(result, other, options) ?? (otherEffects === undefined ? undefined : effectsDiffer(effects, otherEffects, options));
205
+ if (why !== undefined || observe === undefined) return why;
206
+ return sameValue(saw, observe(), options.equal) ? undefined : "The host saw other calls in the tier.";
207
+ };
208
+
209
+ for (const tier of options.tiers ?? ["reference", "traced", "again"]) {
210
+ let other: Result;
211
+ let otherEffects: Effect[] = [];
212
+ if (tier === "reference") {
213
+ other = reference(machine, doc, context, { ...declared, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
214
+ } else if (tier === "traced") {
215
+ const traced = evaluate(machine, doc, context, options.declaredType);
216
+ other = traced.result;
217
+ otherEffects = [...traced.effects];
218
+ } else {
219
+ other = plan.evaluate(context, { effects: otherEffects });
220
+ }
221
+ const why = differs(other, otherEffects);
222
+ if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
223
+ }
224
+
225
+ if (options.other !== undefined) {
226
+ let given: Result | TierResult;
227
+ try {
228
+ given = options.other(doc, context);
229
+ } catch (error) {
230
+ given = thrown(error, "the tier");
231
+ }
232
+ const other = "result" in given ? given.result : given;
233
+ const why = differs(other, "result" in given ? given.effects : undefined);
234
+ if (why !== undefined) return { ok: false, tier: "other", plan: result, other, reason: why };
235
+ }
236
+ return { ok: true, result, effects };
237
+ }
package/src/doc.ts CHANGED
@@ -221,9 +221,17 @@ export function fromJson(value: unknown): Envelope | undefined {
221
221
  }
222
222
 
223
223
  /**
224
- * This function tells if two JSON values are equal. Equal values have the same texts, numbers and truth values,
225
- * the same items in the same order, and the same fields. The order of the fields does not matter. It does not use the stack for the
226
- * depth, thus a deep document cannot overflow it.
224
+ * This function tells if two values are equal. Plain JSON compares by its structure. Equal values have the same
225
+ * texts, numbers and truth values, the same items in the same order, and the same fields. The order of the fields
226
+ * does not matter. Each leaf compares with `Object.is`, thus `-0` is not `0`, and `NaN` is `NaN`.
227
+ *
228
+ * Only lists and plain records (`isRecord`) compare by their items and fields. Each other object compares by
229
+ * identity: a date, a map, a function and an instance of a class are equal only to themselves. Thus an early cutoff
230
+ * with this function does not keep a stale host value.
231
+ *
232
+ * The function does not use the stack for the depth, thus a deep document cannot overflow it. A cycle of lists or
233
+ * records does not make it loop. `sameValue` of `agree` uses the same rules, and it adds rules for closures and for
234
+ * a rule of the caller.
227
235
  *
228
236
  * @param a - The first value.
229
237
  * @param b - The second value.
@@ -231,15 +239,25 @@ export function fromJson(value: unknown): Envelope | undefined {
231
239
  */
232
240
  export function docEquals(a: unknown, b: unknown): boolean {
233
241
  const stack: [unknown, unknown][] = [[a, b]];
242
+ // The pairs of containers that the comparison already visits. It is made at the first pair of containers.
243
+ let seen: Map<object, Set<object>> | undefined;
234
244
  while (stack.length > 0) {
235
245
  const [x, y] = stack.pop() as [unknown, unknown];
236
246
  if (Object.is(x, y)) continue;
237
- if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) return false;
238
- if (Array.isArray(x) !== Array.isArray(y)) return false;
239
- if (Array.isArray(x)) {
247
+ const list = Array.isArray(x);
248
+ if (list !== Array.isArray(y)) return false;
249
+ // An object that is not a list and not a plain record is a host value: Object.is above compared it.
250
+ if (!list && (!isRecord(x) || !isRecord(y))) return false;
251
+ seen ??= new Map();
252
+ const pairs = seen.get(x as object);
253
+ if (pairs?.has(y as object) === true) continue;
254
+ if (pairs === undefined) seen.set(x as object, new Set([y as object]));
255
+ else pairs.add(y as object);
256
+ if (list) {
257
+ const xs = x as readonly unknown[];
240
258
  const ys = y as readonly unknown[];
241
- if (x.length !== ys.length) return false;
242
- for (let index = 0; index < x.length; index += 1) stack.push([x[index], ys[index]]);
259
+ if (xs.length !== ys.length) return false;
260
+ for (let index = 0; index < xs.length; index += 1) stack.push([xs[index], ys[index]]);
243
261
  continue;
244
262
  }
245
263
  const xr = x as Readonly<Record<string, unknown>>;
package/src/errors.ts CHANGED
@@ -104,7 +104,12 @@ export class Failure extends Error {
104
104
  * @returns `true` for a stack overflow.
105
105
  */
106
106
  export function isStackOverflow(error: unknown): boolean {
107
- return error instanceof RangeError && /call stack/i.test(error.message);
107
+ try {
108
+ return error instanceof RangeError && /call stack/i.test(error.message);
109
+ } catch {
110
+ // Near the limit of the stack, the test itself overflows: the regular expression throws a SyntaxError.
111
+ return true;
112
+ }
108
113
  }
109
114
 
110
115
  /**
package/src/evaluate.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
2
- import { type Split, pathMatches, splitParts } from "./parts.ts";
2
+ import { type Split, partsOf, pathMatches } from "./parts.ts";
3
3
  import { ask, type Answer } from "./ladder.ts";
4
- import { type CapsRule, type Lookup, type Side, type World, lookupKind } from "./world.ts";
4
+ import type { CapsRule, Kind, Side, World } from "./world.ts";
5
5
  import { type ErrorCode, type EvalError, type Result, fail, ok, placed, thrown, describe } from "./errors.ts";
6
6
  import {
7
7
  type Cap,
@@ -428,6 +428,7 @@ export type Runner = RunFn | CompiledRunner;
428
428
  *
429
429
  * - `maxDepth`: the deepest nesting of documents. The default is 1000.
430
430
  * - `permits`: the rule of the host for an effect. The default is the function `permits` on the capabilities.
431
+ * - `known`: the names that the evaluators know, for `check`. Without it, `check` does not check such names.
431
432
  */
432
433
  export interface Machine extends Navigator {
433
434
  readonly evaluators: Readonly<Record<string, Evaluator>>;
@@ -435,6 +436,16 @@ export interface Machine extends Navigator {
435
436
  readonly runners?: Readonly<Record<string, Runner>>;
436
437
  readonly maxDepth?: number;
437
438
  readonly permits?: (caps: readonly Cap[], kind: readonly Key[], context: Context) => boolean;
439
+ /**
440
+ * This function tells if an evaluator knows the name in the path of a kind. An example is the operation
441
+ * `["ops", "add"]` of a formula language: a fallback kind names it, and the evaluator finds it in a table.
442
+ * `check` asks it for each document with an evaluator. The function must be pure.
443
+ *
444
+ * @param path - The keys of the kind of the document, after each alias.
445
+ * @param evaluator - The name of the evaluator that the ladder gives the document.
446
+ * @returns `true` when the evaluator knows the name, or else the reason of the `#NAME?` that a run gives.
447
+ */
448
+ readonly known?: (path: readonly Key[], evaluator: string) => true | string;
438
449
  }
439
450
 
440
451
  /** One evaluation in a trace. The children are the evaluations of its parts, in the order that they started. */
@@ -832,7 +843,11 @@ interface Compiler {
832
843
  }
833
844
 
834
845
  function failing(c: Compiler, meta: Meta, place: readonly Key[], code: ErrorCode, reason: string): { readonly code: Code; readonly run: RunCode } {
835
- const result = fail(code, reason, { place });
846
+ return failingWith(c, meta, fail(code, reason, { place }));
847
+ }
848
+
849
+ /** This function gives the code of a document that fails with one result at each evaluation. */
850
+ function failingWith(c: Compiler, meta: Meta, result: Result): { readonly code: Code; readonly run: RunCode } {
836
851
  const plain: Code = () => result;
837
852
  return finish(c, meta, plain, function* () {
838
853
  return result;
@@ -890,12 +905,12 @@ function finish(c: Compiler, meta: Meta, code: Code, run: RunCode): { readonly c
890
905
  * This function finds the evaluator of the verb `eval` without the record of the rungs, for a compilation without a
891
906
  * trace. It gives the same evaluator as `ask`: the own entry, then the kind, then the default of the world.
892
907
  */
893
- function evaluatorOf(world: World, envelope: Envelope | undefined, found: Lookup): { readonly name?: string; readonly unknown: boolean } {
908
+ function evaluatorOf(world: World, envelope: Envelope | undefined, kind: Kind | undefined): { readonly name?: string; readonly unknown: boolean } {
894
909
  const own = envelope?.own;
895
910
  const ownName = own !== undefined && Object.hasOwn(own, "eval") ? own["eval"] : undefined;
896
911
  if (ownName !== undefined) return { name: ownName, unknown: false };
897
- if (!found.ok) return { unknown: true };
898
- const verbs = found.kind.verbs;
912
+ if (kind === undefined) return { unknown: true };
913
+ const verbs = kind.verbs;
899
914
  const name = Object.hasOwn(verbs, "eval") ? verbs["eval"] : Object.hasOwn(world.defaults, "eval") ? world.defaults["eval"] : undefined;
900
915
  return name === undefined ? { unknown: false } : { name, unknown: false };
901
916
  }
@@ -904,17 +919,17 @@ function evaluatorOf(world: World, envelope: Envelope | undefined, found: Lookup
904
919
  const NO_META: Meta = Object.freeze({ path: [], doc: { type: "?", value: null }, declared: false, answer: NO_ANSWER, side: "derive" });
905
920
 
906
921
  function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number, address = false): { readonly code: Code; readonly run: RunCode } {
907
- const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
908
- const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
909
- const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" } : NO_META;
910
- if (envelope === undefined && declared === undefined) {
911
- return failing(c, meta0, place, "#PART?", "The value has no envelope, and its position declares no type.");
922
+ const world = c.machine.world;
923
+ const read = partsOf(world, value, declared);
924
+ if (!read.ok) {
925
+ const meta: Meta = c.traced ? { path: place.map(String), doc: { type: "?", value: value as Json }, declared: true, answer: NO_ANSWER, side: "derive" } : NO_META;
926
+ return failing(c, meta, place, read.code, read.reason);
912
927
  }
928
+ const { doc, kind, split } = read;
929
+ const envelope = read.declared ? undefined : doc;
930
+ const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: read.declared, answer: NO_ANSWER, side: "derive" } : NO_META;
913
931
  if (depth > c.maxDepth) return failing(c, meta0, place, "#DEPTH!", `The nesting is deeper than ${c.maxDepth} documents.`);
914
932
 
915
- const world = c.machine.world;
916
- const found = lookupKind(world, doc.type);
917
- const kind = found.ok ? found.kind : undefined;
918
933
  let meta = meta0;
919
934
  let name: string | undefined;
920
935
  let unknown: boolean;
@@ -924,7 +939,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
924
939
  name = answer.evaluator;
925
940
  unknown = answer.error === "unknown-kind";
926
941
  } else {
927
- const quick = evaluatorOf(world, envelope, found);
942
+ const quick = evaluatorOf(world, envelope, kind);
928
943
  name = quick.name;
929
944
  unknown = quick.unknown;
930
945
  }
@@ -937,11 +952,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
937
952
  return failing(c, meta, place, "#NOEVAL", `No rung gives an evaluator for ${kindText}.`);
938
953
  }
939
954
 
940
- const split = splitParts(kind?.parts, doc.value);
941
- if (!split.ok) {
942
- const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
943
- return failing(c, meta, place, "#VALUE!", `${split.reason}${at} The kind is ${kindText}.`);
944
- }
955
+ if (!split.ok) return failing(c, meta, place, "#VALUE!", split.reason);
945
956
  const parts: CompiledPart[] = split.parts.map((slot) => {
946
957
  const item = slot.item;
947
958
  // The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
@@ -954,7 +965,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
954
965
  ? { key: slot.key, path: slot.path, code: compiled.code, run: compiled.run }
955
966
  : { key: slot.key, path: slot.path, name: header, code: compiled.code, run: compiled.run };
956
967
  });
957
- const typePath = found.ok ? found.path : [];
968
+ const typePath = read.typePath ?? [];
958
969
  const input: CompileInput = {
959
970
  kind: kindText,
960
971
  typePath,
@@ -967,15 +978,24 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
967
978
  fill: fillOf(split, doc),
968
979
  ...(address ? { address: true } : {}),
969
980
  };
981
+ // A compiler that throws fails its own document when that document evaluates, with its place (PLAN.FAIL-LATE).
970
982
  if (runner !== undefined) {
971
- const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
983
+ let inner: RunCode;
984
+ try {
985
+ inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
986
+ } catch (error) {
987
+ return failingWith(c, meta, placed(thrown(error, `the runner ${name}`), place));
988
+ }
972
989
  const run = placedRun(inner, place, name);
973
990
  return finish(c, meta, runnerCode(run, place, kindText), run);
974
991
  }
975
992
 
976
- const inner: Code = isCompiledEvaluator(evaluator as Evaluator)
977
- ? (evaluator as CompiledEvaluator).compile(input)
978
- : interpret(evaluator as EvalFn, input, split.fill);
993
+ let inner: Code;
994
+ try {
995
+ inner = isCompiledEvaluator(evaluator as Evaluator) ? (evaluator as CompiledEvaluator).compile(input) : interpret(evaluator as EvalFn, input, split.fill);
996
+ } catch (error) {
997
+ return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
998
+ }
979
999
  const code = placedCode(inner, place, name);
980
1000
  return finish(c, meta, code, runOf(code));
981
1001
  }
@@ -1022,12 +1042,15 @@ function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode
1022
1042
  };
1023
1043
  }
1024
1044
 
1025
- /** This function gives the code of a runner in an evaluation: its steps, which cannot wait. */
1045
+ /**
1046
+ * This function gives the code of a runner in an evaluation: its steps, which cannot wait. A step that waits gives
1047
+ * `#CALC!` at the place of the runner, where the error started (RUN.WAIT).
1048
+ */
1026
1049
  function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
1027
1050
  return (ctx, rt) =>
1028
1051
  rt.running
1029
1052
  ? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
1030
- : drive(run(ctx, rt));
1053
+ : placed(drive(run(ctx, rt)), place);
1031
1054
  }
1032
1055
 
1033
1056
  /** This function gives the steps of a run of an evaluator. The code runs at the first step, and it does not wait. */
@@ -1119,7 +1142,7 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
1119
1142
  try {
1120
1143
  root = compileNode(c, doc, [], options.declaredType, 0);
1121
1144
  } catch (error) {
1122
- const result = thrown(error, "the compilation");
1145
+ const result = placed(thrown(error, "the compilation"), []);
1123
1146
  root = {
1124
1147
  code: () => result,
1125
1148
  run: function* () {
package/src/formula.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Json, isRecord } from "./doc.ts";
1
+ import { type Json, type Key, isRecord } from "./doc.ts";
2
2
  import type { Kind, WorldEntry } from "./world.ts";
3
3
  import { type Context, type Resolution, valueOfThing } from "./deref.ts";
4
4
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
@@ -142,7 +142,7 @@ export function formulaKinds(): Readonly<Record<string, WorldEntry>> {
142
142
  },
143
143
  fn: {
144
144
  verbs: { eval: "formula.fn" },
145
- parts: { shape: "record", fields: { body: term }, required: ["params", "body"] },
145
+ parts: { shape: "record", fields: { params: { shape: "list", item: { shape: "text" }, distinct: true }, body: term }, required: ["params", "body"] },
146
146
  scope: [{ names: { listAt: ["params"] }, in: [["body"]] }],
147
147
  summary: "A function: its parameters are names for its body.",
148
148
  },
@@ -225,16 +225,18 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
225
225
  if (missing !== undefined && missing(reached)) return fail("#N/A", "The path reaches no value.");
226
226
  return ok(reached);
227
227
  };
228
- // The step documents of a path are its parts when the kind ref has a deep shape. They evaluate first, in order.
229
- const computed = parts.length > 0;
228
+ const held = (ctx: Context): Failed | undefined => {
229
+ if (head === undefined || !Object.hasOwn(ctx.names, head)) return undefined;
230
+ const bound = ctx.names[head];
231
+ return bound instanceof Held ? { ok: false, error: bound.error } : undefined;
232
+ };
233
+ if (parts.length === 0) return (ctx, rt) => held(ctx) ?? give(rt.resolve(ctx, path));
234
+ // The step documents of a path are its parts when the kind ref has a deep shape. They evaluate first, in order,
235
+ // thus the error of a step comes before the error of a binding that the path reads (FORMULA.STEPS).
230
236
  return (ctx, rt) => {
231
- if (head !== undefined && Object.hasOwn(ctx.names, head)) {
232
- const bound = ctx.names[head];
233
- if (bound instanceof Held) return { ok: false, error: bound.error };
234
- }
235
- if (!computed) return give(rt.resolve(ctx, path));
236
237
  const steps = valuesOf(parts, ctx, rt);
237
- return Array.isArray(steps) ? give(rt.resolve(ctx, fill(steps) as readonly unknown[])) : steps;
238
+ if (!Array.isArray(steps)) return steps;
239
+ return held(ctx) ?? give(rt.resolve(ctx, fill(steps) as readonly unknown[]));
238
240
  };
239
241
  }),
240
242
 
@@ -472,18 +474,60 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
472
474
  };
473
475
  }
474
476
 
477
+ /**
478
+ * This function makes the hook `known` of the formula kit for one profile (STATIC.KNOWN). The evaluator
479
+ * `formula.op` knows an operation of `lazy` or of `ops`. A profile whose `ops` is a function answers through a call
480
+ * of that function. A function that throws counts as an answer, because the run then gives `#CALC!`. The evaluator
481
+ * `formula.ext` knows an extension of `ext` and each standard extension. The hook knows each name of the other
482
+ * evaluators.
483
+ *
484
+ * @param profile - The operations and the extensions of the language.
485
+ * @returns The hook: `true`, or the reason of the `#NAME?` that the evaluator gives in a run.
486
+ */
487
+ export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[], evaluator: string) => true | string {
488
+ const hasOp = (name: string): boolean => {
489
+ if (profile.lazy !== undefined && Object.hasOwn(profile.lazy, name)) return true;
490
+ const ops = profile.ops;
491
+ if (ops === undefined) return false;
492
+ if (typeof ops !== "function") return Object.hasOwn(ops, name);
493
+ try {
494
+ return ops(name) !== undefined;
495
+ } catch {
496
+ return true;
497
+ }
498
+ };
499
+ const hasExt = (name: string): boolean => (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) || Object.hasOwn(STD_EXT, name);
500
+ return (path, evaluator) => {
501
+ if (evaluator !== "formula.op" && evaluator !== "formula.ext") return true;
502
+ // The evaluators give the same reasons when they do not find the name.
503
+ const name = path.slice(1).join(".");
504
+ if (evaluator === "formula.op") return hasOp(name) ? true : `The language has no operation ${name}.`;
505
+ return hasExt(name) ? true : `The language has no extension ${name}.`;
506
+ };
507
+ }
508
+
475
509
  /**
476
510
  * This function makes a machine for a formula language. The machine has the kinds and the evaluators of the
477
- * formula kit. It also has the kinds, the evaluators and the walkers that the language adds.
511
+ * formula kit. It also has the kinds, the evaluators and the walkers that the language adds. Its hook `known` comes
512
+ * from the profile (`formulaKnown`). A hook `known` of the language answers each name that the kit knows.
478
513
  *
479
514
  * @param profile - The operations and the rules of the language.
480
515
  * @param more - The parts of a machine that the language adds. Its kinds win over the kinds of the kit.
481
516
  * @returns The machine.
482
517
  */
483
518
  export function formulaMachine(profile: FormulaProfile = {}, more: Partial<Machine> = {}): Machine {
519
+ const kit = formulaKnown(profile);
520
+ // An evaluator or a runner of the language with the name of an evaluator of the kit replaces it, and the kit does not answer for it.
521
+ const replaced = (name: string): boolean => (more.evaluators !== undefined && Object.hasOwn(more.evaluators, name)) || (more.runners !== undefined && Object.hasOwn(more.runners, name));
522
+ const theirs = more.known;
523
+ const known = (path: readonly Key[], evaluator: string): true | string => {
524
+ const answer = replaced(evaluator) ? true : kit(path, evaluator);
525
+ return answer === true && theirs !== undefined ? theirs(path, evaluator) : answer;
526
+ };
484
527
  return {
485
528
  ...more,
486
529
  world: { kinds: { ...formulaKinds(), ...more.world?.kinds }, defaults: { ...more.world?.defaults } },
487
530
  evaluators: { ...formulaEvaluators(profile), ...more.evaluators },
531
+ known,
488
532
  };
489
533
  }