@mark1russell7/coeval 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/build/index.d.ts +545 -233
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2619 -383
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/agree.ts +128 -16
- package/src/conform.ts +75 -5
- package/src/deref.ts +76 -11
- package/src/evaluate.ts +151 -21
- package/src/facts.ts +113 -78
- package/src/formula.ts +67 -11
- package/src/index.ts +23 -1
- package/src/merge.ts +49 -14
- package/src/reference.ts +110 -15
- package/src/replay.ts +1052 -18
- package/src/static.ts +2 -2
- package/src/stdlib.ts +55 -2
- package/src/syntax.ts +357 -52
- package/src/types.ts +520 -17
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/coeval",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.0",
|
|
5
5
|
"description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./build/index.js",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"build",
|
|
17
17
|
"src",
|
|
18
|
-
"!src/**/*.test.ts"
|
|
18
|
+
"!src/**/*.test.ts",
|
|
19
|
+
"!src/**/*.test-support.ts"
|
|
19
20
|
],
|
|
20
21
|
"sideEffects": false,
|
|
21
22
|
"license": "MIT",
|
package/src/agree.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type Key, docEquals, equalWith } from "./doc.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type Context, isHostThing } from "./deref.ts";
|
|
3
3
|
import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
|
|
4
|
-
import { type Effect, type
|
|
4
|
+
import { type Effect, type Machine, type Perform, type Read, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
|
|
5
5
|
import { Held, isClosure } from "./formula.ts";
|
|
6
6
|
import { type Definitions, reference, referenceRun } from "./reference.ts";
|
|
7
|
-
import { type ReplaySettle, recorder, replayer } from "./replay.ts";
|
|
7
|
+
import { type ReplaySettle, recorder, relay, replayer } from "./replay.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* A tier that `agree` compares with the plan.
|
|
@@ -91,7 +91,16 @@ const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.
|
|
|
91
91
|
|
|
92
92
|
/** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
|
|
93
93
|
export type Agreement =
|
|
94
|
-
| {
|
|
94
|
+
| {
|
|
95
|
+
readonly ok: true;
|
|
96
|
+
readonly result: Result;
|
|
97
|
+
readonly effects: readonly Effect[];
|
|
98
|
+
/**
|
|
99
|
+
* The free reads of the plan that `agreeRun` compared with the reference run (RUN.AGREES-READS). It is absent
|
|
100
|
+
* when no read compared, for example in `agree`.
|
|
101
|
+
*/
|
|
102
|
+
readonly reads?: readonly Read[];
|
|
103
|
+
}
|
|
95
104
|
| {
|
|
96
105
|
readonly ok: false;
|
|
97
106
|
readonly tier: Tier;
|
|
@@ -340,12 +349,9 @@ interface Watched {
|
|
|
340
349
|
function watched(perform: Perform): Watched {
|
|
341
350
|
const effects: Effect[] = [];
|
|
342
351
|
const settles: ReplaySettle[] = [];
|
|
343
|
-
const
|
|
344
|
-
const settled = (all: string, item: number): void => {
|
|
352
|
+
const journal = relay(perform, (all, item) => {
|
|
345
353
|
settles.push({ all, item });
|
|
346
|
-
|
|
347
|
-
};
|
|
348
|
-
const journal: Journal = inner?.next === undefined ? { settled } : { settled, next: (all, waiting) => inner.next?.(all, waiting) };
|
|
354
|
+
});
|
|
349
355
|
const host = (effect: Effect): unknown => {
|
|
350
356
|
effects.push(effect);
|
|
351
357
|
return perform(effect);
|
|
@@ -367,6 +373,87 @@ function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]):
|
|
|
367
373
|
return undefined;
|
|
368
374
|
}
|
|
369
375
|
|
|
376
|
+
/** The roots beside `self` that a value without static reads keeps whole (READS.KEPT). */
|
|
377
|
+
const KEPT_ROOTS: readonly string[] = /* @__PURE__ */ Object.freeze(["names", "world", "event", "caps"]);
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* This function tells if the plan does not know the reads of a run. A value without static reads keeps each root
|
|
381
|
+
* (READS.KEPT), thus the list holds a kept read of each whole root.
|
|
382
|
+
*/
|
|
383
|
+
function readsUnknown(reads: readonly Read[]): boolean {
|
|
384
|
+
return KEPT_ROOTS.every((root) => reads.some((read) => read.kept === true && read.path.length === 1 && read.path[0] === root));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* This function gives the reads that `agreeRun` compares: the free reads of the resolutions (RUN.AGREES-READS). A
|
|
389
|
+
* kept read is the bound of the plan for the later reads of a value, and the reference has no such bound.
|
|
390
|
+
*/
|
|
391
|
+
function resolutions(reads: readonly Read[]): readonly Read[] {
|
|
392
|
+
return reads.filter((read) => read.free && read.kept !== true);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function pathText(path: readonly unknown[] | undefined): string {
|
|
396
|
+
if (path === undefined) return "no read";
|
|
397
|
+
try {
|
|
398
|
+
return JSON.stringify(path);
|
|
399
|
+
} catch {
|
|
400
|
+
return path.map(String).join(", ");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* This function gives the rule of a comparison of reads. The rule of the caller answers first. Each tier makes its
|
|
406
|
+
* own nodes of a tree and its own adopted things. Thus two host things are the same when they have the same label,
|
|
407
|
+
* and they are nodes or have the same JSON object.
|
|
408
|
+
*/
|
|
409
|
+
function sameThing(equal: AgreeOptions["equal"]): (a: unknown, b: unknown) => boolean | undefined {
|
|
410
|
+
return (a, b) => {
|
|
411
|
+
const told = equal?.(a, b);
|
|
412
|
+
if (told !== undefined) return told;
|
|
413
|
+
if (!isHostThing(a) || !isHostThing(b)) return undefined;
|
|
414
|
+
if (a.label !== b.label) return false;
|
|
415
|
+
if (a.label.startsWith("node ")) return true;
|
|
416
|
+
try {
|
|
417
|
+
return Object.is(a.toJson(), b.toJson());
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** This function gives the difference of what two reads reached, or `undefined` when they agree. */
|
|
425
|
+
function reachedDiffers(a: Result, b: Result, equal: (a: unknown, b: unknown) => boolean | undefined, strict: boolean): string | undefined {
|
|
426
|
+
if (a.ok && b.ok) return sameValue(a.value, b.value, equal) ? undefined : "The read reaches another value in the tier.";
|
|
427
|
+
if (!a.ok && !b.ok) {
|
|
428
|
+
if (a.error.code !== b.error.code) return `The read gives ${a.error.code} in the plan, and ${b.error.code} in the tier.`;
|
|
429
|
+
return strict && a.error.reason !== b.error.reason ? `The read gives ${a.error.code} with another reason in the tier: "${b.error.reason}" for "${a.error.reason}".` : undefined;
|
|
430
|
+
}
|
|
431
|
+
if (!b.ok) return `The read reaches a value in the plan, and ${b.error.code} in the tier.`;
|
|
432
|
+
return `The read gives ${(a as { readonly error: EvalError }).error.code} in the plan, and a value in the tier.`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* This function gives the first difference of the reads of the plan and of a tier, or `undefined` when they agree
|
|
437
|
+
* (RUN.AGREES-READS). Only the free reads of the resolutions compare, in their order. When the plan does not know its
|
|
438
|
+
* reads, the function compares nothing.
|
|
439
|
+
*/
|
|
440
|
+
function readsDiffer(plan: readonly Read[], tier: readonly Read[], options: AgreeOptions): string | undefined {
|
|
441
|
+
if (readsUnknown(plan)) return undefined;
|
|
442
|
+
const a = resolutions(plan);
|
|
443
|
+
const b = resolutions(tier);
|
|
444
|
+
const equal = sameThing(options.equal);
|
|
445
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
446
|
+
const x = a[index];
|
|
447
|
+
const y = b[index];
|
|
448
|
+
if (x === undefined || y === undefined || !sameValue(x.path, y.path, equal)) {
|
|
449
|
+
return `The free read ${index} is ${pathText(x?.path)} in the plan, and ${pathText(y?.path)} in the tier.`;
|
|
450
|
+
}
|
|
451
|
+
const why = reachedDiffers(x.reached, y.reached, equal, options.strict === true);
|
|
452
|
+
if (why !== undefined) return `The free read ${index} of ${pathText(x.path)} differs. ${why}`;
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
370
457
|
/**
|
|
371
458
|
* This function compares the run of a document in a plan with its run in other tiers (RUN.AGREES). First the plan
|
|
372
459
|
* starts the run with a recorder around the host. Then each tier replays the log of that run. These tiers are the
|
|
@@ -375,6 +462,13 @@ function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]):
|
|
|
375
462
|
* The rules of `agree` compare the results and the effects, with their addresses and outcomes. A tier must also
|
|
376
463
|
* settle the waits of each `all` in the order of the plan, thus a replay keeps the order of the waits.
|
|
377
464
|
*
|
|
465
|
+
* The reference run must also give the free reads of the plan, in their order (RUN.AGREES-READS). Another replay of
|
|
466
|
+
* the plan records these reads. A kept read does not compare. When the plan keeps each root, its reads are not known,
|
|
467
|
+
* and no read compares.
|
|
468
|
+
*
|
|
469
|
+
* One replayer of the log serves each tier and the replay for the reads (RUN.AGREES-ONE-REPLAYER). The replayer keeps
|
|
470
|
+
* no state of a run (RUN.REPLAY-RUNS), thus each tier replays the same log as with a replayer of its own.
|
|
471
|
+
*
|
|
378
472
|
* @param machine - The world, the evaluators and the runners.
|
|
379
473
|
* @param doc - The document.
|
|
380
474
|
* @param context - The names and the capabilities at the root.
|
|
@@ -396,15 +490,22 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
|
|
|
396
490
|
const result = await plan.run(context, { perform: first.perform });
|
|
397
491
|
const saw = observe?.();
|
|
398
492
|
const log = recording.log();
|
|
493
|
+
// The reads of the plan come from another replay, thus the run of the plan above keeps its folds (PLAN.FOLD).
|
|
494
|
+
let planReads: readonly Read[] | undefined;
|
|
495
|
+
let compared: readonly Read[] | undefined;
|
|
496
|
+
// The kernel tells the journal the count of the waits that settled, thus one replayer serves each tier (RUN.AGREES-ONE-REPLAYER).
|
|
497
|
+
const replay = replayer(log);
|
|
399
498
|
|
|
400
499
|
for (const tier of options.tiers ?? DEFAULT_TIERS) {
|
|
401
500
|
// A run has no tier with fixed names (PLAN.FIXED).
|
|
402
501
|
if (tier === "fixed") continue;
|
|
403
|
-
const
|
|
404
|
-
const host = { perform:
|
|
502
|
+
const tierReplay = watched(replay);
|
|
503
|
+
const host = { perform: tierReplay.perform };
|
|
405
504
|
let other: Result;
|
|
505
|
+
let tierReads: Read[] | undefined;
|
|
406
506
|
if (tier === "reference") {
|
|
407
|
-
|
|
507
|
+
tierReads = [];
|
|
508
|
+
other = await referenceRun(machine, doc, context, { ...declared, ...host, reads: tierReads, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
|
|
408
509
|
} else if (tier === "traced") {
|
|
409
510
|
other = (await run(machine, doc, context, { ...declared, ...host })).result;
|
|
410
511
|
} else if (tier === "folds") {
|
|
@@ -416,12 +517,23 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
|
|
|
416
517
|
const compareCalls = observe !== undefined && tier !== "folds";
|
|
417
518
|
const calls = observe?.();
|
|
418
519
|
if (overflowed(result) || overflowed(other)) continue;
|
|
419
|
-
|
|
520
|
+
let why =
|
|
420
521
|
resultDiffers(result, other, options) ??
|
|
421
|
-
effectsDiffer(first.effects,
|
|
422
|
-
settlesDiffer(first.settles,
|
|
522
|
+
effectsDiffer(first.effects, tierReplay.effects, options) ??
|
|
523
|
+
settlesDiffer(first.settles, tierReplay.settles) ??
|
|
423
524
|
(compareCalls && !sameValue(saw, calls, options.equal) ? "The host saw other calls in the tier." : undefined);
|
|
525
|
+
if (why === undefined && tierReads !== undefined) {
|
|
526
|
+
if (planReads === undefined) {
|
|
527
|
+
const reads: Read[] = [];
|
|
528
|
+
await plan.run(context, { perform: replay, reads });
|
|
529
|
+
// The calls of this replay are not a tier, thus the next tier compares only its own calls.
|
|
530
|
+
observe?.();
|
|
531
|
+
planReads = reads;
|
|
532
|
+
}
|
|
533
|
+
why = readsDiffer(planReads, tierReads, options);
|
|
534
|
+
if (why === undefined && !readsUnknown(planReads)) compared = resolutions(planReads);
|
|
535
|
+
}
|
|
424
536
|
if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
|
|
425
537
|
}
|
|
426
|
-
return { ok: true, result, effects: first.effects };
|
|
538
|
+
return compared === undefined ? { ok: true, result, effects: first.effects } : { ok: true, result, effects: first.effects, reads: compared };
|
|
427
539
|
}
|
package/src/conform.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { type PartSlot, type Split, pathMatches, scopesOf, splitParts } from "./
|
|
|
32
32
|
import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
|
|
33
33
|
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
34
34
|
import { sameValue } from "./agree.ts";
|
|
35
|
+
import { declaredTruthOf } from "./formula.ts";
|
|
36
|
+
import { falseCovers, membersOf, typeIssues, typeText } from "./types.ts";
|
|
35
37
|
import { type Random, random } from "./generate.ts";
|
|
36
38
|
import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
37
39
|
|
|
@@ -56,8 +58,12 @@ import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effe
|
|
|
56
58
|
* - `C8`: compiled code gives the result of code that compiles again at the time of the evaluation.
|
|
57
59
|
* - `local`: the code keeps no state from one call to the next.
|
|
58
60
|
* - `agree`: compiled code gives the result of its definition for the reference interpreter.
|
|
61
|
+
* - `truth`: the own `truthy` of a formula profile gives `true` or `false` for each member of its `truthType` (D86).
|
|
62
|
+
* Only `formula.if`, `formula.and` and `formula.or` of the formula kit get this check.
|
|
63
|
+
* - `cover`: each sample of a covered domain of the machine is in one domain of each cover of the domain (D94). Each
|
|
64
|
+
* evaluator and each runner gets this check, because `fits` trusts the covers for each program.
|
|
59
65
|
*/
|
|
60
|
-
export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree";
|
|
66
|
+
export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth" | "cover";
|
|
61
67
|
|
|
62
68
|
/** The probe of a finding: the document, the result of each probe part, the capabilities and the handler. */
|
|
63
69
|
export interface ConformProbe {
|
|
@@ -132,7 +138,7 @@ export interface ConformOptions {
|
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
/** The order of the obligations in the list of findings. */
|
|
135
|
-
const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree"];
|
|
141
|
+
const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth", "cover"];
|
|
136
142
|
|
|
137
143
|
/** A function in the results of the probe parts. It gives the count of its arguments. */
|
|
138
144
|
const PROBE_FN = (...args: readonly unknown[]): number => args.length;
|
|
@@ -1025,6 +1031,56 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
|
|
|
1025
1031
|
}
|
|
1026
1032
|
}
|
|
1027
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* This function checks the declared truth of a profile (D86). The function `truthy` must give `true` or `false` for
|
|
1036
|
+
* each member of `truthType`, and it must not throw. The members come from the lists `values` of the domains of the
|
|
1037
|
+
* type. A type without such a list gets its members from the results of the palette and from the samples of the
|
|
1038
|
+
* domains (D99).
|
|
1039
|
+
*/
|
|
1040
|
+
function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformProbe): void {
|
|
1041
|
+
const truth = declaredTruthOf(code);
|
|
1042
|
+
if (truth === undefined) return;
|
|
1043
|
+
const domains = env.machine.domains;
|
|
1044
|
+
// The check gives an issue for a bad type, and it then needs nothing of a test (D81).
|
|
1045
|
+
if (typeIssues(truth.truthType, domains).length > 0) return;
|
|
1046
|
+
const samples = env.results.flatMap((result) => (result.ok ? [result.value] : []));
|
|
1047
|
+
const of = (value: unknown): string => `the member ${valueText(value)} of the truth type ${typeText(truth.truthType)}`;
|
|
1048
|
+
for (const value of membersOf(truth.truthType, domains, samples)) {
|
|
1049
|
+
let given: unknown;
|
|
1050
|
+
try {
|
|
1051
|
+
given = truth.truthy(value);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
report(env, "truth", `The function truthy of the profile throws for ${of(value)}: ${describe(error)}`, () => probe(value));
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (typeof given === "boolean") continue;
|
|
1057
|
+
const what = given === undefined ? "no truth" : valueText(given);
|
|
1058
|
+
report(env, "truth", `The function truthy of the profile gives ${what} for ${of(value)}. A truth is true or false.`, () => probe(value));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* This function checks the covers of the domains of the machine from samples (D94). Each sample of a covered domain
|
|
1065
|
+
* must be in one domain of each valid cover. The samples of a domain come from its list `values`. A domain without
|
|
1066
|
+
* that list gets its members from the palette and from the samples of the domains, as for the check `truth`.
|
|
1067
|
+
*/
|
|
1068
|
+
function checkCovers(env: Env, probe: (value: unknown) => ConformProbe): void {
|
|
1069
|
+
const domains = env.machine.domains;
|
|
1070
|
+
if (domains === undefined) return;
|
|
1071
|
+
const palette = env.results.flatMap((result) => (result.ok ? [result.value] : []));
|
|
1072
|
+
const samples: Record<string, readonly unknown[]> = {};
|
|
1073
|
+
for (const [name, domain] of Object.entries(domains)) if (domain.covers !== undefined) samples[name] = membersOf(name, domains, palette);
|
|
1074
|
+
const found = falseCovers(domains, samples)[0];
|
|
1075
|
+
if (found === undefined) return;
|
|
1076
|
+
report(
|
|
1077
|
+
env,
|
|
1078
|
+
"cover",
|
|
1079
|
+
`The member ${valueText(found.value)} of the domain ${found.domain} is in no domain of its cover ${found.cover.join(", ")}. The check of types trusts the cover, thus it is unsound for ${found.domain}.`,
|
|
1080
|
+
() => probe(found.value),
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1028
1084
|
/**
|
|
1029
1085
|
* This function examines one evaluator or runner of a machine against the obligations of host code (K92, D36). It
|
|
1030
1086
|
* starts the code directly, without the kernel, with probe parts and a probe runtime. It generates probe documents from
|
|
@@ -1032,8 +1088,11 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
|
|
|
1032
1088
|
*
|
|
1033
1089
|
* The checks follow the obligations C1 to C8 of `docs/SEMANTICS.md`. Two more checks follow the premises of the
|
|
1034
1090
|
* simulation of the plan. The check `local` finds state that stays from one call to the next. The check `agree` compares
|
|
1035
|
-
* compiled code with its definition. The
|
|
1036
|
-
*
|
|
1091
|
+
* compiled code with its definition. The finding of an obligation names the first probe that breaks it. An empty list
|
|
1092
|
+
* means that each probe conforms.
|
|
1093
|
+
*
|
|
1094
|
+
* The check `truth` tests the declared truth of a formula profile (D86). The check `cover` tests the covers of the
|
|
1095
|
+
* domains of the machine from samples (D94).
|
|
1037
1096
|
*
|
|
1038
1097
|
* @param machine - The machine with the code and the world of the kinds.
|
|
1039
1098
|
* @param name - The name of the evaluator or of the runner. A runner wins over an evaluator of the same name.
|
|
@@ -1064,13 +1123,24 @@ export function conforms(machine: Machine, name: string, options: ConformOptions
|
|
|
1064
1123
|
found: new Map(),
|
|
1065
1124
|
};
|
|
1066
1125
|
const types = options.types ?? typesNaming(machine.world, name, verb);
|
|
1126
|
+
let first: Sample | undefined;
|
|
1067
1127
|
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.`);
|
|
1068
1128
|
for (const type of types) {
|
|
1069
1129
|
const found = lookupKind(machine.world, type);
|
|
1070
1130
|
if (!found.ok) throw new Error(`The world has no kind for the type ${typeName(type)}.`);
|
|
1071
1131
|
const contract = contractOf(env, found.kind, found.path, options.caps);
|
|
1072
1132
|
const extra = own(options.values, typeName(type)) ?? own(options.values, found.path.join(".")) ?? [];
|
|
1073
|
-
for (const sample of samplesOf(type, found.path, found.kind, contract, extra))
|
|
1133
|
+
for (const sample of samplesOf(type, found.path, found.kind, contract, extra)) {
|
|
1134
|
+
first ??= sample;
|
|
1135
|
+
examine(env, sample, contract);
|
|
1136
|
+
}
|
|
1074
1137
|
}
|
|
1138
|
+
// A probe of the truth or of a cover gives the value to each part of the first probe document.
|
|
1139
|
+
const probeWith = (value: unknown): ConformProbe =>
|
|
1140
|
+
first === undefined
|
|
1141
|
+
? { type: typeName(types[0] as TypeRef), doc: { type: types[0] as TypeRef, value: null }, results: {}, waits: [], caps: NO_CAPS, running: runner }
|
|
1142
|
+
: probeOf(first, { results: first.split.parts.map(() => ok(value)), waits: [], caps: NO_CAPS, running: runner });
|
|
1143
|
+
checkTruth(env, code, probeWith);
|
|
1144
|
+
checkCovers(env, probeWith);
|
|
1075
1145
|
return ORDER.filter((obligation) => env.found.has(obligation)).map((obligation) => env.found.get(obligation) as Finding);
|
|
1076
1146
|
}
|
package/src/deref.ts
CHANGED
|
@@ -101,8 +101,31 @@ export interface Context {
|
|
|
101
101
|
readonly caps: readonly Cap[];
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The code of a `deref` evaluator: it answers one step of a path on a document of its kind. A field that holds
|
|
106
|
+
* `undefined` also gives `undefined`. The optional function `has` of the code tells the two cases apart
|
|
107
|
+
* (DEREF.KIND-UNDEFINED). A host gives the code a `has` with `Object.assign(code, { has })`.
|
|
108
|
+
*/
|
|
109
|
+
export interface DerefCode {
|
|
110
|
+
/**
|
|
111
|
+
* This function answers one step on a document.
|
|
112
|
+
*
|
|
113
|
+
* @param doc - The document.
|
|
114
|
+
* @param key - The key of the step.
|
|
115
|
+
* @returns What the step reaches, or `undefined` when it reaches nothing.
|
|
116
|
+
*/
|
|
117
|
+
(doc: Envelope, key: Key): Thing | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* This optional function tells if a document has a field for a key. The kernel asks it only when the code gives
|
|
120
|
+
* `undefined`. Then `true` tells that the field holds `undefined`. A code without `has` reads `undefined` as a
|
|
121
|
+
* missing field, as a host thing does (DEREF.HOST-UNDEFINED).
|
|
122
|
+
*
|
|
123
|
+
* @param doc - The document.
|
|
124
|
+
* @param key - The key of the step.
|
|
125
|
+
* @returns `true` when the document has the field.
|
|
126
|
+
*/
|
|
127
|
+
readonly has?: (doc: Envelope, key: Key) => boolean;
|
|
128
|
+
}
|
|
106
129
|
|
|
107
130
|
/**
|
|
108
131
|
* The result of a step document: what it reached, or the reason of a failure. A failure can give its code, for
|
|
@@ -118,13 +141,24 @@ export interface WalkInput {
|
|
|
118
141
|
readonly here: Thing;
|
|
119
142
|
readonly context: Context;
|
|
120
143
|
/**
|
|
121
|
-
* This function answers one step on a thing, through the ladder.
|
|
144
|
+
* This function answers one step on a thing, through the ladder. A field that holds `undefined` also gives
|
|
145
|
+
* `undefined`. The function `has` tells the two cases apart (DEREF.WALKER-UNDEFINED).
|
|
122
146
|
*
|
|
123
147
|
* @param thing - The thing.
|
|
124
148
|
* @param key - The key.
|
|
125
149
|
* @returns What the step reaches, or `undefined`.
|
|
126
150
|
*/
|
|
127
151
|
deref(thing: Thing, key: Key): Thing | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* This optional function tells if a step on a thing reaches a field, through the ladder. A walker needs it only
|
|
154
|
+
* when `deref` gives `undefined`. Then `true` tells that the field holds `undefined` (DEREF.WALKER-UNDEFINED).
|
|
155
|
+
* Each resolution of the kernel gives it. The function asks the ladder again, thus a host thing answers the step again.
|
|
156
|
+
*
|
|
157
|
+
* @param thing - The thing.
|
|
158
|
+
* @param key - The key.
|
|
159
|
+
* @returns `true` when the step reaches a field, also a field that holds `undefined`.
|
|
160
|
+
*/
|
|
161
|
+
has?(thing: Thing, key: Key): boolean;
|
|
128
162
|
/**
|
|
129
163
|
* This optional function records one hop inside the walk (HOP.EACH-STEP). The resolution gives it only when it
|
|
130
164
|
* records hops. The hop of the whole walk comes after these hops, as before.
|
|
@@ -318,8 +352,10 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
|
|
|
318
352
|
// A navigator without the code of a deref needs no lookup of the kind: no rung can answer.
|
|
319
353
|
const answer = derefs === undefined ? undefined : answerOf(nav.world, ownEntries(envelope), kindFor(nav.world, envelope.type), "deref", (name) => codeIn(derefs, name));
|
|
320
354
|
if (answer !== undefined) {
|
|
321
|
-
const
|
|
322
|
-
|
|
355
|
+
const code = (derefs as Readonly<Record<string, DerefCode>>)[answer.name];
|
|
356
|
+
const reached = code?.(envelope, key);
|
|
357
|
+
// DEREF.KIND-UNDEFINED: the `has` of the code tells a field that holds undefined from a missing field.
|
|
358
|
+
return reached !== undefined || code?.has?.(envelope, key) === true ? { thing: reached, by: answer.rung } : undefined;
|
|
323
359
|
}
|
|
324
360
|
const reached = plainStep(envelope.value, key);
|
|
325
361
|
return reached === NOTHING ? undefined : { thing: reached, by: "default" };
|
|
@@ -596,6 +632,38 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
|
|
|
596
632
|
return resolveFully(nav, ctx, path, options.hops === false ? undefined : [], undefined);
|
|
597
633
|
}
|
|
598
634
|
|
|
635
|
+
/** The functions `deref` and `has` of the input of a walker, for one navigator. */
|
|
636
|
+
interface WalkSteps {
|
|
637
|
+
readonly nav: Navigator;
|
|
638
|
+
readonly deref: (thing: Thing, key: Key) => Thing | undefined;
|
|
639
|
+
readonly has: (thing: Thing, key: Key) => boolean;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** The functions of the last navigator that gave a walker its input. A walk with the same navigator makes no new functions. */
|
|
643
|
+
let lastSteps: WalkSteps | undefined;
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* This function gives the functions `deref` and `has` of the input of a walker. They depend only on the navigator,
|
|
647
|
+
* thus the function keeps the functions of the last navigator (K170).
|
|
648
|
+
*/
|
|
649
|
+
function walkSteps(nav: Navigator): WalkSteps {
|
|
650
|
+
if (lastSteps !== undefined && lastSteps.nav === nav) return lastSteps;
|
|
651
|
+
const adopt = nav.adopt;
|
|
652
|
+
lastSteps = {
|
|
653
|
+
nav,
|
|
654
|
+
deref:
|
|
655
|
+
adopt === undefined
|
|
656
|
+
? (thing, key) => derefStep(nav, thing, key)?.thing
|
|
657
|
+
: (thing, key) => {
|
|
658
|
+
const reached = derefStep(nav, thing, key);
|
|
659
|
+
return reached === undefined ? undefined : adopt(reached.thing);
|
|
660
|
+
},
|
|
661
|
+
// DEREF.WALKER-UNDEFINED: the same ladder tells a field that holds undefined from a missing field.
|
|
662
|
+
has: (thing, key) => derefStep(nav, thing, key) !== undefined,
|
|
663
|
+
};
|
|
664
|
+
return lastSteps;
|
|
665
|
+
}
|
|
666
|
+
|
|
599
667
|
/**
|
|
600
668
|
* This function resolves a path with the full algorithm of `resolve`.
|
|
601
669
|
*
|
|
@@ -668,15 +736,12 @@ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], ho
|
|
|
668
736
|
const walker = given === undefined ? walkerOf(nav, step) : (given ?? undefined);
|
|
669
737
|
if (walker === undefined) return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
|
|
670
738
|
const envelope = step as Envelope;
|
|
671
|
-
const deref = (
|
|
672
|
-
const reached = derefStep(nav, thing, key);
|
|
673
|
-
return reached === undefined ? undefined : adopt(reached.thing);
|
|
674
|
-
};
|
|
739
|
+
const { deref, has } = walkSteps(nav);
|
|
675
740
|
// A resolution without a record of hops gives no `visit`, thus a walker records nothing (HOP.EACH-STEP).
|
|
676
741
|
const result = walker(
|
|
677
742
|
hops === undefined
|
|
678
|
-
? { data: envelope.value, here, context: ctx, deref }
|
|
679
|
-
: { data: envelope.value, here, context: ctx, deref, visit: (key, thing) => hops.push({ step: key, thing, by: "step" }) },
|
|
743
|
+
? { data: envelope.value, here, context: ctx, deref, has }
|
|
744
|
+
: { data: envelope.value, here, context: ctx, deref, has, visit: (key, thing) => hops.push({ step: key, thing, by: "step" }) },
|
|
680
745
|
);
|
|
681
746
|
if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
|
|
682
747
|
here = adopt(result.thing);
|