@mark1russell7/coeval 0.6.2 → 0.7.0

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/src/reads.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { docEquals } from "./doc.ts";
2
+ import { type Result, ok } from "./errors.ts";
3
+ import type { Context, Hop } from "./deref.ts";
4
+ import { type Machine, type Plan, type Read, readAgain, rootContext } from "./evaluate.ts";
5
+ import { Held } from "./formula.ts";
6
+ import { sameResult } from "./same.ts";
7
+
8
+ /*
9
+ * The verifying trace of a schedule (R4 P3, D26). An evaluation with the option `reads` records each path that it
10
+ * resolves, and what the path reached. A pure document is a function of its reads. Thus when each free read reaches
11
+ * the same thing in a new context, the old result stays valid, and the schedule keeps it without an evaluation.
12
+ */
13
+
14
+ /** The answer of `verifyReads`: the old result stays valid, or the first read that reaches another thing. */
15
+ export type ReadsCheck =
16
+ | { readonly same: true }
17
+ | {
18
+ readonly same: false;
19
+ /** The index of the first changed read in the list. */
20
+ readonly index: number;
21
+ /** The first changed read, as the evaluation recorded it. */
22
+ readonly read: Read;
23
+ /** What the path of that read reaches in the new context. */
24
+ readonly now: Result;
25
+ };
26
+
27
+ /** The options of `verifyReads`. */
28
+ export interface VerifyOptions {
29
+ /**
30
+ * The rule of the host for two things, as in `sameResult`. It answers first, and `undefined` gives the rules of
31
+ * `sameResult`. An example is a rule for a host thing that keeps a version.
32
+ *
33
+ * @param a - The thing of the recorded read.
34
+ * @param b - The thing in the new context.
35
+ * @returns `true` when the two things are the same, or `undefined` for the rules of `sameResult`.
36
+ */
37
+ readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
38
+ }
39
+
40
+ /** One answer serves each list whose reads are all the same. */
41
+ const SAME: ReadsCheck = /* @__PURE__ */ Object.freeze({ same: true });
42
+
43
+ /** This function tells if two lists of hops are the same: the same steps, rungs and things, in order. */
44
+ function sameHops(a: readonly Hop[], b: readonly Hop[] | undefined, equal: VerifyOptions["equal"]): boolean {
45
+ if (b === undefined || a.length !== b.length) return false;
46
+ for (let index = 0; index < a.length; index += 1) {
47
+ const x = a[index] as Hop;
48
+ const y = b[index] as Hop;
49
+ if (x.by !== y.by || !docEquals(x.step, y.step) || !sameResult(ok(x.thing), ok(y.thing), equal)) return false;
50
+ }
51
+ return true;
52
+ }
53
+
54
+ /**
55
+ * This function gives the error of a name that holds a held error and stops a longer path, or `undefined`. A
56
+ * reference of the formula kit tests the binding of its first name before it resolves the rest of the path
57
+ * (FORMULA.LET). The resolution of the whole path does not show that test.
58
+ */
59
+ function heldStop(root: Context, path: readonly unknown[]): Result | undefined {
60
+ const name = path[1];
61
+ if (path.length <= 2 || path[0] !== "names" || typeof name !== "string" || !Object.hasOwn(root.names, name)) return undefined;
62
+ const bound = root.names[name];
63
+ return bound instanceof Held ? { ok: false, error: bound.error } : undefined;
64
+ }
65
+
66
+ /**
67
+ * This function tells if an old result stays valid in a new context (READS.VERIFY, D26). It resolves the path of
68
+ * each free read again, in the order of the list, and it compares what the path reaches with `sameResult`. It stops at
69
+ * the first read that reaches another thing, and it gives that read (READS.CHANGED).
70
+ *
71
+ * - A read that is not free reads a binding that a kind made from earlier reads, for example a name of `let`. The
72
+ * function does not resolve it again.
73
+ * - A thing compares as in `sameResult`. Thus a host thing or a date is the same only as itself. A host gives a new
74
+ * object for a new state, or the option `equal`.
75
+ * - A longer path through a name that holds a `Held` in the new context is a changed read. A reference of the formula
76
+ * kit gives the error of that binding, and the resolution of the path does not show it.
77
+ * - The answer is sound only for a pure document (FACTS.PURE). A document with an effect, a volatile operation or an
78
+ * extension of a profile can read outside `resolve`. The schedule asks `factsOf` first.
79
+ *
80
+ * The values of the context must not change in place, as PLAN.CACHE says for the documents. A value that changed in
81
+ * place is the same object, thus the function cannot see the change.
82
+ *
83
+ * @param target - The machine of the evaluation, or its plan.
84
+ * @param reads - The reads of the evaluation, from the option `reads`.
85
+ * @param context - The new context.
86
+ * @param options - The rule of the host for two things.
87
+ * @returns `{ same: true }` when each free read reaches the same thing, or else the first changed read.
88
+ */
89
+ export function verifyReads(target: Machine | Plan, reads: readonly Read[], context: Context, options: VerifyOptions = {}): ReadsCheck {
90
+ const machine = "evaluators" in target ? target : target.machine;
91
+ const root = rootContext(machine, context);
92
+ const equal = options.equal;
93
+ for (let index = 0; index < reads.length; index += 1) {
94
+ const read = reads[index] as Read;
95
+ if (!read.free) continue;
96
+ const stop = heldStop(root, read.path);
97
+ if (stop !== undefined) return { same: false, index, read, now: stop };
98
+ const asked = read.hops !== undefined;
99
+ const now = readAgain(machine, root, read.path, asked);
100
+ if (!sameResult(read.reached, now.reached, equal) || (read.hops !== undefined && !sameHops(read.hops, now.hops, equal))) {
101
+ return { same: false, index, read, now: now.reached };
102
+ }
103
+ }
104
+ return SAME;
105
+ }
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,8 @@ import {
19
20
  intersectCaps,
20
21
  makeContext,
21
22
  permits,
23
+ placeText,
24
+ waitedReason,
22
25
  } from "./evaluate.ts";
23
26
  import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
24
27
 
@@ -45,6 +48,23 @@ export interface ReferenceOptions {
45
48
  readonly definitions?: Definitions;
46
49
  /** A list that gets each effect, in its order. */
47
50
  readonly effects?: Effect[];
51
+ /** The mock of the host (RUN.MOCK). It answers each effect at once. Without it, an effect gives its payload. */
52
+ readonly perform?: Perform;
53
+ /**
54
+ * The verb that goes up the ladder at each document, as in `prepare`. The default is `eval`. The reference reads
55
+ * each verb by the same rules (VERB.AGREE).
56
+ */
57
+ readonly verb?: string;
58
+ }
59
+
60
+ /** The scope of the addresses of effects (RUN.ADDRESS): the root of the evaluation, or one item of `all`. */
61
+ interface Scope {
62
+ /** The text before each address of the scope. */
63
+ readonly prefix: string;
64
+ /** The number of earlier effects at each place of the scope, by the text of the place. */
65
+ readonly counts: Map<string, number>;
66
+ /** The number of earlier uses of `all` in the scope. */
67
+ alls: number;
48
68
  }
49
69
 
50
70
  /** The state of one evaluation of the reference. */
@@ -53,6 +73,11 @@ interface Env {
53
73
  readonly definitions: Definitions;
54
74
  readonly effects: Effect[];
55
75
  readonly maxDepth: number;
76
+ readonly perform: Perform | undefined;
77
+ /** The verb that goes up the ladder at each document. Each part evaluates with the same verb. */
78
+ readonly verb: string;
79
+ /** The scope of the code that evaluates at this time. `all` changes it for each item. */
80
+ scope: Scope;
56
81
  }
57
82
 
58
83
  /** The code that answers a document: a function evaluator, a runner, or a gap of the reference. */
@@ -104,8 +129,8 @@ function choose(env: Env, name: string): Chosen | undefined {
104
129
 
105
130
  /**
106
131
  * This function reads one document by the rules, in their order. First come an envelope or a declared type
107
- * (EVAL.ERRORS) and the limit of the depth (PLAN.DEPTH). Then come the ladder (LADDER.*), an evaluator and the shape
108
- * of the parts (SHAPE.*).
132
+ * (EVAL.ERRORS) and the limit of the depth (PLAN.DEPTH). Then come the ladder of the verb (LADDER.*, VERB.LADDER), an
133
+ * evaluator and the shape of the parts (SHAPE.*).
109
134
  */
110
135
  function examine(env: Env, at: At): Seen | Failed {
111
136
  const { place, declared } = at;
@@ -116,7 +141,7 @@ function examine(env: Env, at: At): Seen | Failed {
116
141
  if (at.depth > env.maxDepth) return fail("#DEPTH!", `The nesting is deeper than ${env.maxDepth} documents.`, { place });
117
142
  const { doc, kind, split } = read;
118
143
  const kindText = typeName(doc.type);
119
- const answer = ask(world, read.declared ? {} : doc, "eval", declared === undefined ? {} : { declaredType: declared });
144
+ const answer = ask(world, read.declared ? {} : doc, env.verb, declared === undefined ? {} : { declaredType: declared });
120
145
  if (answer.error === "unknown-kind") return fail("#KIND?", `The world has no kind ${kindText}.`, { place });
121
146
  const name = answer.evaluator;
122
147
  const chosen = name === undefined ? undefined : choose(env, name);
@@ -167,15 +192,52 @@ function resolveFully(machine: Machine, ctx: Context, path: readonly unknown[],
167
192
  return options?.hops === true ? found : { ...found, hops: [] };
168
193
  }
169
194
 
170
- /** This function records an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). */
171
- function record(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Failed | undefined {
195
+ /**
196
+ * This function causes an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). It records the effect with its
197
+ * address (RUN.ADDRESS) and its outcome (RUN.OUTCOME). The mock answers the effect at once (RUN.MOCK). Without a
198
+ * mock, the effect gives its payload (RUN.EVAL).
199
+ */
200
+ function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
172
201
  const keys = typeof kind === "string" ? kind.split(".") : kind;
173
202
  const text = keys.join(".");
174
203
  const machine = env.machine;
175
204
  const allowed = machine.permits === undefined ? permits(ctx.caps, keys) : machine.permits(ctx.caps, keys, ctx);
176
205
  if (!allowed) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
177
- env.effects.push({ index: env.effects.length, kind: text, path: keys, payload, node: -1, at: 0, place, context: ctx });
178
- return undefined;
206
+ const at = placeText(place);
207
+ const count = env.scope.counts.get(at) ?? 0;
208
+ env.scope.counts.set(at, count + 1);
209
+ const effect: { -readonly [K in keyof Effect]: Effect[K] } = {
210
+ index: env.effects.length,
211
+ kind: text,
212
+ path: keys,
213
+ payload,
214
+ node: -1,
215
+ at: 0,
216
+ place,
217
+ context: ctx,
218
+ address: `${env.scope.prefix}${at}#${count}`,
219
+ };
220
+ env.effects.push(effect);
221
+ effect.outcome = answer(env.perform, effect, place);
222
+ return effect.outcome;
223
+ }
224
+
225
+ /**
226
+ * This function gives the answer of the mock to an effect. A throw gives `#CALC!` with the cause, and a `Failure`
227
+ * gives its own error value. A promise gives `#CALC!`, because an evaluation does not wait.
228
+ */
229
+ function answer(perform: Perform | undefined, effect: Effect, place: readonly Key[]): Result {
230
+ if (perform === undefined) return ok(effect.payload);
231
+ let given: unknown;
232
+ try {
233
+ given = perform(effect);
234
+ } catch (error) {
235
+ if (error instanceof Failure) return placed({ ok: false, error: error.error }, place);
236
+ return fail("#CALC!", `The host failed the effect ${effect.kind}: ${describe(error)}`, { place, cause: error });
237
+ }
238
+ if (!(given instanceof Promise)) return ok(given);
239
+ void given.catch(() => undefined);
240
+ return fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place });
179
241
  }
180
242
 
181
243
  /** This function evaluates a document. Each error gets the place of the document where it started (K5). */
@@ -189,7 +251,7 @@ function evaluateSeen(env: Env, seen: Seen, at: At): Result {
189
251
  const { chosen } = seen;
190
252
  const { place } = at;
191
253
  if ("gap" in chosen) return fail("#NOEVAL", chosen.gap, { place });
192
- if ("runner" in chosen) return drive(runSeen(env, seen, at, chosen.runner), place);
254
+ if ("runner" in chosen) return drive(runSeen(env, seen, at, chosen.runner), place, seen.kindText);
193
255
  const input = evalInput(env, seen, at);
194
256
  try {
195
257
  return placed(chosen.evaluator(input), place);
@@ -215,11 +277,11 @@ function* runSeen(env: Env, seen: Seen, at: At, runner: RunFn): Steps {
215
277
  }
216
278
 
217
279
  /** This function drives steps that cannot wait. A step that waits gives `#CALC!` at the document (RUN.WAIT). */
218
- function drive(steps: Steps, place: readonly Key[]): Result {
280
+ function drive(steps: Steps, place: readonly Key[], kindText: string): Result {
219
281
  const step = steps.next();
220
282
  if (step.done === true) return step.value;
221
283
  void step.value.catch(() => undefined);
222
- const waited = fail("#CALC!", "A part waited, and its parent cannot wait.", { place });
284
+ const waited = fail("#CALC!", waitedReason(kindText), { place });
223
285
  steps.return(waited);
224
286
  return waited;
225
287
  }
@@ -247,7 +309,7 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
247
309
  place: at.place,
248
310
  ...(at.address ? { address: true } : {}),
249
311
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
250
- effect: (kind, payload) => record(env, at.ctx, kind, payload, at.place) ?? ok(payload),
312
+ effect: (kind, payload) => cause(env, at.ctx, kind, payload, at.place),
251
313
  fill,
252
314
  };
253
315
  }
@@ -275,9 +337,9 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
275
337
  place: at.place,
276
338
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
277
339
  effect: function* (kind, payload) {
278
- return record(env, at.ctx, kind, payload, at.place) ?? ok(payload);
340
+ return cause(env, at.ctx, kind, payload, at.place);
279
341
  },
280
- all: (list, options) => together(list, options),
342
+ all: (list, options) => together(env, list, options),
281
343
  fill,
282
344
  };
283
345
  }
@@ -287,14 +349,21 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
287
349
  * 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
350
  * limit. With `failFast`, the first failure stops the parts that wait, and the later parts do not start
289
351
  * (RUN.FAIL-FAST). When no part failed and a part waits, the steps wait, and the drive gives `#CALC!`.
352
+ *
353
+ * Each part has its own scope of addresses. Its prefix names the use of `all` in the scope around it, and the part
354
+ * (RUN.ADDRESS).
290
355
  */
291
- function* together(list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
356
+ function* together(env: Env, list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
292
357
  const limit = Math.max(1, options.concurrency ?? list.length);
293
358
  const results: (Result | undefined)[] = list.map(() => undefined);
294
359
  const waiting: { readonly steps: Steps; readonly promise: Promise<unknown> }[] = [];
360
+ const outer = env.scope;
361
+ const address = `${outer.prefix}all${outer.alls}`;
362
+ outer.alls += 1;
295
363
  let failed = false;
296
364
  for (let index = 0; index < list.length && !failed && waiting.length < limit; index += 1) {
297
365
  let result: Result | undefined;
366
+ env.scope = { prefix: `${address}.${index}/`, counts: new Map(), alls: 0 };
298
367
  try {
299
368
  const steps = (list[index] as RunPart).evaluate();
300
369
  const step = steps.next();
@@ -302,6 +371,8 @@ function* together(list: readonly RunPart[], options: AllOptions = {}): Generato
302
371
  else waiting.push({ steps, promise: step.value });
303
372
  } catch (error) {
304
373
  result = thrown(error, "a part");
374
+ } finally {
375
+ env.scope = outer;
305
376
  }
306
377
  results[index] = result;
307
378
  if (result !== undefined && options.failFast === true && !result.ok) failed = true;
@@ -325,16 +396,27 @@ function* together(list: readonly RunPart[], options: AllOptions = {}): Generato
325
396
  /**
326
397
  * This function evaluates a document by the rules, without a compilation. It is the reference of the plans: a plan,
327
398
  * 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!`.
399
+ * gives the answer of the mock or its payload, and a part that waits gives `#CALC!`.
400
+ *
401
+ * The verb of the options goes up the ladder at each document, as in `prepare`. Each verb has the same rules: the
402
+ * ladder, the contract of the kind and the sealed parts (VERB.AGREE).
329
403
  *
330
404
  * @param machine - The world and the code of the evaluators.
331
405
  * @param doc - The document.
332
406
  * @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, and a list for the effects.
407
+ * @param options - The type of a bare root value, the definitions of compiled code, a list for the effects, a mock and the verb.
334
408
  * @returns The result.
335
409
  */
336
410
  export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
337
- const env: Env = { machine, definitions: options.definitions ?? {}, effects: options.effects ?? [], maxDepth: machine.maxDepth ?? 1000 };
411
+ const env: Env = {
412
+ machine,
413
+ definitions: options.definitions ?? {},
414
+ effects: options.effects ?? [],
415
+ maxDepth: machine.maxDepth ?? 1000,
416
+ perform: options.perform,
417
+ verb: options.verb ?? "eval",
418
+ scope: { prefix: "", counts: new Map(), alls: 0 },
419
+ };
338
420
  const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
339
421
  try {
340
422
  return evaluateAt(env, { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx });
@@ -461,6 +543,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
461
543
  if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
462
544
  throw new Failure(result.error);
463
545
  },
546
+ has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)),
464
547
  toJson: () => {
465
548
  const result = jsonOfNode(node, self, []);
466
549
  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
+ }