@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/README.md +5 -3
- package/build/index.d.ts +288 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1291 -86
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +237 -0
- package/src/doc.ts +26 -8
- package/src/errors.ts +6 -1
- package/src/evaluate.ts +51 -28
- package/src/formula.ts +55 -11
- package/src/generate.ts +388 -0
- package/src/index.ts +5 -1
- package/src/parts.ts +59 -3
- package/src/reference.ts +566 -0
- package/src/static.ts +17 -16
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.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
|
|
225
|
-
* the same items in the same order, and the same fields. The order of the fields
|
|
226
|
-
*
|
|
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
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
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 (
|
|
242
|
-
for (let index = 0; 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
|
-
|
|
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,
|
|
2
|
+
import { type Split, partsOf, pathMatches } from "./parts.ts";
|
|
3
3
|
import { ask, type Answer } from "./ladder.ts";
|
|
4
|
-
import {
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
898
|
-
const 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
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
return failing(c,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
}
|