@mark1russell7/coeval 0.15.0 → 0.17.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/README.md +9 -4
- package/build/index.d.ts +828 -249
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3138 -386
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +193 -21
- package/src/conform.ts +49 -5
- package/src/deref.ts +97 -14
- package/src/evaluate.ts +311 -35
- package/src/facts.ts +113 -78
- package/src/formula.ts +55 -10
- package/src/index.ts +33 -1
- package/src/meaning.ts +472 -0
- package/src/merge.ts +46 -14
- package/src/reads.ts +9 -2
- package/src/reference.ts +90 -14
- package/src/replay.ts +964 -18
- package/src/schedule.ts +14 -4
- package/src/static.ts +2 -2
- package/src/stdlib.ts +81 -6
- package/src/syntax.ts +357 -52
- package/src/types.ts +443 -16
package/src/replay.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type
|
|
3
|
-
import { type
|
|
1
|
+
import { decodeValue, encodeValue } from "./codec.ts";
|
|
2
|
+
import { type Json, type Key, equalWith, isKey, isRecord } from "./doc.ts";
|
|
3
|
+
import { type ErrorCode, type EvalError, type Result, Failure, describe, fail, ok } from "./errors.ts";
|
|
4
|
+
import { type Effect, type Journal, type Machine, type Perform, JOURNAL } from "./evaluate.ts";
|
|
5
|
+
import { formulaMachine } from "./formula.ts";
|
|
4
6
|
|
|
5
7
|
/*
|
|
6
8
|
* Record and replay (R2 P2, D19). A recorder wraps the function `perform` of a host and keeps a log. The log has each
|
|
7
9
|
* effect with its address, its request and its outcome, and the order in which the waits of each `all` settled. A
|
|
8
10
|
* replayer answers each effect from the log by its address, thus a replay does no host effect. The kernel gives the
|
|
9
11
|
* address and the outcome of each effect. The journal on the function `perform` carries the order of the waits.
|
|
12
|
+
*
|
|
13
|
+
* A log has a JSON form (RUN.REPLAY-JSON, D75). `encodeLog` writes each value through `encodeValue` (D40), and `decodeLog`
|
|
14
|
+
* reads it again. Thus a test can keep a log as a golden file (RUN.REPLAY-GOLDEN).
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
/** One effect of a log: its address, its request and its outcome. */
|
|
@@ -55,6 +60,12 @@ export interface ReplayOptions {
|
|
|
55
60
|
* value, because it does not wait. The default is `true`: an effect waits when it waited in the recorded run.
|
|
56
61
|
*/
|
|
57
62
|
readonly wait?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The rule of the host for two payloads that are not the same by `Object.is`: one of the run and one of the log.
|
|
65
|
+
* It works as the rule of `sameResult`. Its `undefined` gives the rules of `docEquals`. A log from `decodeLog` holds new copies
|
|
66
|
+
* of the members of a domain, for example of a time. The rule tells that two such copies are the same.
|
|
67
|
+
*/
|
|
68
|
+
readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
/**
|
|
@@ -62,17 +73,19 @@ export interface ReplayOptions {
|
|
|
62
73
|
* Its log keeps each effect with its address, its kind, its payload and the outcome that the kernel gives it. The log
|
|
63
74
|
* also keeps the order in which the waits of each `all` settle.
|
|
64
75
|
*
|
|
76
|
+
* The recorder gives each call of its journal to the journal of the inner function, for example of a replayer. Thus a
|
|
77
|
+
* recording of a replay settles the waits in the order of the replayer, and it gives the same log (RUN.REPLAY-ORDER).
|
|
78
|
+
*
|
|
65
79
|
* @param perform - The function of the host. Without it, each effect gives its payload, as in a run without a host.
|
|
66
80
|
* @returns The function `perform` for the run, and the log.
|
|
67
81
|
*/
|
|
68
82
|
export function recorder(perform?: Perform): Recorder {
|
|
69
83
|
const seen: { readonly effect: Effect; waited: boolean }[] = [];
|
|
70
84
|
const settles: ReplaySettle[] = [];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
85
|
+
// The journal of an inner replayer keeps the order of the waits, thus a recording of a replay keeps it (RUN.REPLAY-ORDER).
|
|
86
|
+
const journal = relay(perform, (all, item) => {
|
|
87
|
+
settles.push({ all, item });
|
|
88
|
+
});
|
|
76
89
|
const host = (effect: Effect): unknown => {
|
|
77
90
|
const entry = { effect, waited: false };
|
|
78
91
|
seen.push(entry);
|
|
@@ -100,14 +113,17 @@ export function recorder(perform?: Perform): Recorder {
|
|
|
100
113
|
* recorded error comes again with its code, its reason, its place and its cause. The waits of each `all` settle in the
|
|
101
114
|
* recorded order. An effect that the log does not have at its address gives `#REPLAY!` (RUN.REPLAY-MISMATCH).
|
|
102
115
|
*
|
|
103
|
-
*
|
|
116
|
+
* One replayer serves more than one run, also two runs at the same time (RUN.REPLAY-RUNS). The kernel tells the journal
|
|
117
|
+
* the number of the waits of each `all` that settled before, thus the replayer keeps no state of a run. When the
|
|
118
|
+
* recorded item of a wait does not wait in the run, that wait follows the order of the host.
|
|
104
119
|
*
|
|
105
|
-
* @param log - The log of a recorder
|
|
106
|
-
* @param options - `wait: false` for an evaluation.
|
|
120
|
+
* @param log - The log of a recorder, or of `decodeLog`.
|
|
121
|
+
* @param options - `wait: false` for an evaluation, and the rule of the host for payloads.
|
|
107
122
|
* @returns The function `perform` for the replay.
|
|
108
123
|
*/
|
|
109
124
|
export function replayer(log: ReplayLog, options: ReplayOptions = {}): Perform {
|
|
110
125
|
const wait = options.wait !== false;
|
|
126
|
+
const equal = options.equal;
|
|
111
127
|
const entries = new Map<string, ReplayEntry>();
|
|
112
128
|
for (const entry of log.effects) entries.set(entry.address, entry);
|
|
113
129
|
const order = new Map<string, number[]>();
|
|
@@ -116,18 +132,22 @@ export function replayer(log: ReplayLog, options: ReplayOptions = {}): Perform {
|
|
|
116
132
|
if (items === undefined) order.set(settle.all, [settle.item]);
|
|
117
133
|
else items.push(settle.item);
|
|
118
134
|
}
|
|
135
|
+
// The places in the order for a copy of an older kernel, which gives no count of the waits that settled.
|
|
136
|
+
let places: Map<string, number> | undefined;
|
|
119
137
|
const journal: Journal = {
|
|
120
|
-
next: (all, waiting) => {
|
|
138
|
+
next: (all, waiting, before) => {
|
|
121
139
|
const items = order.get(all);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
items
|
|
140
|
+
if (items === undefined) return undefined;
|
|
141
|
+
const at = before ?? (places ??= new Map()).get(all) ?? 0;
|
|
142
|
+
const item = items[at];
|
|
143
|
+
if (item === undefined || !waiting.includes(item)) return undefined;
|
|
144
|
+
if (before === undefined) places?.set(all, at + 1);
|
|
125
145
|
return item;
|
|
126
146
|
},
|
|
127
147
|
};
|
|
128
148
|
const perform = (effect: Effect): unknown => {
|
|
129
149
|
const entry = effect.address === undefined ? undefined : entries.get(effect.address);
|
|
130
|
-
const wrong = mismatch(effect, entry);
|
|
150
|
+
const wrong = mismatch(effect, entry, equal);
|
|
131
151
|
if (wrong !== undefined || entry === undefined) throw wrong;
|
|
132
152
|
const outcome = entry.outcome;
|
|
133
153
|
const failure =
|
|
@@ -145,11 +165,11 @@ export function replayer(log: ReplayLog, options: ReplayOptions = {}): Perform {
|
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
/** 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 {
|
|
168
|
+
function mismatch(effect: Effect, entry: ReplayEntry | undefined, equal: ReplayOptions["equal"]): Failure | undefined {
|
|
149
169
|
const at = effect.address ?? "";
|
|
150
170
|
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
171
|
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 (!
|
|
172
|
+
if (!equalWith(entry.payload, effect.payload, equal)) return replayError(effect, `The effect ${effect.kind} at the address ${at} has another payload than the log.`);
|
|
153
173
|
return undefined;
|
|
154
174
|
}
|
|
155
175
|
|
|
@@ -157,3 +177,929 @@ function mismatch(effect: Effect, entry: ReplayEntry | undefined): Failure | und
|
|
|
157
177
|
function replayError(effect: Effect, reason: string): Failure {
|
|
158
178
|
return new Failure(fail("#REPLAY!", reason, { place: effect.place }).error);
|
|
159
179
|
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* This function gives a journal that tells a function of its own each wait that settles. It gives each call to the
|
|
183
|
+
* journal of the inner function `perform`, when that function has one.
|
|
184
|
+
*
|
|
185
|
+
* @param perform - The inner function.
|
|
186
|
+
* @param settled - The function of its own.
|
|
187
|
+
* @returns The journal.
|
|
188
|
+
*/
|
|
189
|
+
export function relay(perform: Perform | undefined, settled: (all: string, item: number) => void): Journal {
|
|
190
|
+
const inner = perform === undefined ? undefined : (perform as { readonly [JOURNAL]?: Journal })[JOURNAL];
|
|
191
|
+
const tell = (all: string, item: number): void => {
|
|
192
|
+
settled(all, item);
|
|
193
|
+
inner?.settled?.(all, item);
|
|
194
|
+
};
|
|
195
|
+
if (inner?.next === undefined) return { settled: tell };
|
|
196
|
+
return { settled: tell, next: (all, waiting, before) => inner.next?.(all, waiting, before) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The JSON form of a log (RUN.REPLAY-JSON). `encodeLog` writes it, and `decodeLog` reads it. */
|
|
200
|
+
export interface ReplayLogJson {
|
|
201
|
+
/** The version of the form. Only the version 1 exists. */
|
|
202
|
+
readonly version: 1;
|
|
203
|
+
readonly effects: readonly ReplayEntryJson[];
|
|
204
|
+
readonly settles: readonly ReplaySettle[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The JSON form of one effect of a log. A value is a term document of `encodeValue` (D40). */
|
|
208
|
+
export interface ReplayEntryJson {
|
|
209
|
+
readonly address: string;
|
|
210
|
+
readonly kind: string;
|
|
211
|
+
/** The term document of the payload. The value `undefined` is `ext.undefined`. */
|
|
212
|
+
readonly payload: Json;
|
|
213
|
+
/** The JSON form of the outcome. It is absent when the recorded run stopped before the answer came. */
|
|
214
|
+
readonly outcome?: ReplayResultJson;
|
|
215
|
+
readonly waited: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The JSON form of an outcome: the term document of the value, or the JSON form of the error. */
|
|
219
|
+
export type ReplayResultJson = { readonly ok: true; readonly value: Json } | { readonly ok: false; readonly error: ReplayErrorJson };
|
|
220
|
+
|
|
221
|
+
/** The JSON form of an error value. It has `cause`, `exception` or `causeParts` only when the error has a cause. */
|
|
222
|
+
export interface ReplayErrorJson {
|
|
223
|
+
readonly code: ErrorCode;
|
|
224
|
+
readonly reason: string;
|
|
225
|
+
readonly place?: readonly Key[];
|
|
226
|
+
/** The term document of the cause, when `encodeValue` writes the cause. */
|
|
227
|
+
readonly cause?: Json;
|
|
228
|
+
/** The JSON form of a cause that is an exception, when no domain with a codec holds it. */
|
|
229
|
+
readonly exception?: ReplayExceptionJson;
|
|
230
|
+
/**
|
|
231
|
+
* The JSON form of a cause without a term document that is a list or a plain record with an exception at some depth
|
|
232
|
+
* (RUN.REPLAY-CAUSE-PARTS, D87). It is a list of parts, the part `record` or the part `share`. The index 0 of `loop` is its
|
|
233
|
+
* first exception, and the index 0 of `share` is its first list or record.
|
|
234
|
+
*/
|
|
235
|
+
readonly causeParts?: ReplayCausePartsJson;
|
|
236
|
+
/** The names of the items of `causeParts` without a form, for example `cause.send`. This list is only a note. */
|
|
237
|
+
readonly dropped?: readonly string[];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The JSON form of a cause without a term document that is a list or a plain record with an exception at some depth
|
|
242
|
+
* (D87). It is a list of parts, the part `record` or the part `share`, as a field in `parts` (D82, D88).
|
|
243
|
+
*/
|
|
244
|
+
export type ReplayCausePartsJson = readonly ReplayPartsJson[] | { readonly record: Readonly<Record<string, ReplayPartsJson>> } | { readonly share: number };
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The JSON form of an exception (RUN.REPLAY-CAUSES, D76). The exceptions of an error and of their causes are a chain.
|
|
248
|
+
* The index 0 of the chain is the exception of the error. Each field after `message` is optional, thus a log of the
|
|
249
|
+
* version 1 without them stays correct.
|
|
250
|
+
*/
|
|
251
|
+
export interface ReplayExceptionJson {
|
|
252
|
+
readonly name: string;
|
|
253
|
+
readonly message: string;
|
|
254
|
+
/** The term documents of the own fields of the exception that `encodeValue` writes, for example `code` or `errno`. */
|
|
255
|
+
readonly fields?: Readonly<Record<string, Json>>;
|
|
256
|
+
/**
|
|
257
|
+
* The own enumerable fields without a term document that are an exception, a list or a plain record (RUN.REPLAY-FIELDS,
|
|
258
|
+
* D78, D82). An example is a list `errors` on an `Error`. A list is a list of parts, and a record is the part `record`.
|
|
259
|
+
* Each exception in them has the rule of a cause. A container without a form in it is a name in `dropped`. A container
|
|
260
|
+
* that an earlier place holds is the part `share` (D88).
|
|
261
|
+
*/
|
|
262
|
+
readonly parts?: Readonly<Record<string, ReplayPartsJson>>;
|
|
263
|
+
/**
|
|
264
|
+
* The names of the own fields without a form, and `cause` for a cause without a form. An item of a container has the
|
|
265
|
+
* name of the container, a dot and its key or index, for example `errors.1` or `cause.send`. This list is only a note:
|
|
266
|
+
* `decodeLog` does not give these fields back.
|
|
267
|
+
*/
|
|
268
|
+
readonly dropped?: readonly string[];
|
|
269
|
+
/**
|
|
270
|
+
* The own fields of the standard classes that are not enumerable (RUN.REPLAY-HIDDEN, D77): `errors` of an
|
|
271
|
+
* `AggregateError`, `error` and `suppressed` of a `SuppressedError`. A list is a list of parts.
|
|
272
|
+
*/
|
|
273
|
+
readonly hidden?: Readonly<Record<string, ReplayPartsJson>>;
|
|
274
|
+
/** The term document of the cause, when `encodeValue` writes the cause. */
|
|
275
|
+
readonly cause?: Json;
|
|
276
|
+
/** The JSON form of a cause that is an exception, when no domain with a codec holds it. */
|
|
277
|
+
readonly exception?: ReplayExceptionJson;
|
|
278
|
+
/** The index in the chain of an earlier exception that is the cause. Thus a cycle of causes stays exact. */
|
|
279
|
+
readonly loop?: number;
|
|
280
|
+
/**
|
|
281
|
+
* The JSON form of a cause without a term document that is a list or a plain record with an exception at some depth
|
|
282
|
+
* (RUN.REPLAY-CAUSE-PARTS, D87). Each exception in it has the rule of a cause, also a `loop`.
|
|
283
|
+
*/
|
|
284
|
+
readonly causeParts?: ReplayCausePartsJson;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The JSON form of one value of a field in `parts` or `hidden` (D77, D78, D82). It is a term document, an exception, a
|
|
289
|
+
* loop, a record of parts, or a share (D88). The walk is depth
|
|
290
|
+
* first: the fields in `parts` in their order, the hidden fields, then the cause. The mark `dropped` keeps the place of
|
|
291
|
+
* an item of a list without a form, and `decodeLog` gives a hole there.
|
|
292
|
+
*
|
|
293
|
+
* The part `share` is a list or a record of parts that an earlier place of the walk holds (RUN.REPLAY-SHARED, D88). Its
|
|
294
|
+
* index counts the lists and the records of parts in the order of the walk. Thus `decodeLog` gives one object for both places.
|
|
295
|
+
*/
|
|
296
|
+
export type ReplayPartJson =
|
|
297
|
+
| { readonly value: Json }
|
|
298
|
+
| { readonly exception: ReplayExceptionJson }
|
|
299
|
+
| { readonly loop: number }
|
|
300
|
+
| { readonly record: Readonly<Record<string, ReplayPartsJson>> }
|
|
301
|
+
| { readonly share: number }
|
|
302
|
+
| { readonly dropped: true };
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The JSON form of a value in `parts`, in `hidden` or in a record of parts (D82). It is one part, or a list of them. A
|
|
306
|
+
* list in a list is a JSON list in a JSON list, thus the rule is the same at each depth.
|
|
307
|
+
*/
|
|
308
|
+
export type ReplayPartsJson = ReplayPartJson | readonly ReplayPartsJson[];
|
|
309
|
+
|
|
310
|
+
/** This function tells if a key is a field of an error value that the JSON form keeps. */
|
|
311
|
+
function errorField(key: string): boolean {
|
|
312
|
+
return key === "code" || key === "reason" || key === "place" || key === "cause";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** This function tells if a key is a field of an exception that the JSON form keeps in a field of its own, or drops. */
|
|
316
|
+
function ownField(key: string): boolean {
|
|
317
|
+
return key === "name" || key === "message" || key === "stack" || key === "cause";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** This function tells if a key is a field of the JSON form of an exception. */
|
|
321
|
+
function exceptionField(key: string): boolean {
|
|
322
|
+
return (
|
|
323
|
+
key === "name" ||
|
|
324
|
+
key === "message" ||
|
|
325
|
+
key === "fields" ||
|
|
326
|
+
key === "parts" ||
|
|
327
|
+
key === "dropped" ||
|
|
328
|
+
key === "hidden" ||
|
|
329
|
+
key === "cause" ||
|
|
330
|
+
key === "exception" ||
|
|
331
|
+
key === "loop" ||
|
|
332
|
+
key === "causeParts"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The own fields that are not enumerable and that the JSON form keeps in `hidden` (D77). The standard classes define
|
|
338
|
+
* them: `errors` of `AggregateError`, `error` and `suppressed` of `SuppressedError`.
|
|
339
|
+
*/
|
|
340
|
+
const HIDDEN: readonly string[] = /* @__PURE__ */ Object.freeze(["errors", "error", "suppressed"]);
|
|
341
|
+
|
|
342
|
+
/** The result of `readField` for a field that the walk of `causeJson` cannot read. */
|
|
343
|
+
const MISSING: unique symbol = /* @__PURE__ */ Symbol("missing");
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The form of one value in the walk of `causeJson`. It is a term document, a loop, a later exception, a later container
|
|
347
|
+
* or an earlier container (D88). A value without a form keeps its name for `dropped`.
|
|
348
|
+
*/
|
|
349
|
+
type Part =
|
|
350
|
+
| { readonly value: Json }
|
|
351
|
+
| { readonly loop: number }
|
|
352
|
+
| { readonly node: number }
|
|
353
|
+
| { readonly holder: number }
|
|
354
|
+
| { readonly same: number; readonly name: string }
|
|
355
|
+
| { readonly none: string };
|
|
356
|
+
|
|
357
|
+
/** One exception of the walk of `causeJson`, before its JSON form. */
|
|
358
|
+
interface ExceptionNode {
|
|
359
|
+
readonly head: { readonly name: string; readonly message: string; readonly fields?: Readonly<Record<string, Json>> };
|
|
360
|
+
/** The keys of the own enumerable fields without a term document, in the order of the keys. */
|
|
361
|
+
readonly keys: readonly string[];
|
|
362
|
+
readonly parts: Part[];
|
|
363
|
+
/** The keys of the hidden fields, in the order of `HIDDEN`. */
|
|
364
|
+
readonly hiddenKeys: string[];
|
|
365
|
+
readonly hidden: Part[];
|
|
366
|
+
cause?: Part;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** One list or plain record of the walk of `causeJson` without a term document, before its JSON form (D82). */
|
|
370
|
+
interface Holder {
|
|
371
|
+
/** The name for `dropped` when no item of the container has a form. */
|
|
372
|
+
readonly name: string;
|
|
373
|
+
readonly list: boolean;
|
|
374
|
+
/** The value `true` marks a hidden list of a standard class. It keeps its marks also without a form (D78). */
|
|
375
|
+
keep: boolean;
|
|
376
|
+
/** The keys of a record in the order of the keys. A list has no keys. */
|
|
377
|
+
readonly keys: readonly string[];
|
|
378
|
+
readonly slots: Part[];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** One value of an exception that the walk of `causeJson` writes next, and the place for its form. */
|
|
382
|
+
interface ExceptionTask {
|
|
383
|
+
readonly value: unknown;
|
|
384
|
+
/** The name for `dropped`. An item has the name of its container, a dot and its key or index, for example `errors.1`. */
|
|
385
|
+
readonly name: string;
|
|
386
|
+
/** The value `true` marks a value that `encodeValue` cannot write, thus the walk does not try again. */
|
|
387
|
+
readonly tried: boolean;
|
|
388
|
+
/** The value `false` marks a cause. A cause is a container only when it holds an exception at some depth (D87). */
|
|
389
|
+
readonly nest: boolean;
|
|
390
|
+
/** The value `true` marks a hidden list of a standard class. It is always a container of parts (D77). */
|
|
391
|
+
readonly keep: boolean;
|
|
392
|
+
readonly put: (part: Part) => void;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The form of one part of `causeJson` after the walk, and its names for `dropped` in the order of the walk. */
|
|
396
|
+
interface Built {
|
|
397
|
+
readonly form?: ReplayPartsJson;
|
|
398
|
+
readonly names: readonly string[];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* This function tells if a value holds a hole of a list at some depth. `encodeValue` writes a hole as `undefined`. Thus
|
|
403
|
+
* the walk of `causeJson` keeps such a value in parts, and the hole keeps its mark (D82).
|
|
404
|
+
*/
|
|
405
|
+
function holdsHole(value: unknown): boolean {
|
|
406
|
+
const seen = new Set<object>();
|
|
407
|
+
const stack: unknown[] = [value];
|
|
408
|
+
while (stack.length > 0) {
|
|
409
|
+
const at = stack.pop();
|
|
410
|
+
if (typeof at !== "object" || at === null || seen.has(at) || !(Array.isArray(at) || isRecord(at))) continue;
|
|
411
|
+
seen.add(at);
|
|
412
|
+
if (Array.isArray(at)) {
|
|
413
|
+
for (let item = 0; item < at.length; item += 1) {
|
|
414
|
+
if (!Object.hasOwn(at, item)) return true;
|
|
415
|
+
stack.push(at[item]);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
for (const key of Object.keys(at)) stack.push(at[key]);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* This function tells if a list or a plain record holds an exception at some depth (D87). It reads the items as the
|
|
426
|
+
* walk of `causeJson` does, thus a cause container with an exception gets a form.
|
|
427
|
+
*/
|
|
428
|
+
function holdsException(value: unknown): boolean {
|
|
429
|
+
const seen = new Set<object>();
|
|
430
|
+
const stack: unknown[] = [value];
|
|
431
|
+
while (stack.length > 0) {
|
|
432
|
+
const at = stack.pop();
|
|
433
|
+
if (at instanceof Error) return true;
|
|
434
|
+
if (typeof at !== "object" || at === null || seen.has(at) || !(Array.isArray(at) || isRecord(at))) continue;
|
|
435
|
+
seen.add(at);
|
|
436
|
+
if (Array.isArray(at)) {
|
|
437
|
+
for (let item = 0; item < at.length; item += 1) if (Object.hasOwn(at, item)) stack.push(readField(at, item));
|
|
438
|
+
} else {
|
|
439
|
+
for (const key of Object.keys(at)) stack.push(readField(at, key));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** The JSON form of a cause: one of the fields `cause`, `exception`, `loop` and `causeParts`, and the names for `dropped`. */
|
|
446
|
+
interface CauseJson {
|
|
447
|
+
readonly link?: { readonly cause: Json } | { readonly exception: ReplayExceptionJson } | { readonly loop: number } | { readonly causeParts: ReplayCausePartsJson };
|
|
448
|
+
readonly names: readonly string[];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** This function gives the field of a cause for its form after the walk (D76, D87). A cause without a form has no field. */
|
|
452
|
+
function causeOf(built: Built): CauseJson {
|
|
453
|
+
const form = built.form;
|
|
454
|
+
if (form === undefined) return { names: built.names };
|
|
455
|
+
if (Array.isArray(form) || "record" in form || "share" in form) return { link: { causeParts: form as ReplayCausePartsJson }, names: built.names };
|
|
456
|
+
return { link: "value" in form ? { cause: form.value } : (form as { readonly exception: ReplayExceptionJson } | { readonly loop: number }), names: built.names };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** This function gives the term document of a value of an exception, or `undefined` when the value has none. */
|
|
460
|
+
function termOrNothing(machine: Pick<Machine, "domains">, value: unknown): Json | undefined {
|
|
461
|
+
if (holdsHole(value)) return undefined;
|
|
462
|
+
try {
|
|
463
|
+
return encodeValue(machine, value);
|
|
464
|
+
} catch {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** This function reads a field of a value, or gives `MISSING` when the read throws. */
|
|
470
|
+
function readField(value: object, key: string | number): unknown {
|
|
471
|
+
try {
|
|
472
|
+
return (value as Record<string | number, unknown>)[key];
|
|
473
|
+
} catch {
|
|
474
|
+
return MISSING;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** This function gives the forms of the parts with a form, with their keys, in the order of the keys. */
|
|
479
|
+
function keptOf(keys: readonly string[], built: readonly Built[]): [string, ReplayPartsJson][] {
|
|
480
|
+
const kept: [string, ReplayPartsJson][] = [];
|
|
481
|
+
for (let place = 0; place < keys.length; place += 1) {
|
|
482
|
+
const form = built[place]?.form;
|
|
483
|
+
if (form !== undefined) kept.push([keys[place] as string, form]);
|
|
484
|
+
}
|
|
485
|
+
return kept;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* This function writes a cause without a term document as JSON (RUN.REPLAY-CAUSES, RUN.REPLAY-HIDDEN, RUN.REPLAY-FIELDS).
|
|
490
|
+
* The cause is an exception with its hidden fields and its causes, or a container with an exception (D87). A host
|
|
491
|
+
* makes the exception, thus a part without a term document is a note in `dropped`, not a refusal (D75, D76).
|
|
492
|
+
*
|
|
493
|
+
* The walk gives each exception an index in the order of a depth-first walk. It takes the fields in `parts` in the
|
|
494
|
+
* order of their keys, the hidden fields in the order of `HIDDEN`, then the cause (D78). A list or a plain record
|
|
495
|
+
* without a term document is a container of parts, with its items in their order (D82). A cause is a container only
|
|
496
|
+
* when it holds an exception (D87). A value that is an earlier exception of the walk is its index in `loop` (D77).
|
|
497
|
+
*
|
|
498
|
+
* A container that an earlier place holds is its index in `share`, also a container that holds itself (D88). The walk is
|
|
499
|
+
* a loop, thus a long chain does not overflow the stack.
|
|
500
|
+
*/
|
|
501
|
+
function causeJson(machine: Pick<Machine, "domains">, root: unknown): CauseJson {
|
|
502
|
+
// The nodes and the containers in the order of the walk. Each item of a node or of a container comes later.
|
|
503
|
+
const items: (ExceptionNode | Holder)[] = [];
|
|
504
|
+
const index = new Map<unknown, number>();
|
|
505
|
+
// The place in `items` of each container of the walk (D88).
|
|
506
|
+
const holders = new Map<object, number>();
|
|
507
|
+
const tasks: ExceptionTask[] = [];
|
|
508
|
+
const later = (children: readonly ExceptionTask[]): void => {
|
|
509
|
+
for (let place = children.length - 1; place >= 0; place -= 1) tasks.push(children[place] as ExceptionTask);
|
|
510
|
+
};
|
|
511
|
+
const hold = (name: string, value: object, keep: boolean, children: ExceptionTask[]): number => {
|
|
512
|
+
const at = items.length;
|
|
513
|
+
holders.set(value, at);
|
|
514
|
+
const list = Array.isArray(value);
|
|
515
|
+
const keys = list ? [] : Object.keys(value);
|
|
516
|
+
const slots: Part[] = [];
|
|
517
|
+
items.push({ name, list, keep, keys, slots });
|
|
518
|
+
const size = list ? (value as readonly unknown[]).length : keys.length;
|
|
519
|
+
for (let place = 0; place < size; place += 1) {
|
|
520
|
+
const key = list ? place : (keys[place] as string);
|
|
521
|
+
const item = list && !Object.hasOwn(value, place) ? MISSING : readField(value, key);
|
|
522
|
+
const put = (part: Part): void => {
|
|
523
|
+
slots[place] = part;
|
|
524
|
+
};
|
|
525
|
+
children.push({ value: item, name: `${name}.${key}`, tried: false, nest: true, keep: false, put });
|
|
526
|
+
}
|
|
527
|
+
return at;
|
|
528
|
+
};
|
|
529
|
+
const open = (current: Error): number => {
|
|
530
|
+
const at = items.length;
|
|
531
|
+
index.set(current, index.size);
|
|
532
|
+
const fields: [string, Json][] = [];
|
|
533
|
+
const linked: [string, unknown][] = [];
|
|
534
|
+
for (const key of Object.keys(current)) {
|
|
535
|
+
if (ownField(key)) continue;
|
|
536
|
+
const value = readField(current, key);
|
|
537
|
+
const term = value === MISSING ? undefined : termOrNothing(machine, value);
|
|
538
|
+
if (term === undefined) linked.push([key, value]);
|
|
539
|
+
else fields.push([key, term]);
|
|
540
|
+
}
|
|
541
|
+
const head = { name: String(current.name), message: String(current.message), ...(fields.length === 0 ? {} : { fields: Object.fromEntries(fields) }) };
|
|
542
|
+
const node: ExceptionNode = { head, keys: linked.map(([key]) => key), parts: [], hiddenKeys: [], hidden: [] };
|
|
543
|
+
items.push(node);
|
|
544
|
+
const children: ExceptionTask[] = [];
|
|
545
|
+
for (let place = 0; place < linked.length; place += 1) {
|
|
546
|
+
const [key, value] = linked[place] as [string, unknown];
|
|
547
|
+
const put = (part: Part): void => {
|
|
548
|
+
node.parts[place] = part;
|
|
549
|
+
};
|
|
550
|
+
children.push({ value, name: key, tried: true, nest: true, keep: false, put });
|
|
551
|
+
}
|
|
552
|
+
for (const key of HIDDEN) {
|
|
553
|
+
if (Object.getOwnPropertyDescriptor(current, key)?.enumerable !== false) continue;
|
|
554
|
+
const place = node.hiddenKeys.length;
|
|
555
|
+
node.hiddenKeys.push(key);
|
|
556
|
+
const value = readField(current, key);
|
|
557
|
+
const put = (part: Part): void => {
|
|
558
|
+
node.hidden[place] = part;
|
|
559
|
+
};
|
|
560
|
+
// A hidden list is always a list of parts, as in D77, also when `encodeValue` writes it.
|
|
561
|
+
children.push({ value, name: key, tried: false, nest: true, keep: Array.isArray(value), put });
|
|
562
|
+
}
|
|
563
|
+
if (Object.hasOwn(current, "cause")) {
|
|
564
|
+
const put = (part: Part): void => {
|
|
565
|
+
node.cause = part;
|
|
566
|
+
};
|
|
567
|
+
children.push({ value: current.cause, name: "cause", tried: false, nest: false, keep: false, put });
|
|
568
|
+
}
|
|
569
|
+
later(children);
|
|
570
|
+
return at;
|
|
571
|
+
};
|
|
572
|
+
let top: Part = { none: "cause" };
|
|
573
|
+
tasks.push({
|
|
574
|
+
value: root,
|
|
575
|
+
name: "cause",
|
|
576
|
+
tried: true,
|
|
577
|
+
nest: false,
|
|
578
|
+
keep: false,
|
|
579
|
+
put: (part) => {
|
|
580
|
+
top = part;
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
|
|
584
|
+
const value = task.value;
|
|
585
|
+
if (value === MISSING) {
|
|
586
|
+
task.put({ none: task.name });
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const loop = index.get(value);
|
|
590
|
+
if (loop !== undefined) {
|
|
591
|
+
task.put({ loop });
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const term = task.tried || task.keep ? undefined : termOrNothing(machine, value);
|
|
595
|
+
if (term !== undefined) task.put({ value: term });
|
|
596
|
+
else if (value instanceof Error) task.put({ node: open(value) });
|
|
597
|
+
else if ((Array.isArray(value) || isRecord(value)) && (task.nest || holdsException(value))) {
|
|
598
|
+
const held = holders.get(value);
|
|
599
|
+
if (held === undefined) {
|
|
600
|
+
const children: ExceptionTask[] = [];
|
|
601
|
+
task.put({ holder: hold(task.name, value, task.keep, children) });
|
|
602
|
+
later(children);
|
|
603
|
+
} else {
|
|
604
|
+
// A container that an earlier place holds, also a container that holds itself, is a share (D88).
|
|
605
|
+
if (task.keep) (items[held] as Holder).keep = true;
|
|
606
|
+
task.put({ same: held, name: task.name });
|
|
607
|
+
}
|
|
608
|
+
} else task.put({ none: task.name });
|
|
609
|
+
}
|
|
610
|
+
const { formed, shares } = formsOf(items);
|
|
611
|
+
// Each item of a node or of a container comes later than its owner, thus the forms grow from the last to the first.
|
|
612
|
+
const forms: (ReplayExceptionJson | Built)[] = [];
|
|
613
|
+
const builtOf = (part: Part): Built => {
|
|
614
|
+
if ("none" in part) return { names: [part.none] };
|
|
615
|
+
if ("node" in part) return { form: { exception: forms[part.node] as ReplayExceptionJson }, names: [] };
|
|
616
|
+
if ("holder" in part) return forms[part.holder] as Built;
|
|
617
|
+
// A share of a container without a form has no form either, thus its name stands for it.
|
|
618
|
+
if ("same" in part) return formed[part.same] === true ? { form: { share: shares[part.same] as number }, names: [] } : { names: [part.name] };
|
|
619
|
+
return { form: part, names: [] };
|
|
620
|
+
};
|
|
621
|
+
for (let at = items.length - 1; at >= 0; at -= 1) {
|
|
622
|
+
const item = items[at] as ExceptionNode | Holder;
|
|
623
|
+
if ("slots" in item) {
|
|
624
|
+
// A container without a form in it has no form, thus its name stands for each of its items (D82).
|
|
625
|
+
if (formed[at] !== true) {
|
|
626
|
+
forms[at] = { names: [item.name] };
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const built = Array.from(item.slots, builtOf);
|
|
630
|
+
const form = item.list ? built.map((part) => part.form ?? { dropped: true as const }) : { record: Object.fromEntries(keptOf(item.keys, built)) };
|
|
631
|
+
forms[at] = { form, names: built.flatMap((part) => part.names) };
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const { head, keys, parts, hiddenKeys, hidden, cause } = item;
|
|
635
|
+
const linked = Array.from(parts, builtOf);
|
|
636
|
+
const kept = Array.from(hidden, builtOf);
|
|
637
|
+
// A cause is never an item of a container, thus it never has the mark.
|
|
638
|
+
const link: CauseJson = cause === undefined ? { names: [] } : causeOf(builtOf(cause));
|
|
639
|
+
const dropped = [...linked, ...kept, link].flatMap((part) => part.names);
|
|
640
|
+
const partsOf = keptOf(keys, linked);
|
|
641
|
+
const hiddenOf = keptOf(hiddenKeys, kept);
|
|
642
|
+
forms[at] = {
|
|
643
|
+
...head,
|
|
644
|
+
...(partsOf.length === 0 ? {} : { parts: Object.fromEntries(partsOf) }),
|
|
645
|
+
...(dropped.length === 0 ? {} : { dropped }),
|
|
646
|
+
...(hiddenOf.length === 0 ? {} : { hidden: Object.fromEntries(hiddenOf) }),
|
|
647
|
+
...link.link,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return causeOf(builtOf(top));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* This function tells which containers of the walk of `causeJson` get a form, and it gives each such container its
|
|
655
|
+
* index for `share` (D88). A container gets a form when it is a hidden list, or when an item has a form. A share has
|
|
656
|
+
* the form of its container. Thus a cycle of containers without another form has no form. The index counts the
|
|
657
|
+
* containers with a form in the order of the walk, as `decodeLog` meets them.
|
|
658
|
+
*/
|
|
659
|
+
function formsOf(items: readonly (ExceptionNode | Holder)[]): { readonly formed: readonly boolean[]; readonly shares: readonly number[] } {
|
|
660
|
+
const formed: boolean[] = [];
|
|
661
|
+
// The containers that get a form when the container of the key gets one.
|
|
662
|
+
const users = new Map<number, number[]>();
|
|
663
|
+
const work: number[] = [];
|
|
664
|
+
for (let at = 0; at < items.length; at += 1) {
|
|
665
|
+
const item = items[at] as ExceptionNode | Holder;
|
|
666
|
+
if (!("slots" in item)) continue;
|
|
667
|
+
let seed = item.keep;
|
|
668
|
+
for (const part of item.slots) {
|
|
669
|
+
const on = "holder" in part ? part.holder : "same" in part ? part.same : undefined;
|
|
670
|
+
if (on === undefined) seed ||= !("none" in part);
|
|
671
|
+
else {
|
|
672
|
+
const list = users.get(on);
|
|
673
|
+
if (list === undefined) users.set(on, [at]);
|
|
674
|
+
else list.push(at);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (seed) {
|
|
678
|
+
formed[at] = true;
|
|
679
|
+
work.push(at);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
for (let at = work.pop(); at !== undefined; at = work.pop()) {
|
|
683
|
+
for (const user of users.get(at) ?? []) {
|
|
684
|
+
if (formed[user] === true) continue;
|
|
685
|
+
formed[user] = true;
|
|
686
|
+
work.push(user);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const shares: number[] = [];
|
|
690
|
+
let count = 0;
|
|
691
|
+
for (let at = 0; at < items.length; at += 1) {
|
|
692
|
+
if (formed[at] !== true) continue;
|
|
693
|
+
shares[at] = count;
|
|
694
|
+
count += 1;
|
|
695
|
+
}
|
|
696
|
+
return { formed, shares };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** This function gives the message of an exception, or a short text for another value. */
|
|
700
|
+
function messageOf(error: unknown): string {
|
|
701
|
+
return error instanceof Error ? error.message : describe(error);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** This function writes a value of a log as a term document, or throws a `TypeError` that names the value. */
|
|
705
|
+
function termOf(machine: Pick<Machine, "domains">, value: unknown, what: () => string): Json {
|
|
706
|
+
try {
|
|
707
|
+
return encodeValue(machine, value);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
throw new TypeError(`encodeLog cannot write ${what()}: ${messageOf(error)}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** This function writes an error value of a log as JSON, or throws a `TypeError` that names the error. */
|
|
714
|
+
function errorJson(machine: Pick<Machine, "domains">, error: EvalError, what: () => string): ReplayErrorJson {
|
|
715
|
+
for (const key of Object.keys(error)) {
|
|
716
|
+
if (!errorField(key)) throw new TypeError(`encodeLog cannot write ${what()}: the error has the field ${key}, and the JSON form has no field for it.`);
|
|
717
|
+
}
|
|
718
|
+
const base = { code: error.code, reason: error.reason, ...(error.place === undefined ? {} : { place: error.place }) };
|
|
719
|
+
if (!Object.hasOwn(error, "cause")) return base;
|
|
720
|
+
const cause: unknown = error.cause;
|
|
721
|
+
try {
|
|
722
|
+
return { ...base, cause: encodeValue(machine, cause) };
|
|
723
|
+
} catch (failure) {
|
|
724
|
+
// An exception, or a list or a record with an exception, is a best effort (D76, D87). Each other cause is a refusal.
|
|
725
|
+
const { link, names } = causeJson(machine, cause);
|
|
726
|
+
if (link === undefined) throw new TypeError(`encodeLog cannot write the cause of ${what()}: ${messageOf(failure)}`);
|
|
727
|
+
return { ...base, ...link, ...(names.length === 0 ? {} : { dropped: names }) };
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* This function writes a log as JSON (RUN.REPLAY-JSON, D75). Then `JSON.stringify` gives the text of a golden file.
|
|
733
|
+
*
|
|
734
|
+
* - Each payload and each value of an outcome is a term document of `encodeValue`. Thus `NaN`, `-0` and `undefined`
|
|
735
|
+
* stay exact, and a member of a domain with a codec is `ext.domain`.
|
|
736
|
+
* - An error keeps its code, its reason, its place and its cause. A cause that `encodeValue` cannot write and that is
|
|
737
|
+
* an exception keeps its name and its message in the field `exception`.
|
|
738
|
+
* - An exception also keeps its own fields that `encodeValue` writes, and the chain of its causes (RUN.REPLAY-CAUSES).
|
|
739
|
+
* The list `dropped` names each other own field. A cause that is an earlier exception of the chain is its index in `loop`.
|
|
740
|
+
* - An exception keeps the hidden fields of the standard classes in `hidden`, for example `errors` of an
|
|
741
|
+
* `AggregateError` (RUN.REPLAY-HIDDEN, D77). Each exception in them has the rule of a cause.
|
|
742
|
+
* - An enumerable field without a term document that is an exception, a list or a plain record is in `parts`
|
|
743
|
+
* (RUN.REPLAY-FIELDS, D78, D82). An item of a list without a form is the mark `dropped` at its place.
|
|
744
|
+
* - A cause without a term document that is a list or a plain record with an exception is in `causeParts`
|
|
745
|
+
* (RUN.REPLAY-CAUSE-PARTS, D87). The rule is the same for the cause of an exception and of an error value.
|
|
746
|
+
* - A list or a record of parts that an earlier place of one error holds is the part `share` with its index
|
|
747
|
+
* (RUN.REPLAY-SHARED, D88). This rule also covers a container that holds itself.
|
|
748
|
+
* - An outcome that the log does not have stays absent.
|
|
749
|
+
*
|
|
750
|
+
* @param machine - The machine of the run, or only its domains.
|
|
751
|
+
* @param log - The log of a recorder.
|
|
752
|
+
* @returns The JSON form. `decodeLog` gives the log again.
|
|
753
|
+
* @throws {TypeError} A value has no term document, for example a closure, a host thing, a symbol, or a value that no
|
|
754
|
+
* domain with a codec holds. The message names the effect and its address. A part of an exception is not a refusal.
|
|
755
|
+
*/
|
|
756
|
+
export function encodeLog(machine: Pick<Machine, "domains">, log: ReplayLog): ReplayLogJson {
|
|
757
|
+
const effects = log.effects.map((entry): ReplayEntryJson => {
|
|
758
|
+
const at = (field: string) => (): string => `the ${field} of the effect ${entry.kind} at the address ${entry.address}`;
|
|
759
|
+
const head = { address: entry.address, kind: entry.kind, payload: termOf(machine, entry.payload, at("payload")) };
|
|
760
|
+
const outcome = entry.outcome;
|
|
761
|
+
if (outcome === undefined) return { ...head, waited: entry.waited };
|
|
762
|
+
const json: ReplayResultJson = outcome.ok
|
|
763
|
+
? { ok: true, value: termOf(machine, outcome.value, at("outcome")) }
|
|
764
|
+
: { ok: false, error: errorJson(machine, outcome.error, at("outcome")) };
|
|
765
|
+
return { ...head, outcome: json, waited: entry.waited };
|
|
766
|
+
});
|
|
767
|
+
return { version: 1, effects, settles: log.settles.map((settle) => ({ all: settle.all, item: settle.item })) };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** This function gives an exception with a name and a message. A standard name gives an instance of its class. */
|
|
771
|
+
function exception(name: string, message: string): Error {
|
|
772
|
+
const made = standard(name, message);
|
|
773
|
+
if (made.name !== name) made.name = name;
|
|
774
|
+
return made;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* This function gives an instance of the standard class with a name, or an `Error`. An `AggregateError` gets an empty
|
|
779
|
+
* list `errors`, and a `SuppressedError` gets `undefined` for `error` and `suppressed`, as their constructors do.
|
|
780
|
+
*/
|
|
781
|
+
function standard(name: string, message: string): Error {
|
|
782
|
+
switch (name) {
|
|
783
|
+
case "TypeError":
|
|
784
|
+
return new TypeError(message);
|
|
785
|
+
case "RangeError":
|
|
786
|
+
return new RangeError(message);
|
|
787
|
+
case "SyntaxError":
|
|
788
|
+
return new SyntaxError(message);
|
|
789
|
+
case "ReferenceError":
|
|
790
|
+
return new ReferenceError(message);
|
|
791
|
+
case "AggregateError":
|
|
792
|
+
return new AggregateError([], message);
|
|
793
|
+
case "SuppressedError":
|
|
794
|
+
return typeof SuppressedError === "function" ? new SuppressedError(undefined, undefined, message) : new Error(message);
|
|
795
|
+
default:
|
|
796
|
+
return new Error(message);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** This function gives an exception a field that is not enumerable, as the constructor of `Error` does for `cause`. */
|
|
801
|
+
function hide(error: Error, key: string, value: unknown): void {
|
|
802
|
+
Object.defineProperty(error, key, { value, writable: true, enumerable: false, configurable: true });
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** This function gives the failed result of a JSON form that `decodeLog` cannot read. */
|
|
806
|
+
function bad(place: readonly Key[], reason: string): { readonly ok: false; readonly error: EvalError } {
|
|
807
|
+
return fail("#VALUE!", `decodeLog cannot read the log: ${reason}`, { place });
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** This function reads a term document of a log again, with the formula kit and the domains of the machine. */
|
|
811
|
+
function readTerm(kit: Machine, doc: unknown, place: readonly Key[]): Result {
|
|
812
|
+
const value = decodeValue(kit, doc);
|
|
813
|
+
return value.ok ? value : bad(place, `the term document gives ${value.error.code}: ${value.error.reason}`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** This function reads the JSON form of an error value again. */
|
|
817
|
+
function readError(kit: Machine, data: unknown, place: readonly Key[]): Result<EvalError> {
|
|
818
|
+
if (!isRecord(data) || typeof data["code"] !== "string" || !data["code"].startsWith("#") || typeof data["reason"] !== "string") {
|
|
819
|
+
return bad(place, "an error is a record with a code that starts with # and a reason.");
|
|
820
|
+
}
|
|
821
|
+
for (const key of Object.keys(data)) {
|
|
822
|
+
if (!errorField(key) && key !== "exception" && key !== "causeParts" && key !== "dropped") return bad(place, `an error has no field ${key}.`);
|
|
823
|
+
}
|
|
824
|
+
const keys = data["place"];
|
|
825
|
+
if (keys !== undefined && !(Array.isArray(keys) && keys.every((key) => isKey(key)))) return bad([...place, "place"], "a place is a list of texts and numbers.");
|
|
826
|
+
const dropped = data["dropped"];
|
|
827
|
+
if (dropped !== undefined && !(Array.isArray(dropped) && dropped.every((name) => typeof name === "string"))) return bad([...place, "dropped"], "the list dropped holds texts.");
|
|
828
|
+
const error: { code: ErrorCode; reason: string; place?: readonly Key[]; cause?: unknown } = { code: data["code"] as ErrorCode, reason: data["reason"] };
|
|
829
|
+
if (keys !== undefined) error.place = keys as readonly Key[];
|
|
830
|
+
const record = data;
|
|
831
|
+
const links = LINKS.filter((key) => key !== "loop" && Object.hasOwn(record, key));
|
|
832
|
+
if (links.length > 1) return bad(place, "an error has a cause, an exception or causeParts, not more than one.");
|
|
833
|
+
const link = links[0];
|
|
834
|
+
if (link === "cause") {
|
|
835
|
+
const cause = readTerm(kit, data["cause"], [...place, "cause"]);
|
|
836
|
+
if (!cause.ok) return cause;
|
|
837
|
+
error.cause = cause.value;
|
|
838
|
+
} else if (link !== undefined) {
|
|
839
|
+
// The index 0 of a loop in causeParts is the first exception in it, because the error value is not an exception (D87).
|
|
840
|
+
const read = readCause(kit, link, data[link], [...place, link]);
|
|
841
|
+
if (!read.ok) return read;
|
|
842
|
+
error.cause = read.value;
|
|
843
|
+
}
|
|
844
|
+
return ok(error);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** One part of the JSON form of a cause that the walk of `readCause` reads next, and the place for its value. */
|
|
848
|
+
interface ReadTask {
|
|
849
|
+
/**
|
|
850
|
+
* The field of the part: `value` or `cause` for a term document, `exception`, `loop`, `record`, `share` or `dropped`.
|
|
851
|
+
* The kind `list` is a list of parts.
|
|
852
|
+
*/
|
|
853
|
+
readonly kind: string;
|
|
854
|
+
readonly data: unknown;
|
|
855
|
+
readonly where: readonly Key[];
|
|
856
|
+
readonly put: (value: unknown) => void;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* This function reads the JSON form of a cause again: an exception in `exception`, or a container in `causeParts`
|
|
861
|
+
* (RUN.REPLAY-CAUSES, RUN.REPLAY-HIDDEN, RUN.REPLAY-FIELDS, D87). The walk has the order of `causeJson`, thus a loop
|
|
862
|
+
* gives the same exception. A list or a record of parts gives a new list or plain record (D82). A share gives the same
|
|
863
|
+
* list or record as its earlier place (D88). The walk is a loop, thus a long chain does not overflow the stack.
|
|
864
|
+
*/
|
|
865
|
+
function readCause(kit: Machine, kind: string, data: unknown, place: readonly Key[]): Result<unknown> {
|
|
866
|
+
const chain: Error[] = [];
|
|
867
|
+
// The lists and the records of parts in the order of the walk, for the part share (D88).
|
|
868
|
+
const held: object[] = [];
|
|
869
|
+
const tasks: ReadTask[] = [];
|
|
870
|
+
const open = (at: unknown, where: readonly Key[]): Result<Error> => {
|
|
871
|
+
if (!isRecord(at) || typeof at["name"] !== "string" || typeof at["message"] !== "string" || !Object.keys(at).every(exceptionField)) {
|
|
872
|
+
return bad(where, "an exception is a record with the texts name and message, and only the fields fields, parts, dropped, hidden, cause, exception, loop and causeParts.");
|
|
873
|
+
}
|
|
874
|
+
const made = exception(at["name"], at["message"]);
|
|
875
|
+
const fields = at["fields"];
|
|
876
|
+
if (fields !== undefined) {
|
|
877
|
+
if (!isRecord(fields)) return bad([...where, "fields"], "the fields of an exception are a record.");
|
|
878
|
+
for (const key of Object.keys(fields)) {
|
|
879
|
+
if (ownField(key)) return bad([...where, "fields", key], `the field ${key} of an exception is not in its fields.`);
|
|
880
|
+
const value = readTerm(kit, fields[key], [...where, "fields", key]);
|
|
881
|
+
if (!value.ok) return value;
|
|
882
|
+
Object.defineProperty(made, key, { value: value.value, writable: true, enumerable: true, configurable: true });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const dropped = at["dropped"];
|
|
886
|
+
if (dropped !== undefined && !(Array.isArray(dropped) && dropped.every((name) => typeof name === "string"))) return bad([...where, "dropped"], "the list dropped holds texts.");
|
|
887
|
+
chain.push(made);
|
|
888
|
+
const children: ReadTask[] = [];
|
|
889
|
+
const taken = (key: string): boolean => isRecord(fields) && Object.hasOwn(fields, key);
|
|
890
|
+
const parts = at["parts"];
|
|
891
|
+
if (parts !== undefined) {
|
|
892
|
+
if (!isRecord(parts)) return bad([...where, "parts"], "the parts of an exception are a record.");
|
|
893
|
+
for (const key of Object.keys(parts)) {
|
|
894
|
+
if (ownField(key) || taken(key)) return bad([...where, "parts", key], `the field ${key} of an exception is not in its parts.`);
|
|
895
|
+
const show = (value: unknown): void => {
|
|
896
|
+
Object.defineProperty(made, key, { value, writable: true, enumerable: true, configurable: true });
|
|
897
|
+
};
|
|
898
|
+
const read = slotTask(parts[key], [...where, "parts", key], false, show);
|
|
899
|
+
if (!read.ok) return read;
|
|
900
|
+
// The field gets its place in the order of the keys at once, and the walk gives its value later.
|
|
901
|
+
show(undefined);
|
|
902
|
+
children.push(read.value);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const hidden = at["hidden"];
|
|
906
|
+
if (hidden !== undefined) {
|
|
907
|
+
if (!isRecord(hidden)) return bad([...where, "hidden"], "the hidden fields of an exception are a record.");
|
|
908
|
+
for (const key of Object.keys(hidden)) {
|
|
909
|
+
if (!HIDDEN.includes(key) || taken(key) || (isRecord(parts) && Object.hasOwn(parts, key))) return bad([...where, "hidden", key], `an exception has no hidden field ${key}.`);
|
|
910
|
+
}
|
|
911
|
+
for (const key of HIDDEN) {
|
|
912
|
+
if (!Object.hasOwn(hidden, key)) continue;
|
|
913
|
+
const read = slotTask(hidden[key], [...where, "hidden", key], false, (value) => hide(made, key, value));
|
|
914
|
+
if (!read.ok) return read;
|
|
915
|
+
children.push(read.value);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const record = at;
|
|
919
|
+
const links = LINKS.filter((key) => Object.hasOwn(record, key));
|
|
920
|
+
if (links.length > 1) return bad(where, "an exception has a cause, an exception, a loop or causeParts, not more than one.");
|
|
921
|
+
const link = links[0];
|
|
922
|
+
if (link !== undefined) {
|
|
923
|
+
const read = linkTask(link, record[link], [...where, link], (value) => hide(made, "cause", value));
|
|
924
|
+
if (!read.ok) return read;
|
|
925
|
+
children.push(read.value);
|
|
926
|
+
}
|
|
927
|
+
for (let index = children.length - 1; index >= 0; index -= 1) tasks.push(children[index] as ReadTask);
|
|
928
|
+
return ok(made);
|
|
929
|
+
};
|
|
930
|
+
let cause: unknown;
|
|
931
|
+
const root = linkTask(kind, data, place, (value) => {
|
|
932
|
+
cause = value;
|
|
933
|
+
});
|
|
934
|
+
if (!root.ok) return root;
|
|
935
|
+
tasks.push(root.value);
|
|
936
|
+
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
|
|
937
|
+
const { kind, data: part, where } = task;
|
|
938
|
+
// The mark keeps the place of an item without a form: the list keeps a hole there (D78).
|
|
939
|
+
if (kind === "dropped") continue;
|
|
940
|
+
if (kind === "loop") {
|
|
941
|
+
if (typeof part !== "number" || !Number.isInteger(part) || part < 0 || part >= chain.length) return bad(where, "a loop is the index of an exception of the chain until this one.");
|
|
942
|
+
task.put(chain[part]);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (kind === "share") {
|
|
946
|
+
if (typeof part !== "number" || !Number.isInteger(part) || part < 0 || part >= held.length) return bad(where, "a share is the index of a list or a record of parts of the walk until this one.");
|
|
947
|
+
task.put(held[part]);
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (kind === "list" || kind === "record") {
|
|
951
|
+
const items = kind === "list" ? listTasks(part, where, held, task.put) : recordTasks(part, where, held, task.put);
|
|
952
|
+
if (!items.ok) return items;
|
|
953
|
+
for (let index = items.value.length - 1; index >= 0; index -= 1) tasks.push(items.value[index] as ReadTask);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const value = kind === "exception" ? open(part, where) : readTerm(kit, part, where);
|
|
957
|
+
if (!value.ok) return value;
|
|
958
|
+
task.put(value.value);
|
|
959
|
+
}
|
|
960
|
+
return ok(cause);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* This function reads the field of a cause: `cause`, `exception`, `loop` or `causeParts`. A value of `causeParts` is a
|
|
965
|
+
* list of parts, the part `record` or the part `share`, and no other part (D87, D88).
|
|
966
|
+
*/
|
|
967
|
+
function linkTask(kind: string, data: unknown, where: readonly Key[], put: (value: unknown) => void): Result<ReadTask> {
|
|
968
|
+
if (kind !== "causeParts") return ok({ kind, data, where, put });
|
|
969
|
+
if (Array.isArray(data)) return ok({ kind: "list", data, where, put });
|
|
970
|
+
const key = isRecord(data) && Object.keys(data).length === 1 ? Object.keys(data)[0] : undefined;
|
|
971
|
+
if (isRecord(data) && (key === "record" || key === "share")) return ok({ kind: key, data: data[key], where: [...where, key], put });
|
|
972
|
+
return bad(where, "a cause of parts is a list of parts, the part record or the part share.");
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* This function reads one value of a field in `parts`, in `hidden` or in a record of parts. A list is a list of parts,
|
|
977
|
+
* and another value is one part.
|
|
978
|
+
*/
|
|
979
|
+
function slotTask(data: unknown, where: readonly Key[], item: boolean, put: (value: unknown) => void): Result<ReadTask> {
|
|
980
|
+
return Array.isArray(data) ? ok({ kind: "list", data, where, put }) : partTask(data, where, item, put);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* This function gives a list of parts its length at once, and it gives the tasks of its items. Thus the mark `dropped`
|
|
985
|
+
* leaves a hole at its place, and each later item keeps its index (D78). The list goes in `held` for a share (D88).
|
|
986
|
+
*/
|
|
987
|
+
function listTasks(data: unknown, where: readonly Key[], held: object[], put: (value: unknown) => void): Result<ReadTask[]> {
|
|
988
|
+
const items = data as readonly unknown[];
|
|
989
|
+
const list: unknown[] = [];
|
|
990
|
+
list.length = items.length;
|
|
991
|
+
held.push(list);
|
|
992
|
+
put(list);
|
|
993
|
+
const tasks: ReadTask[] = [];
|
|
994
|
+
for (let item = 0; item < items.length; item += 1) {
|
|
995
|
+
const task = slotTask(items[item], [...where, item], true, (value) => {
|
|
996
|
+
list[item] = value;
|
|
997
|
+
});
|
|
998
|
+
if (!task.ok) return task;
|
|
999
|
+
tasks.push(task.value);
|
|
1000
|
+
}
|
|
1001
|
+
return ok(tasks);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* This function gives a record of parts a new plain record (D82). It gives the tasks of its fields in the order of
|
|
1006
|
+
* the keys. Each key gets its place at once, thus the record keeps the order of the keys. The record goes in `held`
|
|
1007
|
+
* for a share (D88).
|
|
1008
|
+
*/
|
|
1009
|
+
function recordTasks(data: unknown, where: readonly Key[], held: object[], put: (value: unknown) => void): Result<ReadTask[]> {
|
|
1010
|
+
if (!isRecord(data)) return bad(where, "a record of parts is a record.");
|
|
1011
|
+
const record: Record<string, unknown> = {};
|
|
1012
|
+
held.push(record);
|
|
1013
|
+
put(record);
|
|
1014
|
+
const tasks: ReadTask[] = [];
|
|
1015
|
+
for (const key of Object.keys(data)) {
|
|
1016
|
+
const show = (value: unknown): void => {
|
|
1017
|
+
Object.defineProperty(record, key, { value, writable: true, enumerable: true, configurable: true });
|
|
1018
|
+
};
|
|
1019
|
+
const task = slotTask(data[key], [...where, key], false, show);
|
|
1020
|
+
if (!task.ok) return task;
|
|
1021
|
+
show(undefined);
|
|
1022
|
+
tasks.push(task.value);
|
|
1023
|
+
}
|
|
1024
|
+
return ok(tasks);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* This function reads one part: a record with only one of the fields `value`, `exception`, `loop`, `record` and
|
|
1029
|
+
* `share`. An item of a list can also be the mark `{ "dropped": true }`.
|
|
1030
|
+
*/
|
|
1031
|
+
function partTask(data: unknown, where: readonly Key[], item: boolean, put: (value: unknown) => void): Result<ReadTask> {
|
|
1032
|
+
const keys = isRecord(data) ? Object.keys(data) : [];
|
|
1033
|
+
const kind = keys[0];
|
|
1034
|
+
const mark = kind === "dropped" && item && isRecord(data) && data[kind] === true;
|
|
1035
|
+
if (!isRecord(data) || keys.length !== 1 || kind === undefined || !(PARTS.includes(kind) || mark)) {
|
|
1036
|
+
return bad(where, "a field of parts is a part or a list of parts. A part has only one of the fields value, exception, loop, record and share, or an item is the mark dropped.");
|
|
1037
|
+
}
|
|
1038
|
+
return ok({ kind, data: data[kind], where: [...where, kind], put });
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** The fields of the JSON form of an exception that give its cause. */
|
|
1042
|
+
const LINKS: readonly string[] = /* @__PURE__ */ Object.freeze(["cause", "exception", "loop", "causeParts"]);
|
|
1043
|
+
|
|
1044
|
+
/** The fields of the JSON form of a part of a field in `parts` or `hidden`. */
|
|
1045
|
+
const PARTS: readonly string[] = /* @__PURE__ */ Object.freeze(["value", "exception", "loop", "record", "share"]);
|
|
1046
|
+
|
|
1047
|
+
/** This function reads the JSON form of one effect again. */
|
|
1048
|
+
function readEntry(kit: Machine, data: unknown, place: readonly Key[]): Result<ReplayEntry> {
|
|
1049
|
+
if (!isRecord(data) || typeof data["address"] !== "string" || typeof data["kind"] !== "string" || typeof data["waited"] !== "boolean" || !Object.hasOwn(data, "payload")) {
|
|
1050
|
+
return bad(place, "an effect is a record with the texts address and kind, a payload, and the truth value waited.");
|
|
1051
|
+
}
|
|
1052
|
+
for (const key of Object.keys(data)) {
|
|
1053
|
+
if (key !== "address" && key !== "kind" && key !== "payload" && key !== "outcome" && key !== "waited") return bad(place, `an effect has no field ${key}.`);
|
|
1054
|
+
}
|
|
1055
|
+
const payload = readTerm(kit, data["payload"], [...place, "payload"]);
|
|
1056
|
+
if (!payload.ok) return payload;
|
|
1057
|
+
const head = { address: data["address"], kind: data["kind"], payload: payload.value, waited: data["waited"] };
|
|
1058
|
+
if (!Object.hasOwn(data, "outcome")) return ok(head);
|
|
1059
|
+
const raw = data["outcome"];
|
|
1060
|
+
const at = [...place, "outcome"];
|
|
1061
|
+
if (isRecord(raw) && raw["ok"] === true && Object.hasOwn(raw, "value") && Object.keys(raw).length === 2) {
|
|
1062
|
+
const value = readTerm(kit, raw["value"], [...at, "value"]);
|
|
1063
|
+
return value.ok ? ok({ ...head, outcome: ok(value.value) }) : value;
|
|
1064
|
+
}
|
|
1065
|
+
if (isRecord(raw) && raw["ok"] === false && Object.keys(raw).length === 2) {
|
|
1066
|
+
const error = readError(kit, raw["error"], [...at, "error"]);
|
|
1067
|
+
return error.ok ? ok({ ...head, outcome: { ok: false, error: error.value } }) : error;
|
|
1068
|
+
}
|
|
1069
|
+
return bad(at, "an outcome is a record with ok true and a value, or with ok false and an error.");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* This function reads the JSON form of a log again (RUN.REPLAY-JSON). It reads each term document with the formula kit
|
|
1074
|
+
* and the domains of the machine. A cause in the field `exception` becomes an exception with that name and message.
|
|
1075
|
+
* The exception gets its fields and the chain of its causes again, also a cycle (RUN.REPLAY-CAUSES). A list gets a
|
|
1076
|
+
* hole at the place of the mark `dropped`, thus each later item keeps its index (D78). Each share of a list or a record
|
|
1077
|
+
* gives the same object (RUN.REPLAY-SHARED).
|
|
1078
|
+
*
|
|
1079
|
+
* @param machine - The machine of the run, or only its domains.
|
|
1080
|
+
* @param data - The JSON form, for example from `JSON.parse` of a golden file.
|
|
1081
|
+
* @returns The log for `replayer`, or `#VALUE!` with the place of the first field that it cannot read.
|
|
1082
|
+
*/
|
|
1083
|
+
export function decodeLog(machine: Pick<Machine, "domains">, data: unknown): Result<ReplayLog> {
|
|
1084
|
+
if (!isRecord(data) || data["version"] !== 1) return bad([], "the form is a record with the version 1.");
|
|
1085
|
+
const list = data["effects"];
|
|
1086
|
+
const order = data["settles"];
|
|
1087
|
+
if (!Array.isArray(list) || !Array.isArray(order) || Object.keys(data).length !== 3) return bad([], "the form has only the version and the lists effects and settles.");
|
|
1088
|
+
const domains = machine.domains;
|
|
1089
|
+
const kit: Machine = { ...formulaMachine(), ...(domains === undefined ? {} : { domains }) };
|
|
1090
|
+
const effects: ReplayEntry[] = [];
|
|
1091
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
1092
|
+
const entry = readEntry(kit, list[index], ["effects", index]);
|
|
1093
|
+
if (!entry.ok) return entry;
|
|
1094
|
+
effects.push(entry.value);
|
|
1095
|
+
}
|
|
1096
|
+
const settles: ReplaySettle[] = [];
|
|
1097
|
+
for (let index = 0; index < order.length; index += 1) {
|
|
1098
|
+
const settle: unknown = order[index];
|
|
1099
|
+
if (!isRecord(settle) || typeof settle["all"] !== "string" || !Number.isInteger(settle["item"]) || Object.keys(settle).length !== 2) {
|
|
1100
|
+
return bad(["settles", index], "a settle is a record with only the text all and the whole number item.");
|
|
1101
|
+
}
|
|
1102
|
+
settles.push({ all: settle["all"], item: settle["item"] as number });
|
|
1103
|
+
}
|
|
1104
|
+
return ok({ effects, settles });
|
|
1105
|
+
}
|