@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/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 the function cannot see the change.
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) return drive(runSeen(env, seen, at, chosen.runner), place, seen.kindText);
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
- /** The term documents of the own fields of the exception that `encodeValue` writes, for example `code` or `errno`. */
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
- /** The value `true` marks a hidden list of a standard class. It keeps its marks also without a form (D78). */
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
- /** The value `false` marks a cause. A cause is a container only when it holds an exception at some depth (D87). */
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 container with an exception gets a form.
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). 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).
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). The walk is
499
- * a loop, thus a long chain does not overflow the stack.
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
- const linked: [string, unknown][] = [];
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: true, nest: true, keep: false, put });
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
- children.push({ value: current.cause, name: "cause", tried: false, nest: false, keep: false, put });
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: true,
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
- if (term !== undefined) task.put({ value: term });
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 ((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
- }
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
- 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 }) };
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.