@mark1russell7/coeval 0.17.0 → 0.19.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 +4 -4
- package/build/index.d.ts +302 -98
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3555 -2068
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/codec.ts +30 -10
- package/src/conform.ts +35 -9
- package/src/durable.ts +17 -5
- package/src/evaluate.ts +38 -2
- package/src/facts.ts +55 -19
- package/src/formula.ts +59 -4
- package/src/index.ts +16 -2
- package/src/instances.ts +637 -0
- package/src/merge.ts +6 -3
- package/src/reads.ts +186 -2
- package/src/reference.ts +20 -1
- package/src/replay.ts +123 -35
- package/src/static.ts +357 -50
- package/src/stdlib.ts +127 -6
- package/src/types.ts +103 -23
- package/src/typing.ts +143 -3
- package/src/world.ts +49 -0
package/src/reads.ts
CHANGED
|
@@ -22,8 +22,41 @@ export type ReadsCheck =
|
|
|
22
22
|
readonly read: Read;
|
|
23
23
|
/** What the path of that read reaches in the new context. */
|
|
24
24
|
readonly now: Result;
|
|
25
|
+
/**
|
|
26
|
+
* `true` when the read reaches the same thing by `sameResult`, but the print of that thing changed
|
|
27
|
+
* (READS.IN-PLACE). Only the option `prints` gives this mark.
|
|
28
|
+
*/
|
|
29
|
+
readonly inPlace?: true;
|
|
25
30
|
};
|
|
26
31
|
|
|
32
|
+
/** The options of `printReads` (READS.IN-PLACE, D112). */
|
|
33
|
+
export interface PrintOptions {
|
|
34
|
+
/**
|
|
35
|
+
* The most entries in the print of one value. Each value, each field name and each mark counts as one entry. A
|
|
36
|
+
* larger value gets no print, and `verifyReads` then gives its read as changed. The default is 10000.
|
|
37
|
+
*/
|
|
38
|
+
readonly limit?: number;
|
|
39
|
+
/**
|
|
40
|
+
* The stamp of the host for an object, for example the version of a host thing. A stamp other than `undefined`
|
|
41
|
+
* takes the place of the fields of that object in the print. A stamp compares with `Object.is`.
|
|
42
|
+
*
|
|
43
|
+
* @param value - An object of the value of a read.
|
|
44
|
+
* @returns The stamp, or `undefined` for the walk of the fields.
|
|
45
|
+
*/
|
|
46
|
+
readonly stamp?: (value: object) => unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The prints of a list of reads, from `printReads`. `verifyReads` takes them with the option `prints`. The prints
|
|
51
|
+
* keep their options, thus the verification walks each value with the same rules.
|
|
52
|
+
*/
|
|
53
|
+
export interface ReadPrints {
|
|
54
|
+
/** The print of each read, at the index of the read. A read without an object in its value has no print. */
|
|
55
|
+
readonly prints: readonly (readonly unknown[] | undefined)[];
|
|
56
|
+
/** The options of the prints. */
|
|
57
|
+
readonly options: PrintOptions;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
/** The options of `verifyReads`. */
|
|
28
61
|
export interface VerifyOptions {
|
|
29
62
|
/**
|
|
@@ -35,11 +68,154 @@ export interface VerifyOptions {
|
|
|
35
68
|
* @returns `true` when the two things are the same, or `undefined` for the rules of `sameResult`.
|
|
36
69
|
*/
|
|
37
70
|
readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The prints of the reads, from `printReads` after the evaluation (READS.IN-PLACE). When the prints are given, a
|
|
73
|
+
* read that reaches the same thing also compares its print. Thus a change in place of a host value is a changed
|
|
74
|
+
* read. Without the prints, the verification does no work for them.
|
|
75
|
+
*/
|
|
76
|
+
readonly prints?: ReadPrints;
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
/** One answer serves each list whose reads are all the same. */
|
|
41
80
|
const SAME: ReadsCheck = /* @__PURE__ */ Object.freeze({ same: true });
|
|
42
81
|
|
|
82
|
+
/** The default limit of the entries in the print of one value. */
|
|
83
|
+
const PRINT_LIMIT = 10_000;
|
|
84
|
+
|
|
85
|
+
/** The marks of a print. Each mark is a private object, thus no value of a host is the same as a mark. */
|
|
86
|
+
const OBJECT = /* @__PURE__ */ Object.freeze({ mark: "object" });
|
|
87
|
+
const LIST = /* @__PURE__ */ Object.freeze({ mark: "list" });
|
|
88
|
+
const DATE = /* @__PURE__ */ Object.freeze({ mark: "date" });
|
|
89
|
+
const MAP = /* @__PURE__ */ Object.freeze({ mark: "map" });
|
|
90
|
+
const SET = /* @__PURE__ */ Object.freeze({ mark: "set" });
|
|
91
|
+
const SEEN = /* @__PURE__ */ Object.freeze({ mark: "seen" });
|
|
92
|
+
const STAMP = /* @__PURE__ */ Object.freeze({ mark: "stamp" });
|
|
93
|
+
|
|
94
|
+
/** The print of a value that the walk cannot finish: it is too large, or the host threw. It is not the same as any print. */
|
|
95
|
+
const NO_PRINT: readonly unknown[] = /* @__PURE__ */ Object.freeze([]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* This function gives the print of a value: a flat list of its parts, in the order of a walk (D112). Two prints are
|
|
99
|
+
* the same when they have the same length, and each entry is the same by `Object.is`.
|
|
100
|
+
*
|
|
101
|
+
* - An object starts with a mark and its prototype. A list adds its length, a date its time, and a map or a set
|
|
102
|
+
* its size and its entries. Then come the count of the own enumerable fields, each name and the kind of each field.
|
|
103
|
+
* - A field with a getter gives the getter function. The walk does not start host code of a field.
|
|
104
|
+
* - A primitive value and a function are entries. A function compares by its identity.
|
|
105
|
+
* - An object that the walk already visited gives the mark `SEEN` and its number. Thus a cycle ends.
|
|
106
|
+
* - A value with more than `limit` entries, or an exception of the host, gives `NO_PRINT`.
|
|
107
|
+
*
|
|
108
|
+
* @param value - The value of a read.
|
|
109
|
+
* @param options - The limit and the stamp of the host.
|
|
110
|
+
* @returns The print.
|
|
111
|
+
*/
|
|
112
|
+
function printOf(value: unknown, options: PrintOptions): readonly unknown[] {
|
|
113
|
+
const limit = options.limit ?? PRINT_LIMIT;
|
|
114
|
+
const stamp = options.stamp;
|
|
115
|
+
const out: unknown[] = [];
|
|
116
|
+
const ids = new Map<object, number>();
|
|
117
|
+
const stack: unknown[] = [value];
|
|
118
|
+
try {
|
|
119
|
+
while (stack.length > 0) {
|
|
120
|
+
if (out.length > limit) return NO_PRINT;
|
|
121
|
+
const x = stack.pop();
|
|
122
|
+
if (typeof x !== "object" || x === null) {
|
|
123
|
+
out.push(x);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const id = ids.get(x);
|
|
127
|
+
if (id !== undefined) {
|
|
128
|
+
out.push(SEEN, id);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
ids.set(x, ids.size);
|
|
132
|
+
const stamped = stamp?.(x);
|
|
133
|
+
if (stamped !== undefined) {
|
|
134
|
+
out.push(STAMP, stamped);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const children: unknown[] = [];
|
|
138
|
+
out.push(OBJECT, Object.getPrototypeOf(x));
|
|
139
|
+
if (Array.isArray(x)) out.push(LIST, x.length);
|
|
140
|
+
else if (x instanceof Date) out.push(DATE, Date.prototype.getTime.call(x));
|
|
141
|
+
else if (x instanceof Map) {
|
|
142
|
+
out.push(MAP, x.size);
|
|
143
|
+
if (out.length + 2 * x.size > limit) return NO_PRINT;
|
|
144
|
+
for (const [key, item] of Map.prototype.entries.call(x) as IterableIterator<[unknown, unknown]>) children.push(key, item);
|
|
145
|
+
} else if (x instanceof Set) {
|
|
146
|
+
out.push(SET, x.size);
|
|
147
|
+
if (out.length + x.size > limit) return NO_PRINT;
|
|
148
|
+
for (const item of Set.prototype.values.call(x) as IterableIterator<unknown>) children.push(item);
|
|
149
|
+
}
|
|
150
|
+
const keys = Object.keys(x);
|
|
151
|
+
if (out.length + 3 * keys.length > limit) return NO_PRINT;
|
|
152
|
+
out.push(keys.length);
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
const field = Object.getOwnPropertyDescriptor(x, key);
|
|
155
|
+
// A field that a proxy hides between `keys` and its descriptor reads as a field without a value.
|
|
156
|
+
const data = field === undefined || "value" in field;
|
|
157
|
+
out.push(key, data);
|
|
158
|
+
children.push(field === undefined ? undefined : "value" in field ? field.value : field.get);
|
|
159
|
+
}
|
|
160
|
+
// The stack gives the last entry first, thus the children go on it in the reverse order.
|
|
161
|
+
for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
return NO_PRINT;
|
|
165
|
+
}
|
|
166
|
+
return out.length > limit ? NO_PRINT : out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** This function tells if two prints are the same. `NO_PRINT` is not the same as any print, also not as itself. */
|
|
170
|
+
function samePrint(a: readonly unknown[], b: readonly unknown[]): boolean {
|
|
171
|
+
if (a === NO_PRINT || b === NO_PRINT || a.length !== b.length) return false;
|
|
172
|
+
for (let index = 0; index < a.length; index += 1) if (!Object.is(a[index], b[index])) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** This function gives the print of the value of a result, or `undefined` when the value holds no object. */
|
|
177
|
+
function printOfResult(result: Result, options: PrintOptions, cache: Map<object, readonly unknown[]>): readonly unknown[] | undefined {
|
|
178
|
+
if (!result.ok || typeof result.value !== "object" || result.value === null) return undefined;
|
|
179
|
+
const value = result.value;
|
|
180
|
+
let print = cache.get(value);
|
|
181
|
+
if (print === undefined) {
|
|
182
|
+
print = printOf(value, options);
|
|
183
|
+
cache.set(value, print);
|
|
184
|
+
}
|
|
185
|
+
return print;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* This function gives the prints of the values of a list of reads (READS.IN-PLACE, D112). A host starts it after
|
|
190
|
+
* the evaluation, and it gives the prints to `verifyReads` with the option `prints`.
|
|
191
|
+
*
|
|
192
|
+
* - The print of a value walks its lists, its plain records and the own enumerable fields of each other object. A
|
|
193
|
+
* date adds its time, and a map or a set adds its entries.
|
|
194
|
+
* - A read that is not free, a read that reached an error and a read with a primitive value get no print.
|
|
195
|
+
* - A cycle ends, because the walk visits each object one time.
|
|
196
|
+
* - A value with more entries than the limit gets no print. `verifyReads` then gives that read as changed.
|
|
197
|
+
*
|
|
198
|
+
* The walk does not see the state in a closure or a private field. It does not see a field with a symbol name, a
|
|
199
|
+
* field that is not enumerable or the value of a getter. A host gives a stamp for such an object, for example its version.
|
|
200
|
+
*
|
|
201
|
+
* @param reads - The reads of the evaluation, from the option `reads`.
|
|
202
|
+
* @param options - The limit and the stamp of the host.
|
|
203
|
+
* @returns The prints, for the option `prints` of `verifyReads`.
|
|
204
|
+
*/
|
|
205
|
+
export function printReads(reads: readonly Read[], options: PrintOptions = {}): ReadPrints {
|
|
206
|
+
const cache = new Map<object, readonly unknown[]>();
|
|
207
|
+
const prints = reads.map((read) => (read.free ? printOfResult(read.reached, options, cache) : undefined));
|
|
208
|
+
return { prints, options };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** This function tells if the print of a read changed, when the read has a print. */
|
|
212
|
+
function changedInPlace(prints: ReadPrints, index: number, now: Result, cache: Map<object, readonly unknown[]>): boolean {
|
|
213
|
+
const old = prints.prints[index];
|
|
214
|
+
if (old === undefined) return false;
|
|
215
|
+
const print = printOfResult(now, prints.options, cache);
|
|
216
|
+
return print === undefined || !samePrint(old, print);
|
|
217
|
+
}
|
|
218
|
+
|
|
43
219
|
/** This function tells if two lists of hops are the same: the same steps, rungs and things, in order. */
|
|
44
220
|
function sameHops(a: readonly Hop[], b: readonly Hop[] | undefined, equal: VerifyOptions["equal"]): boolean {
|
|
45
221
|
if (b === undefined || a.length !== b.length) return false;
|
|
@@ -78,18 +254,22 @@ function heldStop(root: Context, path: readonly unknown[]): Result | undefined {
|
|
|
78
254
|
* extension of a profile can read outside `resolve`. The schedule asks `factsOf` first.
|
|
79
255
|
*
|
|
80
256
|
* 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
|
|
257
|
+
* place is the same object, thus `sameResult` cannot see the change. The option `prints` finds such a change
|
|
258
|
+
* (READS.IN-PLACE). The prints come from `printReads` after the evaluation, and the answer then has the mark `inPlace`.
|
|
82
259
|
*
|
|
83
260
|
* @param target - The machine of the evaluation, or its plan.
|
|
84
261
|
* @param reads - The reads of the evaluation, from the option `reads`.
|
|
85
262
|
* @param context - The new context.
|
|
86
|
-
* @param options - The rule of the host for two things.
|
|
263
|
+
* @param options - The rule of the host for two things, and the prints of the reads.
|
|
87
264
|
* @returns `{ same: true }` when each free read reaches the same thing, or else the first changed read.
|
|
88
265
|
*/
|
|
89
266
|
export function verifyReads(target: Machine | Plan, reads: readonly Read[], context: Context, options: VerifyOptions = {}): ReadsCheck {
|
|
90
267
|
const machine = "evaluators" in target ? target : target.machine;
|
|
91
268
|
const root = rootContext(machine, context);
|
|
92
269
|
const equal = options.equal;
|
|
270
|
+
const prints = options.prints;
|
|
271
|
+
// The prints of the new values, made only with the option `prints`. Two reads of one object share its print.
|
|
272
|
+
let printed: Map<object, readonly unknown[]> | undefined;
|
|
93
273
|
for (let index = 0; index < reads.length; index += 1) {
|
|
94
274
|
const read = reads[index] as Read;
|
|
95
275
|
if (!read.free) continue;
|
|
@@ -100,6 +280,7 @@ export function verifyReads(target: Machine | Plan, reads: readonly Read[], cont
|
|
|
100
280
|
const now = keptAgain(machine, root, read.path);
|
|
101
281
|
if (now === undefined) return { same: false, index, read, now: fail("#N/A", "The host does not know the value of the kept read yet.") };
|
|
102
282
|
if (!sameResult(read.reached, now, equal)) return { same: false, index, read, now };
|
|
283
|
+
if (prints !== undefined && changedInPlace(prints, index, now, (printed ??= new Map()))) return { same: false, index, read, now, inPlace: true };
|
|
103
284
|
continue;
|
|
104
285
|
}
|
|
105
286
|
const asked = read.hops !== undefined;
|
|
@@ -107,6 +288,9 @@ export function verifyReads(target: Machine | Plan, reads: readonly Read[], cont
|
|
|
107
288
|
if (!sameResult(read.reached, now.reached, equal) || (read.hops !== undefined && !sameHops(read.hops, now.hops, equal))) {
|
|
108
289
|
return { same: false, index, read, now: now.reached };
|
|
109
290
|
}
|
|
291
|
+
if (prints !== undefined && changedInPlace(prints, index, now.reached, (printed ??= new Map()))) {
|
|
292
|
+
return { same: false, index, read, now: now.reached, inPlace: true };
|
|
293
|
+
}
|
|
110
294
|
}
|
|
111
295
|
return SAME;
|
|
112
296
|
}
|
package/src/reference.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type Steps,
|
|
22
22
|
type StopSignal,
|
|
23
23
|
JOURNAL,
|
|
24
|
+
heldReason,
|
|
24
25
|
makeContext,
|
|
25
26
|
permits,
|
|
26
27
|
placeText,
|
|
@@ -110,6 +111,8 @@ interface Env {
|
|
|
110
111
|
readonly verb: string;
|
|
111
112
|
/** The scope of the code that evaluates at this time. `all` changes it for each item. */
|
|
112
113
|
scope: Scope;
|
|
114
|
+
/** The count of the drives without a wait that are active. In a run, an effect under one does not reach the host (RUN.HELD). */
|
|
115
|
+
held: number;
|
|
113
116
|
/** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
|
|
114
117
|
readonly reading: Reading | undefined;
|
|
115
118
|
}
|
|
@@ -309,6 +312,12 @@ function* causeAndWait(env: Env, ctx: Context, kind: Cap, payload: unknown, plac
|
|
|
309
312
|
if ("ok" in effect) return effect;
|
|
310
313
|
const perform = env.perform;
|
|
311
314
|
if (perform === undefined) return (effect.outcome = ok(payload));
|
|
315
|
+
if (env.held > 0) {
|
|
316
|
+
// The parent cannot wait for the answer of the host, thus the host does not get the effect (RUN.HELD).
|
|
317
|
+
effect.outcome = fail("#CALC!", heldReason(kind), { place });
|
|
318
|
+
yield Promise.resolve(undefined);
|
|
319
|
+
return effect.outcome;
|
|
320
|
+
}
|
|
312
321
|
let given: unknown;
|
|
313
322
|
try {
|
|
314
323
|
given = perform(effect);
|
|
@@ -382,7 +391,16 @@ function evaluateSeen(env: Env, seen: Seen, at: At): Result {
|
|
|
382
391
|
const { chosen } = seen;
|
|
383
392
|
const { place } = at;
|
|
384
393
|
if ("gap" in chosen) return fail("#NOEVAL", chosen.gap, { place });
|
|
385
|
-
if ("runner" in chosen)
|
|
394
|
+
if ("runner" in chosen) {
|
|
395
|
+
if (!env.running) return drive(runSeen(env, seen, at, chosen.runner), place, seen.kindText);
|
|
396
|
+
// In a run, an effect under this drive does not reach the host (RUN.HELD).
|
|
397
|
+
env.held += 1;
|
|
398
|
+
try {
|
|
399
|
+
return drive(runSeen(env, seen, at, chosen.runner), place, seen.kindText);
|
|
400
|
+
} finally {
|
|
401
|
+
env.held -= 1;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
386
404
|
const input = evalInput(env, seen, at);
|
|
387
405
|
try {
|
|
388
406
|
return placed(chosen.evaluator(input), place);
|
|
@@ -655,6 +673,7 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
|
|
|
655
673
|
running,
|
|
656
674
|
verb: options.verb ?? "eval",
|
|
657
675
|
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
676
|
+
held: 0,
|
|
658
677
|
reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
|
|
659
678
|
};
|
|
660
679
|
}
|
package/src/replay.ts
CHANGED
|
@@ -230,7 +230,8 @@ export interface ReplayErrorJson {
|
|
|
230
230
|
/**
|
|
231
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
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.
|
|
233
|
+
* first exception, and the index 0 of `share` is its first list or record. Since D101, a list or a record cause that
|
|
234
|
+
* holds a container at two places is also `causeParts` (RUN.REPLAY-SHARED-VALUES).
|
|
234
235
|
*/
|
|
235
236
|
readonly causeParts?: ReplayCausePartsJson;
|
|
236
237
|
/** The names of the items of `causeParts` without a form, for example `cause.send`. This list is only a note. */
|
|
@@ -251,13 +252,18 @@ export type ReplayCausePartsJson = readonly ReplayPartsJson[] | { readonly recor
|
|
|
251
252
|
export interface ReplayExceptionJson {
|
|
252
253
|
readonly name: string;
|
|
253
254
|
readonly message: string;
|
|
254
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* The term documents of the own fields of the exception that `encodeValue` writes, for example `code` or `errno`. A list
|
|
257
|
+
* or a plain record that two places of the error share is in `parts` (RUN.REPLAY-SHARED-VALUES, D101).
|
|
258
|
+
*/
|
|
255
259
|
readonly fields?: Readonly<Record<string, Json>>;
|
|
256
260
|
/**
|
|
257
261
|
* The own enumerable fields without a term document that are an exception, a list or a plain record (RUN.REPLAY-FIELDS,
|
|
258
262
|
* 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
263
|
* 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
264
|
* that an earlier place holds is the part `share` (D88).
|
|
265
|
+
*
|
|
266
|
+
* Since D101, a shared container with a term document is in `parts` too, and so is each container that holds it.
|
|
261
267
|
*/
|
|
262
268
|
readonly parts?: Readonly<Record<string, ReplayPartsJson>>;
|
|
263
269
|
/**
|
|
@@ -279,7 +285,8 @@ export interface ReplayExceptionJson {
|
|
|
279
285
|
readonly loop?: number;
|
|
280
286
|
/**
|
|
281
287
|
* 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`.
|
|
288
|
+
* (RUN.REPLAY-CAUSE-PARTS, D87). Each exception in it has the rule of a cause, also a `loop`. Since D100, a list or a
|
|
289
|
+
* record without an exception is also `causeParts` when an item has a form (RUN.REPLAY-CAUSE-ITEMS).
|
|
283
290
|
*/
|
|
284
291
|
readonly causeParts?: ReplayCausePartsJson;
|
|
285
292
|
}
|
|
@@ -371,7 +378,10 @@ interface Holder {
|
|
|
371
378
|
/** The name for `dropped` when no item of the container has a form. */
|
|
372
379
|
readonly name: string;
|
|
373
380
|
readonly list: boolean;
|
|
374
|
-
/**
|
|
381
|
+
/**
|
|
382
|
+
* The value `true` marks a hidden list of a standard class. It keeps its marks also without a form (D78). It also marks
|
|
383
|
+
* a shared container with a term document, thus an empty container keeps its form (D101).
|
|
384
|
+
*/
|
|
375
385
|
keep: boolean;
|
|
376
386
|
/** The keys of a record in the order of the keys. A list has no keys. */
|
|
377
387
|
readonly keys: readonly string[];
|
|
@@ -385,7 +395,10 @@ interface ExceptionTask {
|
|
|
385
395
|
readonly name: string;
|
|
386
396
|
/** The value `true` marks a value that `encodeValue` cannot write, thus the walk does not try again. */
|
|
387
397
|
readonly tried: boolean;
|
|
388
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* The value `false` marks the cause of an error value. It is a container only when it holds an exception at some depth
|
|
400
|
+
* (D87). The cause of an exception is a container as a field is (D100).
|
|
401
|
+
*/
|
|
389
402
|
readonly nest: boolean;
|
|
390
403
|
/** The value `true` marks a hidden list of a standard class. It is always a container of parts (D77). */
|
|
391
404
|
readonly keep: boolean;
|
|
@@ -421,9 +434,66 @@ function holdsHole(value: unknown): boolean {
|
|
|
421
434
|
return false;
|
|
422
435
|
}
|
|
423
436
|
|
|
437
|
+
/**
|
|
438
|
+
* This function gives the lists and the plain records that the walk of `causeJson` keeps as containers of parts, also
|
|
439
|
+
* with a term document (D101). Such a container is at two places of the walk, or it holds such a container at some depth.
|
|
440
|
+
* It reads the values as the walk does: the fields of an exception, its hidden fields and its cause, and the items of a
|
|
441
|
+
* container. A term document gives a copy at each place, thus a shared container cannot be one.
|
|
442
|
+
*/
|
|
443
|
+
function sharedOf(root: unknown): ReadonlySet<object> {
|
|
444
|
+
const seen = new Set<object>();
|
|
445
|
+
const twice: object[] = [];
|
|
446
|
+
// The containers that hold each container. A container on the way to a shared container is a container of parts too.
|
|
447
|
+
const owners = new Map<object, object[]>();
|
|
448
|
+
const stack: [unknown, object | undefined][] = [[root, undefined]];
|
|
449
|
+
for (let next = stack.pop(); next !== undefined; next = stack.pop()) {
|
|
450
|
+
const [at, owner] = next;
|
|
451
|
+
if (typeof at !== "object" || at === null) continue;
|
|
452
|
+
const container = Array.isArray(at) || isRecord(at);
|
|
453
|
+
if (!container && !(at instanceof Error)) continue;
|
|
454
|
+
if (container && owner !== undefined) {
|
|
455
|
+
const list = owners.get(at);
|
|
456
|
+
if (list === undefined) owners.set(at, [owner]);
|
|
457
|
+
else list.push(owner);
|
|
458
|
+
}
|
|
459
|
+
if (seen.has(at)) {
|
|
460
|
+
if (container) twice.push(at);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
seen.add(at);
|
|
464
|
+
if (Array.isArray(at)) {
|
|
465
|
+
for (let item = 0; item < at.length; item += 1) if (Object.hasOwn(at, item)) stack.push([readField(at, item), at]);
|
|
466
|
+
} else if (container) {
|
|
467
|
+
for (const key of Object.keys(at)) stack.push([readField(at, key), at]);
|
|
468
|
+
} else {
|
|
469
|
+
// An exception is never a term document of the walk, thus it is not an owner.
|
|
470
|
+
for (const key of Object.keys(at)) if (!ownField(key)) stack.push([readField(at, key), undefined]);
|
|
471
|
+
for (const key of HIDDEN) if (Object.getOwnPropertyDescriptor(at, key)?.enumerable === false) stack.push([readField(at, key), undefined]);
|
|
472
|
+
if (Object.hasOwn(at, "cause")) stack.push([readField(at, "cause"), undefined]);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const shared = new Set<object>();
|
|
476
|
+
for (let at = twice.pop(); at !== undefined; at = twice.pop()) {
|
|
477
|
+
if (shared.has(at)) continue;
|
|
478
|
+
shared.add(at);
|
|
479
|
+
for (const owner of owners.get(at) ?? []) twice.push(owner);
|
|
480
|
+
}
|
|
481
|
+
return shared;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* This function tells if the walk of `causeJson` keeps a value with a term document as a container of parts (D101). The
|
|
486
|
+
* value is in the set of `sharedOf`. A member of a domain stays a term document, because its codec makes it again.
|
|
487
|
+
*/
|
|
488
|
+
function wholeOf(shared: ReadonlySet<object>, value: unknown, term: Json): boolean {
|
|
489
|
+
if (typeof value !== "object" || value === null || !shared.has(value)) return false;
|
|
490
|
+
const type = isRecord(term) ? term["type"] : undefined;
|
|
491
|
+
return !(Array.isArray(type) && type[0] === "ext" && type[1] === "domain");
|
|
492
|
+
}
|
|
493
|
+
|
|
424
494
|
/**
|
|
425
495
|
* 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
|
|
496
|
+
* walk of `causeJson` does, thus a container cause of an error value with an exception gets a form.
|
|
427
497
|
*/
|
|
428
498
|
function holdsException(value: unknown): boolean {
|
|
429
499
|
const seen = new Set<object>();
|
|
@@ -492,13 +562,15 @@ function keptOf(keys: readonly string[], built: readonly Built[]): [string, Repl
|
|
|
492
562
|
*
|
|
493
563
|
* The walk gives each exception an index in the order of a depth-first walk. It takes the fields in `parts` in the
|
|
494
564
|
* 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).
|
|
496
|
-
* when it holds an exception (D87).
|
|
565
|
+
* without a term document is a container of parts, with its items in their order (D82). The cause of an error value
|
|
566
|
+
* is a container only when it holds an exception (D87). The cause of an exception is a container as a field is (D100).
|
|
567
|
+
* A value that is an earlier exception of the walk is its index in `loop` (D77).
|
|
497
568
|
*
|
|
498
|
-
* A container that an earlier place holds is its index in `share`, also a container that holds itself (D88).
|
|
499
|
-
* a
|
|
569
|
+
* A container that an earlier place holds is its index in `share`, also a container that holds itself (D88). A container
|
|
570
|
+
* of `sharedOf` is a container of parts also with a term document, thus each share gives one object (D101). The value
|
|
571
|
+
* `tried` marks a root that `encodeValue` cannot write. The walk is a loop, thus a long chain does not overflow the stack.
|
|
500
572
|
*/
|
|
501
|
-
function causeJson(machine: Pick<Machine, "domains">, root: unknown): CauseJson {
|
|
573
|
+
function causeJson(machine: Pick<Machine, "domains">, root: unknown, shared: ReadonlySet<object>, tried: boolean): CauseJson {
|
|
502
574
|
// The nodes and the containers in the order of the walk. Each item of a node or of a container comes later.
|
|
503
575
|
const items: (ExceptionNode | Holder)[] = [];
|
|
504
576
|
const index = new Map<unknown, number>();
|
|
@@ -530,12 +602,14 @@ function causeJson(machine: Pick<Machine, "domains">, root: unknown): CauseJson
|
|
|
530
602
|
const at = items.length;
|
|
531
603
|
index.set(current, index.size);
|
|
532
604
|
const fields: [string, Json][] = [];
|
|
533
|
-
|
|
605
|
+
// The value `true` marks a field without a term document. A shared container has one, and the walk writes it (D101).
|
|
606
|
+
const linked: [string, unknown, boolean][] = [];
|
|
534
607
|
for (const key of Object.keys(current)) {
|
|
535
608
|
if (ownField(key)) continue;
|
|
536
609
|
const value = readField(current, key);
|
|
537
610
|
const term = value === MISSING ? undefined : termOrNothing(machine, value);
|
|
538
|
-
if (term === undefined) linked.push([key, value]);
|
|
611
|
+
if (term === undefined) linked.push([key, value, true]);
|
|
612
|
+
else if (wholeOf(shared, value, term)) linked.push([key, value, false]);
|
|
539
613
|
else fields.push([key, term]);
|
|
540
614
|
}
|
|
541
615
|
const head = { name: String(current.name), message: String(current.message), ...(fields.length === 0 ? {} : { fields: Object.fromEntries(fields) }) };
|
|
@@ -543,11 +617,11 @@ function causeJson(machine: Pick<Machine, "domains">, root: unknown): CauseJson
|
|
|
543
617
|
items.push(node);
|
|
544
618
|
const children: ExceptionTask[] = [];
|
|
545
619
|
for (let place = 0; place < linked.length; place += 1) {
|
|
546
|
-
const [key, value] = linked[place] as [string, unknown];
|
|
620
|
+
const [key, value, without] = linked[place] as [string, unknown, boolean];
|
|
547
621
|
const put = (part: Part): void => {
|
|
548
622
|
node.parts[place] = part;
|
|
549
623
|
};
|
|
550
|
-
children.push({ value, name: key, tried:
|
|
624
|
+
children.push({ value, name: key, tried: without, nest: true, keep: false, put });
|
|
551
625
|
}
|
|
552
626
|
for (const key of HIDDEN) {
|
|
553
627
|
if (Object.getOwnPropertyDescriptor(current, key)?.enumerable !== false) continue;
|
|
@@ -564,7 +638,8 @@ function causeJson(machine: Pick<Machine, "domains">, root: unknown): CauseJson
|
|
|
564
638
|
const put = (part: Part): void => {
|
|
565
639
|
node.cause = part;
|
|
566
640
|
};
|
|
567
|
-
|
|
641
|
+
// The cause of an exception keeps each item with a form, also without an exception in it (D100).
|
|
642
|
+
children.push({ value: current.cause, name: "cause", tried: false, nest: true, keep: false, put });
|
|
568
643
|
}
|
|
569
644
|
later(children);
|
|
570
645
|
return at;
|
|
@@ -573,7 +648,7 @@ function causeJson(machine: Pick<Machine, "domains">, root: unknown): CauseJson
|
|
|
573
648
|
tasks.push({
|
|
574
649
|
value: root,
|
|
575
650
|
name: "cause",
|
|
576
|
-
tried
|
|
651
|
+
tried,
|
|
577
652
|
nest: false,
|
|
578
653
|
keep: false,
|
|
579
654
|
put: (part) => {
|
|
@@ -591,20 +666,23 @@ function causeJson(machine: Pick<Machine, "domains">, root: unknown): CauseJson
|
|
|
591
666
|
task.put({ loop });
|
|
592
667
|
continue;
|
|
593
668
|
}
|
|
669
|
+
const container = Array.isArray(value) || isRecord(value);
|
|
670
|
+
const held = container ? holders.get(value) : undefined;
|
|
671
|
+
if (held !== undefined) {
|
|
672
|
+
// A container that an earlier place holds, also a container that holds itself, is a share (D88, D101).
|
|
673
|
+
if (task.keep) (items[held] as Holder).keep = true;
|
|
674
|
+
task.put({ same: held, name: task.name });
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
594
677
|
const term = task.tried || task.keep ? undefined : termOrNothing(machine, value);
|
|
595
|
-
|
|
678
|
+
// A shared container with a term document is a container of parts, and it always has a form (D101).
|
|
679
|
+
const whole = term !== undefined && wholeOf(shared, value, term);
|
|
680
|
+
if (term !== undefined && !whole) task.put({ value: term });
|
|
596
681
|
else if (value instanceof Error) task.put({ node: open(value) });
|
|
597
|
-
else if (
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
}
|
|
682
|
+
else if (container && (whole || task.nest || holdsException(value))) {
|
|
683
|
+
const children: ExceptionTask[] = [];
|
|
684
|
+
task.put({ holder: hold(task.name, value as object, task.keep || whole, children) });
|
|
685
|
+
later(children);
|
|
608
686
|
} else task.put({ none: task.name });
|
|
609
687
|
}
|
|
610
688
|
const { formed, shares } = formsOf(items);
|
|
@@ -718,14 +796,20 @@ function errorJson(machine: Pick<Machine, "domains">, error: EvalError, what: ()
|
|
|
718
796
|
const base = { code: error.code, reason: error.reason, ...(error.place === undefined ? {} : { place: error.place }) };
|
|
719
797
|
if (!Object.hasOwn(error, "cause")) return base;
|
|
720
798
|
const cause: unknown = error.cause;
|
|
799
|
+
const shared = sharedOf(cause);
|
|
800
|
+
let term: Json | undefined;
|
|
801
|
+
let failure: unknown;
|
|
721
802
|
try {
|
|
722
|
-
|
|
723
|
-
} catch (
|
|
724
|
-
|
|
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 }) };
|
|
803
|
+
term = encodeValue(machine, cause);
|
|
804
|
+
} catch (thrown) {
|
|
805
|
+
failure = thrown;
|
|
728
806
|
}
|
|
807
|
+
// A cause that holds a shared container is a container of parts, thus each place keeps one object (D101).
|
|
808
|
+
if (term !== undefined && !wholeOf(shared, cause, term)) return { ...base, cause: term };
|
|
809
|
+
// An exception, or a list or a record with an exception, is a best effort (D76, D87). Each other cause is a refusal.
|
|
810
|
+
const { link, names } = causeJson(machine, cause, shared, term === undefined);
|
|
811
|
+
if (link === undefined) throw new TypeError(`encodeLog cannot write the cause of ${what()}: ${messageOf(failure)}`);
|
|
812
|
+
return { ...base, ...link, ...(names.length === 0 ? {} : { dropped: names }) };
|
|
729
813
|
}
|
|
730
814
|
|
|
731
815
|
/**
|
|
@@ -743,8 +827,12 @@ function errorJson(machine: Pick<Machine, "domains">, error: EvalError, what: ()
|
|
|
743
827
|
* (RUN.REPLAY-FIELDS, D78, D82). An item of a list without a form is the mark `dropped` at its place.
|
|
744
828
|
* - A cause without a term document that is a list or a plain record with an exception is in `causeParts`
|
|
745
829
|
* (RUN.REPLAY-CAUSE-PARTS, D87). The rule is the same for the cause of an exception and of an error value.
|
|
830
|
+
* - The cause of an exception is also in `causeParts` without an exception in it, when an item has a form
|
|
831
|
+
* (RUN.REPLAY-CAUSE-ITEMS, D100). Each item without a form is a name in `dropped`, for example `cause.send`.
|
|
746
832
|
* - A list or a record of parts that an earlier place of one error holds is the part `share` with its index
|
|
747
833
|
* (RUN.REPLAY-SHARED, D88). This rule also covers a container that holds itself.
|
|
834
|
+
* - A list or a plain record with a term document that two places of one error share is a container of parts
|
|
835
|
+
* (RUN.REPLAY-SHARED-VALUES, D101). Each later place is a share. A member of a domain stays a term document.
|
|
748
836
|
* - An outcome that the log does not have stays absent.
|
|
749
837
|
*
|
|
750
838
|
* @param machine - The machine of the run, or only its domains.
|