@mark1russell7/coeval 0.14.0 → 0.15.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/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.14.0",
4
+ "version": "0.15.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 } 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.
@@ -257,3 +258,110 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
257
258
  }
258
259
  return { ok: true, result, effects };
259
260
  }
261
+
262
+ /** The options of `agreeRun`: the options of `agree` without the second tier of a language. */
263
+ export interface AgreeRunOptions extends Omit<AgreeOptions, "other" | "perform"> {
264
+ /**
265
+ * The host of the run of the plan (RUN.AGREES). A recorder wraps it, and each other tier replays the log of that run.
266
+ * Thus the host does each effect one time. Without it, an effect gives its payload.
267
+ */
268
+ readonly perform?: Perform;
269
+ }
270
+
271
+ /** The function `perform` of one run, with the effects that it got and the waits of `all` that settled. */
272
+ interface Watched {
273
+ readonly perform: Perform;
274
+ readonly effects: Effect[];
275
+ readonly settles: ReplaySettle[];
276
+ }
277
+
278
+ /**
279
+ * This function wraps the function `perform` of a run. The wrapper keeps each effect and each wait of `all` that
280
+ * settles. It gives each effect to the inner function, and each call of its journal to the inner journal.
281
+ */
282
+ function watched(perform: Perform): Watched {
283
+ const effects: Effect[] = [];
284
+ const settles: ReplaySettle[] = [];
285
+ const inner = (perform as { readonly [JOURNAL]?: Journal })[JOURNAL];
286
+ const settled = (all: string, item: number): void => {
287
+ settles.push({ all, item });
288
+ inner?.settled?.(all, item);
289
+ };
290
+ const journal: Journal = inner?.next === undefined ? { settled } : { settled, next: (all, waiting) => inner.next?.(all, waiting) };
291
+ const host = (effect: Effect): unknown => {
292
+ effects.push(effect);
293
+ return perform(effect);
294
+ };
295
+ return { perform: Object.assign(host, { [JOURNAL]: journal }), effects, settles };
296
+ }
297
+
298
+ function settleText(settle: ReplaySettle | undefined): string {
299
+ return settle === undefined ? "no item" : `the item ${settle.item} of ${settle.all}`;
300
+ }
301
+
302
+ /** This function gives the first difference of two orders of the waits of `all`, or `undefined` when they agree. */
303
+ function settlesDiffer(a: readonly ReplaySettle[], b: readonly ReplaySettle[]): string | undefined {
304
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
305
+ const x = a[index];
306
+ const y = b[index];
307
+ 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.`;
308
+ }
309
+ return undefined;
310
+ }
311
+
312
+ /**
313
+ * This function compares the run of a document in a plan with its run in other tiers (RUN.AGREES). First the plan
314
+ * starts the run with a recorder around the host. Then each tier replays the log of that run. These tiers are the
315
+ * reference run, the traced run, a second run of the plan and the plan with the other choice of folds.
316
+ *
317
+ * The rules of `agree` compare the results and the effects, with their addresses and outcomes. A tier must also
318
+ * settle the waits of each `all` in the order of the plan, thus a replay keeps the order of the waits.
319
+ *
320
+ * @param machine - The world, the evaluators and the runners.
321
+ * @param doc - The document.
322
+ * @param context - The names and the capabilities at the root.
323
+ * @param options - The tiers, the definitions for the reference, the rules of the comparison and the host.
324
+ * @returns The result of the plan, or the first tier that disagrees and the reason.
325
+ */
326
+ export async function agreeRun(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeRunOptions = {}): Promise<Agreement> {
327
+ const declared = {
328
+ ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
329
+ ...(options.verb === undefined ? {} : { verb: options.verb }),
330
+ };
331
+ const observe = options.observe;
332
+ observe?.();
333
+ // A fold moves host calls to the compilation, thus a plan whose calls the host compares has no folds (PLAN.FOLD).
334
+ const folds = observe === undefined;
335
+ const plan = prepare(machine, doc, { ...declared, fold: folds });
336
+ const recording = recorder(options.perform);
337
+ const first = watched(recording.perform);
338
+ const result = await plan.run(context, { perform: first.perform });
339
+ const saw = observe?.();
340
+ const log = recording.log();
341
+
342
+ for (const tier of options.tiers ?? DEFAULT_TIERS) {
343
+ const replay = watched(replayer(log));
344
+ const host = { perform: replay.perform };
345
+ let other: Result;
346
+ if (tier === "reference") {
347
+ other = await referenceRun(machine, doc, context, { ...declared, ...host, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
348
+ } else if (tier === "traced") {
349
+ other = (await run(machine, doc, context, { ...declared, ...host })).result;
350
+ } else if (tier === "folds") {
351
+ other = await prepare(machine, doc, { ...declared, fold: !folds }).run(context, host);
352
+ } else {
353
+ other = await plan.run(context, host);
354
+ }
355
+ // The folds of the tier folds call the host when it compiles. Thus only its result, effects and waits compare.
356
+ const compareCalls = observe !== undefined && tier !== "folds";
357
+ const calls = observe?.();
358
+ if (overflowed(result) || overflowed(other)) continue;
359
+ const why =
360
+ resultDiffers(result, other, options) ??
361
+ effectsDiffer(first.effects, replay.effects, options) ??
362
+ settlesDiffer(first.settles, replay.settles) ??
363
+ (compareCalls && !sameValue(saw, calls, options.equal) ? "The host saw other calls in the tier." : undefined);
364
+ if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
365
+ }
366
+ return { ok: true, result, effects: first.effects };
367
+ }
package/src/evaluate.ts CHANGED
@@ -4,6 +4,7 @@ import { answerOf, ask, type Answer } from "./ladder.ts";
4
4
  import type { CapsRule, Kind, KindFacts, Side } from "./world.ts";
5
5
  import type { Domain } from "./types.ts";
6
6
  import type { TypeRule } from "./typing.ts";
7
+ import type { StaticRead } from "./facts.ts";
7
8
  import { type ErrorCode, type EvalError, type Result, Failure, fail, ok, placed, thrown, describe } from "./errors.ts";
8
9
  import {
9
10
  type Cap,
@@ -17,6 +18,7 @@ import {
17
18
  type Thing,
18
19
  ROOTS,
19
20
  context as makeRootContext,
21
+ derefStep,
20
22
  isHostThing,
21
23
  isRef,
22
24
  resolve,
@@ -269,8 +271,8 @@ export interface Read {
269
271
  */
270
272
  readonly free: boolean;
271
273
  /**
272
- * `true` when the kernel adds the read, because a value keeps a root of the context for later (READS.KEPT). Such a
273
- * read has the whole root as its path.
274
+ * `true` when the kernel adds the read, because a value keeps the context for later (READS.KEPT). Such a read has
275
+ * a whole root as its path, or a path that a closure can read (READS.CLOSURE).
274
276
  */
275
277
  readonly kept?: true;
276
278
  /** The hops of the resolution, only when the code asked for them. `verifyReads` then compares each hop too. */
@@ -459,9 +461,13 @@ export interface Runtime {
459
461
  /**
460
462
  * This optional function tells the runtime that a value keeps the context for later, for example a closure of
461
463
  * `fn`. A later use of the value reads that context, and no evaluation records these reads. Thus the list of reads
462
- * gets a read of each root of the context (READS.KEPT).
464
+ * gets a kept read of each path that the value can read (READS.CLOSURE). Without the static reads, the list gets a
465
+ * read of each root of the context (READS.KEPT).
466
+ *
467
+ * @param context - The context that the value keeps.
468
+ * @param reads - This function gives the static reads of the value, or `undefined` when they are not known.
463
469
  */
464
- keepsContext?(): void;
470
+ keepsContext?(context?: Context, reads?: () => readonly StaticRead[] | undefined): void;
465
471
  }
466
472
 
467
473
  /**
@@ -894,6 +900,32 @@ interface Reading {
894
900
  kept: boolean;
895
901
  /** `true` after the kernel added the read of the root `self`. */
896
902
  self: boolean;
903
+ /** The paths of the kept reads of closures, as JSON texts (READS.CLOSURE). The set is made at the first such read. */
904
+ paths?: Set<string>;
905
+ }
906
+
907
+ /**
908
+ * This function gives the path of a kept read (READS.CLOSURE). The evaluation did not ask for the read. Thus the read
909
+ * does not step into a host thing, and the path ends at the first host thing. The host gives a new thing for a new state.
910
+ * A reference in the data reads the context of its reader, thus the function then gives `undefined`.
911
+ *
912
+ * @param nav - The world and the code of the evaluators.
913
+ * @param root - The context at the root of the evaluation.
914
+ * @param path - The root and the keys of the static read.
915
+ * @returns The path of the kept read, or `undefined` for a reference on the way.
916
+ */
917
+ function keptPath(nav: Navigator, root: Context, path: readonly unknown[]): readonly unknown[] | undefined {
918
+ const adopt = nav.adopt ?? ((value: unknown): Thing => value);
919
+ let here = adopt(root[path[0] as Root]);
920
+ for (let index = 1; ; index += 1) {
921
+ if (isHostThing(here)) return index === path.length ? path : path.slice(0, index);
922
+ if (nav.plain !== true && isRef(here)) return undefined;
923
+ if (index >= path.length) return path;
924
+ const reached = derefStep(nav, here, path[index] as Key);
925
+ // A step that reaches nothing fails, and the resolution of the path fails at the same step.
926
+ if (reached === undefined) return path;
927
+ here = adopt(reached.thing);
928
+ }
897
929
  }
898
930
 
899
931
  /** This function makes the record of the reads of an evaluation, when the host gives a list. */
@@ -995,13 +1027,45 @@ class Session implements Runtime {
995
1027
  return resolve(this.machine, ctx, path, hops ? WITH_HOPS : WITHOUT_HOPS);
996
1028
  }
997
1029
 
998
- keepsContext(): void {
1030
+ keepsContext(ctx?: Context, reads?: () => readonly StaticRead[] | undefined): void {
999
1031
  const reading = this.reading;
1000
1032
  if (reading === undefined || reading.kept) return;
1033
+ const known = ctx === undefined ? undefined : reads?.();
1034
+ // A value without its static reads can read each root later (READS.KEPT).
1035
+ if (ctx !== undefined && known !== undefined && known.every((read) => this.keepRead(reading, ctx, read))) return;
1001
1036
  reading.kept = true;
1002
1037
  this.keep(reading, reading.self ? ROOTS.filter((root) => root !== "self") : ROOTS);
1003
1038
  }
1004
1039
 
1040
+ /**
1041
+ * This function adds the kept read of one static read of a closure (READS.CLOSURE). The read resolves from the
1042
+ * context at the root, one time in an evaluation. A name that a kind binds in the kept context gives no read,
1043
+ * because the reads of its binding are already in the list.
1044
+ *
1045
+ * @returns `false` when the path meets a reference in the data. Then the closure keeps each whole root.
1046
+ */
1047
+ private keepRead(reading: Reading, ctx: Context, read: StaticRead): boolean {
1048
+ const root = read.root;
1049
+ const exact: readonly unknown[] = [root, ...read.keys];
1050
+ if (!isFree(reading.root, ctx, exact)) {
1051
+ // A kind that sets another self can give the old self on, as for a direct read (READS.KEPT).
1052
+ if (root === "self" && !reading.self) this.keep(reading, ["self"]);
1053
+ return true;
1054
+ }
1055
+ // A read of the whole root self already holds each path of self.
1056
+ if (root === "self" && reading.self) return true;
1057
+ // A step document can walk outside the keys of its path, thus such a read keeps the whole root.
1058
+ const path = keptPath(this.machine, reading.root, read.exact ? exact : [root]);
1059
+ if (path === undefined) return false;
1060
+ const paths = (reading.paths ??= new Set());
1061
+ const text = JSON.stringify(path);
1062
+ if (paths.has(text) || paths.has(JSON.stringify([root]))) return true;
1063
+ paths.add(text);
1064
+ if (path.length === 1 && root === "self") reading.self = true;
1065
+ reading.list.push({ path, reached: readAgain(this.machine, reading.root, path, false).reached, free: true, kept: true });
1066
+ return true;
1067
+ }
1068
+
1005
1069
  /**
1006
1070
  * This function adds a read of each whole root to the list, from the context at the root (READS.KEPT). An exception
1007
1071
  * of the host gives the error value of the read, because the evaluation did not ask for the read.
package/src/formula.ts CHANGED
@@ -7,6 +7,7 @@ import { type MachinePart, mergeMachines } from "./merge.ts";
7
7
  import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeText } from "./types.ts";
8
8
  import type { TypeInput, TypeRule } from "./typing.ts";
9
9
  import { decodeDomain, domainTypeOf } from "./codec.ts";
10
+ import { type StaticRead, factsOf } from "./facts.ts";
10
11
 
11
12
  /** An operation of a formula language: a function of the values of its arguments. */
12
13
  export type OpFn = (...args: never[]) => unknown;
@@ -777,7 +778,7 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
777
778
  };
778
779
  }),
779
780
 
780
- "formula.fn": compiled(({ value, data }) => {
781
+ "formula.fn": compiled(({ value, data, doc, machine }) => {
781
782
  const params = isRecord(data) ? data["params"] : undefined;
782
783
  if (!Array.isArray(params) || !params.every((param) => typeof param === "string") || new Set(params).size !== params.length) {
783
784
  const bad = fail("#VALUE!", "The params of fn are a list of different texts.");
@@ -785,9 +786,22 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
785
786
  }
786
787
  const names = params as readonly string[];
787
788
  const body = partsOf(value)["body"] as CompiledPart;
789
+ // The static reads of the function come at the first closure of an evaluation with a list of reads.
790
+ let known: readonly StaticRead[] | undefined | null = null;
791
+ const reads = (): readonly StaticRead[] | undefined => {
792
+ if (known !== null) return known;
793
+ // Only a pure document reads its context through the runtime alone (FACTS.PURE). Each other closure keeps each root.
794
+ try {
795
+ const facts = factsOf(machine, doc);
796
+ known = facts.pure ? facts.reads : undefined;
797
+ } catch {
798
+ known = undefined;
799
+ }
800
+ return known;
801
+ };
788
802
  return (ctx, rt) => {
789
- // A closure keeps the context for each later use, thus the list of reads gets each root (READS.KEPT).
790
- rt.keepsContext?.();
803
+ // A closure keeps the context for each later use, thus the list of reads gets each path that it can read (READS.CLOSURE).
804
+ rt.keepsContext?.(ctx, reads);
791
805
  const closure = (...args: readonly unknown[]): unknown => {
792
806
  const scope: Record<string, unknown> = { ...ctx.names };
793
807
  names.forEach((name, index) => setOwn(scope, name, args[index]));
package/src/index.ts CHANGED
@@ -250,8 +250,8 @@ export {
250
250
  print,
251
251
  syntaxFormat,
252
252
  } from "./syntax.ts";
253
- export { type Definitions, type ReferenceOptions, STD_DEFINITIONS, formulaDefinitions, reference } from "./reference.ts";
254
- export { type AgreeOptions, type Agreement, type Tier, type TierInput, type TierResult, agree, sameValue } from "./agree.ts";
253
+ export { type Definitions, type ReferenceOptions, type ReferenceRunOptions, STD_DEFINITIONS, formulaDefinitions, reference, referenceRun } from "./reference.ts";
254
+ export { type AgreeOptions, type AgreeRunOptions, type Agreement, type Tier, type TierInput, type TierResult, agree, agreeRun, sameValue } from "./agree.ts";
255
255
  export { type ConformOptions, type ConformProbe, type Finding, type Obligation, conforms } from "./conform.ts";
256
256
  export { sameResult } from "./same.ts";
257
257
  export { type ReadsCheck, type VerifyOptions, verifyReads } from "./reads.ts";