@mark1russell7/coeval 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -5
- package/build/index.d.ts +442 -116
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5848 -3680
- package/build/index.js.map +1 -1
- package/package.json +4 -4
- package/src/codec-laws.ts +152 -0
- package/src/codec.ts +2 -2
- package/src/conform.ts +663 -71
- package/src/deref.ts +17 -3
- package/src/durable.ts +21 -9
- package/src/effects.ts +1 -1
- package/src/errors.ts +17 -1
- package/src/evaluate.ts +188 -24
- package/src/formula.ts +60 -17
- package/src/index.ts +15 -0
- package/src/instances.ts +16 -1
- package/src/meaning.ts +43 -10
- package/src/parts.ts +7 -4
- package/src/providers.ts +4 -1
- package/src/reference.ts +122 -2
- package/src/schedule.ts +286 -45
- package/src/static.ts +59 -16
- package/src/stdlib.ts +188 -6
- package/src/syntax.ts +637 -48
- package/src/types.ts +2 -1
- package/src/world.ts +31 -4
package/src/conform.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import { type Envelope, type Json, type Key, type TypeRef, docEquals, isEnvelope, isKey, typeName } from "./doc.ts";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { type Envelope, type Json, type Key, type TypeRef, docEquals, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
|
+
import {
|
|
3
|
+
type Cap,
|
|
4
|
+
type Context,
|
|
5
|
+
type DerefCode,
|
|
6
|
+
type HostThing,
|
|
7
|
+
type Resolution,
|
|
8
|
+
type ResolveOptions,
|
|
9
|
+
type Thing,
|
|
10
|
+
type WalkResult,
|
|
11
|
+
type Walker,
|
|
12
|
+
THING,
|
|
13
|
+
derefStep,
|
|
14
|
+
isHostThing,
|
|
15
|
+
isRef,
|
|
16
|
+
resolve,
|
|
17
|
+
valueOfThing,
|
|
18
|
+
} from "./deref.ts";
|
|
19
|
+
import { type EvalError, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
4
20
|
import {
|
|
5
21
|
type AllOptions,
|
|
6
22
|
type Code,
|
|
@@ -32,8 +48,8 @@ import { type PartSlot, type Split, pathMatches, scopesOf, splitParts } from "./
|
|
|
32
48
|
import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
|
|
33
49
|
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
34
50
|
import { sameValue } from "./agree.ts";
|
|
35
|
-
import { declaredTruthOf } from "./formula.ts";
|
|
36
|
-
import { falseCovers, membersOf, typeIssues, typeText } from "./types.ts";
|
|
51
|
+
import { type FormulaProfile, type ProfileReader, declaredTruthOf, profileReaderOf, signaturesOf } from "./formula.ts";
|
|
52
|
+
import { type ValueType, falseCovers, member, membersOf, typeIssues, typeText } from "./types.ts";
|
|
37
53
|
import { type Random, random } from "./generate.ts";
|
|
38
54
|
import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
39
55
|
|
|
@@ -58,12 +74,20 @@ import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effe
|
|
|
58
74
|
* - `C8`: compiled code gives the result of code that compiles again at the time of the evaluation.
|
|
59
75
|
* - `local`: the code keeps no state from one call to the next.
|
|
60
76
|
* - `agree`: compiled code gives the result of its definition for the reference interpreter.
|
|
77
|
+
* - `parallel`: the code gives the same result, the same parts and the same effects in each order of the waits of
|
|
78
|
+
* `all` (D128). Only a runner gets this check.
|
|
61
79
|
* - `truth`: the own `truthy` of a formula profile gives `true` or `false` for each member of its `truthType` (D86).
|
|
62
80
|
* Only `formula.if`, `formula.and` and `formula.or` of the formula kit get this check.
|
|
63
81
|
* - `cover`: each sample of a covered domain of the machine is in one domain of each cover of the domain (D94). Each
|
|
64
82
|
* evaluator and each runner gets this check, because `fits` trusts the covers for each program.
|
|
83
|
+
* - `result`: a walker, the code of a `deref` or a function of a profile gives the form that its type states. A walker
|
|
84
|
+
* and the code of a `deref` throw nothing, except a `Failure` (D137).
|
|
85
|
+
* - `ladder`: a walker steps on a host thing only through `deref` and `has` of its input (D137).
|
|
86
|
+
* - `hops`: each `visit` of a walker tells a step that the walk followed. Without `visit`, the walk stays the same (D137).
|
|
87
|
+
* - `has`: the function `has` of the code of a `deref` gives a truth, and `true` for each key that the code answers (D137).
|
|
88
|
+
* - `typed`: a walker keeps its type rule, an operation its signatures and an extension its entry in `extTypes` (D137).
|
|
65
89
|
*/
|
|
66
|
-
export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth" | "cover";
|
|
90
|
+
export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "parallel" | "truth" | "cover" | "result" | "ladder" | "hops" | "has" | "typed";
|
|
67
91
|
|
|
68
92
|
/** The probe of a finding: the document, the result of each probe part, the capabilities and the handler. */
|
|
69
93
|
export interface ConformProbe {
|
|
@@ -79,6 +103,10 @@ export interface ConformProbe {
|
|
|
79
103
|
readonly caps: readonly Cap[];
|
|
80
104
|
/** `true` in a run, in which the host does each effect and a part can wait. */
|
|
81
105
|
readonly running: boolean;
|
|
106
|
+
/** The thing before the step, in a finding of a walker. */
|
|
107
|
+
readonly here?: unknown;
|
|
108
|
+
/** The key of the step, in a finding of the code of a `deref`. */
|
|
109
|
+
readonly key?: Key;
|
|
82
110
|
}
|
|
83
111
|
|
|
84
112
|
/** One obligation that the code breaks, with the reason and the first probe that shows it. */
|
|
@@ -95,6 +123,9 @@ export interface ConformOptions {
|
|
|
95
123
|
* The verb that the evaluator serves. The default is `eval`. The facts `pure` and `lazy` of a contract describe only
|
|
96
124
|
* `eval`, thus `conforms` checks C3 and C4 only for `eval` (D23). `conforms` checks C2 for each verb that the
|
|
97
125
|
* contract declares effects for: `eval`, and each verb with an entry in `verbEffects` (D49).
|
|
126
|
+
*
|
|
127
|
+
* The verb `step` selects the walker of the name, and the verb `deref` selects the code of the `deref` of the name
|
|
128
|
+
* (D137). Without the option, a name that no evaluator and no runner has selects its walker, and then its `deref`.
|
|
98
129
|
*/
|
|
99
130
|
readonly verb?: string;
|
|
100
131
|
/**
|
|
@@ -117,11 +148,16 @@ export interface ConformOptions {
|
|
|
117
148
|
* definition from `STD_DEFINITIONS`.
|
|
118
149
|
*/
|
|
119
150
|
readonly definitions?: Definitions;
|
|
151
|
+
/**
|
|
152
|
+
* More things before the step of a walker (D137). Use them for the things that a walker needs, for example a host
|
|
153
|
+
* thing of the language. The default holds three probe nodes, a document, `undefined` and the values of the results.
|
|
154
|
+
*/
|
|
155
|
+
readonly things?: readonly unknown[];
|
|
120
156
|
/** The capabilities of half of the probes. The other half has none. The default is the declared effects and `probe`. */
|
|
121
157
|
readonly caps?: readonly Cap[];
|
|
122
158
|
/** The names of the context of each probe. */
|
|
123
159
|
readonly names?: Readonly<Record<string, unknown>>;
|
|
124
|
-
/** The seed of the random results
|
|
160
|
+
/** The seed of the random results and of the orders of `all`. The same seed gives the same probes. The default is 1. */
|
|
125
161
|
readonly seed?: number;
|
|
126
162
|
/** The count of probes with random results for each probe document. The default is 12. */
|
|
127
163
|
readonly runs?: number;
|
|
@@ -138,7 +174,7 @@ export interface ConformOptions {
|
|
|
138
174
|
}
|
|
139
175
|
|
|
140
176
|
/** The order of the obligations in the list of findings. */
|
|
141
|
-
const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth", "cover"];
|
|
177
|
+
const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "parallel", "truth", "cover", "result", "ladder", "hops", "has", "typed"];
|
|
142
178
|
|
|
143
179
|
/** A function in the results of the probe parts. It gives the count of its arguments. */
|
|
144
180
|
const PROBE_FN = (...args: readonly unknown[]): number => args.length;
|
|
@@ -170,6 +206,18 @@ const STOPPED: Result = { ok: false, error: { code: "#CALC!", reason: "The probe
|
|
|
170
206
|
/** The result of an item of `all` after a failure with `failFast`. */
|
|
171
207
|
const ABORTED: Result = { ok: false, error: { code: "#ABORT", reason: "The part stopped: a part before it failed, and the parts run with failFast." } };
|
|
172
208
|
|
|
209
|
+
/** The order of the waits of one evaluation: this function gives the index of the next wait in the list of the waits of `all`. */
|
|
210
|
+
type Pick = (waiting: number) => number;
|
|
211
|
+
|
|
212
|
+
/** The waits settle in the order that they began. Each shot uses this order first. */
|
|
213
|
+
const FORWARD: Pick = () => 0;
|
|
214
|
+
|
|
215
|
+
/** The waits settle in the other order: the last wait that began settles first. */
|
|
216
|
+
const BACKWARD: Pick = (waiting) => waiting - 1;
|
|
217
|
+
|
|
218
|
+
/** The count of orders from the seed of a probe document, for each scenario with a choice (D128). */
|
|
219
|
+
const SEEDED_ORDERS = 4;
|
|
220
|
+
|
|
173
221
|
/** The default names of the context of each probe. The generated names of a value (`a`, `b` and `c`) are other names. */
|
|
174
222
|
const NAMES: Readonly<Record<string, unknown>> = { x: "outer x", y: 2.5, z: ["outer"] };
|
|
175
223
|
|
|
@@ -262,27 +310,35 @@ interface Wait {
|
|
|
262
310
|
done: boolean;
|
|
263
311
|
}
|
|
264
312
|
|
|
265
|
-
/** The state of `conforms`
|
|
266
|
-
interface
|
|
313
|
+
/** The state of `conforms` that each form of code needs. */
|
|
314
|
+
interface BaseEnv {
|
|
267
315
|
readonly machine: Machine;
|
|
268
316
|
readonly name: string;
|
|
269
317
|
readonly verb: string;
|
|
318
|
+
readonly results: readonly Result[];
|
|
319
|
+
readonly names: Readonly<Record<string, unknown>>;
|
|
320
|
+
readonly equal: ((a: unknown, b: unknown) => boolean | undefined) | undefined;
|
|
321
|
+
readonly found: Map<Obligation, Finding>;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The state of `conforms` for one evaluator. */
|
|
325
|
+
interface Env extends BaseEnv {
|
|
270
326
|
readonly under: Under;
|
|
271
327
|
readonly definition: Under | undefined;
|
|
272
328
|
readonly runner: boolean;
|
|
273
|
-
readonly results: readonly Result[];
|
|
274
|
-
readonly names: Readonly<Record<string, unknown>>;
|
|
275
329
|
readonly runs: number;
|
|
276
330
|
readonly random: Random;
|
|
331
|
+
/** The option `seed`. With the text of a probe document, it gives the orders of the waits of `all` (D128). */
|
|
332
|
+
readonly seed: number;
|
|
277
333
|
readonly perform: Perform | undefined;
|
|
278
|
-
readonly equal: ((a: unknown, b: unknown) => boolean | undefined) | undefined;
|
|
279
|
-
readonly found: Map<Obligation, Finding>;
|
|
280
334
|
}
|
|
281
335
|
|
|
282
336
|
/** The facts of the kind of one type, as the contract and the hook `facts` of the machine give them. */
|
|
283
337
|
interface Contract {
|
|
284
338
|
readonly declared: readonly Cap[];
|
|
285
339
|
readonly pure: boolean;
|
|
340
|
+
/** `true` when the results of the code can change while its input stays the same (FACTS.VOLATILE). */
|
|
341
|
+
readonly volatile: boolean;
|
|
286
342
|
readonly lazy: readonly LazyRule[] | undefined;
|
|
287
343
|
/** The parts that the runner evaluates with its code, without a wait (RUN.DRIVES). */
|
|
288
344
|
readonly drives: readonly LazyRule[] | undefined;
|
|
@@ -298,6 +354,21 @@ interface Contract {
|
|
|
298
354
|
* resolution. In a run, a probe part and an effect wait with a promise that the probe driver answers at once.
|
|
299
355
|
*/
|
|
300
356
|
class Shot implements Runtime {
|
|
357
|
+
readonly machine: Machine;
|
|
358
|
+
/** `true` when the code under test is a runner. */
|
|
359
|
+
readonly runner: boolean;
|
|
360
|
+
readonly scenario: Scenario;
|
|
361
|
+
/** The context of the document. */
|
|
362
|
+
readonly context: Context;
|
|
363
|
+
/** The key of each part, for the reasons. */
|
|
364
|
+
readonly keys: readonly string[];
|
|
365
|
+
/** `true` for each part that the runner can evaluate with its code, by the rules `drives` (D119). */
|
|
366
|
+
private readonly driven: readonly boolean[];
|
|
367
|
+
private readonly perform: Perform | undefined;
|
|
368
|
+
/** The resolutions of an earlier evaluation. Each resolution with the same path gets the same answer (C4). */
|
|
369
|
+
private readonly replay: readonly Asked[] | undefined;
|
|
370
|
+
/** The order of the waits of `all`. */
|
|
371
|
+
private readonly pick: Pick;
|
|
301
372
|
readonly calls: Call[] = [];
|
|
302
373
|
readonly caused: Caused[] = [];
|
|
303
374
|
readonly asked: Asked[] = [];
|
|
@@ -306,25 +377,36 @@ class Shot implements Runtime {
|
|
|
306
377
|
readonly waits = new Map<Promise<unknown>, Wait>();
|
|
307
378
|
/** The results of the calls of a function that the result gives. */
|
|
308
379
|
readonly later: Result[] = [];
|
|
380
|
+
/** The waits of each `all`, in the order that they settled. */
|
|
381
|
+
readonly settled: string[] = [];
|
|
382
|
+
/** The count of the times that `all` selected one wait from two or more. Without a choice, each order gives this shot. */
|
|
383
|
+
choices = 0;
|
|
384
|
+
/** `true` when a failure with `failFast` stopped an `all`. Which items stop then depends on the order (RUN.ALL). */
|
|
385
|
+
stopped = false;
|
|
309
386
|
result: Result = NO_RESULT;
|
|
310
387
|
private afterwards = false;
|
|
311
388
|
readonly running: boolean;
|
|
312
389
|
|
|
313
390
|
constructor(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
readonly
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
private readonly driven: readonly boolean[],
|
|
324
|
-
private readonly perform: Perform | undefined,
|
|
325
|
-
/** The resolutions of an earlier evaluation. Each resolution with the same path gets the same answer (C4). */
|
|
326
|
-
private readonly replay: readonly Asked[] | undefined,
|
|
391
|
+
machine: Machine,
|
|
392
|
+
runner: boolean,
|
|
393
|
+
scenario: Scenario,
|
|
394
|
+
context: Context,
|
|
395
|
+
keys: readonly string[],
|
|
396
|
+
driven: readonly boolean[],
|
|
397
|
+
perform: Perform | undefined,
|
|
398
|
+
replay: readonly Asked[] | undefined,
|
|
399
|
+
pick: Pick,
|
|
327
400
|
) {
|
|
401
|
+
this.machine = machine;
|
|
402
|
+
this.runner = runner;
|
|
403
|
+
this.scenario = scenario;
|
|
404
|
+
this.context = context;
|
|
405
|
+
this.keys = keys;
|
|
406
|
+
this.driven = driven;
|
|
407
|
+
this.perform = perform;
|
|
408
|
+
this.replay = replay;
|
|
409
|
+
this.pick = pick;
|
|
328
410
|
this.running = scenario.running;
|
|
329
411
|
}
|
|
330
412
|
|
|
@@ -401,25 +483,74 @@ class Shot implements Runtime {
|
|
|
401
483
|
return fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place });
|
|
402
484
|
}
|
|
403
485
|
|
|
486
|
+
/**
|
|
487
|
+
* This function is the rule ALL of the kernel, with the order of `pick` in place of the order of the host. The items
|
|
488
|
+
* start in their order up to the limit. Each item goes on to its first wait, and the chosen wait resumes its item.
|
|
489
|
+
*/
|
|
404
490
|
*all(list: readonly (() => Steps)[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
|
|
405
491
|
if (!this.runner) this.problems.push("The code of an evaluator starts the steps of all. Only a runner waits.");
|
|
406
|
-
|
|
407
|
-
const
|
|
492
|
+
const results: Result[] = new Array<Result>(list.length);
|
|
493
|
+
const limit = Math.max(1, options.concurrency ?? list.length);
|
|
494
|
+
const failFast = options.failFast === true;
|
|
495
|
+
interface Active {
|
|
496
|
+
readonly index: number;
|
|
497
|
+
readonly steps: Steps;
|
|
498
|
+
readonly pending: Promise<unknown>;
|
|
499
|
+
}
|
|
500
|
+
const active: Active[] = [];
|
|
501
|
+
let next = 0;
|
|
408
502
|
let failed = false;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let
|
|
503
|
+
const settle = (index: number, result: Result): void => {
|
|
504
|
+
results[index] = result;
|
|
505
|
+
if (failFast && !result.ok) failed = true;
|
|
506
|
+
};
|
|
507
|
+
const advance = (index: number, steps: Steps, input: unknown): void => {
|
|
508
|
+
let step: IteratorResult<Promise<unknown>, Result>;
|
|
415
509
|
try {
|
|
416
|
-
|
|
510
|
+
step = steps.next(input);
|
|
417
511
|
} catch (error) {
|
|
418
|
-
|
|
512
|
+
settle(index, thrown(error, "a part"));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (step.done === true) settle(index, step.value);
|
|
516
|
+
else active.push({ index, steps, pending: step.value });
|
|
517
|
+
};
|
|
518
|
+
const start = (): void => {
|
|
519
|
+
while (!failed && active.length < limit && next < list.length) {
|
|
520
|
+
const index = next;
|
|
521
|
+
next += 1;
|
|
522
|
+
try {
|
|
523
|
+
advance(index, (list[index] as () => Steps)(), undefined);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
settle(index, thrown(error, "a part"));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
try {
|
|
530
|
+
start();
|
|
531
|
+
while (active.length > 0 && !failed) {
|
|
532
|
+
if (active.length > 1) this.choices += 1;
|
|
533
|
+
const [item] = active.splice(this.pick(active.length), 1) as [Active];
|
|
534
|
+
const wait = this.waits.get(item.pending);
|
|
535
|
+
if (wait !== undefined) this.settled.push(wait.what);
|
|
536
|
+
const input: unknown = yield item.pending;
|
|
537
|
+
advance(item.index, item.steps, input);
|
|
538
|
+
start();
|
|
539
|
+
}
|
|
540
|
+
} finally {
|
|
541
|
+
// An item that stops gives up its wait, as in the kernel. The probe also stops the items of an `all` that stops.
|
|
542
|
+
for (const item of active) {
|
|
543
|
+
const wait = this.waits.get(item.pending);
|
|
544
|
+
if (wait !== undefined) wait.done = true;
|
|
545
|
+
try {
|
|
546
|
+
item.steps.return(ABORTED);
|
|
547
|
+
} catch {
|
|
548
|
+
// The finally code of a part threw. The part stops with the abort.
|
|
549
|
+
}
|
|
419
550
|
}
|
|
420
|
-
results.push(result);
|
|
421
|
-
if (options.failFast === true && !result.ok) failed = true;
|
|
422
551
|
}
|
|
552
|
+
if (failed) this.stopped = true;
|
|
553
|
+
for (let index = 0; index < list.length; index += 1) results[index] ??= ABORTED;
|
|
423
554
|
return results;
|
|
424
555
|
}
|
|
425
556
|
|
|
@@ -501,7 +632,7 @@ function typesNaming(world: World, name: string, verb: string): readonly TypeRef
|
|
|
501
632
|
}
|
|
502
633
|
|
|
503
634
|
/** This function gives the facts of the kind of a type. The hook `facts` of the machine wins over the contract. */
|
|
504
|
-
function contractOf(env:
|
|
635
|
+
function contractOf(env: BaseEnv, kind: Kind, typePath: readonly Key[], caps: readonly Cap[] | undefined): Contract {
|
|
505
636
|
const facts = env.machine.facts?.(typePath, env.name);
|
|
506
637
|
// A verb without a declaration keeps the effects of eval for the capabilities of the probes, as before D49.
|
|
507
638
|
// For eval, the effects of the answer of the hook win over the contract, as in check (D118).
|
|
@@ -511,6 +642,7 @@ function contractOf(env: Env, kind: Kind, typePath: readonly Key[], caps: readon
|
|
|
511
642
|
return {
|
|
512
643
|
declared,
|
|
513
644
|
pure: (facts?.pure ?? kind.pure) === true,
|
|
645
|
+
volatile: (facts?.volatile ?? kind.volatile) === true,
|
|
514
646
|
lazy: facts?.lazy ?? kind.lazy,
|
|
515
647
|
drives: env.verb === "eval" ? (facts?.drives ?? kind.drives) : undefined,
|
|
516
648
|
describes: env.verb === "eval",
|
|
@@ -735,7 +867,7 @@ function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sampl
|
|
|
735
867
|
const fn = under.fn;
|
|
736
868
|
return (shot) => {
|
|
737
869
|
const ctx = shot.context;
|
|
738
|
-
const parts: RunPart[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.steps(index, "evaluate", given ?? ctx) }));
|
|
870
|
+
const parts: RunPart[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.steps(index, "evaluate", given ?? ctx), handled: () => shot.steps(index, "evaluate", ctx) }));
|
|
739
871
|
const input: RunInput = {
|
|
740
872
|
...base,
|
|
741
873
|
value: fill(parts),
|
|
@@ -776,9 +908,9 @@ function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sampl
|
|
|
776
908
|
}
|
|
777
909
|
|
|
778
910
|
/** This function evaluates a probe document one time in a new shot. */
|
|
779
|
-
function shoot(env: Env, instance: Instance, sample: Sample, scenario: Scenario, more: { readonly context?: Context; readonly replay?: readonly Asked[] } = {}): Shot {
|
|
911
|
+
function shoot(env: Env, instance: Instance, sample: Sample, scenario: Scenario, more: { readonly context?: Context; readonly replay?: readonly Asked[]; readonly pick?: Pick } = {}): Shot {
|
|
780
912
|
const context = more.context ?? rootContext(env.machine, makeContext(env.names, scenario.caps));
|
|
781
|
-
const shot = new Shot(env.machine, env.runner, scenario, context, sample.split.parts.map((slot) => slot.key), sample.driven, env.perform, more.replay);
|
|
913
|
+
const shot = new Shot(env.machine, env.runner, scenario, context, sample.split.parts.map((slot) => slot.key), sample.driven, env.perform, more.replay, more.pick ?? FORWARD);
|
|
782
914
|
instance(shot);
|
|
783
915
|
return shot;
|
|
784
916
|
}
|
|
@@ -861,6 +993,49 @@ function differs(a: Shot, b: Shot, equal: Env["equal"], paths = true): string |
|
|
|
861
993
|
return undefined;
|
|
862
994
|
}
|
|
863
995
|
|
|
996
|
+
/** This function tells if two lists of effects hold the same effects. Under `all`, the order follows the host (X-RECORD). */
|
|
997
|
+
function sameEffects(a: readonly Caused[], b: readonly Caused[], equal: Env["equal"]): boolean {
|
|
998
|
+
const left = [...b];
|
|
999
|
+
return (
|
|
1000
|
+
a.length === b.length &&
|
|
1001
|
+
a.every((effect) => {
|
|
1002
|
+
const at = left.findIndex((other) => other.kind === effect.kind && sameValue(effect.payload, other.payload, equal));
|
|
1003
|
+
if (at >= 0) left.splice(at, 1);
|
|
1004
|
+
return at >= 0;
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* This function gives the difference of two shots of one probe in two orders of the waits, or `undefined` when they
|
|
1011
|
+
* agree (D128). Two orders agree when they give the same results, use the same parts and cause the same effects. The
|
|
1012
|
+
* order of the parts and of the effects does not count, because it follows the host. The resolutions do not count, as
|
|
1013
|
+
* in the simulation of the plan.
|
|
1014
|
+
*/
|
|
1015
|
+
function differsByOrder(a: Shot, b: Shot, equal: Env["equal"]): string | undefined {
|
|
1016
|
+
if (!sameOutcome(a.result, b.result, equal)) return `The first order gives ${resultText(a.result)}, and the second gives ${resultText(b.result)}.`;
|
|
1017
|
+
if (!sameResults(a, b, equal)) return "The function that the result gives returns another result.";
|
|
1018
|
+
const parts = (shot: Shot): string =>
|
|
1019
|
+
shot.calls
|
|
1020
|
+
.map((call) => `${call.later ? "later " : ""}${shot.keys[call.part] ?? String(call.part)}`)
|
|
1021
|
+
.sort()
|
|
1022
|
+
.join(", ");
|
|
1023
|
+
if (parts(a) !== parts(b)) return `The parts that evaluate differ: [${parts(a)}] and [${parts(b)}].`;
|
|
1024
|
+
if (sameEffects(a.caused, b.caused, equal)) return undefined;
|
|
1025
|
+
const effects = (shot: Shot): string => shot.caused.map((effect) => `${effect.kind} ${valueText(effect.payload)}`).join(", ");
|
|
1026
|
+
return `The effects differ: [${effects(a)}] and [${effects(b)}].`;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* This function gives the seed of the orders of a probe document (FNV-1a of its JSON). No time goes into it (D128).
|
|
1031
|
+
* The home of a hash for seeds is `@mark1russell7/rigor-generators`. This copy stays here because core has no dependencies.
|
|
1032
|
+
*/
|
|
1033
|
+
function orderSeed(doc: Envelope, seed: number): number {
|
|
1034
|
+
let hash = (2166136261 ^ seed) >>> 0;
|
|
1035
|
+
for (const unit of JSON.stringify(doc)) hash = Math.imul(hash ^ (unit.codePointAt(0) as number), 16777619) >>> 0;
|
|
1036
|
+
return hash;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
864
1039
|
/** This function gives the probe of a finding. */
|
|
865
1040
|
function probeOf(sample: Sample, scenario: Scenario, doc: Envelope = sample.doc): ConformProbe {
|
|
866
1041
|
const results: Record<string, Result> = {};
|
|
@@ -870,7 +1045,7 @@ function probeOf(sample: Sample, scenario: Scenario, doc: Envelope = sample.doc)
|
|
|
870
1045
|
}
|
|
871
1046
|
|
|
872
1047
|
/** This function keeps the first finding of each obligation. */
|
|
873
|
-
function report(env:
|
|
1048
|
+
function report(env: BaseEnv, obligation: Obligation, reason: string, probe: () => ConformProbe): void {
|
|
874
1049
|
if (!env.found.has(obligation)) env.found.set(obligation, { obligation, reason, probe: probe() });
|
|
875
1050
|
}
|
|
876
1051
|
|
|
@@ -1036,7 +1211,24 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
|
|
|
1036
1211
|
|
|
1037
1212
|
// C6 and the other checks of one shot in a run: each probe part can wait.
|
|
1038
1213
|
if (env.runner) {
|
|
1039
|
-
|
|
1214
|
+
const seeded = random(orderSeed(sample.doc, env.seed));
|
|
1215
|
+
const pickSeeded: Pick = (waiting) => seeded.int(waiting);
|
|
1216
|
+
for (const scenario of scenariosOf(env, sample, contract, true)) {
|
|
1217
|
+
const forward = shoot(env, code, sample, scenario);
|
|
1218
|
+
checkShot(env, sample, contract, scenario, forward);
|
|
1219
|
+
// parallel: a shot with a choice runs again in the other orders (D128). Each order also gets the checks of one shot.
|
|
1220
|
+
if (forward.choices === 0) continue;
|
|
1221
|
+
for (const pick of [BACKWARD, ...Array.from({ length: SEEDED_ORDERS }, () => pickSeeded)]) {
|
|
1222
|
+
const other = shoot(env, code, sample, scenario, { pick });
|
|
1223
|
+
checkShot(env, sample, contract, scenario, other);
|
|
1224
|
+
// The first failure with failFast stops other items in another order. That difference is the rule ALL.
|
|
1225
|
+
if (forward.stopped || other.stopped) continue;
|
|
1226
|
+
const why = differsByOrder(forward, other, env.equal);
|
|
1227
|
+
if (why === undefined) continue;
|
|
1228
|
+
const orders = `the order [${forward.settled.join(", ")}] and the order [${other.settled.join(", ")}]`;
|
|
1229
|
+
report(env, "parallel", `The code of ${sample.type} depends on the order of its parallel parts. The waits of all settle in ${orders}. ${why}`, () => probeOf(sample, scenario));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1040
1232
|
}
|
|
1041
1233
|
}
|
|
1042
1234
|
|
|
@@ -1090,6 +1282,396 @@ function checkCovers(env: Env, probe: (value: unknown) => ConformProbe): void {
|
|
|
1090
1282
|
);
|
|
1091
1283
|
}
|
|
1092
1284
|
|
|
1285
|
+
/** The code of the verb `step` or `deref` that `conforms` examines (D137). */
|
|
1286
|
+
type StepCode = { readonly verb: "step"; readonly code: Walker } | { readonly verb: "deref"; readonly code: DerefCode };
|
|
1287
|
+
|
|
1288
|
+
/** The keys of each probe of the code of a `deref`. The keys of a record value come after them. */
|
|
1289
|
+
const STEP_KEYS: readonly Key[] = ["a", "x", "length", 0, "missing"];
|
|
1290
|
+
|
|
1291
|
+
/** The most lists of arguments that one signature gets. */
|
|
1292
|
+
const MAX_ARGUMENTS = 64;
|
|
1293
|
+
|
|
1294
|
+
/** One step of a walk through the input: the key, and what the input gave. */
|
|
1295
|
+
interface Stepped {
|
|
1296
|
+
readonly key: Key;
|
|
1297
|
+
readonly thing: Thing;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/** The record of the reads of the probe nodes. A read while `open` is `false` skips the input of the walker. */
|
|
1301
|
+
interface Gate {
|
|
1302
|
+
open: boolean;
|
|
1303
|
+
direct: Key[];
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** The record of one call of a walker. */
|
|
1307
|
+
interface Walked {
|
|
1308
|
+
/** The result of the walk, as the rule R-WALKER reads it. A throw gives its error. */
|
|
1309
|
+
readonly outcome: Result;
|
|
1310
|
+
/** The text of a throw or of a wrong form. */
|
|
1311
|
+
readonly problem: string | undefined;
|
|
1312
|
+
readonly steps: readonly Stepped[];
|
|
1313
|
+
/** The key of each `visit` that names no step of the input. */
|
|
1314
|
+
readonly stray: readonly Key[];
|
|
1315
|
+
readonly direct: readonly Key[];
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/** One probe of a walker, and its first walk. */
|
|
1319
|
+
interface Trial {
|
|
1320
|
+
readonly input: { readonly data: Json; readonly here: Thing; readonly context: Context };
|
|
1321
|
+
readonly probe: () => ConformProbe;
|
|
1322
|
+
readonly first: Walked;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/** This function makes a host thing whose fields are probe values. The gate gets each read outside the input of a walker. */
|
|
1326
|
+
function probeNode(gate: Gate, label: string, type: string | undefined, fields: Readonly<Record<string, Thing>>): HostThing {
|
|
1327
|
+
const read = (key: Key): boolean => {
|
|
1328
|
+
if (!gate.open) gate.direct.push(key);
|
|
1329
|
+
return Object.hasOwn(fields, String(key));
|
|
1330
|
+
};
|
|
1331
|
+
const node: HostThing & { readonly type: string | undefined } = { [THING]: true, label, type, deref: (key) => (read(key) ? fields[String(key)] : undefined), has: read, toJson: () => ({ node: label }) };
|
|
1332
|
+
return node;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/** This function gives the things before a step: a leaf and its two parents, a document, `undefined` and the values of the results. */
|
|
1336
|
+
function probeThings(gate: Gate, results: readonly Result[]): readonly Thing[] {
|
|
1337
|
+
const root = probeNode(gate, "the root", undefined, { a: 1 });
|
|
1338
|
+
const middle = probeNode(gate, "the middle", "panel", { parent: root, names: { a: "named" } });
|
|
1339
|
+
const leaf = probeNode(gate, "the leaf", "box", { parent: middle, x: undefined });
|
|
1340
|
+
return [leaf, middle, root, { type: "probe", value: { a: 1 } }, undefined, ...results.flatMap((result) => (result.ok ? [result.value] : []))];
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** This function gives a context for a walker. A frozen context refuses a write to its capabilities, its names and its roots. */
|
|
1344
|
+
function stepContext(machine: Machine, ctx: Context): Context {
|
|
1345
|
+
return Object.freeze({ ...rootContext(machine, ctx), names: Object.freeze({ ...ctx.names }), caps: Object.freeze([...ctx.caps]) });
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** This function calls host code. A `Failure` gives its error (E-THROW). Each other throw also gives the text of the throw. */
|
|
1349
|
+
function attempt<T>(call: () => T, who: string): { readonly result: Result<T>; readonly threw: string | undefined } {
|
|
1350
|
+
try {
|
|
1351
|
+
return { result: ok(call()), threw: undefined };
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
return { result: thrown(error, who), threw: error instanceof Failure ? undefined : describe(error) };
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function isObject(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
1358
|
+
return typeof value === "object" && value !== null;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function isErrorCode(value: unknown): boolean {
|
|
1362
|
+
return typeof value === "string" && value.startsWith("#");
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function isEvalError(value: unknown): value is EvalError {
|
|
1366
|
+
return isObject(value) && isErrorCode(value["code"]) && typeof value["reason"] === "string";
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function isResult(value: unknown): value is Result {
|
|
1370
|
+
return isObject(value) && (value["ok"] === true ? Object.hasOwn(value, "value") : value["ok"] === false && isEvalError(value["error"]));
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function isWalkResult(value: unknown): value is WalkResult {
|
|
1374
|
+
if (!isObject(value)) return false;
|
|
1375
|
+
if (value["ok"] === true) return Object.hasOwn(value, "thing");
|
|
1376
|
+
return value["ok"] === false && typeof value["reason"] === "string" && (value["code"] === undefined || isErrorCode(value["code"]));
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** This function starts a walker one time, with `visit` when `hops` is `true`. */
|
|
1380
|
+
function walkOnce(env: BaseEnv, walker: Walker, gate: Gate, input: Trial["input"], hops: boolean): Walked {
|
|
1381
|
+
const steps: Stepped[] = [];
|
|
1382
|
+
const stray: Key[] = [];
|
|
1383
|
+
const direct: Key[] = [];
|
|
1384
|
+
gate.direct = direct;
|
|
1385
|
+
const adopt = env.machine.adopt ?? ((value: unknown): Thing => value);
|
|
1386
|
+
const ladder = (thing: Thing, key: Key): ReturnType<typeof derefStep> => {
|
|
1387
|
+
gate.open = true;
|
|
1388
|
+
try {
|
|
1389
|
+
return derefStep(env.machine, thing, key);
|
|
1390
|
+
} finally {
|
|
1391
|
+
gate.open = false;
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
const visit = (key: Key, thing: Thing): void => {
|
|
1395
|
+
if (!steps.some((step) => String(step.key) === String(key) && Object.is(step.thing, thing))) stray.push(key);
|
|
1396
|
+
};
|
|
1397
|
+
const deref = (thing: Thing, key: Key): Thing => {
|
|
1398
|
+
const reached = ladder(thing, key);
|
|
1399
|
+
const given = reached === undefined ? undefined : adopt(reached.thing);
|
|
1400
|
+
steps.push({ key, thing: given });
|
|
1401
|
+
return given;
|
|
1402
|
+
};
|
|
1403
|
+
const has = (thing: Thing, key: Key): boolean => ladder(thing, key) !== undefined;
|
|
1404
|
+
const tried = attempt(() => walker({ ...input, deref, has, ...(hops ? { visit } : {}) }), `the walker ${env.name}`);
|
|
1405
|
+
const close = (outcome: Result, problem: string | undefined): Walked => ({ outcome, problem, steps, stray, direct });
|
|
1406
|
+
if (!tried.result.ok) return close(tried.result, tried.threw === undefined ? undefined : `throws: ${tried.threw}`);
|
|
1407
|
+
const given: unknown = tried.result.value;
|
|
1408
|
+
if (!isWalkResult(given)) return close(fail("#CALC!", "The walker gives no result."), `gives ${valueText(given)}, and a walker gives a thing or the reason of a failure,`);
|
|
1409
|
+
return close(given.ok ? ok(given.thing) : fail(given.code ?? "#N/A", given.reason), undefined);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** This function gives the difference of two walks with one input, or `undefined` when they agree. */
|
|
1413
|
+
function walkDiffers(a: Walked, b: Walked, equal: BaseEnv["equal"]): string | undefined {
|
|
1414
|
+
if (!sameOutcome(a.outcome, b.outcome, equal)) return `The first walk gives ${resultText(a.outcome)}, and the second gives ${resultText(b.outcome)}.`;
|
|
1415
|
+
const keys = (walked: Walked): string => walked.steps.map((step) => String(step.key)).join(", ");
|
|
1416
|
+
return keys(a) === keys(b) ? undefined : `The steps differ: [${keys(a)}] and [${keys(b)}].`;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function thingText(thing: Thing): string {
|
|
1420
|
+
return isHostThing(thing) ? thing.label : valueText(thing);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** This function gives the value type of a thing for the rule of a walker. A JSON value has its plain type, and each other thing has `unknown`. */
|
|
1424
|
+
function plainType(thing: Thing): ValueType {
|
|
1425
|
+
if (isHostThing(thing)) return "unknown";
|
|
1426
|
+
if (typeof thing === "number") return "number";
|
|
1427
|
+
if (typeof thing === "boolean") return "bool";
|
|
1428
|
+
if (typeof thing === "string") return "text";
|
|
1429
|
+
if (thing === null || thing === undefined) return String(thing);
|
|
1430
|
+
if (Array.isArray(thing)) return { list: "unknown" };
|
|
1431
|
+
if (!isRecord(thing)) return "unknown";
|
|
1432
|
+
return { record: Object.fromEntries(Object.keys(thing).map((key) => [key, plainType(thing[key])])) };
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/** This function checks the type rule of a walker on one walk, from the type `unknown` and from the plain type of the thing (TYPE.WALKER). */
|
|
1436
|
+
function checkWalkType(env: BaseEnv, walker: Walker, sample: Sample, data: Json, trial: Trial): void {
|
|
1437
|
+
const rule = walker.type;
|
|
1438
|
+
const outcome = trial.first.outcome;
|
|
1439
|
+
// A step document with parts has no rule, and a path follows a reference that the walker reaches.
|
|
1440
|
+
if (rule === undefined || sample.split.parts.length > 0 || !outcome.ok || isRef(outcome.value)) return;
|
|
1441
|
+
const domains = env.machine.domains;
|
|
1442
|
+
for (const type of ["unknown", plainType(trial.input.here)]) {
|
|
1443
|
+
const reached = attempt(() => rule({ data, here: type }), "the type rule").result;
|
|
1444
|
+
// A rule that throws, or a bad type, is a type issue of the check (TYPE.RULE).
|
|
1445
|
+
if (!reached.ok || typeIssues(reached.value, domains).length > 0) continue;
|
|
1446
|
+
const value = valueOfThing(outcome.value);
|
|
1447
|
+
if (member(value, reached.value, domains)) continue;
|
|
1448
|
+
report(env, "typed", `The walker ${env.name} reaches ${valueText(value)} from ${thingText(trial.input.here)} of the type ${typeText(type)}, and its type rule gives ${typeText(reached.value)}.`, trial.probe);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/** This function gives the probe of a finding of a walker or of the code of a `deref`. */
|
|
1454
|
+
function stepProbe(sample: Sample, caps: readonly Cap[], at: { readonly here: unknown } | { readonly key: Key }): ConformProbe {
|
|
1455
|
+
return { type: sample.type, doc: sample.doc, results: {}, waits: [], caps, running: false, ...at };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/** This function examines one walker on one step document, from each thing. */
|
|
1459
|
+
function examineWalker(env: BaseEnv, walker: Walker, gate: Gate, things: readonly Thing[], sample: Sample, contract: Contract): void {
|
|
1460
|
+
// A step document with parts gets the values of its parts (FORMULA.STEPS).
|
|
1461
|
+
const { parts, fill } = sample.split;
|
|
1462
|
+
const data = (parts.length === 0 ? sample.doc.value : fill(parts.map(() => 1))) as Json;
|
|
1463
|
+
const trials = things.map((here, index): Trial => {
|
|
1464
|
+
const input = { data, here, context: stepContext(env.machine, makeContext(env.names, index % 2 === 0 ? contract.caps : NO_CAPS)) };
|
|
1465
|
+
return { input, probe: () => stepProbe(sample, input.context.caps, { here }), first: walkOnce(env, walker, gate, input, true) };
|
|
1466
|
+
});
|
|
1467
|
+
for (const trial of trials) {
|
|
1468
|
+
const { first, probe } = trial;
|
|
1469
|
+
const here = thingText(trial.input.here);
|
|
1470
|
+
if (first.problem !== undefined) report(env, "result", `The walker ${env.name} ${first.problem} on the step ${sample.type} from ${here}.`, probe);
|
|
1471
|
+
const key = first.direct[0];
|
|
1472
|
+
if (key !== undefined) report(env, "ladder", `The walker ${env.name} reads the key ${String(key)} of ${here} directly. A walker steps through deref and has of its input.`, probe);
|
|
1473
|
+
const lost = first.stray[0];
|
|
1474
|
+
if (lost !== undefined) report(env, "hops", `The walker ${env.name} gives a visit for the key ${String(lost)} from ${here}, and no step of its deref gave that thing.`, probe);
|
|
1475
|
+
checkWalkType(env, walker, sample, data, trial);
|
|
1476
|
+
}
|
|
1477
|
+
// A volatile kind declares that its results change (FACTS.VOLATILE).
|
|
1478
|
+
if (contract.volatile) return;
|
|
1479
|
+
const compare = (other: (trial: Trial) => Walked, obligation: Obligation, reason: (why: string) => string): boolean => {
|
|
1480
|
+
let clean = true;
|
|
1481
|
+
for (const trial of trials) {
|
|
1482
|
+
const why = walkDiffers(trial.first, other(trial), env.equal);
|
|
1483
|
+
if (why === undefined) continue;
|
|
1484
|
+
clean = false;
|
|
1485
|
+
report(env, obligation, reason(why), trial.probe);
|
|
1486
|
+
}
|
|
1487
|
+
return clean;
|
|
1488
|
+
};
|
|
1489
|
+
// local: the same input gives the same walk in the other order of the probes.
|
|
1490
|
+
const later = new Map([...trials].reverse().map((trial) => [trial, walkOnce(env, walker, gate, trial.input, true)] as const));
|
|
1491
|
+
const stable = compare((trial) => later.get(trial) as Walked, "local", (why) => `The walker ${env.name} keeps state from one call to the next. The same input gives another walk in a later call. ${why}`);
|
|
1492
|
+
// The other comparisons need code without state, because each of them starts the walk again.
|
|
1493
|
+
if (!stable) return;
|
|
1494
|
+
// hops: a resolution without hops gives no visit (DEREF.HOPS).
|
|
1495
|
+
compare((trial) => walkOnce(env, walker, gate, trial.input, false), "hops", (why) => `The walker ${env.name} walks in another way without visit, and a resolution without hops gives no visit. ${why}`);
|
|
1496
|
+
// C4: the marks of a contract describe the walker of a step document too (FACTS.VERB).
|
|
1497
|
+
if (!contract.pure) return;
|
|
1498
|
+
const varied = (trial: Trial): Walked => walkOnce(env, walker, gate, { ...trial.input, context: stepContext(env.machine, variedContext(trial.input.context, [])) }, true);
|
|
1499
|
+
compare(varied, "C4", (why) => `The kind ${sample.type} is pure, and the walker ${env.name} reads the context. Other values in the roots change the walk. ${why}`);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/** This function examines the code of a `deref` on one document, for each key. */
|
|
1503
|
+
function examineDeref(env: BaseEnv, code: DerefCode, sample: Sample, contract: Contract): void {
|
|
1504
|
+
const doc = sample.doc;
|
|
1505
|
+
const keys = [...STEP_KEYS, ...(isRecord(doc.value) ? Object.keys(doc.value) : [])];
|
|
1506
|
+
const who = `the deref ${env.name}`;
|
|
1507
|
+
const ask = (key: Key): { readonly key: Key; readonly reached: Result; readonly has: Result; readonly threw: string | undefined } => {
|
|
1508
|
+
const reached = attempt(() => code(doc, key), who);
|
|
1509
|
+
const has = attempt(() => code.has?.(doc, key), `the function has of ${who}`);
|
|
1510
|
+
return { key, reached: reached.result, has: has.result, threw: reached.threw ?? has.threw };
|
|
1511
|
+
};
|
|
1512
|
+
const first = keys.map(ask);
|
|
1513
|
+
for (const { key, reached, has, threw } of first) {
|
|
1514
|
+
const probe = (): ConformProbe => stepProbe(sample, NO_CAPS, { key });
|
|
1515
|
+
if (threw !== undefined) report(env, "result", `The deref ${env.name} throws for the key ${String(key)} of ${sample.type}: ${threw}`, probe);
|
|
1516
|
+
if (code.has === undefined || !has.ok) continue;
|
|
1517
|
+
if (typeof has.value !== "boolean") report(env, "has", `The function has of the deref ${env.name} gives ${valueText(has.value)} for the key ${String(key)}. It gives true or false.`, probe);
|
|
1518
|
+
else if (reached.ok && reached.value !== undefined && !has.value) report(env, "has", `The deref ${env.name} answers the key ${String(key)} of ${sample.type}, and its function has gives false.`, probe);
|
|
1519
|
+
}
|
|
1520
|
+
if (contract.volatile) return;
|
|
1521
|
+
// local: the same document and key give the same answer in the other order of the probes.
|
|
1522
|
+
for (const before of [...first].reverse()) {
|
|
1523
|
+
const again = ask(before.key);
|
|
1524
|
+
if (sameOutcome(before.reached, again.reached, env.equal) && sameOutcome(before.has, again.has, env.equal)) continue;
|
|
1525
|
+
report(env, "local", `The deref ${env.name} keeps state from one call to the next. The key ${String(before.key)} gives ${resultText(before.reached)}, and later ${resultText(again.reached)}.`, () => stepProbe(sample, NO_CAPS, { key: before.key }));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/** This function gives the argument lists of a signature: the members of each parameter, and one more argument for a rest parameter. */
|
|
1530
|
+
function argumentsOf(types: readonly ValueType[], domains: Machine["domains"], palette: readonly unknown[]): readonly (readonly unknown[])[] {
|
|
1531
|
+
const lists = types.map((type) => membersOf(type, domains, palette));
|
|
1532
|
+
const count = Math.min(
|
|
1533
|
+
MAX_ARGUMENTS,
|
|
1534
|
+
lists.reduce((product, list) => product * list.length, 1),
|
|
1535
|
+
);
|
|
1536
|
+
return Array.from({ length: count }, (_unused, index) => {
|
|
1537
|
+
let rest = index;
|
|
1538
|
+
return lists.map((list) => {
|
|
1539
|
+
const item = list[rest % list.length];
|
|
1540
|
+
rest = Math.floor(rest / list.length);
|
|
1541
|
+
return item;
|
|
1542
|
+
});
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/** This function gives the names of the operations of a profile that its tables list. */
|
|
1547
|
+
function opNames(profile: FormulaProfile): readonly string[] {
|
|
1548
|
+
const { ops, lazy, sigs } = profile;
|
|
1549
|
+
return [...new Set([ops, lazy, sigs].flatMap((table) => (table === undefined || typeof table === "function" ? [] : Object.keys(table))))];
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/** This function gives the document of a probe of an operation, with the arguments as the results of its parts. */
|
|
1553
|
+
function opProbe(name: string, args: readonly unknown[]): ConformProbe {
|
|
1554
|
+
const results: Record<string, Result> = {};
|
|
1555
|
+
args.forEach((value, index) => setField(results, String(index), ok(value)));
|
|
1556
|
+
const value: Json = args.map((_unused, index) => ({ type: "probe", value: { probe: index } }));
|
|
1557
|
+
return { type: `ops.${name}`, doc: { type: ["ops", ...name.split(".")], value }, results, waits: [], caps: NO_CAPS, running: false };
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/** This function tests that each operation keeps its signatures, with the hook `check` of the profile (TYPE.SOUND). */
|
|
1561
|
+
function checkSignatures(env: Env, profile: FormulaProfile, palette: readonly unknown[]): void {
|
|
1562
|
+
const domains = env.machine.domains;
|
|
1563
|
+
const { ops, lazy, check } = profile;
|
|
1564
|
+
const answer = (name: string, args: readonly unknown[]): Result | undefined => {
|
|
1565
|
+
const delayed = own(lazy, name);
|
|
1566
|
+
if (delayed !== undefined) return delayed(args.map((value) => () => ok(value)));
|
|
1567
|
+
const fn = (typeof ops === "function" ? ops(name) : own(ops, name)) as ((...values: readonly unknown[]) => unknown) | undefined;
|
|
1568
|
+
if (fn === undefined) return undefined;
|
|
1569
|
+
const out = fn(...args);
|
|
1570
|
+
return check?.(out, name) ?? ok(out);
|
|
1571
|
+
};
|
|
1572
|
+
for (const name of opNames(profile)) {
|
|
1573
|
+
for (const sig of signaturesOf(profile, name)) {
|
|
1574
|
+
const types = [...sig.params, ...(sig.rest === undefined ? [] : [sig.rest])];
|
|
1575
|
+
// A bad type is a type issue of the check.
|
|
1576
|
+
if ([...types, sig.to].some((type) => typeIssues(type, domains).length > 0)) continue;
|
|
1577
|
+
for (const args of argumentsOf(types, domains, palette)) {
|
|
1578
|
+
// A throw and an error are the errors of the language (E-THROW). A signature describes only a value.
|
|
1579
|
+
const out = attempt(() => answer(name, args), `the operation ${name}`).result;
|
|
1580
|
+
const given = out.ok ? out.value : undefined;
|
|
1581
|
+
if (given === undefined || !given.ok || member(given.value, sig.to, domains)) continue;
|
|
1582
|
+
report(env, "typed", `The operation ${name} gives ${valueText(given.value)} for the arguments ${valueText(args)}, and its signature gives ${typeText(sig.to)}.`, () => opProbe(name, args));
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/** This function tests that each extension gives a result, and a member of its type in `extTypes`. */
|
|
1590
|
+
function checkExtensions(env: Env, profile: FormulaProfile, options: ConformOptions): void {
|
|
1591
|
+
const domains = env.machine.domains;
|
|
1592
|
+
const context = rootContext(env.machine, makeContext(env.names));
|
|
1593
|
+
for (const name of Object.keys(profile.ext ?? {})) {
|
|
1594
|
+
const fn = own(profile.ext, name);
|
|
1595
|
+
const declared = own(profile.extTypes, name);
|
|
1596
|
+
const type = declared !== undefined && typeIssues(declared, domains).length === 0 ? declared : undefined;
|
|
1597
|
+
for (const data of [...DATA, ...(own(options.values, `ext.${name}`) ?? [])]) {
|
|
1598
|
+
const probe = (): ConformProbe => ({ type: `ext.${name}`, doc: { type: ["ext", ...name.split(".")], value: data }, results: {}, waits: [], caps: NO_CAPS, running: false });
|
|
1599
|
+
const given = attempt(() => fn?.(data, context, domains) as unknown, `the extension ${name}`).result;
|
|
1600
|
+
if (!given.ok) continue;
|
|
1601
|
+
if (!isResult(given.value)) report(env, "result", `The extension ${name} gives ${valueText(given.value)}. It gives a result.`, probe);
|
|
1602
|
+
else if (type !== undefined && given.value.ok && !member(given.value.value, type, domains)) report(env, "typed", `The extension ${name} gives ${valueText(given.value.value)}, and its entry in extTypes gives ${typeText(type)}.`, probe);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* This function tests the functions of a profile that the evaluator reads, by the forms of their types (D137). A throw
|
|
1609
|
+
* gets no finding here, because the kit gives it an error (E-THROW). Only `truthy` has a stated law against a throw.
|
|
1610
|
+
*/
|
|
1611
|
+
function checkProfile(env: Env, reader: ProfileReader, options: ConformOptions, probe: (value: unknown, result?: Result) => ConformProbe): void {
|
|
1612
|
+
const { profile } = reader;
|
|
1613
|
+
const { missing, omit, combine, check, lazy } = profile;
|
|
1614
|
+
const values = env.results.flatMap((result) => (result.ok ? [result.value] : []));
|
|
1615
|
+
const errors = env.results.flatMap((result) => (result.ok ? [] : [result.error]));
|
|
1616
|
+
const form = (what: string, call: () => unknown, good: (value: unknown) => boolean, want: string, at: () => ConformProbe): void => {
|
|
1617
|
+
const given = attempt(call, what).result;
|
|
1618
|
+
if (given.ok && !good(given.value)) report(env, "result", `The function ${what} of the profile gives ${valueText(given.value)}. It gives ${want}.`, at);
|
|
1619
|
+
};
|
|
1620
|
+
const isBool = (value: unknown): boolean => typeof value === "boolean";
|
|
1621
|
+
if (reader.name === "formula.ref" && missing !== undefined) for (const value of values) form("missing", () => missing(value), isBool, "true or false", () => probe(value));
|
|
1622
|
+
if (reader.name === "formula.record" && omit !== undefined) for (const error of errors) form("omit", () => omit(error), isBool, "true or false", () => probe(error, { ok: false, error }));
|
|
1623
|
+
if (reader.name === "formula.ext") checkExtensions(env, profile, options);
|
|
1624
|
+
if (reader.name !== "formula.op") return;
|
|
1625
|
+
checkSignatures(env, profile, values);
|
|
1626
|
+
if (combine !== undefined && errors.length > 0) form("combine", () => combine([...errors, ...errors]), isEvalError, "an error with a code and a reason", () => probe(errors[0], { ok: false, error: errors[0] as EvalError }));
|
|
1627
|
+
for (const name of Object.keys(lazy ?? {})) {
|
|
1628
|
+
const fn = own(lazy, name);
|
|
1629
|
+
for (const value of values) form(`lazy ${name}`, () => fn?.([() => ok(value), () => ok(value)]), isResult, "a result", () => opProbe(name, [value, value]));
|
|
1630
|
+
}
|
|
1631
|
+
if (check !== undefined) for (const value of values) form("check", () => check(value, "probe"), (given) => given === undefined || isResult(given), "a result or no value", () => probe(value));
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/** This function gives the types of the operations or the extensions of a profile that the evaluator serves (D137). */
|
|
1635
|
+
function profileTypes(machine: Machine, name: string, verb: string, reader: ProfileReader | undefined): readonly TypeRef[] {
|
|
1636
|
+
if (reader === undefined || (reader.name !== "formula.op" && reader.name !== "formula.ext")) return [];
|
|
1637
|
+
const { profile } = reader;
|
|
1638
|
+
const extensions = reader.name === "formula.ext";
|
|
1639
|
+
const names = extensions ? Object.keys(profile.ext ?? {}) : opNames(profile);
|
|
1640
|
+
const paths = names.filter((item) => profile.volatile?.includes(item) !== true).map((item) => [extensions ? "ext" : "ops", ...item.split(".")]);
|
|
1641
|
+
return paths.filter((path) => {
|
|
1642
|
+
const found = lookupKind(machine.world, path);
|
|
1643
|
+
return found.ok && own(found.kind.verbs, verb) === name;
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/** This function gives the code that the name and the verb select: a walker or the code of a `deref`. The default is `undefined`. */
|
|
1648
|
+
function stepCodeOf(machine: Machine, name: string, verb: string | undefined): StepCode | undefined {
|
|
1649
|
+
const walker = own(machine.walkers, name);
|
|
1650
|
+
const code = own(machine.derefs, name);
|
|
1651
|
+
if (verb === "step" && walker !== undefined) return { verb, code: walker };
|
|
1652
|
+
if (verb === "deref" && code !== undefined) return { verb, code };
|
|
1653
|
+
if (verb !== undefined || own(machine.evaluators, name) !== undefined || own(machine.runners, name) !== undefined) return undefined;
|
|
1654
|
+
if (walker !== undefined) return { verb: "step", code: walker };
|
|
1655
|
+
return code === undefined ? undefined : { verb: "deref", code };
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/** This function gives each probe document of each type, with the contract of its kind. */
|
|
1659
|
+
function eachSample(env: BaseEnv, types: readonly TypeRef[], options: ConformOptions, visit: (sample: Sample, contract: Contract) => void): void {
|
|
1660
|
+
if (types.length === 0) throw new Error(`No kind of the world names ${env.name} for the verb ${env.verb}. Give the types of the probe documents in the option types.`);
|
|
1661
|
+
for (const type of types) {
|
|
1662
|
+
const found = lookupKind(env.machine.world, type);
|
|
1663
|
+
if (!found.ok) throw new Error(`The world has no kind for the type ${typeName(type)}.`);
|
|
1664
|
+
const contract = contractOf(env, found.kind, found.path, options.caps);
|
|
1665
|
+
const extra = own(options.values, typeName(type)) ?? own(options.values, found.path.join(".")) ?? [];
|
|
1666
|
+
for (const sample of samplesOf(type, found.path, found.kind, contract, extra)) visit(sample, contract);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/** This function gives the findings of the state of `conforms`, in the order of the obligations. */
|
|
1671
|
+
function findingsOf(env: BaseEnv): readonly Finding[] {
|
|
1672
|
+
return ORDER.filter((obligation) => env.found.has(obligation)).map((obligation) => env.found.get(obligation) as Finding);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1093
1675
|
/**
|
|
1094
1676
|
* This function examines one evaluator or runner of a machine against the obligations of host code (K92, D36). It
|
|
1095
1677
|
* starts the code directly, without the kernel, with probe parts and a probe runtime. It generates probe documents from
|
|
@@ -1100,56 +1682,66 @@ function checkCovers(env: Env, probe: (value: unknown) => ConformProbe): void {
|
|
|
1100
1682
|
* compiled code with its definition. The finding of an obligation names the first probe that breaks it. An empty list
|
|
1101
1683
|
* means that each probe conforms.
|
|
1102
1684
|
*
|
|
1685
|
+
* The check `parallel` starts a runner with a choice of waits under `all` again, in other orders of the waits (D128).
|
|
1103
1686
|
* The check `truth` tests the declared truth of a formula profile (D86). The check `cover` tests the covers of the
|
|
1104
1687
|
* domains of the machine from samples (D94).
|
|
1105
1688
|
*
|
|
1689
|
+
* The name can also select a walker or the code of a `deref` (D137). The checks of a walker are `result`, `local`,
|
|
1690
|
+
* `ladder`, `hops`, `typed` and `C4`. The checks of the code of a `deref` are `result`, `local` and `has`. The evaluators
|
|
1691
|
+
* `formula.op` and `formula.ext` also examine each operation and each extension that the tables of their profile list.
|
|
1692
|
+
* The checks `typed` and `result` test the functions of the profile directly.
|
|
1693
|
+
*
|
|
1106
1694
|
* @param machine - The machine with the code and the world of the kinds.
|
|
1107
|
-
* @param name - The name of the
|
|
1695
|
+
* @param name - The name of the code. A runner wins over an evaluator of the same name, and each of them wins over a
|
|
1696
|
+
* walker or a `deref` without the option `verb`.
|
|
1108
1697
|
* @param options - The verb, the types and the values of the probe documents, and the results of the probe parts. Also
|
|
1109
1698
|
* the definitions, the capabilities, the names, the seed, the mock and the rule of equality.
|
|
1110
1699
|
* @returns The first finding of each obligation that the code breaks, in the order of the obligations.
|
|
1111
1700
|
* @throws {Error} The machine has no code with the name, or no kind names it and the options give no types.
|
|
1112
1701
|
*/
|
|
1113
1702
|
export function conforms(machine: Machine, name: string, options: ConformOptions = {}): readonly Finding[] {
|
|
1114
|
-
const
|
|
1703
|
+
const step = stepCodeOf(machine, name, options.verb);
|
|
1704
|
+
const verb = step?.verb ?? options.verb ?? "eval";
|
|
1705
|
+
const base: BaseEnv = { machine, name, verb, results: options.results ?? RESULTS, names: options.names ?? NAMES, equal: options.equal, found: new Map() };
|
|
1706
|
+
if (step !== undefined) {
|
|
1707
|
+
const gate: Gate = { open: false, direct: [] };
|
|
1708
|
+
const things = [...probeThings(gate, base.results), ...(options.things ?? [])];
|
|
1709
|
+
eachSample(base, options.types ?? typesNaming(machine.world, name, verb), options, (sample, contract) => {
|
|
1710
|
+
if (step.verb === "step") examineWalker(base, step.code, gate, things, sample, contract);
|
|
1711
|
+
else examineDeref(base, step.code, sample, contract);
|
|
1712
|
+
});
|
|
1713
|
+
return findingsOf(base);
|
|
1714
|
+
}
|
|
1115
1715
|
const runnerCode = own(machine.runners, name);
|
|
1116
1716
|
const code = runnerCode ?? own(machine.evaluators, name);
|
|
1117
|
-
if (code === undefined) throw new Error(`The machine has no evaluator and no runner with the name ${name}.`);
|
|
1717
|
+
if (code === undefined) throw new Error(`The machine has no evaluator and no runner with the name ${name}, and no walker and no deref.`);
|
|
1118
1718
|
const runner = runnerCode !== undefined;
|
|
1119
1719
|
const env: Env = {
|
|
1120
|
-
|
|
1121
|
-
name,
|
|
1122
|
-
verb,
|
|
1720
|
+
...base,
|
|
1123
1721
|
under: underOf(code, runner),
|
|
1124
1722
|
definition: definitionOf(machine, name, runner, options.definitions),
|
|
1125
1723
|
runner,
|
|
1126
|
-
results: options.results ?? RESULTS,
|
|
1127
|
-
names: options.names ?? NAMES,
|
|
1128
1724
|
runs: options.runs ?? 12,
|
|
1129
1725
|
random: random(options.seed ?? 1),
|
|
1726
|
+
seed: options.seed ?? 1,
|
|
1130
1727
|
perform: options.perform,
|
|
1131
|
-
equal: options.equal,
|
|
1132
|
-
found: new Map(),
|
|
1133
1728
|
};
|
|
1134
|
-
const
|
|
1729
|
+
const reader = profileReaderOf(code);
|
|
1730
|
+
const types = options.types ?? [...typesNaming(machine.world, name, verb), ...profileTypes(machine, name, verb, reader)];
|
|
1135
1731
|
let first: Sample | undefined;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const contract = contractOf(env, found.kind, found.path, options.caps);
|
|
1141
|
-
const extra = own(options.values, typeName(type)) ?? own(options.values, found.path.join(".")) ?? [];
|
|
1142
|
-
for (const sample of samplesOf(type, found.path, found.kind, contract, extra)) {
|
|
1143
|
-
first ??= sample;
|
|
1144
|
-
examine(env, sample, contract);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1732
|
+
eachSample(env, types, options, (sample, contract) => {
|
|
1733
|
+
first ??= sample;
|
|
1734
|
+
examine(env, sample, contract);
|
|
1735
|
+
});
|
|
1147
1736
|
// A probe of the truth or of a cover gives the value to each part of the first probe document.
|
|
1148
|
-
const probeWith = (value: unknown): ConformProbe =>
|
|
1149
|
-
first
|
|
1737
|
+
const probeWith = (value: unknown, result: Result = ok(value)): ConformProbe => {
|
|
1738
|
+
const sample = first as Sample | undefined;
|
|
1739
|
+
return sample === undefined
|
|
1150
1740
|
? { type: typeName(types[0] as TypeRef), doc: { type: types[0] as TypeRef, value: null }, results: {}, waits: [], caps: NO_CAPS, running: runner }
|
|
1151
|
-
: probeOf(
|
|
1741
|
+
: probeOf(sample, { results: sample.split.parts.map(() => result), waits: [], caps: NO_CAPS, running: runner });
|
|
1742
|
+
};
|
|
1152
1743
|
checkTruth(env, code, probeWith);
|
|
1153
1744
|
checkCovers(env, probeWith);
|
|
1154
|
-
|
|
1745
|
+
if (reader !== undefined) checkProfile(env, reader, options, probeWith);
|
|
1746
|
+
return findingsOf(env);
|
|
1155
1747
|
}
|