@mark1russell7/coeval 0.4.1 → 0.4.2

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.2",
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,231 @@
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`. Two closures of `fn` are the same when they have the same parameters. Two `Held` values
78
+ * are the same when their errors have the same code and place. Identity is not compared.
79
+ *
80
+ * @param a - The first value.
81
+ * @param b - The second value.
82
+ * @param equal - The rule of the caller for other values. It answers first, and `undefined` gives the rules above.
83
+ * @returns `true` when the values are the same.
84
+ */
85
+ export function sameValue(a: unknown, b: unknown, equal?: (a: unknown, b: unknown) => boolean | undefined): boolean {
86
+ const stack: (readonly [unknown, unknown])[] = [[a, b]];
87
+ const seen = new Map<object, Set<object>>();
88
+ while (stack.length > 0) {
89
+ const [x, y] = stack.pop() as readonly [unknown, unknown];
90
+ if (Object.is(x, y)) continue;
91
+ const told = equal?.(x, y);
92
+ if (told !== undefined) {
93
+ if (told) continue;
94
+ return false;
95
+ }
96
+ if (typeof x !== "object" && typeof x !== "function") return false;
97
+ if (typeof y !== "object" && typeof y !== "function") return false;
98
+ if (x === null || y === null) return false;
99
+ // A pair that the comparison already visits is the same, unless another pair shows a difference.
100
+ const pairs = seen.get(x) ?? new Set<object>();
101
+ if (pairs.has(y)) continue;
102
+ pairs.add(y);
103
+ seen.set(x, pairs);
104
+ if (isClosure(x) || isClosure(y)) {
105
+ if (!(isClosure(x) && isClosure(y))) return false;
106
+ stack.push([closureParams(x), closureParams(y)]);
107
+ continue;
108
+ }
109
+ if (x instanceof Held || y instanceof Held) {
110
+ if (!(x instanceof Held && y instanceof Held) || errorDiffers(x.error, y.error, false) !== undefined) return false;
111
+ continue;
112
+ }
113
+ if (Array.isArray(x) || Array.isArray(y)) {
114
+ if (!Array.isArray(x) || !Array.isArray(y) || x.length !== y.length) return false;
115
+ for (let index = 0; index < x.length; index += 1) stack.push([x[index], y[index]]);
116
+ continue;
117
+ }
118
+ if (!isRecord(x) || !isRecord(y)) return false;
119
+ const keys = Object.keys(x);
120
+ if (keys.length !== Object.keys(y).length) return false;
121
+ for (const key of keys) {
122
+ if (!Object.hasOwn(y, key)) return false;
123
+ stack.push([x[key], y[key]]);
124
+ }
125
+ }
126
+ return true;
127
+ }
128
+
129
+ const CLOSURE = Symbol.for("coeval.closure");
130
+
131
+ function closureParams(fn: object): unknown {
132
+ return (fn as { readonly [CLOSURE]?: { readonly params: readonly string[] } })[CLOSURE]?.params;
133
+ }
134
+
135
+ function codeAndPlace(error: EvalError): string {
136
+ return error.place === undefined ? error.code : `${error.code} at [${error.place.join(", ")}]`;
137
+ }
138
+
139
+ /** This function gives the difference of two errors, or `undefined` when they agree. */
140
+ function errorDiffers(a: EvalError, b: EvalError, strict: boolean): string | undefined {
141
+ if (a.code !== b.code) return `The plan gives ${codeAndPlace(a)}, and the tier gives ${codeAndPlace(b)}.`;
142
+ if (strict && (a.place === undefined || b.place === undefined)) return `An error ${a.code} has no place.`;
143
+ if (a.place !== undefined && b.place !== undefined && !docEquals(a.place as readonly Key[], b.place as readonly Key[])) {
144
+ return `The error ${a.code} has the place [${a.place.join(", ")}] in the plan, and [${b.place.join(", ")}] in the tier.`;
145
+ }
146
+ if (strict && a.reason !== b.reason) return `The error ${a.code} has another reason in the tier: "${b.reason}" for "${a.reason}".`;
147
+ return undefined;
148
+ }
149
+
150
+ /** A host stack overflow is the one permitted difference (VALUE.DEPTH): the tiers use different numbers of frames. */
151
+ function overflowed(result: Result): boolean {
152
+ return !result.ok && result.error.code === "#DEPTH!" && isStackOverflow(result.error.cause);
153
+ }
154
+
155
+ /** This function gives the difference of two results, or `undefined` when they agree. */
156
+ function resultDiffers(a: Result, b: Result, options: AgreeOptions): string | undefined {
157
+ if (a.ok && b.ok) return sameValue(a.value, b.value, options.equal) ? undefined : "The plan and the tier give different values.";
158
+ if (!a.ok && !b.ok) return errorDiffers(a.error, b.error, options.strict === true);
159
+ 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.`;
160
+ }
161
+
162
+ /** This function gives the difference of two lists of effects, or `undefined` when they agree (EFFECT.ORDER). */
163
+ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: AgreeOptions): string | undefined {
164
+ if (a.length !== b.length) return `The plan causes ${a.length} effects, and the tier causes ${b.length}.`;
165
+ for (let index = 0; index < a.length; index += 1) {
166
+ const x = a[index] as Effect;
167
+ const y = b[index] as Effect;
168
+ if (x.kind !== y.kind || !docEquals(x.place as readonly Key[], y.place as readonly Key[]) || !sameValue(x.payload, y.payload, options.equal)) {
169
+ return `The effect ${index} differs: ${x.kind} at [${x.place.join(", ")}] in the plan, and ${y.kind} at [${y.place.join(", ")}] in the tier.`;
170
+ }
171
+ }
172
+ return undefined;
173
+ }
174
+
175
+ /**
176
+ * This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
177
+ * results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
178
+ * both have one. The effects must have the same kinds, places and payloads, in the same order. A language gives its
179
+ * own tier with the option `other`.
180
+ *
181
+ * @param machine - The world and the code of the evaluators.
182
+ * @param doc - The document.
183
+ * @param context - The names and the capabilities at the root.
184
+ * @param options - The tiers, the definitions for the reference, the rules of the comparison and the second tier.
185
+ * @returns The result of the plan, or the first tier that disagrees and the reason.
186
+ */
187
+ export function agree(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeOptions = {}): Agreement {
188
+ const declared = options.declaredType === undefined ? {} : { declaredType: options.declaredType };
189
+ const observe = options.observe;
190
+ observe?.();
191
+ const plan = prepare(machine, doc, declared);
192
+ const effects: Effect[] = [];
193
+ const result = plan.evaluate(context, { effects });
194
+ const saw = observe?.();
195
+
196
+ const differs = (other: Result, otherEffects: readonly Effect[] | undefined): string | undefined => {
197
+ if (overflowed(result) || overflowed(other)) return undefined;
198
+ const why = resultDiffers(result, other, options) ?? (otherEffects === undefined ? undefined : effectsDiffer(effects, otherEffects, options));
199
+ if (why !== undefined || observe === undefined) return why;
200
+ return sameValue(saw, observe(), options.equal) ? undefined : "The host saw other calls in the tier.";
201
+ };
202
+
203
+ for (const tier of options.tiers ?? ["reference", "traced", "again"]) {
204
+ let other: Result;
205
+ let otherEffects: Effect[] = [];
206
+ if (tier === "reference") {
207
+ other = reference(machine, doc, context, { ...declared, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
208
+ } else if (tier === "traced") {
209
+ const traced = evaluate(machine, doc, context, options.declaredType);
210
+ other = traced.result;
211
+ otherEffects = [...traced.effects];
212
+ } else {
213
+ other = plan.evaluate(context, { effects: otherEffects });
214
+ }
215
+ const why = differs(other, otherEffects);
216
+ if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
217
+ }
218
+
219
+ if (options.other !== undefined) {
220
+ let given: Result | TierResult;
221
+ try {
222
+ given = options.other(doc, context);
223
+ } catch (error) {
224
+ given = thrown(error, "the tier");
225
+ }
226
+ const other = "result" in given ? given.result : given;
227
+ const why = differs(other, "result" in given ? given.effects : undefined);
228
+ if (why !== undefined) return { ok: false, tier: "other", plan: result, other, reason: why };
229
+ }
230
+ return { ok: true, result, effects };
231
+ }
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
@@ -832,7 +832,11 @@ interface Compiler {
832
832
  }
833
833
 
834
834
  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 });
835
+ return failingWith(c, meta, fail(code, reason, { place }));
836
+ }
837
+
838
+ /** This function gives the code of a document that fails with one result at each evaluation. */
839
+ function failingWith(c: Compiler, meta: Meta, result: Result): { readonly code: Code; readonly run: RunCode } {
836
840
  const plain: Code = () => result;
837
841
  return finish(c, meta, plain, function* () {
838
842
  return result;
@@ -967,15 +971,24 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
967
971
  fill: fillOf(split, doc),
968
972
  ...(address ? { address: true } : {}),
969
973
  };
974
+ // A compiler that throws fails its own document when that document evaluates, with its place (PLAN.FAIL-LATE).
970
975
  if (runner !== undefined) {
971
- const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
976
+ let inner: RunCode;
977
+ try {
978
+ inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
979
+ } catch (error) {
980
+ return failingWith(c, meta, placed(thrown(error, `the runner ${name}`), place));
981
+ }
972
982
  const run = placedRun(inner, place, name);
973
983
  return finish(c, meta, runnerCode(run, place, kindText), run);
974
984
  }
975
985
 
976
- const inner: Code = isCompiledEvaluator(evaluator as Evaluator)
977
- ? (evaluator as CompiledEvaluator).compile(input)
978
- : interpret(evaluator as EvalFn, input, split.fill);
986
+ let inner: Code;
987
+ try {
988
+ inner = isCompiledEvaluator(evaluator as Evaluator) ? (evaluator as CompiledEvaluator).compile(input) : interpret(evaluator as EvalFn, input, split.fill);
989
+ } catch (error) {
990
+ return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
991
+ }
979
992
  const code = placedCode(inner, place, name);
980
993
  return finish(c, meta, code, runOf(code));
981
994
  }
@@ -1022,12 +1035,15 @@ function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode
1022
1035
  };
1023
1036
  }
1024
1037
 
1025
- /** This function gives the code of a runner in an evaluation: its steps, which cannot wait. */
1038
+ /**
1039
+ * This function gives the code of a runner in an evaluation: its steps, which cannot wait. A step that waits gives
1040
+ * `#CALC!` at the place of the runner, where the error started (RUN.WAIT).
1041
+ */
1026
1042
  function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
1027
1043
  return (ctx, rt) =>
1028
1044
  rt.running
1029
1045
  ? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
1030
- : drive(run(ctx, rt));
1046
+ : placed(drive(run(ctx, rt)), place);
1031
1047
  }
1032
1048
 
1033
1049
  /** 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 +1135,7 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
1119
1135
  try {
1120
1136
  root = compileNode(c, doc, [], options.declaredType, 0);
1121
1137
  } catch (error) {
1122
- const result = thrown(error, "the compilation");
1138
+ const result = placed(thrown(error, "the compilation"), []);
1123
1139
  root = {
1124
1140
  code: () => result,
1125
1141
  run: function* () {
package/src/formula.ts CHANGED
@@ -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