@mark1russell7/coeval 0.15.0 → 0.17.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 +828 -249
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3138 -386
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +193 -21
- package/src/conform.ts +49 -5
- package/src/deref.ts +97 -14
- package/src/evaluate.ts +311 -35
- package/src/facts.ts +113 -78
- package/src/formula.ts +55 -10
- package/src/index.ts +33 -1
- package/src/meaning.ts +472 -0
- package/src/merge.ts +46 -14
- package/src/reads.ts +9 -2
- package/src/reference.ts +90 -14
- package/src/replay.ts +964 -18
- package/src/schedule.ts +14 -4
- package/src/static.ts +2 -2
- package/src/stdlib.ts +81 -6
- package/src/syntax.ts +357 -52
- package/src/types.ts +443 -16
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.17.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",
|
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.
|
|
@@ -14,9 +14,11 @@ import { type ReplaySettle, recorder, replayer } from "./replay.ts";
|
|
|
14
14
|
* - `again`: a second evaluation of the same plan.
|
|
15
15
|
* - `folds`: a second plan with the other choice of folds (PLAN.FOLD). It has no folds when the plan has them, and
|
|
16
16
|
* it has folds when the option `observe` stops the folds of the plan.
|
|
17
|
+
* - `fixed`: a plan that declares each name of plain data fixed (PLAN.FIXED). It evaluates with a frozen copy of
|
|
18
|
+
* each such name, and then with the context of `agree`. `agreeRun` does not have this tier.
|
|
17
19
|
* - `other`: the second tier of a language, from the option `other`.
|
|
18
20
|
*/
|
|
19
|
-
export type Tier = "reference" | "traced" | "again" | "folds" | "other";
|
|
21
|
+
export type Tier = "reference" | "traced" | "again" | "folds" | "fixed" | "other";
|
|
20
22
|
|
|
21
23
|
/** What a second tier gives: its result, and its effects when it records them. */
|
|
22
24
|
export interface TierResult {
|
|
@@ -45,8 +47,8 @@ export interface AgreeOptions {
|
|
|
45
47
|
/** The definitional code of the compiled evaluators of the machine, for the reference. */
|
|
46
48
|
readonly definitions?: Definitions;
|
|
47
49
|
/**
|
|
48
|
-
* The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation
|
|
49
|
-
*
|
|
50
|
+
* The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation, the
|
|
51
|
+
* plan with the other choice of folds and the plan with fixed names.
|
|
50
52
|
*/
|
|
51
53
|
readonly tiers?: readonly Exclude<Tier, "other">[];
|
|
52
54
|
/**
|
|
@@ -85,11 +87,20 @@ export interface AgreeOptions {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** The tiers that `agree` compares when its options name none. */
|
|
88
|
-
const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.freeze(["reference", "traced", "again", "folds"]);
|
|
90
|
+
const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.freeze(["reference", "traced", "again", "folds", "fixed"]);
|
|
89
91
|
|
|
90
92
|
/** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
|
|
91
93
|
export type Agreement =
|
|
92
|
-
| {
|
|
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
|
+
}
|
|
93
104
|
| {
|
|
94
105
|
readonly ok: false;
|
|
95
106
|
readonly tier: Tier;
|
|
@@ -132,7 +143,8 @@ function sameInTiers(x: unknown, y: unknown): boolean | undefined {
|
|
|
132
143
|
|
|
133
144
|
const CLOSURE = /* @__PURE__ */ Symbol.for("coeval.closure");
|
|
134
145
|
|
|
135
|
-
function
|
|
146
|
+
/** This function gives the parameters of a closure of `fn`, or `undefined` for another function. */
|
|
147
|
+
export function closureParams(fn: object): unknown {
|
|
136
148
|
return (fn as { readonly [CLOSURE]?: { readonly params: readonly string[] } })[CLOSURE]?.params;
|
|
137
149
|
}
|
|
138
150
|
|
|
@@ -184,6 +196,57 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
|
|
|
184
196
|
return undefined;
|
|
185
197
|
}
|
|
186
198
|
|
|
199
|
+
/**
|
|
200
|
+
* This function compares the plan with the tier `fixed` (PLAN.FIXED). The tier declares each name of plain data fixed,
|
|
201
|
+
* with a frozen copy of its value. It evaluates with these copies, thus the folds with fixed names give the result.
|
|
202
|
+
* Then it evaluates with the context of `agree`, thus the code without the fold gives the result. A fold moves the
|
|
203
|
+
* calls of the host to the compilation, thus only the result and the effects compare.
|
|
204
|
+
*/
|
|
205
|
+
function fixedDiffers(
|
|
206
|
+
machine: Machine,
|
|
207
|
+
doc: unknown,
|
|
208
|
+
context: Context,
|
|
209
|
+
result: Result,
|
|
210
|
+
effects: readonly Effect[],
|
|
211
|
+
given: { readonly declaredType?: string; readonly verb?: string; readonly perform?: Perform },
|
|
212
|
+
options: AgreeOptions,
|
|
213
|
+
): { readonly other: Result; readonly reason: string } | undefined {
|
|
214
|
+
const { perform, ...declared } = given;
|
|
215
|
+
const held = fixedContext(context);
|
|
216
|
+
const plan = prepare(machine, doc, { ...declared, fixed: { names: held.fixed } });
|
|
217
|
+
for (const each of [held.context, context]) {
|
|
218
|
+
const tierEffects: Effect[] = [];
|
|
219
|
+
const other = plan.evaluate(each, { effects: tierEffects, ...(perform === undefined ? {} : { perform }) });
|
|
220
|
+
options.observe?.();
|
|
221
|
+
if (overflowed(result) || overflowed(other)) continue;
|
|
222
|
+
const reason = resultDiffers(result, other, options) ?? effectsDiffer(effects, tierEffects, options);
|
|
223
|
+
if (reason !== undefined) return { other, reason };
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* This function gives the context of the tier `fixed`. Each name of plain data gets a deeply frozen copy of its value,
|
|
230
|
+
* and the record of the fixed names holds the same copy. Each other name keeps its value.
|
|
231
|
+
*/
|
|
232
|
+
function fixedContext(context: Context): { readonly context: Context; readonly fixed: Readonly<Record<string, unknown>> } {
|
|
233
|
+
const given = context.names;
|
|
234
|
+
const names = Object.create(Object.getPrototypeOf(given) as object | null) as Record<string, unknown>;
|
|
235
|
+
const fixed = Object.create(null) as Record<string, unknown>;
|
|
236
|
+
for (const key of Reflect.ownKeys(given)) {
|
|
237
|
+
let field = Object.getOwnPropertyDescriptor(given, key) as PropertyDescriptor;
|
|
238
|
+
if (typeof key === "string" && "value" in field) {
|
|
239
|
+
const copy = sharedCopy(field.value);
|
|
240
|
+
if (copy !== undefined || field.value === undefined) {
|
|
241
|
+
Object.defineProperty(fixed, key, { value: copy, enumerable: true });
|
|
242
|
+
field = { ...field, value: copy };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
Object.defineProperty(names, key, field);
|
|
246
|
+
}
|
|
247
|
+
return { context: { ...context, names }, fixed };
|
|
248
|
+
}
|
|
249
|
+
|
|
187
250
|
/**
|
|
188
251
|
* This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
|
|
189
252
|
* results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
|
|
@@ -228,6 +291,10 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
228
291
|
const traced = evaluate(machine, doc, context, { ...declared, ...mock });
|
|
229
292
|
other = traced.result;
|
|
230
293
|
otherEffects = [...traced.effects];
|
|
294
|
+
} else if (tier === "fixed") {
|
|
295
|
+
const why = fixedDiffers(machine, doc, context, result, effects, { ...declared, ...mock }, options);
|
|
296
|
+
if (why !== undefined) return { ok: false, tier, plan: result, other: why.other, reason: why.reason };
|
|
297
|
+
continue;
|
|
231
298
|
} else if (tier === "folds") {
|
|
232
299
|
other = prepare(machine, doc, { ...declared, fold: !folds }).evaluate(context, { effects: otherEffects, ...mock });
|
|
233
300
|
if (observe !== undefined) {
|
|
@@ -282,12 +349,9 @@ interface Watched {
|
|
|
282
349
|
function watched(perform: Perform): Watched {
|
|
283
350
|
const effects: Effect[] = [];
|
|
284
351
|
const settles: ReplaySettle[] = [];
|
|
285
|
-
const
|
|
286
|
-
const settled = (all: string, item: number): void => {
|
|
352
|
+
const journal = relay(perform, (all, item) => {
|
|
287
353
|
settles.push({ all, item });
|
|
288
|
-
|
|
289
|
-
};
|
|
290
|
-
const journal: Journal = inner?.next === undefined ? { settled } : { settled, next: (all, waiting) => inner.next?.(all, waiting) };
|
|
354
|
+
});
|
|
291
355
|
const host = (effect: Effect): unknown => {
|
|
292
356
|
effects.push(effect);
|
|
293
357
|
return perform(effect);
|
|
@@ -309,6 +373,87 @@ function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]):
|
|
|
309
373
|
return undefined;
|
|
310
374
|
}
|
|
311
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
|
+
|
|
312
457
|
/**
|
|
313
458
|
* This function compares the run of a document in a plan with its run in other tiers (RUN.AGREES). First the plan
|
|
314
459
|
* starts the run with a recorder around the host. Then each tier replays the log of that run. These tiers are the
|
|
@@ -317,6 +462,13 @@ function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]):
|
|
|
317
462
|
* The rules of `agree` compare the results and the effects, with their addresses and outcomes. A tier must also
|
|
318
463
|
* settle the waits of each `all` in the order of the plan, thus a replay keeps the order of the waits.
|
|
319
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
|
+
*
|
|
320
472
|
* @param machine - The world, the evaluators and the runners.
|
|
321
473
|
* @param doc - The document.
|
|
322
474
|
* @param context - The names and the capabilities at the root.
|
|
@@ -338,13 +490,22 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
|
|
|
338
490
|
const result = await plan.run(context, { perform: first.perform });
|
|
339
491
|
const saw = observe?.();
|
|
340
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);
|
|
341
498
|
|
|
342
499
|
for (const tier of options.tiers ?? DEFAULT_TIERS) {
|
|
343
|
-
|
|
344
|
-
|
|
500
|
+
// A run has no tier with fixed names (PLAN.FIXED).
|
|
501
|
+
if (tier === "fixed") continue;
|
|
502
|
+
const tierReplay = watched(replay);
|
|
503
|
+
const host = { perform: tierReplay.perform };
|
|
345
504
|
let other: Result;
|
|
505
|
+
let tierReads: Read[] | undefined;
|
|
346
506
|
if (tier === "reference") {
|
|
347
|
-
|
|
507
|
+
tierReads = [];
|
|
508
|
+
other = await referenceRun(machine, doc, context, { ...declared, ...host, reads: tierReads, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
|
|
348
509
|
} else if (tier === "traced") {
|
|
349
510
|
other = (await run(machine, doc, context, { ...declared, ...host })).result;
|
|
350
511
|
} else if (tier === "folds") {
|
|
@@ -356,12 +517,23 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
|
|
|
356
517
|
const compareCalls = observe !== undefined && tier !== "folds";
|
|
357
518
|
const calls = observe?.();
|
|
358
519
|
if (overflowed(result) || overflowed(other)) continue;
|
|
359
|
-
|
|
520
|
+
let why =
|
|
360
521
|
resultDiffers(result, other, options) ??
|
|
361
|
-
effectsDiffer(first.effects,
|
|
362
|
-
settlesDiffer(first.settles,
|
|
522
|
+
effectsDiffer(first.effects, tierReplay.effects, options) ??
|
|
523
|
+
settlesDiffer(first.settles, tierReplay.settles) ??
|
|
363
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
|
+
}
|
|
364
536
|
if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
|
|
365
537
|
}
|
|
366
|
-
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 };
|
|
367
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 { 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,10 @@ 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.
|
|
59
63
|
*/
|
|
60
|
-
export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree";
|
|
64
|
+
export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth";
|
|
61
65
|
|
|
62
66
|
/** The probe of a finding: the document, the result of each probe part, the capabilities and the handler. */
|
|
63
67
|
export interface ConformProbe {
|
|
@@ -132,7 +136,7 @@ export interface ConformOptions {
|
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/** 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"];
|
|
139
|
+
const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth"];
|
|
136
140
|
|
|
137
141
|
/** A function in the results of the probe parts. It gives the count of its arguments. */
|
|
138
142
|
const PROBE_FN = (...args: readonly unknown[]): number => args.length;
|
|
@@ -1025,6 +1029,34 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
|
|
|
1025
1029
|
}
|
|
1026
1030
|
}
|
|
1027
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* This function checks the declared truth of a profile (D86). The function `truthy` must give `true` or `false` for
|
|
1034
|
+
* each member of `truthType`, and it must not throw. The members come from the lists `values` of the domains of the
|
|
1035
|
+
* type. A type without such a list gets the values of the results of the palette that are its members.
|
|
1036
|
+
*/
|
|
1037
|
+
function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformProbe): void {
|
|
1038
|
+
const truth = declaredTruthOf(code);
|
|
1039
|
+
if (truth === undefined) return;
|
|
1040
|
+
const domains = env.machine.domains;
|
|
1041
|
+
// The check gives an issue for a bad type, and it then needs nothing of a test (D81).
|
|
1042
|
+
if (typeIssues(truth.truthType, domains).length > 0) return;
|
|
1043
|
+
const samples = env.results.flatMap((result) => (result.ok ? [result.value] : []));
|
|
1044
|
+
const of = (value: unknown): string => `the member ${valueText(value)} of the truth type ${typeText(truth.truthType)}`;
|
|
1045
|
+
for (const value of membersOf(truth.truthType, domains, samples)) {
|
|
1046
|
+
let given: unknown;
|
|
1047
|
+
try {
|
|
1048
|
+
given = truth.truthy(value);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
report(env, "truth", `The function truthy of the profile throws for ${of(value)}: ${describe(error)}`, () => probe(value));
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof given === "boolean") continue;
|
|
1054
|
+
const what = given === undefined ? "no truth" : valueText(given);
|
|
1055
|
+
report(env, "truth", `The function truthy of the profile gives ${what} for ${of(value)}. A truth is true or false.`, () => probe(value));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1028
1060
|
/**
|
|
1029
1061
|
* This function examines one evaluator or runner of a machine against the obligations of host code (K92, D36). It
|
|
1030
1062
|
* starts the code directly, without the kernel, with probe parts and a probe runtime. It generates probe documents from
|
|
@@ -1032,8 +1064,10 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
|
|
|
1032
1064
|
*
|
|
1033
1065
|
* The checks follow the obligations C1 to C8 of `docs/SEMANTICS.md`. Two more checks follow the premises of the
|
|
1034
1066
|
* 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
|
-
*
|
|
1067
|
+
* compiled code with its definition. The finding of an obligation names the first probe that breaks it. An empty list
|
|
1068
|
+
* means that each probe conforms.
|
|
1069
|
+
*
|
|
1070
|
+
* The check `truth` tests the declared truth of a formula profile (D86).
|
|
1037
1071
|
*
|
|
1038
1072
|
* @param machine - The machine with the code and the world of the kinds.
|
|
1039
1073
|
* @param name - The name of the evaluator or of the runner. A runner wins over an evaluator of the same name.
|
|
@@ -1064,13 +1098,23 @@ export function conforms(machine: Machine, name: string, options: ConformOptions
|
|
|
1064
1098
|
found: new Map(),
|
|
1065
1099
|
};
|
|
1066
1100
|
const types = options.types ?? typesNaming(machine.world, name, verb);
|
|
1101
|
+
let first: Sample | undefined;
|
|
1067
1102
|
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
1103
|
for (const type of types) {
|
|
1069
1104
|
const found = lookupKind(machine.world, type);
|
|
1070
1105
|
if (!found.ok) throw new Error(`The world has no kind for the type ${typeName(type)}.`);
|
|
1071
1106
|
const contract = contractOf(env, found.kind, found.path, options.caps);
|
|
1072
1107
|
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))
|
|
1108
|
+
for (const sample of samplesOf(type, found.path, found.kind, contract, extra)) {
|
|
1109
|
+
first ??= sample;
|
|
1110
|
+
examine(env, sample, contract);
|
|
1111
|
+
}
|
|
1074
1112
|
}
|
|
1113
|
+
// A probe of the truth gives the member to each part of the first probe document.
|
|
1114
|
+
checkTruth(env, code, (value) =>
|
|
1115
|
+
first === undefined
|
|
1116
|
+
? { type: typeName(types[0] as TypeRef), doc: { type: types[0] as TypeRef, value: null }, results: {}, waits: [], caps: NO_CAPS, running: runner }
|
|
1117
|
+
: probeOf(first, { results: first.split.parts.map(() => ok(value)), waits: [], caps: NO_CAPS, running: runner }),
|
|
1118
|
+
);
|
|
1075
1119
|
return ORDER.filter((obligation) => env.found.has(obligation)).map((obligation) => env.found.get(obligation) as Finding);
|
|
1076
1120
|
}
|
package/src/deref.ts
CHANGED
|
@@ -48,6 +48,15 @@ export interface HostThing {
|
|
|
48
48
|
* @returns What the keys reach, or `undefined` when they reach nothing. `{ thing: undefined }` tells that the keys reach a field that holds `undefined`.
|
|
49
49
|
*/
|
|
50
50
|
resolve?(keys: readonly Key[]): { readonly thing: Thing } | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* This optional function answers one step only when the host has its value without new work (SCHED.CLOSURE). A
|
|
53
|
+
* kept read of a closure uses it, because the evaluation did not ask for that read. A schedule gives the value of
|
|
54
|
+
* a node that has its result in the data epoch. A throw gives the error of the step.
|
|
55
|
+
*
|
|
56
|
+
* @param key - The key of the step.
|
|
57
|
+
* @returns What the step reaches, or `undefined` when the host does not know it yet. Then the kept read ends at the thing.
|
|
58
|
+
*/
|
|
59
|
+
known?(key: Key): { readonly thing: Thing } | undefined;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
/** Something that a path can reach: any value of the host, or a host thing. */
|
|
@@ -92,8 +101,31 @@ export interface Context {
|
|
|
92
101
|
readonly caps: readonly Cap[];
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
/**
|
|
96
|
-
|
|
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
|
+
}
|
|
97
129
|
|
|
98
130
|
/**
|
|
99
131
|
* The result of a step document: what it reached, or the reason of a failure. A failure can give its code, for
|
|
@@ -109,13 +141,32 @@ export interface WalkInput {
|
|
|
109
141
|
readonly here: Thing;
|
|
110
142
|
readonly context: Context;
|
|
111
143
|
/**
|
|
112
|
-
* 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).
|
|
113
146
|
*
|
|
114
147
|
* @param thing - The thing.
|
|
115
148
|
* @param key - The key.
|
|
116
149
|
* @returns What the step reaches, or `undefined`.
|
|
117
150
|
*/
|
|
118
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;
|
|
162
|
+
/**
|
|
163
|
+
* This optional function records one hop inside the walk (HOP.EACH-STEP). The resolution gives it only when it
|
|
164
|
+
* records hops. The hop of the whole walk comes after these hops, as before.
|
|
165
|
+
*
|
|
166
|
+
* @param key - The key that the walk followed.
|
|
167
|
+
* @param thing - The thing that the key reached.
|
|
168
|
+
*/
|
|
169
|
+
readonly visit?: (key: Key, thing: Thing) => void;
|
|
119
170
|
}
|
|
120
171
|
|
|
121
172
|
/** A walker: the code of the verb `step` of a kind of step documents. */
|
|
@@ -301,8 +352,10 @@ export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly th
|
|
|
301
352
|
// A navigator without the code of a deref needs no lookup of the kind: no rung can answer.
|
|
302
353
|
const answer = derefs === undefined ? undefined : answerOf(nav.world, ownEntries(envelope), kindFor(nav.world, envelope.type), "deref", (name) => codeIn(derefs, name));
|
|
303
354
|
if (answer !== undefined) {
|
|
304
|
-
const
|
|
305
|
-
|
|
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;
|
|
306
359
|
}
|
|
307
360
|
const reached = plainStep(envelope.value, key);
|
|
308
361
|
return reached === NOTHING ? undefined : { thing: reached, by: "default" };
|
|
@@ -579,6 +632,38 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
|
|
|
579
632
|
return resolveFully(nav, ctx, path, options.hops === false ? undefined : [], undefined);
|
|
580
633
|
}
|
|
581
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
|
+
|
|
582
667
|
/**
|
|
583
668
|
* This function resolves a path with the full algorithm of `resolve`.
|
|
584
669
|
*
|
|
@@ -651,15 +736,13 @@ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], ho
|
|
|
651
736
|
const walker = given === undefined ? walkerOf(nav, step) : (given ?? undefined);
|
|
652
737
|
if (walker === undefined) return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
|
|
653
738
|
const envelope = step as Envelope;
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
},
|
|
662
|
-
});
|
|
739
|
+
const { deref, has } = walkSteps(nav);
|
|
740
|
+
// A resolution without a record of hops gives no `visit`, thus a walker records nothing (HOP.EACH-STEP).
|
|
741
|
+
const result = walker(
|
|
742
|
+
hops === undefined
|
|
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" }) },
|
|
745
|
+
);
|
|
663
746
|
if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
|
|
664
747
|
here = adopt(result.thing);
|
|
665
748
|
hops?.push({ step: envelope as Json, thing: here, by: "step" });
|