@mark1russell7/coeval 0.4.0 → 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 +4 -3
- package/build/index.d.ts +597 -331
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1195 -41
- 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 +96 -11
- package/src/formula.ts +10 -8
- package/src/generate.ts +367 -0
- package/src/index.ts +6 -0
- package/src/reference.ts +571 -0
- package/src/static.ts +39 -7
- package/src/stdlib.ts +6 -2
- package/src/world.ts +24 -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
|
|
2
|
-
import { type Split, splitParts } from "./parts.ts";
|
|
2
|
+
import { type Split, pathMatches, splitParts } from "./parts.ts";
|
|
3
3
|
import { ask, type Answer } from "./ladder.ts";
|
|
4
|
-
import { type Lookup, type Side, type World, lookupKind } from "./world.ts";
|
|
4
|
+
import { type CapsRule, type Lookup, type Side, type World, lookupKind } 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,
|
|
@@ -74,6 +74,58 @@ export function narrow(parent: Context, next: Context): Context {
|
|
|
74
74
|
return caps.length === next.caps.length ? next : { ...next, caps };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** No capabilities. One list serves each masked part. */
|
|
78
|
+
const NO_CAPS: readonly Cap[] = Object.freeze([]);
|
|
79
|
+
|
|
80
|
+
/** No capability rules. One list serves each part that no rule narrows. */
|
|
81
|
+
const NO_RULES: readonly CapsRule[] = Object.freeze([]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* This function gives the capabilities that two lists both permit. An effect is permitted by the result when each
|
|
85
|
+
* list permits it. When the list `b` permits each capability of `a`, the result is the list `a` itself.
|
|
86
|
+
*
|
|
87
|
+
* @param a - The first list, for example the capabilities of a document.
|
|
88
|
+
* @param b - The second list, for example the list of a rule `only`.
|
|
89
|
+
* @returns The capabilities that both lists permit.
|
|
90
|
+
*/
|
|
91
|
+
export function intersectCaps(a: readonly Cap[], b: readonly Cap[]): readonly Cap[] {
|
|
92
|
+
if (a.every((cap) => permits(b, cap))) return a;
|
|
93
|
+
const out: Cap[] = [];
|
|
94
|
+
for (const cap of b) if (permits(a, cap)) out.push(cap);
|
|
95
|
+
for (const cap of a) if (permits(b, cap) && !permits(out, cap)) out.push(cap);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** This function gives the capability rules of a kind that cover a part and change its capabilities. */
|
|
100
|
+
function rulesFor(rules: readonly CapsRule[] | undefined, path: readonly Key[]): readonly CapsRule[] {
|
|
101
|
+
if (rules === undefined || rules.length === 0) return NO_RULES;
|
|
102
|
+
const out = rules.filter((rule) => rule.rule !== "same" && (rule.in === undefined || rule.in.some((pattern) => pathMatches(path, pattern))));
|
|
103
|
+
return out.length === 0 ? NO_RULES : out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** This function applies the rules that cover a part to the capabilities of its document. */
|
|
107
|
+
function capsUnder(rules: readonly CapsRule[], caps: readonly Cap[]): readonly Cap[] {
|
|
108
|
+
let out = caps;
|
|
109
|
+
for (const rule of rules) {
|
|
110
|
+
if (rule.rule === "mask") return NO_CAPS;
|
|
111
|
+
if (rule.rule === "only") out = intersectCaps(out, rule.caps);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* This function gives the capabilities that a kind gives one of its parts, by the capability rules of its contract.
|
|
118
|
+
* The kernel and the static kit both use it, thus a run and a check narrow the same way.
|
|
119
|
+
*
|
|
120
|
+
* @param rules - The capability rules of the kind (`Kind.caps`).
|
|
121
|
+
* @param path - The path of the part in the value of the document.
|
|
122
|
+
* @param caps - The capabilities of the document.
|
|
123
|
+
* @returns The capabilities of the part: the same list or a smaller one.
|
|
124
|
+
*/
|
|
125
|
+
export function partCaps(rules: readonly CapsRule[] | undefined, path: readonly Key[], caps: readonly Cap[]): readonly Cap[] {
|
|
126
|
+
return capsUnder(rulesFor(rules, path), caps);
|
|
127
|
+
}
|
|
128
|
+
|
|
77
129
|
/**
|
|
78
130
|
* The steps of an evaluation that can wait. It yields a promise only when the host makes it wait. A runner takes the
|
|
79
131
|
* result of a part with `yield*`, thus a run goes on at once until an effect waits. A runner does not yield a
|
|
@@ -780,7 +832,11 @@ interface Compiler {
|
|
|
780
832
|
}
|
|
781
833
|
|
|
782
834
|
function failing(c: Compiler, meta: Meta, place: readonly Key[], code: ErrorCode, reason: string): { readonly code: Code; readonly run: RunCode } {
|
|
783
|
-
|
|
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 } {
|
|
784
840
|
const plain: Code = () => result;
|
|
785
841
|
return finish(c, meta, plain, function* () {
|
|
786
842
|
return result;
|
|
@@ -892,7 +948,10 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
892
948
|
}
|
|
893
949
|
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
894
950
|
const item = slot.item;
|
|
895
|
-
|
|
951
|
+
// The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
|
|
952
|
+
const own = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
|
|
953
|
+
const rules = rulesFor(kind?.caps, slot.path);
|
|
954
|
+
const compiled = rules.length === 0 ? own : gated(rules, own);
|
|
896
955
|
// The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
|
|
897
956
|
const header = typeof (item as { readonly name?: unknown } | null)?.name === "string" && isEnvelope(item) ? (item as { readonly name: string }).name : undefined;
|
|
898
957
|
return header === undefined
|
|
@@ -912,15 +971,24 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
912
971
|
fill: fillOf(split, doc),
|
|
913
972
|
...(address ? { address: true } : {}),
|
|
914
973
|
};
|
|
974
|
+
// A compiler that throws fails its own document when that document evaluates, with its place (PLAN.FAIL-LATE).
|
|
915
975
|
if (runner !== undefined) {
|
|
916
|
-
|
|
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
|
+
}
|
|
917
982
|
const run = placedRun(inner, place, name);
|
|
918
983
|
return finish(c, meta, runnerCode(run, place, kindText), run);
|
|
919
984
|
}
|
|
920
985
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
: 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
|
+
}
|
|
924
992
|
const code = placedCode(inner, place, name);
|
|
925
993
|
return finish(c, meta, code, runOf(code));
|
|
926
994
|
}
|
|
@@ -942,6 +1010,20 @@ function placedCode(inner: Code, place: readonly Key[], name: string): Code {
|
|
|
942
1010
|
};
|
|
943
1011
|
}
|
|
944
1012
|
|
|
1013
|
+
/**
|
|
1014
|
+
* This function gives the code of a part that capability rules cover. The code narrows the context of the part by
|
|
1015
|
+
* the rules before the part starts.
|
|
1016
|
+
*/
|
|
1017
|
+
function gated(rules: readonly CapsRule[], part: { readonly code: Code; readonly run: RunCode }): { readonly code: Code; readonly run: RunCode } {
|
|
1018
|
+
const inner = part.code;
|
|
1019
|
+
const steps = part.run;
|
|
1020
|
+
const under = (ctx: Context): Context => {
|
|
1021
|
+
const caps = capsUnder(rules, ctx.caps);
|
|
1022
|
+
return caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0) ? ctx : { ...ctx, caps };
|
|
1023
|
+
};
|
|
1024
|
+
return { code: (ctx, rt) => inner(under(ctx), rt), run: (ctx, rt) => steps(under(ctx), rt) };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
945
1027
|
/** This function gives the steps of a runner, with the place of each error and of each exception. */
|
|
946
1028
|
function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
|
|
947
1029
|
return function* (ctx, rt) {
|
|
@@ -953,12 +1035,15 @@ function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode
|
|
|
953
1035
|
};
|
|
954
1036
|
}
|
|
955
1037
|
|
|
956
|
-
/**
|
|
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
|
+
*/
|
|
957
1042
|
function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
|
|
958
1043
|
return (ctx, rt) =>
|
|
959
1044
|
rt.running
|
|
960
1045
|
? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
|
|
961
|
-
: drive(run(ctx, rt));
|
|
1046
|
+
: placed(drive(run(ctx, rt)), place);
|
|
962
1047
|
}
|
|
963
1048
|
|
|
964
1049
|
/** This function gives the steps of a run of an evaluator. The code runs at the first step, and it does not wait. */
|
|
@@ -1050,7 +1135,7 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
|
|
|
1050
1135
|
try {
|
|
1051
1136
|
root = compileNode(c, doc, [], options.declaredType, 0);
|
|
1052
1137
|
} catch (error) {
|
|
1053
|
-
const result = thrown(error, "the compilation");
|
|
1138
|
+
const result = placed(thrown(error, "the compilation"), []);
|
|
1054
1139
|
root = {
|
|
1055
1140
|
code: () => result,
|
|
1056
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
|
|