@mark1russell7/coeval 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +471 -190
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1209 -182
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +175 -7
- package/src/deref.ts +27 -9
- package/src/evaluate.ts +258 -12
- package/src/formula.ts +17 -3
- package/src/index.ts +12 -2
- package/src/meaning.ts +472 -0
- package/src/reads.ts +9 -2
- package/src/reference.ts +247 -42
- package/src/schedule.ts +14 -4
- package/src/stdlib.ts +26 -4
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.16.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,9 +1,10 @@
|
|
|
1
1
|
import { type Key, docEquals, equalWith } from "./doc.ts";
|
|
2
2
|
import type { Context } from "./deref.ts";
|
|
3
3
|
import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
|
|
4
|
-
import { type Effect, type Machine, type Perform, evaluate, makeContext, prepare } from "./evaluate.ts";
|
|
4
|
+
import { type Effect, type Journal, type Machine, type Perform, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
|
|
5
5
|
import { Held, isClosure } from "./formula.ts";
|
|
6
|
-
import { type Definitions, reference } from "./reference.ts";
|
|
6
|
+
import { type Definitions, reference, referenceRun } from "./reference.ts";
|
|
7
|
+
import { type ReplaySettle, recorder, replayer } from "./replay.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A tier that `agree` compares with the plan.
|
|
@@ -13,9 +14,11 @@ import { type Definitions, reference } from "./reference.ts";
|
|
|
13
14
|
* - `again`: a second evaluation of the same plan.
|
|
14
15
|
* - `folds`: a second plan with the other choice of folds (PLAN.FOLD). It has no folds when the plan has them, and
|
|
15
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.
|
|
16
19
|
* - `other`: the second tier of a language, from the option `other`.
|
|
17
20
|
*/
|
|
18
|
-
export type Tier = "reference" | "traced" | "again" | "folds" | "other";
|
|
21
|
+
export type Tier = "reference" | "traced" | "again" | "folds" | "fixed" | "other";
|
|
19
22
|
|
|
20
23
|
/** What a second tier gives: its result, and its effects when it records them. */
|
|
21
24
|
export interface TierResult {
|
|
@@ -44,8 +47,8 @@ export interface AgreeOptions {
|
|
|
44
47
|
/** The definitional code of the compiled evaluators of the machine, for the reference. */
|
|
45
48
|
readonly definitions?: Definitions;
|
|
46
49
|
/**
|
|
47
|
-
* The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation
|
|
48
|
-
*
|
|
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.
|
|
49
52
|
*/
|
|
50
53
|
readonly tiers?: readonly Exclude<Tier, "other">[];
|
|
51
54
|
/**
|
|
@@ -84,7 +87,7 @@ export interface AgreeOptions {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
/** The tiers that `agree` compares when its options name none. */
|
|
87
|
-
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"]);
|
|
88
91
|
|
|
89
92
|
/** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
|
|
90
93
|
export type Agreement =
|
|
@@ -131,7 +134,8 @@ function sameInTiers(x: unknown, y: unknown): boolean | undefined {
|
|
|
131
134
|
|
|
132
135
|
const CLOSURE = /* @__PURE__ */ Symbol.for("coeval.closure");
|
|
133
136
|
|
|
134
|
-
function
|
|
137
|
+
/** This function gives the parameters of a closure of `fn`, or `undefined` for another function. */
|
|
138
|
+
export function closureParams(fn: object): unknown {
|
|
135
139
|
return (fn as { readonly [CLOSURE]?: { readonly params: readonly string[] } })[CLOSURE]?.params;
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -183,6 +187,57 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
|
|
|
183
187
|
return undefined;
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
/**
|
|
191
|
+
* This function compares the plan with the tier `fixed` (PLAN.FIXED). The tier declares each name of plain data fixed,
|
|
192
|
+
* with a frozen copy of its value. It evaluates with these copies, thus the folds with fixed names give the result.
|
|
193
|
+
* Then it evaluates with the context of `agree`, thus the code without the fold gives the result. A fold moves the
|
|
194
|
+
* calls of the host to the compilation, thus only the result and the effects compare.
|
|
195
|
+
*/
|
|
196
|
+
function fixedDiffers(
|
|
197
|
+
machine: Machine,
|
|
198
|
+
doc: unknown,
|
|
199
|
+
context: Context,
|
|
200
|
+
result: Result,
|
|
201
|
+
effects: readonly Effect[],
|
|
202
|
+
given: { readonly declaredType?: string; readonly verb?: string; readonly perform?: Perform },
|
|
203
|
+
options: AgreeOptions,
|
|
204
|
+
): { readonly other: Result; readonly reason: string } | undefined {
|
|
205
|
+
const { perform, ...declared } = given;
|
|
206
|
+
const held = fixedContext(context);
|
|
207
|
+
const plan = prepare(machine, doc, { ...declared, fixed: { names: held.fixed } });
|
|
208
|
+
for (const each of [held.context, context]) {
|
|
209
|
+
const tierEffects: Effect[] = [];
|
|
210
|
+
const other = plan.evaluate(each, { effects: tierEffects, ...(perform === undefined ? {} : { perform }) });
|
|
211
|
+
options.observe?.();
|
|
212
|
+
if (overflowed(result) || overflowed(other)) continue;
|
|
213
|
+
const reason = resultDiffers(result, other, options) ?? effectsDiffer(effects, tierEffects, options);
|
|
214
|
+
if (reason !== undefined) return { other, reason };
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* This function gives the context of the tier `fixed`. Each name of plain data gets a deeply frozen copy of its value,
|
|
221
|
+
* and the record of the fixed names holds the same copy. Each other name keeps its value.
|
|
222
|
+
*/
|
|
223
|
+
function fixedContext(context: Context): { readonly context: Context; readonly fixed: Readonly<Record<string, unknown>> } {
|
|
224
|
+
const given = context.names;
|
|
225
|
+
const names = Object.create(Object.getPrototypeOf(given) as object | null) as Record<string, unknown>;
|
|
226
|
+
const fixed = Object.create(null) as Record<string, unknown>;
|
|
227
|
+
for (const key of Reflect.ownKeys(given)) {
|
|
228
|
+
let field = Object.getOwnPropertyDescriptor(given, key) as PropertyDescriptor;
|
|
229
|
+
if (typeof key === "string" && "value" in field) {
|
|
230
|
+
const copy = sharedCopy(field.value);
|
|
231
|
+
if (copy !== undefined || field.value === undefined) {
|
|
232
|
+
Object.defineProperty(fixed, key, { value: copy, enumerable: true });
|
|
233
|
+
field = { ...field, value: copy };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
Object.defineProperty(names, key, field);
|
|
237
|
+
}
|
|
238
|
+
return { context: { ...context, names }, fixed };
|
|
239
|
+
}
|
|
240
|
+
|
|
186
241
|
/**
|
|
187
242
|
* This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
|
|
188
243
|
* results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
|
|
@@ -227,6 +282,10 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
227
282
|
const traced = evaluate(machine, doc, context, { ...declared, ...mock });
|
|
228
283
|
other = traced.result;
|
|
229
284
|
otherEffects = [...traced.effects];
|
|
285
|
+
} else if (tier === "fixed") {
|
|
286
|
+
const why = fixedDiffers(machine, doc, context, result, effects, { ...declared, ...mock }, options);
|
|
287
|
+
if (why !== undefined) return { ok: false, tier, plan: result, other: why.other, reason: why.reason };
|
|
288
|
+
continue;
|
|
230
289
|
} else if (tier === "folds") {
|
|
231
290
|
other = prepare(machine, doc, { ...declared, fold: !folds }).evaluate(context, { effects: otherEffects, ...mock });
|
|
232
291
|
if (observe !== undefined) {
|
|
@@ -257,3 +316,112 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
257
316
|
}
|
|
258
317
|
return { ok: true, result, effects };
|
|
259
318
|
}
|
|
319
|
+
|
|
320
|
+
/** The options of `agreeRun`: the options of `agree` without the second tier of a language. */
|
|
321
|
+
export interface AgreeRunOptions extends Omit<AgreeOptions, "other" | "perform"> {
|
|
322
|
+
/**
|
|
323
|
+
* The host of the run of the plan (RUN.AGREES). A recorder wraps it, and each other tier replays the log of that run.
|
|
324
|
+
* Thus the host does each effect one time. Without it, an effect gives its payload.
|
|
325
|
+
*/
|
|
326
|
+
readonly perform?: Perform;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The function `perform` of one run, with the effects that it got and the waits of `all` that settled. */
|
|
330
|
+
interface Watched {
|
|
331
|
+
readonly perform: Perform;
|
|
332
|
+
readonly effects: Effect[];
|
|
333
|
+
readonly settles: ReplaySettle[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* This function wraps the function `perform` of a run. The wrapper keeps each effect and each wait of `all` that
|
|
338
|
+
* settles. It gives each effect to the inner function, and each call of its journal to the inner journal.
|
|
339
|
+
*/
|
|
340
|
+
function watched(perform: Perform): Watched {
|
|
341
|
+
const effects: Effect[] = [];
|
|
342
|
+
const settles: ReplaySettle[] = [];
|
|
343
|
+
const inner = (perform as { readonly [JOURNAL]?: Journal })[JOURNAL];
|
|
344
|
+
const settled = (all: string, item: number): void => {
|
|
345
|
+
settles.push({ all, item });
|
|
346
|
+
inner?.settled?.(all, item);
|
|
347
|
+
};
|
|
348
|
+
const journal: Journal = inner?.next === undefined ? { settled } : { settled, next: (all, waiting) => inner.next?.(all, waiting) };
|
|
349
|
+
const host = (effect: Effect): unknown => {
|
|
350
|
+
effects.push(effect);
|
|
351
|
+
return perform(effect);
|
|
352
|
+
};
|
|
353
|
+
return { perform: Object.assign(host, { [JOURNAL]: journal }), effects, settles };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function settleText(settle: ReplaySettle | undefined): string {
|
|
357
|
+
return settle === undefined ? "no item" : `the item ${settle.item} of ${settle.all}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** This function gives the first difference of two orders of the waits of `all`, or `undefined` when they agree. */
|
|
361
|
+
function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]): string | undefined {
|
|
362
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
363
|
+
const x = a[index];
|
|
364
|
+
const y = b[index];
|
|
365
|
+
if (x?.all !== y?.all || x?.item !== y?.item) return `The wait ${index} settles ${settleText(x)} in the plan, and ${settleText(y)} in the tier.`;
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* This function compares the run of a document in a plan with its run in other tiers (RUN.AGREES). First the plan
|
|
372
|
+
* starts the run with a recorder around the host. Then each tier replays the log of that run. These tiers are the
|
|
373
|
+
* reference run, the traced run, a second run of the plan and the plan with the other choice of folds.
|
|
374
|
+
*
|
|
375
|
+
* The rules of `agree` compare the results and the effects, with their addresses and outcomes. A tier must also
|
|
376
|
+
* settle the waits of each `all` in the order of the plan, thus a replay keeps the order of the waits.
|
|
377
|
+
*
|
|
378
|
+
* @param machine - The world, the evaluators and the runners.
|
|
379
|
+
* @param doc - The document.
|
|
380
|
+
* @param context - The names and the capabilities at the root.
|
|
381
|
+
* @param options - The tiers, the definitions for the reference, the rules of the comparison and the host.
|
|
382
|
+
* @returns The result of the plan, or the first tier that disagrees and the reason.
|
|
383
|
+
*/
|
|
384
|
+
export async function agreeRun(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeRunOptions = {}): Promise<Agreement> {
|
|
385
|
+
const declared = {
|
|
386
|
+
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
387
|
+
...(options.verb === undefined ? {} : { verb: options.verb }),
|
|
388
|
+
};
|
|
389
|
+
const observe = options.observe;
|
|
390
|
+
observe?.();
|
|
391
|
+
// A fold moves host calls to the compilation, thus a plan whose calls the host compares has no folds (PLAN.FOLD).
|
|
392
|
+
const folds = observe === undefined;
|
|
393
|
+
const plan = prepare(machine, doc, { ...declared, fold: folds });
|
|
394
|
+
const recording = recorder(options.perform);
|
|
395
|
+
const first = watched(recording.perform);
|
|
396
|
+
const result = await plan.run(context, { perform: first.perform });
|
|
397
|
+
const saw = observe?.();
|
|
398
|
+
const log = recording.log();
|
|
399
|
+
|
|
400
|
+
for (const tier of options.tiers ?? DEFAULT_TIERS) {
|
|
401
|
+
// A run has no tier with fixed names (PLAN.FIXED).
|
|
402
|
+
if (tier === "fixed") continue;
|
|
403
|
+
const replay = watched(replayer(log));
|
|
404
|
+
const host = { perform: replay.perform };
|
|
405
|
+
let other: Result;
|
|
406
|
+
if (tier === "reference") {
|
|
407
|
+
other = await referenceRun(machine, doc, context, { ...declared, ...host, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
|
|
408
|
+
} else if (tier === "traced") {
|
|
409
|
+
other = (await run(machine, doc, context, { ...declared, ...host })).result;
|
|
410
|
+
} else if (tier === "folds") {
|
|
411
|
+
other = await prepare(machine, doc, { ...declared, fold: !folds }).run(context, host);
|
|
412
|
+
} else {
|
|
413
|
+
other = await plan.run(context, host);
|
|
414
|
+
}
|
|
415
|
+
// The folds of the tier folds call the host when it compiles. Thus only its result, effects and waits compare.
|
|
416
|
+
const compareCalls = observe !== undefined && tier !== "folds";
|
|
417
|
+
const calls = observe?.();
|
|
418
|
+
if (overflowed(result) || overflowed(other)) continue;
|
|
419
|
+
const why =
|
|
420
|
+
resultDiffers(result, other, options) ??
|
|
421
|
+
effectsDiffer(first.effects, replay.effects, options) ??
|
|
422
|
+
settlesDiffer(first.settles, replay.settles) ??
|
|
423
|
+
(compareCalls && !sameValue(saw, calls, options.equal) ? "The host saw other calls in the tier." : undefined);
|
|
424
|
+
if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
|
|
425
|
+
}
|
|
426
|
+
return { ok: true, result, effects: first.effects };
|
|
427
|
+
}
|
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. */
|
|
@@ -116,6 +125,14 @@ export interface WalkInput {
|
|
|
116
125
|
* @returns What the step reaches, or `undefined`.
|
|
117
126
|
*/
|
|
118
127
|
deref(thing: Thing, key: Key): Thing | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* This optional function records one hop inside the walk (HOP.EACH-STEP). The resolution gives it only when it
|
|
130
|
+
* records hops. The hop of the whole walk comes after these hops, as before.
|
|
131
|
+
*
|
|
132
|
+
* @param key - The key that the walk followed.
|
|
133
|
+
* @param thing - The thing that the key reached.
|
|
134
|
+
*/
|
|
135
|
+
readonly visit?: (key: Key, thing: Thing) => void;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
/** A walker: the code of the verb `step` of a kind of step documents. */
|
|
@@ -651,15 +668,16 @@ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], ho
|
|
|
651
668
|
const walker = given === undefined ? walkerOf(nav, step) : (given ?? undefined);
|
|
652
669
|
if (walker === undefined) return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
|
|
653
670
|
const envelope = step as Envelope;
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
671
|
+
const deref = (thing: Thing, key: Key): Thing | undefined => {
|
|
672
|
+
const reached = derefStep(nav, thing, key);
|
|
673
|
+
return reached === undefined ? undefined : adopt(reached.thing);
|
|
674
|
+
};
|
|
675
|
+
// A resolution without a record of hops gives no `visit`, thus a walker records nothing (HOP.EACH-STEP).
|
|
676
|
+
const result = walker(
|
|
677
|
+
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" }) },
|
|
680
|
+
);
|
|
663
681
|
if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
|
|
664
682
|
here = adopt(result.thing);
|
|
665
683
|
hops?.push({ step: envelope as Json, thing: here, by: "step" });
|