@mark1russell7/coeval 0.15.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/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.15.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,7 +1,7 @@
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 Journal, type Machine, type Perform, JOURNAL, evaluate, makeContext, prepare, run } 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
6
  import { type Definitions, reference, referenceRun } from "./reference.ts";
7
7
  import { type ReplaySettle, recorder, replayer } from "./replay.ts";
@@ -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 and
49
- * the plan with the other choice of folds.
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,7 +87,7 @@ 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 =
@@ -132,7 +134,8 @@ function sameInTiers(x: unknown, y: unknown): boolean | undefined {
132
134
 
133
135
  const CLOSURE = /* @__PURE__ */ Symbol.for("coeval.closure");
134
136
 
135
- function closureParams(fn: object): unknown {
137
+ /** This function gives the parameters of a closure of `fn`, or `undefined` for another function. */
138
+ export function closureParams(fn: object): unknown {
136
139
  return (fn as { readonly [CLOSURE]?: { readonly params: readonly string[] } })[CLOSURE]?.params;
137
140
  }
138
141
 
@@ -184,6 +187,57 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
184
187
  return undefined;
185
188
  }
186
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
+
187
241
  /**
188
242
  * This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
189
243
  * results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
@@ -228,6 +282,10 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
228
282
  const traced = evaluate(machine, doc, context, { ...declared, ...mock });
229
283
  other = traced.result;
230
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;
231
289
  } else if (tier === "folds") {
232
290
  other = prepare(machine, doc, { ...declared, fold: !folds }).evaluate(context, { effects: otherEffects, ...mock });
233
291
  if (observe !== undefined) {
@@ -340,6 +398,8 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
340
398
  const log = recording.log();
341
399
 
342
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;
343
403
  const replay = watched(replayer(log));
344
404
  const host = { perform: replay.perform };
345
405
  let other: Result;
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 result = walker({
655
- data: envelope.value,
656
- here,
657
- context: ctx,
658
- deref: (thing, key) => {
659
- const reached = derefStep(nav, thing, key);
660
- return reached === undefined ? undefined : adopt(reached.thing);
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" });
package/src/evaluate.ts CHANGED
@@ -904,30 +904,95 @@ interface Reading {
904
904
  paths?: Set<string>;
905
905
  }
906
906
 
907
+ /** The walk of a kept path: the path, and what it reached when a host thing answered a step with `known`. */
908
+ interface KeptWalk {
909
+ readonly path: readonly unknown[];
910
+ /** What the path reached. Without it, the read resolves its path with `readAgain`. */
911
+ readonly reached?: Result;
912
+ }
913
+
907
914
  /**
908
915
  * 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.
916
+ * does not step into a host thing with `deref`. A host thing with `known` answers a step without new work
917
+ * (SCHED.CLOSURE). Each other host thing ends the path, and a host thing that does not know the step ends it too.
910
918
  * A reference in the data reads the context of its reader, thus the function then gives `undefined`.
911
919
  *
912
920
  * @param nav - The world and the code of the evaluators.
913
921
  * @param root - The context at the root of the evaluation.
914
922
  * @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.
923
+ * @returns The path of the kept read with what it reached after a step of `known`, or `undefined` for a reference on the way.
916
924
  */
917
- function keptPath(nav: Navigator, root: Context, path: readonly unknown[]): readonly unknown[] | undefined {
925
+ function keptPath(nav: Navigator, root: Context, path: readonly unknown[]): KeptWalk | undefined {
918
926
  const adopt = nav.adopt ?? ((value: unknown): Thing => value);
919
927
  let here = adopt(root[path[0] as Root]);
928
+ // This mark is `true` after a step of `known`. Then the walk gives what the path reached, and no resolution follows.
929
+ let known = false;
930
+ const end = (index: number): KeptWalk => {
931
+ const kept = index >= path.length ? path : path.slice(0, index);
932
+ return known ? { path: kept, reached: ok(here) } : { path: kept };
933
+ };
920
934
  for (let index = 1; ; index += 1) {
921
- if (isHostThing(here)) return index === path.length ? path : path.slice(0, index);
935
+ if (isHostThing(here)) {
936
+ if (index >= path.length || here.known === undefined) return end(index);
937
+ let step: { readonly thing: Thing } | undefined;
938
+ try {
939
+ step = here.known(path[index] as Key);
940
+ } catch (error) {
941
+ // A failed value gives its error to the read, as an exception in `readAgain` does.
942
+ return { path: path.slice(0, index + 1), reached: thrown(error, READ_FAILED) };
943
+ }
944
+ if (step === undefined) return end(index);
945
+ known = true;
946
+ here = adopt(step.thing);
947
+ continue;
948
+ }
922
949
  if (nav.plain !== true && isRef(here)) return undefined;
923
- if (index >= path.length) return path;
950
+ if (index >= path.length) return end(index);
924
951
  const reached = derefStep(nav, here, path[index] as Key);
925
952
  // A step that reaches nothing fails, and the resolution of the path fails at the same step.
926
- if (reached === undefined) return path;
953
+ if (reached === undefined) return known ? end(index) : { path };
927
954
  here = adopt(reached.thing);
928
955
  }
929
956
  }
930
957
 
958
+ /**
959
+ * This function resolves the path of a kept read again, for `verifyReads` (SCHED.CLOSURE). It walks the path as
960
+ * `keptPath` did. A host thing with `known` answers its step. A path without such a step resolves with `readAgain`.
961
+ *
962
+ * @param nav - The world and the code of the evaluators.
963
+ * @param ctx - The new context.
964
+ * @param path - The path of the kept read.
965
+ * @returns What the path reaches, or `undefined` when a host does not know a step yet. Then the read changed.
966
+ */
967
+ export function keptAgain(nav: Navigator, ctx: Context, path: readonly unknown[]): Result | undefined {
968
+ const adopt = nav.adopt ?? ((value: unknown): Thing => value);
969
+ let here = adopt(ctx[path[0] as Root]);
970
+ let known = false;
971
+ // Before a step of `known`, each other answer of the walk is the answer of `readAgain`, as before SCHED.CLOSURE.
972
+ const other = (): Result | undefined => (known ? undefined : readAgain(nav, ctx, path, false).reached);
973
+ for (let index = 1; index < path.length; index += 1) {
974
+ if (isHostThing(here)) {
975
+ if (here.known === undefined) return other();
976
+ let step: { readonly thing: Thing } | undefined;
977
+ try {
978
+ step = here.known(path[index] as Key);
979
+ } catch (error) {
980
+ return thrown(error, READ_FAILED);
981
+ }
982
+ if (step === undefined) return undefined;
983
+ known = true;
984
+ here = adopt(step.thing);
985
+ continue;
986
+ }
987
+ if (nav.plain !== true && isRef(here)) return other();
988
+ const reached = derefStep(nav, here, path[index] as Key);
989
+ if (reached === undefined) return other();
990
+ here = adopt(reached.thing);
991
+ }
992
+ if (!known) return other();
993
+ return nav.plain !== true && isRef(here) ? undefined : ok(here);
994
+ }
995
+
931
996
  /** This function makes the record of the reads of an evaluation, when the host gives a list. */
932
997
  function readingOf(list: Read[] | undefined, root: Context): Reading | undefined {
933
998
  return list === undefined ? undefined : { list, root, kept: false, self: false };
@@ -1055,14 +1120,15 @@ class Session implements Runtime {
1055
1120
  // A read of the whole root self already holds each path of self.
1056
1121
  if (root === "self" && reading.self) return true;
1057
1122
  // 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;
1123
+ const walk = keptPath(this.machine, reading.root, read.exact ? exact : [root]);
1124
+ if (walk === undefined) return false;
1125
+ const path = walk.path;
1060
1126
  const paths = (reading.paths ??= new Set());
1061
1127
  const text = JSON.stringify(path);
1062
1128
  if (paths.has(text) || paths.has(JSON.stringify([root]))) return true;
1063
1129
  paths.add(text);
1064
1130
  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 });
1131
+ reading.list.push({ path, reached: walk.reached ?? readAgain(this.machine, reading.root, path, false).reached, free: true, kept: true });
1066
1132
  return true;
1067
1133
  }
1068
1134
 
@@ -1402,6 +1468,15 @@ export interface PrepareOptions {
1402
1468
  * evaluates one time, when the plan compiles. Its result is one frozen value for each evaluation (PLAN.FOLD).
1403
1469
  */
1404
1470
  readonly fold?: boolean;
1471
+ /**
1472
+ * The values that the host declares fixed for each evaluation of the plan (PLAN.FIXED, D60). The field `names` gives
1473
+ * a value for each fixed name. A path from a fixed name then resolves one time, when the plan compiles, and a pure
1474
+ * part that reads only fixed names folds. Only a deeply frozen value of plain data fixes its name.
1475
+ *
1476
+ * Each folded part compares the value of its names with one own-key test and `Object.is`. A different value gives
1477
+ * the code without the fold. A plan without folds ignores this option.
1478
+ */
1479
+ readonly fixed?: { readonly names?: Readonly<Record<string, unknown>> };
1405
1480
  }
1406
1481
 
1407
1482
  /** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
@@ -1440,6 +1515,8 @@ interface Compiler {
1440
1515
  readonly noAnswer: Answer;
1441
1516
  /** `true` when closed pure parts fold (PLAN.FOLD). A plan with a trace does not fold. */
1442
1517
  readonly fold: boolean;
1518
+ /** The fixed names and their values, or `undefined` when the plan has none (PLAN.FIXED). */
1519
+ readonly fixed: Readonly<Record<string, unknown>> | undefined;
1443
1520
  /** The runtime of the folds of this compilation. The first fold makes it. */
1444
1521
  probe?: Probe;
1445
1522
  }
@@ -1600,7 +1677,11 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1600
1677
  if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
1601
1678
  const free = freeOf(doc, kind, split.parts, frees);
1602
1679
  if (free === undefined) return done;
1603
- return free.length === 0 ? folded(c, done) : { code: done.code, run: done.run, free };
1680
+ if (free.length === 0) return folded(c, done, undefined);
1681
+ // A part that reads only fixed names folds, and its code compares the values of these names (PLAN.FIXED).
1682
+ const fixed = c.fixed;
1683
+ if (fixed !== undefined && free.every((each) => Object.hasOwn(fixed, each))) return folded(c, done, free);
1684
+ return { code: done.code, run: done.run, free };
1604
1685
  }
1605
1686
 
1606
1687
  /*
@@ -1693,6 +1774,20 @@ class Probe implements Runtime {
1693
1774
  this.root = root;
1694
1775
  }
1695
1776
 
1777
+ /** The context of a part that folds with fixed names: the context of the probe, with the fixed names (PLAN.FIXED). */
1778
+ private withFixed: Context | undefined;
1779
+
1780
+ /**
1781
+ * This function gives the context of a part that folds with fixed names. Its root `names` holds only the fixed
1782
+ * names, thus the probe resolves each fixed name and refuses each other name.
1783
+ *
1784
+ * @param fixed - The fixed names and their values.
1785
+ * @returns The context.
1786
+ */
1787
+ fixedRoot(fixed: Readonly<Record<string, unknown>>): Context {
1788
+ return (this.withFixed ??= { ...this.root, names: fixed });
1789
+ }
1790
+
1696
1791
  resolve(ctx: Context, path: readonly unknown[]): Resolution {
1697
1792
  const name = path[1];
1698
1793
  if (path[0] === "names" && isKey(name) && Object.hasOwn(ctx.names, String(name))) {
@@ -1757,25 +1852,106 @@ function trap(refuse: () => void, copies: boolean): object {
1757
1852
  * one deeply frozen copy, and the code gives that copy at each evaluation (PLAN.SHARED). A fold that the probe cancels
1758
1853
  * gives the code without free names, thus no parent tries the fold again.
1759
1854
  */
1760
- function folded(c: Compiler, done: Compiled): Compiled {
1855
+ function folded(c: Compiler, done: Compiled, fixed: readonly string[] | undefined): Compiled {
1761
1856
  const probe = (c.probe ??= new Probe(c.machine));
1762
1857
  probe.refused = false;
1763
1858
  probe.reads = 0;
1764
1859
  let constant: Result | undefined;
1765
1860
  try {
1766
- const result = done.code(probe.root, probe);
1861
+ const root = fixed === undefined ? probe.root : probe.fixedRoot(c.fixed as Readonly<Record<string, unknown>>);
1862
+ const result = done.code(root, probe);
1767
1863
  if (!probe.refused && (result.ok || result.error.code !== "#DEPTH!")) constant = sharedCopy(result) as Result | undefined;
1768
1864
  } catch {
1769
1865
  constant = undefined;
1770
1866
  }
1771
1867
  if (constant === undefined) return { code: done.code, run: done.run };
1772
1868
  const value = constant;
1869
+ const original = done.code;
1870
+ if (fixed !== undefined) return { code: fixedCode(fixed, c.fixed as Readonly<Record<string, unknown>>, value, original), run: undefined, free: fixed };
1773
1871
  if (probe.reads === 0) return { code: () => value, run: undefined, free: CLOSED };
1774
1872
  // The part reads its own names. A list of reads gets each read, thus the code evaluates again (READS.RECORD).
1775
- const original = done.code;
1776
1873
  return { code: (ctx, rt) => (rt instanceof Session && !rt.traced && !rt.recording ? value : original(ctx, rt)), run: undefined, free: CLOSED };
1777
1874
  }
1778
1875
 
1876
+ /**
1877
+ * This function gives the code of a part that folded with fixed names (PLAN.FIXED, D60). The code gives the folded
1878
+ * value when the context has the fixed value of each name, by an own-key test and `Object.is`. A fixed value is
1879
+ * deeply frozen, thus the same object gives the same result. Each other context, a list of reads and another runtime
1880
+ * get the code without the fold. The probe of a parent fold gets the folded value, and it counts a read.
1881
+ */
1882
+ function fixedCode(names: readonly string[], fixed: Readonly<Record<string, unknown>>, value: Result, original: Code): Code {
1883
+ if (names.length === 1) {
1884
+ const name = names[0] as string;
1885
+ const want = fixed[name];
1886
+ return (ctx, rt) => {
1887
+ const given = ctx.names;
1888
+ return Object.hasOwn(given, name) && Object.is(given[name], want) && folds(rt) ? value : original(ctx, rt);
1889
+ };
1890
+ }
1891
+ const wants = names.map((name) => fixed[name]);
1892
+ return (ctx, rt) => {
1893
+ const given = ctx.names;
1894
+ for (let index = 0; index < names.length; index += 1) {
1895
+ const name = names[index] as string;
1896
+ if (!Object.hasOwn(given, name) || !Object.is(given[name], wants[index])) return original(ctx, rt);
1897
+ }
1898
+ return folds(rt) ? value : original(ctx, rt);
1899
+ };
1900
+ }
1901
+
1902
+ /**
1903
+ * This function tells if the code of a part with fixed names can give its folded value in a runtime. The probe of a
1904
+ * fold counts the read of the part, as the code without the fold reads the names.
1905
+ */
1906
+ function folds(rt: Runtime): boolean {
1907
+ if (rt instanceof Session) return !rt.recording;
1908
+ if (!(rt instanceof Probe)) return false;
1909
+ rt.reads += 1;
1910
+ return true;
1911
+ }
1912
+
1913
+ /**
1914
+ * This function gives the fixed names of the options of `prepare` (PLAN.FIXED). A name fixes only when its value is
1915
+ * deeply frozen plain data. Then no change in place can make the folded value stale.
1916
+ *
1917
+ * @returns A record without a prototype, or `undefined` when no name fixes.
1918
+ */
1919
+ function fixedNamesOf(given: Readonly<Record<string, unknown>> | undefined): Readonly<Record<string, unknown>> | undefined {
1920
+ if (given === undefined) return undefined;
1921
+ let out: Record<string, unknown> | undefined;
1922
+ for (const name of Object.keys(given)) {
1923
+ const value = given[name];
1924
+ if (!frozenPlain(value)) continue;
1925
+ out ??= Object.create(null) as Record<string, unknown>;
1926
+ Object.defineProperty(out, name, { value, enumerable: true });
1927
+ }
1928
+ return out;
1929
+ }
1930
+
1931
+ /** This function tells if a value is plain data, as `sharedCopy` reads it, and if each of its objects is frozen. */
1932
+ function frozenPlain(value: unknown): boolean {
1933
+ const seen = new Set<object>();
1934
+ const walk = (here: unknown): boolean => {
1935
+ if (typeof here === "function") return false;
1936
+ if (typeof here !== "object" || here === null || SHARED.has(here) || seen.has(here)) return true;
1937
+ if (!Object.isFrozen(here)) return false;
1938
+ seen.add(here);
1939
+ const proto: unknown = Object.getPrototypeOf(here);
1940
+ if (Array.isArray(here) ? proto !== Array.prototype : proto !== Object.prototype && proto !== null) return false;
1941
+ for (const key of Reflect.ownKeys(here)) {
1942
+ const field = Object.getOwnPropertyDescriptor(here, key);
1943
+ if (typeof key !== "string" || field === undefined || !("value" in field) || !walk(field.value)) return false;
1944
+ }
1945
+ return true;
1946
+ };
1947
+ try {
1948
+ return walk(value);
1949
+ } catch {
1950
+ // A very deep value overflows the stack of the host.
1951
+ return false;
1952
+ }
1953
+ }
1954
+
1779
1955
  /** The objects that a fold made. Each one is deeply frozen, thus a later fold uses it again and does not copy it. */
1780
1956
  const SHARED: WeakSet<object> = /* @__PURE__ */ new WeakSet();
1781
1957
 
@@ -1785,9 +1961,13 @@ const NOT_PLAIN: unique symbol = /* @__PURE__ */ Symbol("not plain");
1785
1961
  /**
1786
1962
  * This function gives a deeply frozen copy of plain data, or `undefined` for other values. Plain data is a primitive or
1787
1963
  * an array without holes and extra keys. It is also a record of enumerable data fields with the prototype of an object
1788
- * or no prototype. The copy keeps cycles and shared objects. The objects of the host do not change.
1964
+ * or no prototype. The copy keeps cycles and shared objects. The objects of the host do not change. The tier `fixed`
1965
+ * of `agree` uses it. The package does not export it.
1966
+ *
1967
+ * @param value - The value.
1968
+ * @returns The frozen copy, or `undefined`.
1789
1969
  */
1790
- function sharedCopy(value: unknown): unknown {
1970
+ export function sharedCopy(value: unknown): unknown {
1791
1971
  try {
1792
1972
  const out = copyPlain(value, new Map());
1793
1973
  return out === NOT_PLAIN ? undefined : out;
@@ -2042,7 +2222,9 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
2042
2222
  const verb = options.verb ?? "eval";
2043
2223
  const noAnswer: Answer = verb === "eval" ? NO_ANSWER : { verb, error: "no-answer", steps: [] };
2044
2224
  const traced = options.trace === true;
2045
- const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold: !traced && options.fold !== false };
2225
+ const fold = !traced && options.fold !== false;
2226
+ const fixed = fold ? fixedNamesOf(options.fixed?.names) : undefined;
2227
+ const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold, fixed };
2046
2228
  let root: Compiled;
2047
2229
  try {
2048
2230
  root = compileNode(c, doc, [], options.declaredType, 0);
package/src/index.ts CHANGED
@@ -176,6 +176,7 @@ export {
176
176
  STD_WORLD,
177
177
  attributeTrace,
178
178
  nearestWalker,
179
+ placeOf,
179
180
  } from "./stdlib.ts";
180
181
  export {
181
182
  type Edge,
@@ -266,6 +267,15 @@ export {
266
267
  edgesOf,
267
268
  schedule,
268
269
  } from "./schedule.ts";
270
+ export {
271
+ type AgreeScheduleOptions,
272
+ type ScheduleAgreement,
273
+ type ScheduleAgreementCounts,
274
+ type ScheduleEngine,
275
+ type ScheduleEngineFactory,
276
+ type ScheduleEngineStart,
277
+ agreeSchedule,
278
+ } from "./meaning.ts";
269
279
  export { type GraphCycle, type GraphEdge, type GraphNode, type GraphOptions, type NodeGraph, components, cycles, heights, necessary, nodeLayers } from "./graph.ts";
270
280
  export { type Recorder, type ReplayEntry, type ReplayLog, type ReplayOptions, type ReplaySettle, recorder, replayer } from "./replay.ts";
271
281
  export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";