@mark1russell7/coeval 0.6.2 → 0.6.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 +6 -4
- package/build/index.d.ts +189 -22
- package/build/index.d.ts.map +1 -1
- package/build/index.js +370 -85
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +43 -54
- package/src/deref.ts +14 -3
- package/src/doc.ts +23 -2
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +192 -21
- package/src/index.ts +6 -1
- package/src/reference.ts +83 -12
- package/src/replay.ts +159 -0
- package/src/same.ts +42 -0
- package/src/stdlib.ts +4 -0
package/src/index.ts
CHANGED
|
@@ -118,7 +118,9 @@ export {
|
|
|
118
118
|
type EvalFn,
|
|
119
119
|
type EvalInput,
|
|
120
120
|
type EvalOptions,
|
|
121
|
+
type EvaluateOptions,
|
|
121
122
|
type Evaluator,
|
|
123
|
+
type Journal,
|
|
122
124
|
type Machine,
|
|
123
125
|
type Part,
|
|
124
126
|
type Perform,
|
|
@@ -135,6 +137,7 @@ export {
|
|
|
135
137
|
type Steps,
|
|
136
138
|
type StopSignal,
|
|
137
139
|
type TraceNode,
|
|
140
|
+
JOURNAL,
|
|
138
141
|
capKeys,
|
|
139
142
|
evaluate,
|
|
140
143
|
failures,
|
|
@@ -198,5 +201,7 @@ export {
|
|
|
198
201
|
jsTruthy,
|
|
199
202
|
} from "./formula.ts";
|
|
200
203
|
export { type Definitions, type ReferenceOptions, STD_DEFINITIONS, formulaDefinitions, reference } from "./reference.ts";
|
|
201
|
-
export { type AgreeOptions, type Agreement, type Tier, type TierResult, agree, sameValue } from "./agree.ts";
|
|
204
|
+
export { type AgreeOptions, type Agreement, type Tier, type TierInput, type TierResult, agree, sameValue } from "./agree.ts";
|
|
205
|
+
export { sameResult } from "./same.ts";
|
|
206
|
+
export { type Recorder, type ReplayEntry, type ReplayLog, type ReplayOptions, type ReplaySettle, recorder, replayer } from "./replay.ts";
|
|
202
207
|
export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
|
package/src/reference.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { CapsRule, Kind } from "./world.ts";
|
|
|
4
4
|
import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
|
|
6
6
|
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
7
|
-
import { type EvalError, type Failed, type Result, Failure, fail, ok, placed, thrown } from "./errors.ts";
|
|
7
|
+
import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
8
8
|
import {
|
|
9
9
|
type AllOptions,
|
|
10
10
|
type Effect,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type EvalInput,
|
|
13
13
|
type Machine,
|
|
14
14
|
type Part,
|
|
15
|
+
type Perform,
|
|
15
16
|
type RunFn,
|
|
16
17
|
type RunInput,
|
|
17
18
|
type RunPart,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
intersectCaps,
|
|
20
21
|
makeContext,
|
|
21
22
|
permits,
|
|
23
|
+
placeText,
|
|
22
24
|
} from "./evaluate.ts";
|
|
23
25
|
import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
|
|
24
26
|
|
|
@@ -45,6 +47,18 @@ export interface ReferenceOptions {
|
|
|
45
47
|
readonly definitions?: Definitions;
|
|
46
48
|
/** A list that gets each effect, in its order. */
|
|
47
49
|
readonly effects?: Effect[];
|
|
50
|
+
/** The mock of the host (RUN.MOCK). It answers each effect at once. Without it, an effect gives its payload. */
|
|
51
|
+
readonly perform?: Perform;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The scope of the addresses of effects (RUN.ADDRESS): the root of the evaluation, or one item of `all`. */
|
|
55
|
+
interface Scope {
|
|
56
|
+
/** The text before each address of the scope. */
|
|
57
|
+
readonly prefix: string;
|
|
58
|
+
/** The number of earlier effects at each place of the scope, by the text of the place. */
|
|
59
|
+
readonly counts: Map<string, number>;
|
|
60
|
+
/** The number of earlier uses of `all` in the scope. */
|
|
61
|
+
alls: number;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/** The state of one evaluation of the reference. */
|
|
@@ -53,6 +67,9 @@ interface Env {
|
|
|
53
67
|
readonly definitions: Definitions;
|
|
54
68
|
readonly effects: Effect[];
|
|
55
69
|
readonly maxDepth: number;
|
|
70
|
+
readonly perform: Perform | undefined;
|
|
71
|
+
/** The scope of the code that evaluates at this time. `all` changes it for each item. */
|
|
72
|
+
scope: Scope;
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
/** The code that answers a document: a function evaluator, a runner, or a gap of the reference. */
|
|
@@ -167,15 +184,52 @@ function resolveFully(machine: Machine, ctx: Context, path: readonly unknown[],
|
|
|
167
184
|
return options?.hops === true ? found : { ...found, hops: [] };
|
|
168
185
|
}
|
|
169
186
|
|
|
170
|
-
/**
|
|
171
|
-
function
|
|
187
|
+
/**
|
|
188
|
+
* This function causes an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). It records the effect with its
|
|
189
|
+
* address (RUN.ADDRESS) and its outcome (RUN.OUTCOME). The mock answers the effect at once (RUN.MOCK). Without a
|
|
190
|
+
* mock, the effect gives its payload (RUN.EVAL).
|
|
191
|
+
*/
|
|
192
|
+
function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
|
|
172
193
|
const keys = typeof kind === "string" ? kind.split(".") : kind;
|
|
173
194
|
const text = keys.join(".");
|
|
174
195
|
const machine = env.machine;
|
|
175
196
|
const allowed = machine.permits === undefined ? permits(ctx.caps, keys) : machine.permits(ctx.caps, keys, ctx);
|
|
176
197
|
if (!allowed) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
const at = placeText(place);
|
|
199
|
+
const count = env.scope.counts.get(at) ?? 0;
|
|
200
|
+
env.scope.counts.set(at, count + 1);
|
|
201
|
+
const effect: { -readonly [K in keyof Effect]: Effect[K] } = {
|
|
202
|
+
index: env.effects.length,
|
|
203
|
+
kind: text,
|
|
204
|
+
path: keys,
|
|
205
|
+
payload,
|
|
206
|
+
node: -1,
|
|
207
|
+
at: 0,
|
|
208
|
+
place,
|
|
209
|
+
context: ctx,
|
|
210
|
+
address: `${env.scope.prefix}${at}#${count}`,
|
|
211
|
+
};
|
|
212
|
+
env.effects.push(effect);
|
|
213
|
+
effect.outcome = answer(env.perform, effect, place);
|
|
214
|
+
return effect.outcome;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* This function gives the answer of the mock to an effect. A throw gives `#CALC!` with the cause, and a `Failure`
|
|
219
|
+
* gives its own error value. A promise gives `#CALC!`, because an evaluation does not wait.
|
|
220
|
+
*/
|
|
221
|
+
function answer(perform: Perform | undefined, effect: Effect, place: readonly Key[]): Result {
|
|
222
|
+
if (perform === undefined) return ok(effect.payload);
|
|
223
|
+
let given: unknown;
|
|
224
|
+
try {
|
|
225
|
+
given = perform(effect);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof Failure) return placed({ ok: false, error: error.error }, place);
|
|
228
|
+
return fail("#CALC!", `The host failed the effect ${effect.kind}: ${describe(error)}`, { place, cause: error });
|
|
229
|
+
}
|
|
230
|
+
if (!(given instanceof Promise)) return ok(given);
|
|
231
|
+
void given.catch(() => undefined);
|
|
232
|
+
return fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place });
|
|
179
233
|
}
|
|
180
234
|
|
|
181
235
|
/** This function evaluates a document. Each error gets the place of the document where it started (K5). */
|
|
@@ -247,7 +301,7 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
|
|
|
247
301
|
place: at.place,
|
|
248
302
|
...(at.address ? { address: true } : {}),
|
|
249
303
|
resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
|
|
250
|
-
effect: (kind, payload) =>
|
|
304
|
+
effect: (kind, payload) => cause(env, at.ctx, kind, payload, at.place),
|
|
251
305
|
fill,
|
|
252
306
|
};
|
|
253
307
|
}
|
|
@@ -275,9 +329,9 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
|
|
|
275
329
|
place: at.place,
|
|
276
330
|
resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
|
|
277
331
|
effect: function* (kind, payload) {
|
|
278
|
-
return
|
|
332
|
+
return cause(env, at.ctx, kind, payload, at.place);
|
|
279
333
|
},
|
|
280
|
-
all: (list, options) => together(list, options),
|
|
334
|
+
all: (list, options) => together(env, list, options),
|
|
281
335
|
fill,
|
|
282
336
|
};
|
|
283
337
|
}
|
|
@@ -287,14 +341,21 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
|
|
|
287
341
|
* goes on until it ends or waits. In an evaluation, a part that waits does not end, thus it keeps its place in the
|
|
288
342
|
* limit. With `failFast`, the first failure stops the parts that wait, and the later parts do not start
|
|
289
343
|
* (RUN.FAIL-FAST). When no part failed and a part waits, the steps wait, and the drive gives `#CALC!`.
|
|
344
|
+
*
|
|
345
|
+
* Each part has its own scope of addresses. Its prefix names the use of `all` in the scope around it, and the part
|
|
346
|
+
* (RUN.ADDRESS).
|
|
290
347
|
*/
|
|
291
|
-
function* together(list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
|
|
348
|
+
function* together(env: Env, list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
|
|
292
349
|
const limit = Math.max(1, options.concurrency ?? list.length);
|
|
293
350
|
const results: (Result | undefined)[] = list.map(() => undefined);
|
|
294
351
|
const waiting: { readonly steps: Steps; readonly promise: Promise<unknown> }[] = [];
|
|
352
|
+
const outer = env.scope;
|
|
353
|
+
const address = `${outer.prefix}all${outer.alls}`;
|
|
354
|
+
outer.alls += 1;
|
|
295
355
|
let failed = false;
|
|
296
356
|
for (let index = 0; index < list.length && !failed && waiting.length < limit; index += 1) {
|
|
297
357
|
let result: Result | undefined;
|
|
358
|
+
env.scope = { prefix: `${address}.${index}/`, counts: new Map(), alls: 0 };
|
|
298
359
|
try {
|
|
299
360
|
const steps = (list[index] as RunPart).evaluate();
|
|
300
361
|
const step = steps.next();
|
|
@@ -302,6 +363,8 @@ function* together(list: readonly RunPart[], options: AllOptions = {}): Generato
|
|
|
302
363
|
else waiting.push({ steps, promise: step.value });
|
|
303
364
|
} catch (error) {
|
|
304
365
|
result = thrown(error, "a part");
|
|
366
|
+
} finally {
|
|
367
|
+
env.scope = outer;
|
|
305
368
|
}
|
|
306
369
|
results[index] = result;
|
|
307
370
|
if (result !== undefined && options.failFast === true && !result.ok) failed = true;
|
|
@@ -325,16 +388,23 @@ function* together(list: readonly RunPart[], options: AllOptions = {}): Generato
|
|
|
325
388
|
/**
|
|
326
389
|
* This function evaluates a document by the rules, without a compilation. It is the reference of the plans: a plan,
|
|
327
390
|
* a traced plan and the second tier of a language must give its results. It covers evaluations, not runs: an effect
|
|
328
|
-
* gives its payload, and a part that waits gives `#CALC!`.
|
|
391
|
+
* gives the answer of the mock or its payload, and a part that waits gives `#CALC!`.
|
|
329
392
|
*
|
|
330
393
|
* @param machine - The world and the code of the evaluators.
|
|
331
394
|
* @param doc - The document.
|
|
332
395
|
* @param context - The names and the capabilities at the root.
|
|
333
|
-
* @param options - The type of a bare root value, the definitions of compiled code,
|
|
396
|
+
* @param options - The type of a bare root value, the definitions of compiled code, a list for the effects and a mock.
|
|
334
397
|
* @returns The result.
|
|
335
398
|
*/
|
|
336
399
|
export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
|
|
337
|
-
const env: Env = {
|
|
400
|
+
const env: Env = {
|
|
401
|
+
machine,
|
|
402
|
+
definitions: options.definitions ?? {},
|
|
403
|
+
effects: options.effects ?? [],
|
|
404
|
+
maxDepth: machine.maxDepth ?? 1000,
|
|
405
|
+
perform: options.perform,
|
|
406
|
+
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
407
|
+
};
|
|
338
408
|
const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
|
|
339
409
|
try {
|
|
340
410
|
return evaluateAt(env, { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx });
|
|
@@ -461,6 +531,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
|
|
|
461
531
|
if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
|
|
462
532
|
throw new Failure(result.error);
|
|
463
533
|
},
|
|
534
|
+
has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)),
|
|
464
535
|
toJson: () => {
|
|
465
536
|
const result = jsonOfNode(node, self, []);
|
|
466
537
|
if (result.ok) return result.value;
|
package/src/replay.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { docEquals } from "./doc.ts";
|
|
2
|
+
import { type Result, Failure, fail } from "./errors.ts";
|
|
3
|
+
import { type Effect, type Journal, type Perform, JOURNAL } from "./evaluate.ts";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Record and replay (R2 P2, D19). A recorder wraps the function `perform` of a host and keeps a log. The log has each
|
|
7
|
+
* effect with its address, its request and its outcome, and the order in which the waits of each `all` settled. A
|
|
8
|
+
* replayer answers each effect from the log by its address, thus a replay does no host effect. The kernel gives the
|
|
9
|
+
* address and the outcome of each effect. The journal on the function `perform` carries the order of the waits.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One effect of a log: its address, its request and its outcome. */
|
|
13
|
+
export interface ReplayEntry {
|
|
14
|
+
/** The address of the effect (RUN.ADDRESS), for example `1#0` or `all0.1/1#0`. */
|
|
15
|
+
readonly address: string;
|
|
16
|
+
/** The kind of the effect, as a text with dots. */
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
readonly payload: unknown;
|
|
19
|
+
/** The outcome of the effect (RUN.OUTCOME). It is absent when the recorded run stopped before the answer came. */
|
|
20
|
+
readonly outcome?: Result;
|
|
21
|
+
/** `true` when the host made the run wait for the outcome. */
|
|
22
|
+
readonly waited: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** An item of an `all` whose wait settled in the recorded run. */
|
|
26
|
+
export interface ReplaySettle {
|
|
27
|
+
/** The address of the `all`, for example `all0` or `all0.1/all0`. */
|
|
28
|
+
readonly all: string;
|
|
29
|
+
/** The index of the item. */
|
|
30
|
+
readonly item: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The log of a run: each effect in the order of the requests, and the order in which the waits of each `all` settled. */
|
|
34
|
+
export interface ReplayLog {
|
|
35
|
+
readonly effects: readonly ReplayEntry[];
|
|
36
|
+
readonly settles: readonly ReplaySettle[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A recorder: the function `perform` for a run, and the log of that run (RUN.RECORD). */
|
|
40
|
+
export interface Recorder {
|
|
41
|
+
/** The function to give the run. It gives each effect to the function `perform` of the host, and it records the effect. */
|
|
42
|
+
readonly perform: Perform;
|
|
43
|
+
/**
|
|
44
|
+
* This function gives the log of the run until this time. An effect gets its outcome when the answer comes.
|
|
45
|
+
*
|
|
46
|
+
* @returns The log.
|
|
47
|
+
*/
|
|
48
|
+
log(): ReplayLog;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The options of a replayer. */
|
|
52
|
+
export interface ReplayOptions {
|
|
53
|
+
/**
|
|
54
|
+
* `false`: each answer comes at once, also for an effect that waited in the recorded run. An evaluation needs this
|
|
55
|
+
* value, because it does not wait. The default is `true`: an effect waits when it waited in the recorded run.
|
|
56
|
+
*/
|
|
57
|
+
readonly wait?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* This function makes a recorder (RUN.RECORD). The recorder gives each effect to the function `perform` of the host.
|
|
62
|
+
* Its log keeps each effect with its address, its kind, its payload and the outcome that the kernel gives it. The log
|
|
63
|
+
* also keeps the order in which the waits of each `all` settle.
|
|
64
|
+
*
|
|
65
|
+
* @param perform - The function of the host. Without it, each effect gives its payload, as in a run without a host.
|
|
66
|
+
* @returns The function `perform` for the run, and the log.
|
|
67
|
+
*/
|
|
68
|
+
export function recorder(perform?: Perform): Recorder {
|
|
69
|
+
const seen: { readonly effect: Effect; waited: boolean }[] = [];
|
|
70
|
+
const settles: ReplaySettle[] = [];
|
|
71
|
+
const journal: Journal = {
|
|
72
|
+
settled: (all, item) => {
|
|
73
|
+
settles.push({ all, item });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const host = (effect: Effect): unknown => {
|
|
77
|
+
const entry = { effect, waited: false };
|
|
78
|
+
seen.push(entry);
|
|
79
|
+
const answer = perform === undefined ? effect.payload : perform(effect);
|
|
80
|
+
if (answer instanceof Promise) entry.waited = true;
|
|
81
|
+
return answer;
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
perform: Object.assign(host, { [JOURNAL]: journal }),
|
|
85
|
+
log: () => ({
|
|
86
|
+
effects: seen.map(({ effect, waited }) => ({
|
|
87
|
+
address: effect.address ?? "",
|
|
88
|
+
kind: effect.kind,
|
|
89
|
+
payload: effect.payload,
|
|
90
|
+
waited,
|
|
91
|
+
...(effect.outcome === undefined ? {} : { outcome: effect.outcome }),
|
|
92
|
+
})),
|
|
93
|
+
settles: [...settles],
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* This function makes a replayer: a function `perform` that answers each effect from a log, by its address (RUN.REPLAY). A
|
|
100
|
+
* recorded error comes again with its code, its reason, its place and its cause. The waits of each `all` settle in the
|
|
101
|
+
* recorded order. An effect that the log does not have at its address gives `#REPLAY!` (RUN.REPLAY-MISMATCH).
|
|
102
|
+
*
|
|
103
|
+
* A replayer keeps its place in the order of the waits, thus each run needs a new replayer.
|
|
104
|
+
*
|
|
105
|
+
* @param log - The log of a recorder.
|
|
106
|
+
* @param options - `wait: false` for an evaluation.
|
|
107
|
+
* @returns The function `perform` for the replay.
|
|
108
|
+
*/
|
|
109
|
+
export function replayer(log: ReplayLog, options: ReplayOptions = {}): Perform {
|
|
110
|
+
const wait = options.wait !== false;
|
|
111
|
+
const entries = new Map<string, ReplayEntry>();
|
|
112
|
+
for (const entry of log.effects) entries.set(entry.address, entry);
|
|
113
|
+
const order = new Map<string, number[]>();
|
|
114
|
+
for (const settle of log.settles) {
|
|
115
|
+
const items = order.get(settle.all);
|
|
116
|
+
if (items === undefined) order.set(settle.all, [settle.item]);
|
|
117
|
+
else items.push(settle.item);
|
|
118
|
+
}
|
|
119
|
+
const journal: Journal = {
|
|
120
|
+
next: (all, waiting) => {
|
|
121
|
+
const items = order.get(all);
|
|
122
|
+
const item = items?.[0];
|
|
123
|
+
if (items === undefined || item === undefined || !waiting.includes(item)) return undefined;
|
|
124
|
+
items.shift();
|
|
125
|
+
return item;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
const perform = (effect: Effect): unknown => {
|
|
129
|
+
const entry = effect.address === undefined ? undefined : entries.get(effect.address);
|
|
130
|
+
const wrong = mismatch(effect, entry);
|
|
131
|
+
if (wrong !== undefined || entry === undefined) throw wrong;
|
|
132
|
+
const outcome = entry.outcome;
|
|
133
|
+
const failure =
|
|
134
|
+
outcome === undefined
|
|
135
|
+
? replayError(effect, `The log has no outcome for the effect ${effect.kind} at ${effect.address ?? ""}: the recorded run stopped before the answer.`)
|
|
136
|
+
: outcome.ok
|
|
137
|
+
? undefined
|
|
138
|
+
: new Failure(outcome.error);
|
|
139
|
+
const value = outcome?.ok === true ? outcome.value : undefined;
|
|
140
|
+
if (wait && entry.waited) return failure === undefined ? Promise.resolve(value) : Promise.reject(failure);
|
|
141
|
+
if (failure !== undefined) throw failure;
|
|
142
|
+
return value;
|
|
143
|
+
};
|
|
144
|
+
return Object.assign(perform, { [JOURNAL]: journal });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** This function gives the error of an effect that the log does not have at its address, or `undefined`. */
|
|
148
|
+
function mismatch(effect: Effect, entry: ReplayEntry | undefined): Failure | undefined {
|
|
149
|
+
const at = effect.address ?? "";
|
|
150
|
+
if (entry === undefined) return replayError(effect, `The log has no effect at the address ${at}, and the run asks for the effect ${effect.kind}.`);
|
|
151
|
+
if (entry.kind !== effect.kind) return replayError(effect, `The log has the effect ${entry.kind} at the address ${at}, and the run asks for the effect ${effect.kind}.`);
|
|
152
|
+
if (!docEquals(entry.payload, effect.payload)) return replayError(effect, `The effect ${effect.kind} at the address ${at} has another payload than the log.`);
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** This function gives the exception that carries `#REPLAY!` to the kernel, with the place of the effect. */
|
|
157
|
+
function replayError(effect: Effect, reason: string): Failure {
|
|
158
|
+
return new Failure(fail("#REPLAY!", reason, { place: effect.place }).error);
|
|
159
|
+
}
|
package/src/same.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Key, docEquals, equalWith } from "./doc.ts";
|
|
2
|
+
import type { EvalError, Result } from "./errors.ts";
|
|
3
|
+
import { Held } from "./formula.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This function tells if two results are the same for an early cutoff (RESULT.SAME). A schedule uses it to stop the
|
|
7
|
+
* work when a part that evaluates again gives the same result. A wrong `true` keeps a stale value, but a wrong `false`
|
|
8
|
+
* only costs work. Thus each rule gives `false` when it is not sure.
|
|
9
|
+
*
|
|
10
|
+
* - Two values compare by the rules of `docEquals`. Lists and plain records compare by their items and fields. Each
|
|
11
|
+
* other value compares with `Object.is`.
|
|
12
|
+
* - Thus `-0` is not `0`, and `NaN` is `NaN`. A date, a map and a closure of `fn` are the same only as themselves.
|
|
13
|
+
* - Two errors are the same when they have the same code, reason and place (RESULT.SAME-ERROR). The cause is not
|
|
14
|
+
* compared. A `Held` value compares its error by the same rule.
|
|
15
|
+
* - A value and an error are not the same.
|
|
16
|
+
*
|
|
17
|
+
* `sameValue` of `agree` uses the same walk, with rules for two tiers (D20). It compares two closures by their
|
|
18
|
+
* parameters, and two errors by their code and place.
|
|
19
|
+
*
|
|
20
|
+
* @param a - The earlier result.
|
|
21
|
+
* @param b - The new result.
|
|
22
|
+
* @param equal - The rule of the host for two values, for example two dates. It answers first, and `undefined` gives the rules above.
|
|
23
|
+
* @returns `true` when the results are the same.
|
|
24
|
+
*/
|
|
25
|
+
export function sameResult(a: Result<unknown>, b: Result<unknown>, equal?: (a: unknown, b: unknown) => boolean | undefined): boolean {
|
|
26
|
+
if (a.ok && b.ok) return equalWith(a.value, b.value, equal === undefined ? sameHeld : (x, y) => equal(x, y) ?? sameHeld(x, y));
|
|
27
|
+
if (!a.ok && !b.ok) return sameError(a.error, b.error);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The rule of `sameResult` for held errors. Two `Held` values are the same when their errors are the same. */
|
|
32
|
+
function sameHeld(x: unknown, y: unknown): boolean | undefined {
|
|
33
|
+
if (x instanceof Held || y instanceof Held) return x instanceof Held && y instanceof Held && sameError(x.error, y.error);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Two errors are the same when they have the same code, reason and place. An error without a place has no place in both. */
|
|
38
|
+
function sameError(a: EvalError, b: EvalError): boolean {
|
|
39
|
+
if (a.code !== b.code || a.reason !== b.reason) return false;
|
|
40
|
+
if (a.place === undefined || b.place === undefined) return a.place === b.place;
|
|
41
|
+
return docEquals(a.place as readonly Key[], b.place as readonly Key[]);
|
|
42
|
+
}
|
package/src/stdlib.ts
CHANGED
|
@@ -324,6 +324,10 @@ function treeThing(parts: readonly Part[], context: Context, type: string, place
|
|
|
324
324
|
if (!result.ok) throw new Failure(result.error);
|
|
325
325
|
return result.value;
|
|
326
326
|
},
|
|
327
|
+
has(key: Key): boolean {
|
|
328
|
+
// DEREF.HOST-UNDEFINED: an attribute with the value undefined is a field, not a missing step.
|
|
329
|
+
return key === "parent" || key === "type" || parts.some((part) => part.key === String(key));
|
|
330
|
+
},
|
|
327
331
|
toJson(): unknown {
|
|
328
332
|
const result = state.json(new Set());
|
|
329
333
|
if (!result.ok) throw new Failure(result.error);
|