@mark1russell7/coeval 0.6.3 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -5
- package/build/index.d.ts +1771 -254
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5809 -511
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +15 -3
- package/src/codec.ts +185 -0
- package/src/conform.ts +1068 -0
- package/src/deref.ts +284 -47
- package/src/durable.ts +238 -0
- package/src/effects.ts +110 -0
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +480 -113
- package/src/facts.ts +252 -0
- package/src/formula.ts +450 -42
- package/src/graph.ts +399 -0
- package/src/index.ts +46 -0
- package/src/ladder.ts +40 -1
- package/src/merge.ts +91 -4
- package/src/parts.ts +26 -11
- package/src/providers.ts +286 -0
- package/src/reads.ts +105 -0
- package/src/reference.ts +23 -9
- package/src/schedule.ts +436 -0
- package/src/static.ts +266 -29
- package/src/stdlib.ts +24 -7
- package/src/syntax.ts +846 -0
- package/src/types.ts +768 -0
- package/src/typing.ts +390 -0
- package/src/world.ts +63 -9
package/src/conform.ts
ADDED
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import { type Envelope, type Json, type Key, type TypeRef, docEquals, isEnvelope, isKey, typeName } from "./doc.ts";
|
|
2
|
+
import { type Cap, type Context, type Resolution, type ResolveOptions, resolve } from "./deref.ts";
|
|
3
|
+
import { type Result, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
4
|
+
import {
|
|
5
|
+
type AllOptions,
|
|
6
|
+
type Code,
|
|
7
|
+
type CompileInput,
|
|
8
|
+
type CompiledEvaluator,
|
|
9
|
+
type CompiledPart,
|
|
10
|
+
type CompiledRunner,
|
|
11
|
+
type Effect,
|
|
12
|
+
type EvalFn,
|
|
13
|
+
type EvalInput,
|
|
14
|
+
type Evaluator,
|
|
15
|
+
type Machine,
|
|
16
|
+
type Part,
|
|
17
|
+
type Perform,
|
|
18
|
+
type RunCode,
|
|
19
|
+
type RunFn,
|
|
20
|
+
type RunInput,
|
|
21
|
+
type RunPart,
|
|
22
|
+
type Runner,
|
|
23
|
+
type Runtime,
|
|
24
|
+
type Steps,
|
|
25
|
+
capKeys,
|
|
26
|
+
makeContext,
|
|
27
|
+
permits,
|
|
28
|
+
rootContext,
|
|
29
|
+
} from "./evaluate.ts";
|
|
30
|
+
import { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
|
|
31
|
+
import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
|
|
32
|
+
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
33
|
+
import { sameValue } from "./agree.ts";
|
|
34
|
+
import { type Random, random } from "./generate.ts";
|
|
35
|
+
import { scopesOf } from "./static.ts";
|
|
36
|
+
import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* The conformance tests of one evaluator (K92, R7 P3, D36). `conforms` calls the code of one evaluator or runner
|
|
40
|
+
* directly, without the kernel. It gives the code probe parts and a probe runtime. A probe part records each call, its
|
|
41
|
+
* context and its order, and it gives a chosen result. The probe runtime records each resolution and each effect. The
|
|
42
|
+
* checks compare these records with the contract of the kind and with the obligations C1 to C8 of SEMANTICS.md.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An obligation that `conforms` checks. `C1` to `C8` are the obligations of host code in `docs/SEMANTICS.md`. `local`
|
|
47
|
+
* and `agree` are two premises of the simulation of the plan (SIM-PLAN).
|
|
48
|
+
*
|
|
49
|
+
* - `C1`: each context that the code gives a part, or an effect, has only the capabilities of its document.
|
|
50
|
+
* - `C2`: the code causes only the effects that the contract of the kind declares.
|
|
51
|
+
* - `C3`: each eager part evaluates when the result is a value, and a lazy part evaluates only when its result counts.
|
|
52
|
+
* - `C4`: the code of a pure kind causes no effect, and it reads the context only through `resolve`.
|
|
53
|
+
* - `C5`: the code reads its parts only through their handles, not through the JSON of the document.
|
|
54
|
+
* - `C6`: only a runner waits, and a runner passes each wait of its parts and its effects through.
|
|
55
|
+
* - `C7`: the code binds for a part each name that the scope rules give the part, and no other name.
|
|
56
|
+
* - `C8`: compiled code gives the result of code that compiles again at the time of the evaluation.
|
|
57
|
+
* - `local`: the code keeps no state from one call to the next.
|
|
58
|
+
* - `agree`: compiled code gives the result of its definition for the reference interpreter.
|
|
59
|
+
*/
|
|
60
|
+
export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree";
|
|
61
|
+
|
|
62
|
+
/** The probe of a finding: the document, the result of each probe part, the capabilities and the handler. */
|
|
63
|
+
export interface ConformProbe {
|
|
64
|
+
/** The text of the type of the document. */
|
|
65
|
+
readonly type: string;
|
|
66
|
+
/** The document. A probe document takes the place of each part. */
|
|
67
|
+
readonly doc: Envelope;
|
|
68
|
+
/** The result of each probe part, by the key of the part. */
|
|
69
|
+
readonly results: Readonly<Record<string, Result>>;
|
|
70
|
+
/** The keys of the probe parts that wait for the host before they give their result. Only a run has them. */
|
|
71
|
+
readonly waits: readonly string[];
|
|
72
|
+
/** The capabilities of the context of the document. */
|
|
73
|
+
readonly caps: readonly Cap[];
|
|
74
|
+
/** `true` in a run, in which the host does each effect and a part can wait. */
|
|
75
|
+
readonly running: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One obligation that the code breaks, with the reason and the first probe that shows it. */
|
|
79
|
+
export interface Finding {
|
|
80
|
+
readonly obligation: Obligation;
|
|
81
|
+
/** One or two sentences that tell what the code did. */
|
|
82
|
+
readonly reason: string;
|
|
83
|
+
readonly probe: ConformProbe;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The options of `conforms`. */
|
|
87
|
+
export interface ConformOptions {
|
|
88
|
+
/**
|
|
89
|
+
* The verb that the evaluator serves. The default is `eval`. The facts of a contract (`effects`, `pure` and `lazy`)
|
|
90
|
+
* describe only `eval`, thus `conforms` checks C2, C3 and C4 only for `eval` (D23).
|
|
91
|
+
*/
|
|
92
|
+
readonly verb?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The types of the probe documents. The default is each kind of the world whose verb names the evaluator. A fallback
|
|
95
|
+
* kind gives the key `probe`, for example `["ops", "probe"]`. The contract of the kind of each type applies.
|
|
96
|
+
*/
|
|
97
|
+
readonly types?: readonly TypeRef[];
|
|
98
|
+
/**
|
|
99
|
+
* More values of probe documents, by the text of their type. A probe document takes the place of each part of such a
|
|
100
|
+
* value. Use them for data that the code needs, for example the key of `write`.
|
|
101
|
+
*/
|
|
102
|
+
readonly values?: Readonly<Record<string, readonly Json[]>>;
|
|
103
|
+
/**
|
|
104
|
+
* The results that a probe part can give. The default holds numbers, truth values, a text, `null`, a list, a
|
|
105
|
+
* record, a function and two errors.
|
|
106
|
+
*/
|
|
107
|
+
readonly results?: readonly Result[];
|
|
108
|
+
/**
|
|
109
|
+
* The definitional code of the compiled evaluators and runners, as for `reference`. A standard evaluator gets its
|
|
110
|
+
* definition from `STD_DEFINITIONS`.
|
|
111
|
+
*/
|
|
112
|
+
readonly definitions?: Definitions;
|
|
113
|
+
/** The capabilities of half of the probes. The other half has none. The default is the declared effects and `probe`. */
|
|
114
|
+
readonly caps?: readonly Cap[];
|
|
115
|
+
/** The names of the context of each probe. */
|
|
116
|
+
readonly names?: Readonly<Record<string, unknown>>;
|
|
117
|
+
/** The seed of the random results. The same seed gives the same probes. The default is 1. */
|
|
118
|
+
readonly seed?: number;
|
|
119
|
+
/** The count of probes with random results for each probe document. The default is 12. */
|
|
120
|
+
readonly runs?: number;
|
|
121
|
+
/** The mock of the host (RUN.MOCK). It answers each effect at once. Without it, an effect gives its payload. */
|
|
122
|
+
readonly perform?: Perform;
|
|
123
|
+
/**
|
|
124
|
+
* This function compares two values that the rules of `sameValue` do not know, as in `agree`.
|
|
125
|
+
*
|
|
126
|
+
* @param a - The first value.
|
|
127
|
+
* @param b - The second value.
|
|
128
|
+
* @returns `true` or `false`, or `undefined` for the rules of `sameValue`.
|
|
129
|
+
*/
|
|
130
|
+
readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The order of the obligations in the list of findings. */
|
|
134
|
+
const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree"];
|
|
135
|
+
|
|
136
|
+
/** A function in the results of the probe parts. It gives the count of its arguments. */
|
|
137
|
+
const PROBE_FN = (...args: readonly unknown[]): number => args.length;
|
|
138
|
+
|
|
139
|
+
/** The arguments of a call of a function that a result gives. */
|
|
140
|
+
const PROBE_ARGS: readonly unknown[] = [1, 2];
|
|
141
|
+
|
|
142
|
+
/** The default results of the probe parts. */
|
|
143
|
+
const RESULTS: readonly Result[] = [
|
|
144
|
+
{ ok: true, value: 1 },
|
|
145
|
+
{ ok: true, value: 2 },
|
|
146
|
+
{ ok: true, value: true },
|
|
147
|
+
{ ok: true, value: false },
|
|
148
|
+
{ ok: true, value: "a" },
|
|
149
|
+
{ ok: true, value: null },
|
|
150
|
+
{ ok: true, value: [1, 2] },
|
|
151
|
+
{ ok: true, value: { a: 1 } },
|
|
152
|
+
{ ok: true, value: PROBE_FN },
|
|
153
|
+
{ ok: false, error: { code: "#VALUE!", reason: "The probe part fails." } },
|
|
154
|
+
{ ok: false, error: { code: "#N/A", reason: "The probe part has no value." } },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/** The result of a probe part without a chosen result. */
|
|
158
|
+
const NO_RESULT: Result = { ok: true, value: null };
|
|
159
|
+
|
|
160
|
+
/** The result of a runner that the probe driver stops. */
|
|
161
|
+
const STOPPED: Result = { ok: false, error: { code: "#CALC!", reason: "The probe driver stopped the runner." } };
|
|
162
|
+
|
|
163
|
+
/** The result of an item of `all` after a failure with `failFast`. */
|
|
164
|
+
const ABORTED: Result = { ok: false, error: { code: "#ABORT", reason: "The part stopped: a part before it failed, and the parts run with failFast." } };
|
|
165
|
+
|
|
166
|
+
/** The default names of the context of each probe. The generated names of a value (`a`, `b` and `c`) are other names. */
|
|
167
|
+
const NAMES: Readonly<Record<string, unknown>> = { x: "outer x", y: 2.5, z: ["outer"] };
|
|
168
|
+
|
|
169
|
+
/** The values of a document whose kind has no parts. */
|
|
170
|
+
const DATA: readonly Json[] = [1, "a", true, null, ["names", "x"], { a: 1 }, ["names", "y", "c"]];
|
|
171
|
+
|
|
172
|
+
/** The count of generated values of each type. */
|
|
173
|
+
const VARIANTS = 8;
|
|
174
|
+
|
|
175
|
+
/** The lengths of a generated list. */
|
|
176
|
+
const LENGTHS: readonly number[] = [2, 0, 1, 3];
|
|
177
|
+
|
|
178
|
+
/** The texts of generated data. */
|
|
179
|
+
const TEXTS: readonly string[] = ["a", "b", "c"];
|
|
180
|
+
|
|
181
|
+
/** The place of each probe document. */
|
|
182
|
+
const PLACE: readonly Key[] = [];
|
|
183
|
+
|
|
184
|
+
/** No capabilities: the context of half of the probes. */
|
|
185
|
+
const NO_CAPS: readonly Cap[] = [];
|
|
186
|
+
|
|
187
|
+
/** The code under test, in one of its four forms. */
|
|
188
|
+
type Under =
|
|
189
|
+
| { readonly form: "eval"; readonly fn: EvalFn }
|
|
190
|
+
| { readonly form: "compile"; readonly compile: (input: CompileInput) => Code }
|
|
191
|
+
| { readonly form: "run"; readonly fn: RunFn }
|
|
192
|
+
| { readonly form: "compileRun"; readonly compile: (input: CompileInput) => RunCode };
|
|
193
|
+
|
|
194
|
+
/** The split of a value that has the shape of its kind. */
|
|
195
|
+
type SplitOk = Extract<Split, { readonly ok: true }>;
|
|
196
|
+
|
|
197
|
+
/** One probe document, with what its contract tells about each part. */
|
|
198
|
+
interface Sample {
|
|
199
|
+
readonly type: string;
|
|
200
|
+
readonly typePath: readonly Key[];
|
|
201
|
+
readonly kind: Kind;
|
|
202
|
+
/** The document, with a probe document at the place of each part. */
|
|
203
|
+
readonly doc: Envelope;
|
|
204
|
+
readonly split: SplitOk;
|
|
205
|
+
/** The same document with other JSON in each probe document (C5). */
|
|
206
|
+
readonly other: Envelope;
|
|
207
|
+
readonly otherSplit: SplitOk;
|
|
208
|
+
/** `true` for each part that a lazy rule covers. */
|
|
209
|
+
readonly lazy: readonly boolean[];
|
|
210
|
+
/** The names that the scope rules give each part. */
|
|
211
|
+
readonly names: readonly (readonly string[])[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The results, the waits and the capabilities of one evaluation of a probe document. */
|
|
215
|
+
interface Scenario {
|
|
216
|
+
readonly results: readonly Result[];
|
|
217
|
+
readonly waits: readonly boolean[];
|
|
218
|
+
readonly caps: readonly Cap[];
|
|
219
|
+
readonly running: boolean;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** One call of a probe part. */
|
|
223
|
+
interface Call {
|
|
224
|
+
readonly part: number;
|
|
225
|
+
/** The function that the code called: `evaluate` of a handle, or `code` or `run` of a compiled part. */
|
|
226
|
+
readonly via: "evaluate" | "code" | "run";
|
|
227
|
+
/** The context that the code gave the part. */
|
|
228
|
+
readonly context: Context;
|
|
229
|
+
/** `true` for a call in a function that the result gives, after the evaluation. */
|
|
230
|
+
readonly later: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** One effect that the code caused. */
|
|
234
|
+
interface Caused {
|
|
235
|
+
readonly kind: string;
|
|
236
|
+
readonly payload: unknown;
|
|
237
|
+
readonly context: Context;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** One resolution that the code asked for. */
|
|
241
|
+
interface Asked {
|
|
242
|
+
readonly path: readonly unknown[];
|
|
243
|
+
readonly found: Resolution;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One wait of a probe part or of an effect in a run. */
|
|
247
|
+
interface Wait {
|
|
248
|
+
readonly what: string;
|
|
249
|
+
readonly answer: unknown;
|
|
250
|
+
/** `true` after the runner gave the promise to the driver. */
|
|
251
|
+
seen: boolean;
|
|
252
|
+
/** `true` after the probe got its answer. */
|
|
253
|
+
done: boolean;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The state of `conforms` for one evaluator. */
|
|
257
|
+
interface Env {
|
|
258
|
+
readonly machine: Machine;
|
|
259
|
+
readonly name: string;
|
|
260
|
+
readonly verb: string;
|
|
261
|
+
readonly under: Under;
|
|
262
|
+
readonly definition: Under | undefined;
|
|
263
|
+
readonly runner: boolean;
|
|
264
|
+
readonly results: readonly Result[];
|
|
265
|
+
readonly names: Readonly<Record<string, unknown>>;
|
|
266
|
+
readonly runs: number;
|
|
267
|
+
readonly random: Random;
|
|
268
|
+
readonly perform: Perform | undefined;
|
|
269
|
+
readonly equal: ((a: unknown, b: unknown) => boolean | undefined) | undefined;
|
|
270
|
+
readonly found: Map<Obligation, Finding>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The facts of the kind of one type, as the contract and the hook `facts` of the machine give them. */
|
|
274
|
+
interface Contract {
|
|
275
|
+
readonly declared: readonly Cap[];
|
|
276
|
+
readonly pure: boolean;
|
|
277
|
+
readonly lazy: readonly LazyRule[] | undefined;
|
|
278
|
+
/** `true` when the facts describe the verb: they describe only `eval`. */
|
|
279
|
+
readonly describes: boolean;
|
|
280
|
+
readonly caps: readonly Cap[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The probe runtime of one evaluation, and its record. It records each call of a part, each effect and each
|
|
285
|
+
* resolution. In a run, a probe part and an effect wait with a promise that the probe driver answers at once.
|
|
286
|
+
*/
|
|
287
|
+
class Shot implements Runtime {
|
|
288
|
+
readonly calls: Call[] = [];
|
|
289
|
+
readonly caused: Caused[] = [];
|
|
290
|
+
readonly asked: Asked[] = [];
|
|
291
|
+
/** The problems of the waits (C6). */
|
|
292
|
+
readonly problems: string[] = [];
|
|
293
|
+
readonly waits = new Map<Promise<unknown>, Wait>();
|
|
294
|
+
/** The results of the calls of a function that the result gives. */
|
|
295
|
+
readonly later: Result[] = [];
|
|
296
|
+
result: Result = NO_RESULT;
|
|
297
|
+
private afterwards = false;
|
|
298
|
+
readonly running: boolean;
|
|
299
|
+
|
|
300
|
+
constructor(
|
|
301
|
+
readonly machine: Machine,
|
|
302
|
+
/** `true` when the code under test is a runner. */
|
|
303
|
+
readonly runner: boolean,
|
|
304
|
+
readonly scenario: Scenario,
|
|
305
|
+
/** The context of the document. */
|
|
306
|
+
readonly context: Context,
|
|
307
|
+
/** The key of each part, for the reasons. */
|
|
308
|
+
readonly keys: readonly string[],
|
|
309
|
+
private readonly perform: Perform | undefined,
|
|
310
|
+
/** The resolutions of an earlier evaluation. Each resolution with the same path gets the same answer (C4). */
|
|
311
|
+
private readonly replay: readonly Asked[] | undefined,
|
|
312
|
+
) {
|
|
313
|
+
this.running = scenario.running;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
resolve(ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution {
|
|
317
|
+
const old = this.replay?.[this.asked.length];
|
|
318
|
+
const found = old !== undefined && docEquals(old.path, path) ? old.found : resolve(this.machine, ctx, path, options);
|
|
319
|
+
this.asked.push({ path, found });
|
|
320
|
+
return found;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** This function records a call of a part, and gives the result that the probe part gives. */
|
|
324
|
+
call(part: number, via: Call["via"], ctx: Context): Result {
|
|
325
|
+
this.calls.push({ part, via, context: ctx, later: this.afterwards });
|
|
326
|
+
const key = this.keys[part] ?? String(part);
|
|
327
|
+
if (via === "run" && !this.runner) this.problems.push(`The code of an evaluator starts the steps of the part ${key}. Only a runner waits.`);
|
|
328
|
+
if (via === "code" && this.runner && this.running) {
|
|
329
|
+
this.problems.push(`The runner evaluates the part ${key} with its code, and that code cannot wait. A runner evaluates a part with its steps (K65).`);
|
|
330
|
+
}
|
|
331
|
+
return this.scenario.results[part] ?? NO_RESULT;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** This function records a call of a part, and gives the steps of the probe part. In a run, the part can wait. */
|
|
335
|
+
*steps(part: number, via: Call["via"], ctx: Context): Steps {
|
|
336
|
+
const result = this.call(part, via, ctx);
|
|
337
|
+
if (this.running && this.scenario.waits[part] === true) yield* this.wait(`the part ${this.keys[part] ?? String(part)}`);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** This function gives the steps of one wait. The probe driver answers the promise with a new object. */
|
|
342
|
+
private *wait(what: string): Generator<Promise<unknown>, void, unknown> {
|
|
343
|
+
const answer = { answer: what };
|
|
344
|
+
const promise = new Promise<unknown>(() => undefined);
|
|
345
|
+
const wait: Wait = { what, answer, seen: false, done: false };
|
|
346
|
+
this.waits.set(promise, wait);
|
|
347
|
+
const got = yield promise;
|
|
348
|
+
wait.done = true;
|
|
349
|
+
if (got !== answer) this.problems.push(`The runner gives ${what} another answer than the answer of the host.`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
|
|
353
|
+
const text = capKeys(kind).join(".");
|
|
354
|
+
this.caused.push({ kind: text, payload, context: ctx });
|
|
355
|
+
if (this.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
|
|
356
|
+
return this.answer(ctx, kind, payload, place);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
*runEffect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
|
|
360
|
+
const text = capKeys(kind).join(".");
|
|
361
|
+
this.caused.push({ kind: text, payload, context: ctx });
|
|
362
|
+
if (!this.runner) this.problems.push(`The code of an evaluator starts the steps of the effect ${text}. Only a runner waits.`);
|
|
363
|
+
const answered = this.answer(ctx, kind, payload, place);
|
|
364
|
+
if (!answered.ok || !this.running) return answered;
|
|
365
|
+
yield* this.wait(`the effect ${text}`);
|
|
366
|
+
return answered;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** This function answers an effect as the kernel does: it checks the capability, and it asks the mock. */
|
|
370
|
+
private answer(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
|
|
371
|
+
const keys = capKeys(kind);
|
|
372
|
+
// The kernel keeps only the capabilities that the document permits (H-GRANT).
|
|
373
|
+
const caps = ctx.caps.filter((cap) => permits(this.context.caps, cap));
|
|
374
|
+
const allowed = this.machine.permits === undefined ? permits(caps, keys) : this.machine.permits(caps, keys, { ...ctx, caps });
|
|
375
|
+
if (!allowed) return fail("#CAP!", `The context has no capability for the effect ${keys.join(".")}.`, { place });
|
|
376
|
+
if (this.perform === undefined) return ok(payload);
|
|
377
|
+
const effect: Effect = { index: this.caused.length - 1, kind: keys.join("."), path: keys, payload, node: -1, at: 0, place, context: { ...ctx, caps } };
|
|
378
|
+
let answer: unknown;
|
|
379
|
+
try {
|
|
380
|
+
answer = this.perform(effect);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return placed(thrown(error, `the mock of the effect ${effect.kind}`), place);
|
|
383
|
+
}
|
|
384
|
+
if (!(answer instanceof Promise)) return ok(answer);
|
|
385
|
+
void answer.catch(() => undefined);
|
|
386
|
+
return fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
*all(list: readonly (() => Steps)[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
|
|
390
|
+
if (!this.runner) this.problems.push("The code of an evaluator starts the steps of all. Only a runner waits.");
|
|
391
|
+
// The probe runtime starts the items one after the other. Each wait of an item passes through, as in the kernel.
|
|
392
|
+
const results: Result[] = [];
|
|
393
|
+
let failed = false;
|
|
394
|
+
for (const begin of list) {
|
|
395
|
+
if (failed) {
|
|
396
|
+
results.push(ABORTED);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
let result: Result;
|
|
400
|
+
try {
|
|
401
|
+
result = yield* begin();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
result = thrown(error, "a part");
|
|
404
|
+
}
|
|
405
|
+
results.push(result);
|
|
406
|
+
if (options.failFast === true && !result.ok) failed = true;
|
|
407
|
+
}
|
|
408
|
+
return results;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
keepsContext(): void {
|
|
412
|
+
// The probe runtime records no reads, thus a closure needs no read of the roots.
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** This function starts a function that the result gives, with probe arguments. The calls of the parts are later calls. */
|
|
416
|
+
callLater(): void {
|
|
417
|
+
const result = this.result;
|
|
418
|
+
if (!result.ok || typeof result.value !== "function") return;
|
|
419
|
+
this.afterwards = true;
|
|
420
|
+
try {
|
|
421
|
+
this.later.push(ok((result.value as (...args: readonly unknown[]) => unknown)(...PROBE_ARGS)));
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this.later.push(thrown(error, "the function of the result"));
|
|
424
|
+
} finally {
|
|
425
|
+
this.afterwards = false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** The code of one probe document, made one time. It evaluates the document in each shot. */
|
|
431
|
+
type Instance = (shot: Shot) => Result;
|
|
432
|
+
|
|
433
|
+
/** This function sets an own field, also the field `__proto__`. */
|
|
434
|
+
function setField(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
435
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function own<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
|
|
439
|
+
return table !== undefined && Object.hasOwn(table, name) ? table[name] : undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** This function gives the form of an evaluator or of a runner. */
|
|
443
|
+
function underOf(code: Evaluator | Runner, runner: boolean): Under {
|
|
444
|
+
if (runner) {
|
|
445
|
+
if (typeof code === "function") return { form: "run", fn: code as RunFn };
|
|
446
|
+
return { form: "compileRun", compile: (input) => (code as CompiledRunner).compileRun(input) };
|
|
447
|
+
}
|
|
448
|
+
if (typeof code === "function") return { form: "eval", fn: code as EvalFn };
|
|
449
|
+
return { form: "compile", compile: (input) => (code as CompiledEvaluator).compile(input) };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function compiledForm(under: Under): boolean {
|
|
453
|
+
return under.form === "compile" || under.form === "compileRun";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** This function gives the definition of the code for the reference, when it has one that is other code. */
|
|
457
|
+
function definitionOf(machine: Machine, name: string, runner: boolean, definitions: Definitions | undefined): Under | undefined {
|
|
458
|
+
if (runner) {
|
|
459
|
+
const defined = own(definitions?.runners, name);
|
|
460
|
+
return defined === undefined || defined === own(machine.runners, name) ? undefined : { form: "run", fn: defined };
|
|
461
|
+
}
|
|
462
|
+
const evaluator = own(machine.evaluators, name);
|
|
463
|
+
const standard = evaluator !== undefined && evaluator === own(STD_EVALUATORS, name) ? own(STD_DEFINITIONS.evaluators, name) : undefined;
|
|
464
|
+
const defined = own(definitions?.evaluators, name) ?? standard;
|
|
465
|
+
return defined === undefined || defined === evaluator ? undefined : { form: "eval", fn: defined };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** This function gives the type of each kind of the world whose verb names the evaluator. */
|
|
469
|
+
function typesNaming(world: World, name: string, verb: string): readonly TypeRef[] {
|
|
470
|
+
const out: (readonly Key[])[] = [];
|
|
471
|
+
const names = (kind: Kind | undefined): boolean => kind !== undefined && own(kind.verbs, verb) === name;
|
|
472
|
+
const visit = (entries: Readonly<Record<string, WorldEntry>>, prefix: readonly Key[]): void => {
|
|
473
|
+
for (const key of Object.keys(entries)) {
|
|
474
|
+
const entry = entries[key];
|
|
475
|
+
if (isKind(entry)) {
|
|
476
|
+
if (names(entry)) out.push([...prefix, key]);
|
|
477
|
+
} else if (isNamespace(entry)) {
|
|
478
|
+
visit(entry.kinds, [...prefix, key]);
|
|
479
|
+
if (names(entry.fallback)) out.push([...prefix, key, "probe"]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
visit(world.kinds, []);
|
|
484
|
+
if (names(world.fallback)) out.push(["probe"]);
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** This function gives the facts of the kind of a type. The hook `facts` of the machine wins over the contract. */
|
|
489
|
+
function contractOf(env: Env, kind: Kind, typePath: readonly Key[], caps: readonly Cap[] | undefined): Contract {
|
|
490
|
+
const facts = env.machine.facts?.(typePath, env.name);
|
|
491
|
+
const effects = kind.effects;
|
|
492
|
+
const declared: readonly Cap[] = effects === undefined ? [] : effects === "type" ? [typePath] : effects;
|
|
493
|
+
return {
|
|
494
|
+
declared,
|
|
495
|
+
pure: (facts?.pure ?? kind.pure) === true,
|
|
496
|
+
lazy: facts?.lazy ?? kind.lazy,
|
|
497
|
+
describes: env.verb === "eval",
|
|
498
|
+
caps: caps ?? [...declared, "probe"],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** This function tells if a lazy rule covers the part with an index, as `factsOf` reads the rule. */
|
|
503
|
+
function covers(rule: LazyRule, slots: readonly PartSlot[], index: number): boolean {
|
|
504
|
+
const matches = (slot: PartSlot): boolean => rule.in === undefined || rule.in.some((pattern) => pathMatches(slot.path, pattern));
|
|
505
|
+
const slot = slots[index] as PartSlot;
|
|
506
|
+
if (!matches(slot)) return false;
|
|
507
|
+
const after = rule.after ?? 0;
|
|
508
|
+
if (after <= 0) return true;
|
|
509
|
+
let position = 0;
|
|
510
|
+
for (let at = 0; at < index; at += 1) if (matches(slots[at] as PartSlot)) position += 1;
|
|
511
|
+
return position >= after;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** The counter of the probe documents of one value. */
|
|
515
|
+
interface Counter {
|
|
516
|
+
next: number;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** This function gives the JSON of a probe document. A position that permits only some kinds gets the first of them. */
|
|
520
|
+
function probeJson(counter: Counter, kinds: readonly (readonly Key[])[] | undefined, named: boolean): Json {
|
|
521
|
+
const id = counter.next;
|
|
522
|
+
counter.next += 1;
|
|
523
|
+
const first = kinds?.[0];
|
|
524
|
+
return { type: first === undefined ? "probe" : (first as readonly Json[]), value: { probe: id }, ...(named ? { name: `p${id}` } : {}) };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** This function gives a number that a number shape permits. */
|
|
528
|
+
function numberIn(shape: Extract<Shape, { readonly shape: "number" }>, variant: number): number {
|
|
529
|
+
const start = shape.min ?? (shape.max === undefined ? 1 : shape.max - 2);
|
|
530
|
+
const base = shape.whole === true ? Math.ceil(start) : start;
|
|
531
|
+
const value = base + (variant % 3);
|
|
532
|
+
return shape.max !== undefined && value > shape.max ? shape.max : value;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* This function generates a value of a shape, with a probe document at the place of each part. The variant selects the
|
|
537
|
+
* length of each list, the fields of a record that are absent, and the binding names of the probe documents.
|
|
538
|
+
*/
|
|
539
|
+
function valueOf(shape: Shape | undefined, variant: number, counter: Counter, named: boolean): Json {
|
|
540
|
+
if (shape === undefined || shape.shape === "none") return DATA[variant % DATA.length] as Json;
|
|
541
|
+
switch (shape.shape) {
|
|
542
|
+
case "text":
|
|
543
|
+
return TEXTS[variant % TEXTS.length] as string;
|
|
544
|
+
case "number":
|
|
545
|
+
return numberIn(shape, variant);
|
|
546
|
+
case "one":
|
|
547
|
+
return shape.values[variant % Math.max(1, shape.values.length)] ?? null;
|
|
548
|
+
case "term":
|
|
549
|
+
return probeJson(counter, shape.kinds, named);
|
|
550
|
+
case "deep": {
|
|
551
|
+
const pick = variant % 4;
|
|
552
|
+
if (pick === 0) return [probeJson(counter, undefined, named)];
|
|
553
|
+
if (pick === 1) return { a: probeJson(counter, undefined, named), b: [probeJson(counter, undefined, named)] };
|
|
554
|
+
if (pick === 2) return ["names", probeJson(counter, undefined, named)];
|
|
555
|
+
return ["names", "a"];
|
|
556
|
+
}
|
|
557
|
+
case "list": {
|
|
558
|
+
const item: Shape = shape.item ?? { shape: "term", ...(shape.of === undefined ? {} : { of: shape.of }) };
|
|
559
|
+
const length = LENGTHS[variant % LENGTHS.length] as number;
|
|
560
|
+
// The index selects the data of each item, thus the items of a list of data are different.
|
|
561
|
+
return Array.from({ length }, (_unused, index) => valueOf(item, item.shape === "term" ? variant : index, counter, named));
|
|
562
|
+
}
|
|
563
|
+
case "map": {
|
|
564
|
+
const item: Shape = shape.item ?? { shape: "term", ...(shape.of === undefined ? {} : { of: shape.of }) };
|
|
565
|
+
const out: Record<string, Json> = {};
|
|
566
|
+
for (const key of TEXTS.slice(0, [2, 0, 1][variant % 3])) setField(out, key, valueOf(item, variant, counter, named));
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
case "record": {
|
|
570
|
+
const fields: Readonly<Record<string, Shape>> = Array.isArray(shape.fields)
|
|
571
|
+
? Object.fromEntries((shape.fields as readonly string[]).map((key) => [key, { shape: "term", ...(shape.of === undefined ? {} : { of: shape.of }) } as Shape]))
|
|
572
|
+
: (shape.fields as Readonly<Record<string, Shape>>);
|
|
573
|
+
const keys = Object.keys(fields);
|
|
574
|
+
const required = new Set(shape.required ?? []);
|
|
575
|
+
const optional = keys.filter((key) => !required.has(key));
|
|
576
|
+
// The variant 0 has each field. Each other variant leaves out one optional field.
|
|
577
|
+
const absent = variant === 0 || optional.length === 0 ? undefined : optional[(variant - 1) % optional.length];
|
|
578
|
+
const out: Record<string, Json> = {};
|
|
579
|
+
for (const key of keys) if (key !== absent) setField(out, key, valueOf(fields[key], variant, counter, named));
|
|
580
|
+
return out;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** This function gives the JSON of a probe document with other JSON: another value, and another type where it can. */
|
|
586
|
+
function otherJson(item: Json): Json {
|
|
587
|
+
if (!isEnvelope(item)) return item;
|
|
588
|
+
return { ...item, type: item.type === "probe" ? ["probe", "other"] : item.type, value: { other: [true, "json"], was: item.value } };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** This function gives a probe document for a part of a value of the options. It keeps the type and the name of an envelope. */
|
|
592
|
+
function probeFor(counter: Counter, item: Json): Json {
|
|
593
|
+
if (!isEnvelope(item)) return probeJson(counter, undefined, false);
|
|
594
|
+
const id = counter.next;
|
|
595
|
+
counter.next += 1;
|
|
596
|
+
return { ...item, value: { probe: id } };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** This function gives the probe documents of a type: generated values and the values of the options. */
|
|
600
|
+
function samplesOf(type: TypeRef, typePath: readonly Key[], kind: Kind, contract: Contract, extra: readonly Json[]): readonly Sample[] {
|
|
601
|
+
const values: Json[] = [];
|
|
602
|
+
for (let variant = 0; variant < VARIANTS; variant += 1) values.push(valueOf(kind.parts, variant, { next: 0 }, variant % 2 === 1));
|
|
603
|
+
for (const value of extra) {
|
|
604
|
+
const split = splitParts(kind.parts, value);
|
|
605
|
+
if (!split.ok) continue;
|
|
606
|
+
const counter: Counter = { next: 0 };
|
|
607
|
+
values.push(split.fill(split.parts.map((slot) => probeFor(counter, slot.item))) as Json);
|
|
608
|
+
}
|
|
609
|
+
const out: Sample[] = [];
|
|
610
|
+
const seen: Json[] = [];
|
|
611
|
+
for (const value of values) {
|
|
612
|
+
if (seen.some((other) => docEquals(other, value))) continue;
|
|
613
|
+
seen.push(value);
|
|
614
|
+
const split = splitParts(kind.parts, value);
|
|
615
|
+
if (!split.ok) continue;
|
|
616
|
+
const otherValue = split.parts.length === 0 ? value : (split.fill(split.parts.map((slot) => otherJson(slot.item))) as Json);
|
|
617
|
+
const otherSplit = splitParts(kind.parts, otherValue);
|
|
618
|
+
if (!otherSplit.ok || otherSplit.parts.length !== split.parts.length) continue;
|
|
619
|
+
out.push({
|
|
620
|
+
type: typeName(type),
|
|
621
|
+
typePath,
|
|
622
|
+
kind,
|
|
623
|
+
doc: { type, value },
|
|
624
|
+
split,
|
|
625
|
+
other: { type, value: otherValue },
|
|
626
|
+
otherSplit,
|
|
627
|
+
lazy: split.parts.map((_slot, index) => contract.lazy?.some((rule) => covers(rule, split.parts, index)) === true),
|
|
628
|
+
names: scopesOf(kind.scope, value, split.parts).sees,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** This function gives the handle fields of a part: its key, its path and its binding name. */
|
|
635
|
+
function handleOf(slot: PartSlot): { readonly key: string; readonly path: readonly Key[]; readonly name?: string } {
|
|
636
|
+
const item = slot.item;
|
|
637
|
+
const name = isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
|
|
638
|
+
return name === undefined ? { key: slot.key, path: slot.path } : { key: slot.key, path: slot.path, name };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* This function drives the steps of a runner. It answers each wait of a probe part or of an effect at once. A promise
|
|
643
|
+
* that no probe gave, or a wait that the runner gives two times, stops the steps (C6).
|
|
644
|
+
*/
|
|
645
|
+
function drive(shot: Shot, steps: Steps): Result {
|
|
646
|
+
let input: unknown;
|
|
647
|
+
for (;;) {
|
|
648
|
+
let step: IteratorResult<Promise<unknown>, Result>;
|
|
649
|
+
try {
|
|
650
|
+
step = steps.next(input);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
return thrown(error, "the runner");
|
|
653
|
+
}
|
|
654
|
+
if (step.done === true) return step.value;
|
|
655
|
+
const wait = shot.waits.get(step.value);
|
|
656
|
+
if (wait === undefined || wait.seen) {
|
|
657
|
+
shot.problems.push(
|
|
658
|
+
wait === undefined
|
|
659
|
+
? "The runner yields a promise that no part and no effect gave. A runner passes on only the waits of its parts and its effects."
|
|
660
|
+
: `The runner yields the wait of ${wait.what} two times.`,
|
|
661
|
+
);
|
|
662
|
+
if (step.value instanceof Promise) void step.value.catch(() => undefined);
|
|
663
|
+
try {
|
|
664
|
+
steps.return(STOPPED);
|
|
665
|
+
} catch {
|
|
666
|
+
// The finally code of the runner threw. The probe stops the runner.
|
|
667
|
+
}
|
|
668
|
+
return STOPPED;
|
|
669
|
+
}
|
|
670
|
+
wait.seen = true;
|
|
671
|
+
input = wait.answer;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** This function ends one evaluation: it keeps the result, calls a function that the result gives, and finds the waits that the runner dropped. */
|
|
676
|
+
function finish(shot: Shot, start: () => Result, who: string): Result {
|
|
677
|
+
let result: Result;
|
|
678
|
+
try {
|
|
679
|
+
result = placed(start(), PLACE);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
result = placed(thrown(error, who), PLACE);
|
|
682
|
+
}
|
|
683
|
+
for (const wait of shot.waits.values()) if (!wait.done) shot.problems.push(`The runner drops the wait of ${wait.what}.`);
|
|
684
|
+
shot.result = result;
|
|
685
|
+
shot.callLater();
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* This function makes the code of one probe document. A compiler compiles the document one time, with compiled probe
|
|
691
|
+
* parts. Each call of the code then evaluates the document in one shot.
|
|
692
|
+
*/
|
|
693
|
+
function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sample: Sample): Instance {
|
|
694
|
+
const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));
|
|
695
|
+
const base = { kind: sample.type, typePath: sample.typePath, doc, data: split.data, place: PLACE, machine: env.machine, fill };
|
|
696
|
+
const who = `the ${env.runner ? "runner" : "evaluator"} ${env.name}`;
|
|
697
|
+
if (under.form === "eval" || under.form === "run") {
|
|
698
|
+
if (under.form === "eval") {
|
|
699
|
+
const fn = under.fn;
|
|
700
|
+
return (shot) => {
|
|
701
|
+
const ctx = shot.context;
|
|
702
|
+
const parts: Part[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.call(index, "evaluate", given ?? ctx) }));
|
|
703
|
+
const input: EvalInput = {
|
|
704
|
+
...base,
|
|
705
|
+
value: fill(parts),
|
|
706
|
+
parts,
|
|
707
|
+
context: ctx,
|
|
708
|
+
resolve: (path, options) => shot.resolve(ctx, path, options),
|
|
709
|
+
effect: (kind, payload) => shot.effect(ctx, kind, payload, PLACE),
|
|
710
|
+
};
|
|
711
|
+
return finish(shot, () => fn(input), who);
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const fn = under.fn;
|
|
715
|
+
return (shot) => {
|
|
716
|
+
const ctx = shot.context;
|
|
717
|
+
const parts: RunPart[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.steps(index, "evaluate", given ?? ctx) }));
|
|
718
|
+
const input: RunInput = {
|
|
719
|
+
...base,
|
|
720
|
+
value: fill(parts),
|
|
721
|
+
parts,
|
|
722
|
+
context: ctx,
|
|
723
|
+
resolve: (path, options) => shot.resolve(ctx, path, options),
|
|
724
|
+
effect: (kind, payload) => shot.runEffect(ctx, kind, payload, PLACE),
|
|
725
|
+
all: (list, options) => shot.all(list.map((part) => () => part.evaluate()), options),
|
|
726
|
+
};
|
|
727
|
+
return finish(shot, () => drive(shot, fn(input)), who);
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
// A compiled part finds the shot of the evaluation through this holder, because the parts exist before each shot.
|
|
731
|
+
const holder: { shot: Shot | undefined } = { shot: undefined };
|
|
732
|
+
const early = fail("#CALC!", "The compiler evaluated a part before an evaluation.");
|
|
733
|
+
const parts: CompiledPart[] = split.parts.map((slot, index) => ({
|
|
734
|
+
...handleOf(slot),
|
|
735
|
+
code: (ctx) => holder.shot?.call(index, "code", ctx) ?? early,
|
|
736
|
+
*run(ctx) {
|
|
737
|
+
const shot = holder.shot;
|
|
738
|
+
return shot === undefined ? early : yield* shot.steps(index, "run", ctx);
|
|
739
|
+
},
|
|
740
|
+
}));
|
|
741
|
+
const input: CompileInput = { ...base, value: fill(parts), parts, machine: env.machine };
|
|
742
|
+
let made: Code | RunCode | Result;
|
|
743
|
+
try {
|
|
744
|
+
made = under.compile(input);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
made = placed(thrown(error, who), PLACE);
|
|
747
|
+
}
|
|
748
|
+
const compiled = made;
|
|
749
|
+
return (shot) => {
|
|
750
|
+
holder.shot = shot;
|
|
751
|
+
if (typeof compiled !== "function") return finish(shot, () => compiled, who);
|
|
752
|
+
if (under.form === "compile") return finish(shot, () => (compiled as Code)(shot.context, shot), who);
|
|
753
|
+
return finish(shot, () => drive(shot, (compiled as RunCode)(shot.context, shot)), who);
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** This function evaluates a probe document one time in a new shot. */
|
|
758
|
+
function shoot(env: Env, instance: Instance, sample: Sample, scenario: Scenario, more: { readonly context?: Context; readonly replay?: readonly Asked[] } = {}): Shot {
|
|
759
|
+
const context = more.context ?? rootContext(env.machine, makeContext(env.names, scenario.caps));
|
|
760
|
+
const shot = new Shot(env.machine, env.runner, scenario, context, sample.split.parts.map((slot) => slot.key), env.perform, more.replay);
|
|
761
|
+
instance(shot);
|
|
762
|
+
return shot;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** This function gives the scenarios of a probe document: each result of the palette in each part, then random results. */
|
|
766
|
+
function scenariosOf(env: Env, sample: Sample, contract: Contract, running: boolean): readonly Scenario[] {
|
|
767
|
+
const count = sample.split.parts.length;
|
|
768
|
+
const palette = env.results;
|
|
769
|
+
const capsAt = (index: number): readonly Cap[] => (index % 2 === 0 ? contract.caps : NO_CAPS);
|
|
770
|
+
const out: Scenario[] = [];
|
|
771
|
+
const add = (results: readonly Result[], waits: readonly boolean[]): void => {
|
|
772
|
+
out.push({ results, waits, caps: capsAt(out.length), running });
|
|
773
|
+
};
|
|
774
|
+
if (count === 0) {
|
|
775
|
+
add([], []);
|
|
776
|
+
add([], []);
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
const all = Array.from({ length: count }, () => true);
|
|
780
|
+
for (const result of palette) add(Array.from({ length: count }, () => result), all);
|
|
781
|
+
for (let run = 0; run < env.runs; run += 1) {
|
|
782
|
+
add(
|
|
783
|
+
Array.from({ length: count }, () => env.random.pick(palette)),
|
|
784
|
+
Array.from({ length: count }, () => env.random.chance(0.5)),
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** This function gives a short text of a value for a reason. */
|
|
791
|
+
function valueText(value: unknown): string {
|
|
792
|
+
if (typeof value === "function") return "a function";
|
|
793
|
+
let text: string;
|
|
794
|
+
try {
|
|
795
|
+
text = JSON.stringify(value) ?? describe(value);
|
|
796
|
+
} catch {
|
|
797
|
+
text = describe(value);
|
|
798
|
+
}
|
|
799
|
+
return text.length > 40 ? `${text.slice(0, 37)}...` : text;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function resultText(result: Result): string {
|
|
803
|
+
return result.ok ? `the value ${valueText(result.value)}` : `the error ${result.error.code}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function capsText(caps: readonly Cap[]): string {
|
|
807
|
+
return caps.length === 0 ? "no capabilities" : `the capabilities ${caps.map((cap) => capKeys(cap).join(".")).join(", ")}`;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** This function tells if two results agree, as in `agree`: the same value, or errors with the same code and place. */
|
|
811
|
+
function sameOutcome(a: Result, b: Result, equal: Env["equal"]): boolean {
|
|
812
|
+
if (a.ok && b.ok) return sameValue(a.value, b.value, equal);
|
|
813
|
+
if (a.ok || b.ok) return false;
|
|
814
|
+
if (a.error.code !== b.error.code) return false;
|
|
815
|
+
return a.error.place === undefined || b.error.place === undefined || docEquals(a.error.place as readonly Key[], b.error.place as readonly Key[]);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** This function tells if two shots give the same result and the same results of a later call. */
|
|
819
|
+
function sameResults(a: Shot, b: Shot, equal: Env["equal"]): boolean {
|
|
820
|
+
return sameOutcome(a.result, b.result, equal) && a.later.length === b.later.length && a.later.every((result, index) => sameOutcome(result, b.later[index] as Result, equal));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* This function gives the difference of two shots of the same probe, or `undefined` when they agree. Two shots agree
|
|
825
|
+
* when they give the same results, use the parts in the same order and cause the same effects. Without `paths`, the
|
|
826
|
+
* resolutions do not count, as in the simulation of the plan.
|
|
827
|
+
*/
|
|
828
|
+
function differs(a: Shot, b: Shot, equal: Env["equal"], paths = true): string | undefined {
|
|
829
|
+
if (!sameOutcome(a.result, b.result, equal)) return `The first evaluation gives ${resultText(a.result)}, and the second gives ${resultText(b.result)}.`;
|
|
830
|
+
if (!sameResults(a, b, equal)) return "The function that the result gives returns another result.";
|
|
831
|
+
const order = (shot: Shot): string => shot.calls.map((call) => `${call.later ? "later " : ""}${shot.keys[call.part] ?? String(call.part)}`).join(", ");
|
|
832
|
+
if (order(a) !== order(b)) return `The parts evaluate in another order: [${order(a)}] and [${order(b)}].`;
|
|
833
|
+
const effects = (shot: Shot): string => shot.caused.map((effect) => effect.kind).join(", ");
|
|
834
|
+
if (a.caused.length !== b.caused.length || a.caused.some((effect, index) => effect.kind !== b.caused[index]?.kind || !sameValue(effect.payload, b.caused[index]?.payload, equal))) {
|
|
835
|
+
return `The effects differ: [${effects(a)}] and [${effects(b)}].`;
|
|
836
|
+
}
|
|
837
|
+
if (!paths) return undefined;
|
|
838
|
+
const asked = (shot: Shot): string => shot.asked.map((each) => valueText(each.path)).join(", ");
|
|
839
|
+
if (a.asked.length !== b.asked.length || a.asked.some((each, index) => !docEquals(each.path, b.asked[index]?.path))) return `The resolutions differ: [${asked(a)}] and [${asked(b)}].`;
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** This function gives the probe of a finding. */
|
|
844
|
+
function probeOf(sample: Sample, scenario: Scenario, doc: Envelope = sample.doc): ConformProbe {
|
|
845
|
+
const results: Record<string, Result> = {};
|
|
846
|
+
sample.split.parts.forEach((slot, index) => setField(results, slot.key, scenario.results[index] ?? NO_RESULT));
|
|
847
|
+
const waits = scenario.running ? sample.split.parts.filter((_slot, index) => scenario.waits[index] === true).map((slot) => slot.key) : [];
|
|
848
|
+
return { type: sample.type, doc, results, waits, caps: scenario.caps, running: scenario.running };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** This function keeps the first finding of each obligation. */
|
|
852
|
+
function report(env: Env, obligation: Obligation, reason: string, probe: () => ConformProbe): void {
|
|
853
|
+
if (!env.found.has(obligation)) env.found.set(obligation, { obligation, reason, probe: probe() });
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function thenable(value: unknown): boolean {
|
|
857
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof (value as { readonly then?: unknown }).then === "function";
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** This function checks the record of one shot: the capabilities, the effects, the eager parts, the waits and the names. */
|
|
861
|
+
function checkShot(env: Env, sample: Sample, contract: Contract, scenario: Scenario, shot: Shot): void {
|
|
862
|
+
const probe = (): ConformProbe => probeOf(sample, scenario);
|
|
863
|
+
const docCaps = shot.context.caps;
|
|
864
|
+
const keyOf = (part: number): string => shot.keys[part] ?? String(part);
|
|
865
|
+
// C1: each context that the code gives a part or an effect has only the capabilities of its document.
|
|
866
|
+
for (const call of shot.calls) {
|
|
867
|
+
if (!call.context.caps.every((cap) => permits(docCaps, cap))) {
|
|
868
|
+
report(env, "C1", `The code gives the part ${keyOf(call.part)} ${capsText(call.context.caps)}, and its document has ${capsText(docCaps)}.`, probe);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const effect of shot.caused) {
|
|
872
|
+
if (!effect.context.caps.every((cap) => permits(docCaps, cap))) {
|
|
873
|
+
report(env, "C1", `The code gives its effect ${effect.kind} ${capsText(effect.context.caps)}, and its document has ${capsText(docCaps)}.`, probe);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (contract.describes) {
|
|
877
|
+
// C2: only the declared effects.
|
|
878
|
+
for (const effect of shot.caused) {
|
|
879
|
+
if (!permits(contract.declared, effect.kind)) {
|
|
880
|
+
report(env, "C2", `The code causes the effect ${effect.kind}, and the contract of ${sample.type} declares ${contract.declared.length === 0 ? "no effects" : capsText(contract.declared)}.`, probe);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// C4: a pure kind causes no effect.
|
|
884
|
+
const first = shot.caused[0];
|
|
885
|
+
if (contract.pure && first !== undefined) report(env, "C4", `The kind ${sample.type} is pure, and the code causes the effect ${first.kind}.`, probe);
|
|
886
|
+
// C3: each eager part evaluates when the result is a value.
|
|
887
|
+
if (shot.result.ok) {
|
|
888
|
+
sample.split.parts.forEach((_slot, index) => {
|
|
889
|
+
if (sample.lazy[index] === true || shot.calls.some((call) => call.part === index && !call.later)) return;
|
|
890
|
+
report(env, "C3", `The result is a value, and the code does not evaluate the part ${keyOf(index)}. No lazy rule of ${sample.type} covers the part.`, probe);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// C6: only a runner waits, and it passes each wait through.
|
|
895
|
+
for (const problem of shot.problems) report(env, "C6", problem, probe);
|
|
896
|
+
if (!env.runner && shot.result.ok && thenable(shot.result.value)) {
|
|
897
|
+
report(env, "C6", "The code of an evaluator gives a promise. Only a runner waits, with the steps of its parts and its effects.", probe);
|
|
898
|
+
}
|
|
899
|
+
// C7: the names of each part are the names of the scope rules.
|
|
900
|
+
const outer = shot.context.names;
|
|
901
|
+
for (const call of shot.calls) {
|
|
902
|
+
const given = call.context.names;
|
|
903
|
+
const allowed = new Set(sample.names[call.part] ?? []);
|
|
904
|
+
const bound = Object.keys(given).filter((key) => !Object.hasOwn(outer, key) || !Object.is(given[key], outer[key]));
|
|
905
|
+
const extra = bound.filter((key) => !allowed.has(key));
|
|
906
|
+
const lost = Object.keys(outer).filter((key) => !Object.hasOwn(given, key));
|
|
907
|
+
const missing = [...allowed].filter((key) => !Object.hasOwn(given, key));
|
|
908
|
+
const part = keyOf(call.part);
|
|
909
|
+
if (extra.length > 0) {
|
|
910
|
+
report(env, "C7", `The code binds ${extra.join(", ")} for the part ${part}, and the scope rules of ${sample.type} give it ${allowed.size === 0 ? "no names" : [...allowed].join(", ")}.`, probe);
|
|
911
|
+
} else if (missing.length > 0) {
|
|
912
|
+
report(env, "C7", `The scope rules of ${sample.type} give the part ${part} the names ${missing.join(", ")}, and the code does not bind them.`, probe);
|
|
913
|
+
} else if (lost.length > 0) {
|
|
914
|
+
report(env, "C7", `The part ${part} does not see the names ${lost.join(", ")} of its document.`, probe);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* This function gives a context with other values in each name and root that no resolution read (C4). The world and the
|
|
921
|
+
* capabilities stay. A value that the code resolves keeps its value, because the list of reads holds that read.
|
|
922
|
+
*/
|
|
923
|
+
function variedContext(ctx: Context, asked: readonly Asked[]): Context {
|
|
924
|
+
const read = new Set<string>();
|
|
925
|
+
for (const { path } of asked) {
|
|
926
|
+
const root = path[0];
|
|
927
|
+
read.add(root === "names" && isKey(path[1]) ? `names.${String(path[1])}` : String(root));
|
|
928
|
+
}
|
|
929
|
+
const names: Record<string, unknown> = {};
|
|
930
|
+
for (const key of Object.keys(ctx.names)) setField(names, key, read.has("names") || read.has(`names.${key}`) ? ctx.names[key] : { other: key });
|
|
931
|
+
return { ...ctx, names, self: read.has("self") ? ctx.self : { other: "self" }, event: read.has("event") ? ctx.event : { other: "event" } };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** This function examines one probe document with each check. */
|
|
935
|
+
function examine(env: Env, sample: Sample, contract: Contract): void {
|
|
936
|
+
const { under } = env;
|
|
937
|
+
const fresh = (): Instance => instanceOf(env, under, sample.doc, sample.split, sample);
|
|
938
|
+
const scenarios = scenariosOf(env, sample, contract, false);
|
|
939
|
+
const code = fresh();
|
|
940
|
+
const first = scenarios.map((scenario) => shoot(env, code, sample, scenario));
|
|
941
|
+
scenarios.forEach((scenario, index) => checkShot(env, sample, contract, scenario, first[index] as Shot));
|
|
942
|
+
|
|
943
|
+
// local: the same code gives the same record in the other order of the probes.
|
|
944
|
+
const second: Shot[] = new Array<Shot>(scenarios.length);
|
|
945
|
+
for (let index = scenarios.length - 1; index >= 0; index -= 1) second[index] = shoot(env, code, sample, scenarios[index] as Scenario);
|
|
946
|
+
let clean = true;
|
|
947
|
+
scenarios.forEach((scenario, index) => {
|
|
948
|
+
const why = differs(first[index] as Shot, second[index] as Shot, env.equal);
|
|
949
|
+
if (why === undefined) return;
|
|
950
|
+
clean = false;
|
|
951
|
+
report(env, "local", `The code keeps state from one call to the next. The same probe gives another record in a later call. ${why}`, () => probeOf(sample, scenario));
|
|
952
|
+
});
|
|
953
|
+
// C8: code that compiles again at the time of the evaluation gives the same record.
|
|
954
|
+
if (clean && compiledForm(under)) {
|
|
955
|
+
scenarios.forEach((scenario, index) => {
|
|
956
|
+
const why = differs(second[index] as Shot, shoot(env, fresh(), sample, scenario), env.equal);
|
|
957
|
+
if (why === undefined) return;
|
|
958
|
+
clean = false;
|
|
959
|
+
report(env, "C8", `The code reads a table of the machine or of the profile when it compiles, and the answer changes later. The code of an earlier compilation and new code differ. ${why}`, () => probeOf(sample, scenario));
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
// The other comparisons need code without state, because each of them evaluates the probes again.
|
|
963
|
+
if (!clean) return;
|
|
964
|
+
|
|
965
|
+
// C5: other JSON in each part, and the same results of the parts, give the same record.
|
|
966
|
+
if (sample.split.parts.length > 0) {
|
|
967
|
+
const other = instanceOf(env, under, sample.other, sample.otherSplit, sample);
|
|
968
|
+
const again = compiledForm(under) ? fresh() : code;
|
|
969
|
+
scenarios.forEach((scenario) => {
|
|
970
|
+
const why = differs(shoot(env, again, sample, scenario), shoot(env, other, sample, scenario), env.equal);
|
|
971
|
+
if (why !== undefined) report(env, "C5", `The code reads the JSON of its parts. Two documents whose parts have other JSON and the same results differ. ${why}`, () => probeOf(sample, scenario));
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (contract.describes) {
|
|
976
|
+
// C4: a pure kind gives the same record in a context with other values, when each resolution gives the same answer.
|
|
977
|
+
if (contract.pure) {
|
|
978
|
+
scenarios.forEach((scenario, index) => {
|
|
979
|
+
const before = first[index] as Shot;
|
|
980
|
+
const varied = shoot(env, code, sample, scenario, { context: variedContext(before.context, before.asked), replay: before.asked });
|
|
981
|
+
const why = differs(before, varied, env.equal);
|
|
982
|
+
if (why !== undefined) {
|
|
983
|
+
report(env, "C4", `The kind ${sample.type} is pure, and the code reads the context outside resolve. Other values in the roots, with the same answers of resolve, change the record. ${why}`, () => probeOf(sample, scenario));
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
// C3: a lazy part evaluates only when its result counts.
|
|
988
|
+
scenarios.forEach((scenario, index) => {
|
|
989
|
+
const before = first[index] as Shot;
|
|
990
|
+
const evaluated = new Set(before.calls.filter((call) => !call.later && sample.lazy[call.part] === true).map((call) => call.part));
|
|
991
|
+
for (const part of evaluated) {
|
|
992
|
+
const counts = env.results.some((result) => {
|
|
993
|
+
if (result === scenario.results[part]) return false;
|
|
994
|
+
const results = scenario.results.map((each, at) => (at === part ? result : each));
|
|
995
|
+
return !sameResults(before, shoot(env, code, sample, { ...scenario, results }), env.equal);
|
|
996
|
+
});
|
|
997
|
+
if (!counts) {
|
|
998
|
+
report(env, "C3", `The code evaluates the lazy part ${before.keys[part] ?? String(part)}, and no result of that part changes the result. A lazy part evaluates only when its result counts.`, () => probeOf(sample, scenario));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// agree: the compiled code and its definition give the same record (the local lemma of SIM-PLAN).
|
|
1005
|
+
const definition = env.definition;
|
|
1006
|
+
if (definition !== undefined) {
|
|
1007
|
+
const defined = instanceOf(env, definition, sample.doc, sample.split, sample);
|
|
1008
|
+
scenarios.forEach((scenario, index) => {
|
|
1009
|
+
const why = differs(first[index] as Shot, shoot(env, defined, sample, scenario), env.equal, false);
|
|
1010
|
+
if (why !== undefined) report(env, "agree", `The code and its definition for the reference differ. ${why}`, () => probeOf(sample, scenario));
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// C6 and the other checks of one shot in a run: each probe part can wait.
|
|
1015
|
+
if (env.runner) {
|
|
1016
|
+
for (const scenario of scenariosOf(env, sample, contract, true)) checkShot(env, sample, contract, scenario, shoot(env, code, sample, scenario));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* This function examines one evaluator or runner of a machine against the obligations of host code (K92, D36). It
|
|
1022
|
+
* starts the code directly, without the kernel, with probe parts and a probe runtime. It generates probe documents from
|
|
1023
|
+
* the contract of each kind that names the code, and it gives the probe parts chosen and random results.
|
|
1024
|
+
*
|
|
1025
|
+
* The checks follow the obligations C1 to C8 of `docs/SEMANTICS.md`. Two more checks follow the premises of the
|
|
1026
|
+
* simulation of the plan. The check `local` finds state that stays from one call to the next. The check `agree` compares
|
|
1027
|
+
* compiled code with its definition. The
|
|
1028
|
+
* finding of an obligation names the first probe that breaks it. An empty list means that each probe conforms.
|
|
1029
|
+
*
|
|
1030
|
+
* @param machine - The machine with the code and the world of the kinds.
|
|
1031
|
+
* @param name - The name of the evaluator or of the runner. A runner wins over an evaluator of the same name.
|
|
1032
|
+
* @param options - The verb, the types and the values of the probe documents, and the results of the probe parts. Also
|
|
1033
|
+
* the definitions, the capabilities, the names, the seed, the mock and the rule of equality.
|
|
1034
|
+
* @returns The first finding of each obligation that the code breaks, in the order of the obligations.
|
|
1035
|
+
* @throws {Error} The machine has no code with the name, or no kind names it and the options give no types.
|
|
1036
|
+
*/
|
|
1037
|
+
export function conforms(machine: Machine, name: string, options: ConformOptions = {}): readonly Finding[] {
|
|
1038
|
+
const verb = options.verb ?? "eval";
|
|
1039
|
+
const runnerCode = own(machine.runners, name);
|
|
1040
|
+
const code = runnerCode ?? own(machine.evaluators, name);
|
|
1041
|
+
if (code === undefined) throw new Error(`The machine has no evaluator and no runner with the name ${name}.`);
|
|
1042
|
+
const runner = runnerCode !== undefined;
|
|
1043
|
+
const env: Env = {
|
|
1044
|
+
machine,
|
|
1045
|
+
name,
|
|
1046
|
+
verb,
|
|
1047
|
+
under: underOf(code, runner),
|
|
1048
|
+
definition: definitionOf(machine, name, runner, options.definitions),
|
|
1049
|
+
runner,
|
|
1050
|
+
results: options.results ?? RESULTS,
|
|
1051
|
+
names: options.names ?? NAMES,
|
|
1052
|
+
runs: options.runs ?? 12,
|
|
1053
|
+
random: random(options.seed ?? 1),
|
|
1054
|
+
perform: options.perform,
|
|
1055
|
+
equal: options.equal,
|
|
1056
|
+
found: new Map(),
|
|
1057
|
+
};
|
|
1058
|
+
const types = options.types ?? typesNaming(machine.world, name, verb);
|
|
1059
|
+
if (types.length === 0) throw new Error(`No kind of the world names ${name} for the verb ${verb}. Give the types of the probe documents in the option types.`);
|
|
1060
|
+
for (const type of types) {
|
|
1061
|
+
const found = lookupKind(machine.world, type);
|
|
1062
|
+
if (!found.ok) throw new Error(`The world has no kind for the type ${typeName(type)}.`);
|
|
1063
|
+
const contract = contractOf(env, found.kind, found.path, options.caps);
|
|
1064
|
+
const extra = own(options.values, typeName(type)) ?? own(options.values, found.path.join(".")) ?? [];
|
|
1065
|
+
for (const sample of samplesOf(type, found.path, found.kind, contract, extra)) examine(env, sample, contract);
|
|
1066
|
+
}
|
|
1067
|
+
return ORDER.filter((obligation) => env.found.has(obligation)).map((obligation) => env.found.get(obligation) as Finding);
|
|
1068
|
+
}
|