@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/README.md +1 -0
- package/build/index.d.ts +208 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1096 -25
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +231 -0
- package/src/errors.ts +6 -1
- package/src/evaluate.ts +24 -8
- package/src/formula.ts +10 -8
- package/src/generate.ts +367 -0
- package/src/index.ts +3 -0
- package/src/reference.ts +571 -0
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|