@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/build/index.d.ts +39 -78
- package/build/index.d.ts.map +1 -1
- package/build/index.js +313 -133
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/conform.ts +35 -9
- package/src/evaluate.ts +38 -2
- package/src/formula.ts +14 -3
- package/src/merge.ts +6 -3
- package/src/reference.ts +20 -1
- package/src/replay.ts +123 -35
- package/src/types.ts +97 -21
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.
|
|
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
|
|
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
|
|
1114
|
-
|
|
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) =>
|
|
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 `
|
|
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)
|
|
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
|
|
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)
|
|
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.
|