@mark1russell7/coeval 0.17.0 → 0.18.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.17.0",
4
+ "version": "0.18.0",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
@@ -15,7 +15,8 @@
15
15
  "files": [
16
16
  "build",
17
17
  "src",
18
- "!src/**/*.test.ts"
18
+ "!src/**/*.test.ts",
19
+ "!src/**/*.test-support.ts"
19
20
  ],
20
21
  "sideEffects": false,
21
22
  "license": "MIT",
package/src/conform.ts CHANGED
@@ -33,7 +33,7 @@ import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
33
33
  import { STD_EVALUATORS } from "./stdlib.ts";
34
34
  import { sameValue } from "./agree.ts";
35
35
  import { declaredTruthOf } from "./formula.ts";
36
- import { membersOf, typeIssues, typeText } from "./types.ts";
36
+ import { falseCovers, membersOf, typeIssues, typeText } from "./types.ts";
37
37
  import { type Random, random } from "./generate.ts";
38
38
  import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
39
39
 
@@ -60,8 +60,10 @@ import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effe
60
60
  * - `agree`: compiled code gives the result of its definition for the reference interpreter.
61
61
  * - `truth`: the own `truthy` of a formula profile gives `true` or `false` for each member of its `truthType` (D86).
62
62
  * Only `formula.if`, `formula.and` and `formula.or` of the formula kit get this check.
63
+ * - `cover`: each sample of a covered domain of the machine is in one domain of each cover of the domain (D94). Each
64
+ * evaluator and each runner gets this check, because `fits` trusts the covers for each program.
63
65
  */
64
- export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth";
66
+ export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth" | "cover";
65
67
 
66
68
  /** The probe of a finding: the document, the result of each probe part, the capabilities and the handler. */
67
69
  export interface ConformProbe {
@@ -136,7 +138,7 @@ export interface ConformOptions {
136
138
  }
137
139
 
138
140
  /** The order of the obligations in the list of findings. */
139
- const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth"];
141
+ const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth", "cover"];
140
142
 
141
143
  /** A function in the results of the probe parts. It gives the count of its arguments. */
142
144
  const PROBE_FN = (...args: readonly unknown[]): number => args.length;
@@ -1032,7 +1034,8 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
1032
1034
  /**
1033
1035
  * This function checks the declared truth of a profile (D86). The function `truthy` must give `true` or `false` for
1034
1036
  * each member of `truthType`, and it must not throw. The members come from the lists `values` of the domains of the
1035
- * type. A type without such a list gets the values of the results of the palette that are its members.
1037
+ * type. A type without such a list gets its members from the results of the palette and from the samples of the
1038
+ * domains (D99).
1036
1039
  */
1037
1040
  function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformProbe): void {
1038
1041
  const truth = declaredTruthOf(code);
@@ -1057,6 +1060,27 @@ function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformP
1057
1060
  }
1058
1061
  }
1059
1062
 
1063
+ /**
1064
+ * This function checks the covers of the domains of the machine from samples (D94). Each sample of a covered domain
1065
+ * must be in one domain of each valid cover. The samples of a domain come from its list `values`. A domain without
1066
+ * that list gets its members from the palette and from the samples of the domains, as for the check `truth`.
1067
+ */
1068
+ function checkCovers(env: Env, probe: (value: unknown) => ConformProbe): void {
1069
+ const domains = env.machine.domains;
1070
+ if (domains === undefined) return;
1071
+ const palette = env.results.flatMap((result) => (result.ok ? [result.value] : []));
1072
+ const samples: Record<string, readonly unknown[]> = {};
1073
+ for (const [name, domain] of Object.entries(domains)) if (domain.covers !== undefined) samples[name] = membersOf(name, domains, palette);
1074
+ const found = falseCovers(domains, samples)[0];
1075
+ if (found === undefined) return;
1076
+ report(
1077
+ env,
1078
+ "cover",
1079
+ `The member ${valueText(found.value)} of the domain ${found.domain} is in no domain of its cover ${found.cover.join(", ")}. The check of types trusts the cover, thus it is unsound for ${found.domain}.`,
1080
+ () => probe(found.value),
1081
+ );
1082
+ }
1083
+
1060
1084
  /**
1061
1085
  * This function examines one evaluator or runner of a machine against the obligations of host code (K92, D36). It
1062
1086
  * starts the code directly, without the kernel, with probe parts and a probe runtime. It generates probe documents from
@@ -1067,7 +1091,8 @@ function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformP
1067
1091
  * compiled code with its definition. The finding of an obligation names the first probe that breaks it. An empty list
1068
1092
  * means that each probe conforms.
1069
1093
  *
1070
- * The check `truth` tests the declared truth of a formula profile (D86).
1094
+ * The check `truth` tests the declared truth of a formula profile (D86). The check `cover` tests the covers of the
1095
+ * domains of the machine from samples (D94).
1071
1096
  *
1072
1097
  * @param machine - The machine with the code and the world of the kinds.
1073
1098
  * @param name - The name of the evaluator or of the runner. A runner wins over an evaluator of the same name.
@@ -1110,11 +1135,12 @@ export function conforms(machine: Machine, name: string, options: ConformOptions
1110
1135
  examine(env, sample, contract);
1111
1136
  }
1112
1137
  }
1113
- // A probe of the truth gives the member to each part of the first probe document.
1114
- checkTruth(env, code, (value) =>
1138
+ // A probe of the truth or of a cover gives the value to each part of the first probe document.
1139
+ const probeWith = (value: unknown): ConformProbe =>
1115
1140
  first === undefined
1116
1141
  ? { type: typeName(types[0] as TypeRef), doc: { type: types[0] as TypeRef, value: null }, results: {}, waits: [], caps: NO_CAPS, running: runner }
1117
- : probeOf(first, { results: first.split.parts.map(() => ok(value)), waits: [], caps: NO_CAPS, running: runner }),
1118
- );
1142
+ : probeOf(first, { results: first.split.parts.map(() => ok(value)), waits: [], caps: NO_CAPS, running: runner });
1143
+ checkTruth(env, code, probeWith);
1144
+ checkCovers(env, probeWith);
1119
1145
  return ORDER.filter((obligation) => env.found.has(obligation)).map((obligation) => env.found.get(obligation) as Finding);
1120
1146
  }
package/src/evaluate.ts CHANGED
@@ -1053,6 +1053,11 @@ class Session implements Runtime {
1053
1053
  private list: Effect[] | undefined;
1054
1054
  tick = 0;
1055
1055
  nextId = 0;
1056
+ /**
1057
+ * The count of the drives without a wait that are active at this time. In a run, an effect under such a drive does
1058
+ * not reach the host (RUN.HELD).
1059
+ */
1060
+ held = 0;
1056
1061
  current: Building | undefined;
1057
1062
  readonly holder: Building | undefined;
1058
1063
  /**
@@ -1265,6 +1270,13 @@ class Session implements Runtime {
1265
1270
  if ("ok" in made) return made;
1266
1271
  const perform = this.perform;
1267
1272
  if (!this.running || perform === undefined) return this.answer(made, place);
1273
+ if (this.held > 0) {
1274
+ // The parent cannot wait for the answer of the host, thus the host does not get the effect (RUN.HELD).
1275
+ made.outcome = fail("#CALC!", heldReason(made.kind), { place });
1276
+ // The drive stops at this wait, and gives the #CALC! of the runner.
1277
+ yield HELD;
1278
+ return made.outcome;
1279
+ }
1268
1280
  let done: unknown;
1269
1281
  try {
1270
1282
  done = perform(made);
@@ -1452,6 +1464,21 @@ export function waitedReason(kindText: string): string {
1452
1464
  return `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`;
1453
1465
  }
1454
1466
 
1467
+ /**
1468
+ * This function gives the reason of an effect that a run does not give to the host: the parent of its runner cannot
1469
+ * wait (RUN.HELD). The plan and the reference give the same reason.
1470
+ *
1471
+ * @param kind - The kind of the effect.
1472
+ * @returns The reason.
1473
+ */
1474
+ export function heldReason(kind: Cap): string {
1475
+ const text = typeof kind === "string" ? kind : kind.join(".");
1476
+ return `The parent of the runner cannot wait, thus the host does not get the effect ${text}.`;
1477
+ }
1478
+
1479
+ /** The wait of an effect that a run does not give to the host. A drive without a wait stops at it (RUN.HELD). */
1480
+ const HELD: Promise<unknown> = /* @__PURE__ */ Promise.resolve(undefined);
1481
+
1455
1482
  /** This function drives steps that cannot wait. A step that waits stops the steps, and gives `#CALC!`. */
1456
1483
  function drive(steps: Steps, kindText: string): Result {
1457
1484
  const step = steps.next();
@@ -2148,10 +2175,19 @@ function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode
2148
2175
  /**
2149
2176
  * This function gives the code of a runner under a parent that cannot wait: its steps, driven without a wait. An
2150
2177
  * evaluation and a run both drive them (RUN.DRIVE-NESTED). Only a step that really waits gives `#CALC!`, at the place
2151
- * of the runner, where the error started (RUN.WAIT).
2178
+ * of the runner, where the error started (RUN.WAIT). In a run, an effect under the runner does not reach the host,
2179
+ * because the parent cannot wait for its answer (RUN.HELD).
2152
2180
  */
2153
2181
  function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
2154
- return (ctx, rt) => placed(drive(run(ctx, rt), kindText), place);
2182
+ return (ctx, rt) => {
2183
+ if (!rt.running || !(rt instanceof Session)) return placed(drive(run(ctx, rt), kindText), place);
2184
+ rt.held += 1;
2185
+ try {
2186
+ return placed(drive(run(ctx, rt), kindText), place);
2187
+ } finally {
2188
+ rt.held -= 1;
2189
+ }
2190
+ };
2155
2191
  }
2156
2192
 
2157
2193
  /**
package/src/formula.ts CHANGED
@@ -31,6 +31,8 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
31
31
  * - `lazy`: the operations that evaluate their arguments themselves.
32
32
  * - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined`, `ext.number` and
33
33
  * `ext.domain`.
34
+ * - `extTypes`: the value type of each extension of `ext`, by its name, for a check of types (TYPE.EXT, D103). An
35
+ * extension without an entry gives `unknown` (TYPE.GRADUAL). An entry for a standard extension has no effect.
34
36
  * - `truthy`: the truth of a value for `if`, `and` and `or`. `undefined` is an error `#VALUE!`. The default
35
37
  * accepts only `true` and `false`.
36
38
  * - `truthType`: the value type of a truth for the own `truthy`, for a check of types (TYPE.TRUTH, D81). A test of
@@ -54,12 +56,13 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
54
56
  *
55
57
  * A check of types trusts the signatures. Each operation must give a member of the result of each signature that
56
58
  * its arguments match. The hook `check` must keep each result in that type. The function `truthy` must give a truth
57
- * for each member of `truthType`.
59
+ * for each member of `truthType`. Each extension must give a member of its type in `extTypes`.
58
60
  */
59
61
  export interface FormulaProfile {
60
62
  readonly ops?: Readonly<Record<string, OpFn>> | ((name: string) => OpFn | undefined);
61
63
  readonly lazy?: Readonly<Record<string, LazyOp>>;
62
64
  readonly ext?: Readonly<Record<string, ExtFn>>;
65
+ readonly extTypes?: Readonly<Record<string, ValueType>>;
63
66
  readonly truthy?: (value: unknown) => boolean | undefined;
64
67
  readonly truthType?: ValueType;
65
68
  readonly otherwise?: unknown;
@@ -272,7 +275,7 @@ function paramsOf(value: unknown): readonly string[] {
272
275
  /**
273
276
  * This function makes the type rules of the formula kit for one profile (TYPE.SYNTH, D25). Each evaluator of the kit
274
277
  * holds its rule. The rules read the rules of the profile that change a type: `truthy`, `truthType`, `logic`,
275
- * `otherwise`, `omit` and `sigs`.
278
+ * `otherwise`, `omit`, `sigs` and `extTypes`.
276
279
  */
277
280
  function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule>> {
278
281
  // The default truth needs a truth value. An own truth needs its declared type, or nothing without a declaration (D81).
@@ -510,7 +513,14 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
510
513
  synth: (input) => {
511
514
  const name = nameOf(input);
512
515
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
513
- if (own) return "unknown";
516
+ if (own) {
517
+ // The declared type is a contract. A type with a problem gives unknown, thus one problem gives one issue (D103).
518
+ const types = profile.extTypes;
519
+ const declared = types !== undefined && Object.hasOwn(types, name) ? types[name] : undefined;
520
+ if (declared === undefined) return "unknown";
521
+ const type = input.contract(declared, `type of ext.${name} in extTypes`);
522
+ return typeIssues(type, input.machine.domains).length > 0 ? "unknown" : type;
523
+ }
514
524
  // The standard extensions give the values that JSON cannot write, thus each has a base type (D48).
515
525
  if (name === "undefined") return "undefined";
516
526
  if (name === "number") return "number";
@@ -580,6 +590,7 @@ const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
580
590
  "ops",
581
591
  "lazy",
582
592
  "ext",
593
+ "extTypes",
583
594
  "truthy",
584
595
  "truthType",
585
596
  "otherwise",
package/src/merge.ts CHANGED
@@ -89,7 +89,7 @@ function same(a: unknown, b: unknown): boolean {
89
89
 
90
90
  /**
91
91
  * Two domains are the same when they are one object. They are also the same when they have the same test `is`,
92
- * parents, summary, steps, codec, list of values and covers. Thus two languages that give one test under one name give one
92
+ * parents, summary, steps, codec, list of values, samples and covers. Thus two languages that give one test under one name give one
93
93
  * domain. Two codecs are the same when they have the same functions `encode` and `decode` (D80).
94
94
  */
95
95
  function sameDomain(a: Domain, b: Domain): boolean {
@@ -102,6 +102,7 @@ function sameDomain(a: Domain, b: Domain): boolean {
102
102
  steps: same(a.steps ?? {}, b.steps ?? {}),
103
103
  codec: same(a.codec ?? null, b.codec ?? null),
104
104
  values: same(a.values ?? null, b.values ?? null),
105
+ samples: same(a.samples ?? null, b.samples ?? null),
105
106
  covers: same(a.covers ?? [], b.covers ?? []),
106
107
  };
107
108
  return Object.values(fields).every(Boolean);
@@ -123,8 +124,8 @@ export interface DomainExtension {
123
124
  /**
124
125
  * This function makes a part of a merge that adds parents, steps or covers to a domain of an earlier part
125
126
  * (TYPE.PARENTS). An example is a domain of another language. The part reads the domain from the merge before it
126
- * (`prev`). It gives a copy with the same test, summary, codec and list of values, and it names `["domains", name]`
127
- * in `replaces` (D17).
127
+ * (`prev`). It gives a copy with the same test, summary, codec, list of values and samples, and it names
128
+ * `["domains", name]` in `replaces` (D17).
128
129
  * A domain of the kernel that the earlier parts do not name comes from `STD_DOMAINS`.
129
130
  *
130
131
  * An extension does not change the codec. Thus each member keeps its document (D80).
@@ -158,6 +159,8 @@ export function extendDomain(name: string, more: DomainExtension): (prev: Machin
158
159
  codec: found.codec,
159
160
  // A new parent only adds a test, thus the list of values stays complete (TYPE.FITS-FINITE).
160
161
  values: found.values,
162
+ // A sample outside the new line does not count, thus the samples stay (D99).
163
+ samples: found.samples,
161
164
  // A new parent only adds a test, thus each cover stays true (TYPE.FITS-COVER). A new cover is a claim of the extension.
162
165
  covers: covers.length === 0 ? undefined : covers,
163
166
  };
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.