@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/src/evaluate.ts CHANGED
@@ -146,13 +146,23 @@ const GRANT = /* @__PURE__ */ Symbol("coeval.grant");
146
146
  /**
147
147
  * The key of the epoch of a grant (CAP.NARROW-KEPT). The epoch is the list of capabilities that the host gave the root
148
148
  * of the evaluation of the grant. A spread of a context keeps its epoch, as it keeps its grant. An
149
- * effect and a resolution read a context of another epoch as a context without a grant.
149
+ * effect and a resolution read a context of another epoch as a context without a grant. The root has no field of
150
+ * the epoch, because its grant is the list of the host (`epochOf`).
150
151
  */
151
152
  const EPOCH = /* @__PURE__ */ Symbol("coeval.epoch");
152
153
 
153
154
  /** A context with the grant of the kernel and the epoch of that grant. */
154
155
  type Granted = Context & { readonly [GRANT]?: readonly Cap[]; readonly [EPOCH]?: readonly Cap[] | undefined };
155
156
 
157
+ /**
158
+ * This function gives the epoch of the grant of a context (CAP.NARROW-KEPT). A context without the field of the epoch
159
+ * has the epoch of its grant. Only a root has no field, thus its grant is the list of the host. The root then has
160
+ * one field fewer, and each evaluation makes it faster (K154).
161
+ */
162
+ function epochOf(ctx: Context): readonly Cap[] | undefined {
163
+ return (ctx as Granted)[EPOCH] ?? (ctx as Granted)[GRANT];
164
+ }
165
+
156
166
  /**
157
167
  * This function gives the capabilities that a context and its grant both permit, the meet `intersectCaps` (CAP.MEET).
158
168
  * A context whose grant is its own list gives that list after one comparison. A context without a grant gives none.
@@ -171,10 +181,37 @@ function grantedCaps(ctx: Context): readonly Cap[] {
171
181
  function granted(ctx: Context): Context {
172
182
  if ((ctx as Granted)[GRANT] === ctx.caps) return ctx;
173
183
  const caps = grantedCaps(ctx);
174
- const out: Granted = { ...ctx, caps, [GRANT]: caps };
184
+ const out: Granted = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx) };
175
185
  return out;
176
186
  }
177
187
 
188
+ /**
189
+ * This function tells if a list of capabilities is equal to the epoch of the evaluation (CAP.EPOCH-EQUAL). An equal list
190
+ * has the same length, and at each index the same text or the same keys in the same order. The runtime uses it only
191
+ * when the two lists are not the same object. Thus a host that gives a new copy of its list for each evaluation keeps
192
+ * the grant of a kept context.
193
+ */
194
+ function sameCaps(caps: readonly Cap[] | undefined, epoch: readonly Cap[]): boolean {
195
+ if (caps === undefined || caps.length !== epoch.length) return false;
196
+ for (let index = 0; index < caps.length; index += 1) {
197
+ const a = caps[index] as Cap;
198
+ const b = epoch[index] as Cap;
199
+ if (a === b) continue;
200
+ if (typeof a === "string" || typeof b === "string" || a.length !== b.length) return false;
201
+ for (let key = 0; key < a.length; key += 1) if (!Object.is(a[key], b[key])) return false;
202
+ }
203
+ return true;
204
+ }
205
+
206
+ /**
207
+ * This function tells if a context has the epoch of the evaluation: the same list, or an equal list (CAP.NARROW-KEPT,
208
+ * CAP.EPOCH-EQUAL). The comparison of the entries happens only when the two lists are not the same object.
209
+ */
210
+ function inEpoch(ctx: Context, epoch: readonly Cap[]): boolean {
211
+ const own = epochOf(ctx);
212
+ return own === epoch || sameCaps(own, epoch);
213
+ }
214
+
178
215
  /**
179
216
  * This function gives a context of another epoch the epoch of the evaluation, without capabilities (CAP.NARROW-KEPT).
180
217
  * Thus the context reads as a context without a grant. The kernel cannot know the document of the context in this
@@ -190,13 +227,23 @@ function ungranted(ctx: Context, epoch: readonly Cap[]): Context {
190
227
  * The context has the meet of its list and its grant, and a context without a grant has none. Thus a read of the root
191
228
  * `caps` and a walker of a step document get the capabilities of the document from a widened context.
192
229
  * A context that the code narrowed keeps its own list. A context whose list does not change stays the same object.
193
- * A context of another epoch has no capabilities (CAP.NARROW-KEPT).
230
+ *
231
+ * A context of another epoch has no capabilities (CAP.NARROW-KEPT). An equal list of the host is the same epoch
232
+ * (CAP.EPOCH-EQUAL). The first test answers for a context of the epoch whose grant is its list, for example the root.
233
+ * The other contexts go to `readableOther`, thus the hot part of the function stays small (K154).
194
234
  */
195
235
  function readable(ctx: Context, epoch: readonly Cap[]): Context {
196
- if ((ctx as Granted)[EPOCH] !== epoch) return ctx.caps.length === 0 ? ctx : ungranted(ctx, epoch);
236
+ const grant = (ctx as Granted)[GRANT];
237
+ if (grant === ctx.caps && ((ctx as Granted)[EPOCH] ?? grant) === epoch) return ctx;
238
+ return readableOther(ctx, epoch);
239
+ }
240
+
241
+ /** This function gives the result of `readable` for each other context: a context of another epoch or a narrowed context. */
242
+ function readableOther(ctx: Context, epoch: readonly Cap[]): Context {
243
+ if (!inEpoch(ctx, epoch)) return ctx.caps.length === 0 ? ctx : ungranted(ctx, epoch);
197
244
  const caps = grantedCaps(ctx);
198
245
  if (caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0)) return ctx;
199
- const out: Granted = { ...ctx, caps, [GRANT]: caps };
246
+ const out: Granted = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx) };
200
247
  return out;
201
248
  }
202
249
 
@@ -207,8 +254,8 @@ function readable(ctx: Context, epoch: readonly Cap[]): Context {
207
254
  */
208
255
  function handed(parent: Context, next: Context): Context {
209
256
  const ctx = narrow(parent, next);
210
- const epoch = (parent as Granted)[EPOCH];
211
- if ((ctx as Granted)[GRANT] === ctx.caps && (ctx as Granted)[EPOCH] === epoch) return ctx;
257
+ const epoch = epochOf(parent);
258
+ if ((ctx as Granted)[GRANT] === ctx.caps && epochOf(ctx) === epoch) return ctx;
212
259
  const out: Granted = { ...ctx, [GRANT]: ctx.caps, [EPOCH]: epoch };
213
260
  return out;
214
261
  }
@@ -358,9 +405,11 @@ export interface Journal {
358
405
  *
359
406
  * @param all - The address of the `all`.
360
407
  * @param waiting - The indices of the items that wait.
408
+ * @param before - The number of the waits of this `all` that settled before this call. The kernel gives it, thus a
409
+ * replayer keeps no state of a run (RUN.REPLAY-RUNS). An older copy of the kernel does not give it.
361
410
  * @returns The index of an item that waits, or `undefined` for the order of the host.
362
411
  */
363
- readonly next?: (all: string, waiting: readonly number[]) => number | undefined;
412
+ readonly next?: (all: string, waiting: readonly number[], before?: number) => number | undefined;
364
413
  }
365
414
 
366
415
  /** This function gives the journal of a function `perform`, when it has one. */
@@ -904,30 +953,95 @@ interface Reading {
904
953
  paths?: Set<string>;
905
954
  }
906
955
 
956
+ /** The walk of a kept path: the path, and what it reached when a host thing answered a step with `known`. */
957
+ interface KeptWalk {
958
+ readonly path: readonly unknown[];
959
+ /** What the path reached. Without it, the read resolves its path with `readAgain`. */
960
+ readonly reached?: Result;
961
+ }
962
+
907
963
  /**
908
964
  * 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.
965
+ * does not step into a host thing with `deref`. A host thing with `known` answers a step without new work
966
+ * (SCHED.CLOSURE). Each other host thing ends the path, and a host thing that does not know the step ends it too.
910
967
  * A reference in the data reads the context of its reader, thus the function then gives `undefined`.
911
968
  *
912
969
  * @param nav - The world and the code of the evaluators.
913
970
  * @param root - The context at the root of the evaluation.
914
971
  * @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.
972
+ * @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
973
  */
917
- function keptPath(nav: Navigator, root: Context, path: readonly unknown[]): readonly unknown[] | undefined {
974
+ function keptPath(nav: Navigator, root: Context, path: readonly unknown[]): KeptWalk | undefined {
918
975
  const adopt = nav.adopt ?? ((value: unknown): Thing => value);
919
976
  let here = adopt(root[path[0] as Root]);
977
+ // This mark is `true` after a step of `known`. Then the walk gives what the path reached, and no resolution follows.
978
+ let known = false;
979
+ const end = (index: number): KeptWalk => {
980
+ const kept = index >= path.length ? path : path.slice(0, index);
981
+ return known ? { path: kept, reached: ok(here) } : { path: kept };
982
+ };
920
983
  for (let index = 1; ; index += 1) {
921
- if (isHostThing(here)) return index === path.length ? path : path.slice(0, index);
984
+ if (isHostThing(here)) {
985
+ if (index >= path.length || here.known === undefined) return end(index);
986
+ let step: { readonly thing: Thing } | undefined;
987
+ try {
988
+ step = here.known(path[index] as Key);
989
+ } catch (error) {
990
+ // A failed value gives its error to the read, as an exception in `readAgain` does.
991
+ return { path: path.slice(0, index + 1), reached: thrown(error, READ_FAILED) };
992
+ }
993
+ if (step === undefined) return end(index);
994
+ known = true;
995
+ here = adopt(step.thing);
996
+ continue;
997
+ }
922
998
  if (nav.plain !== true && isRef(here)) return undefined;
923
- if (index >= path.length) return path;
999
+ if (index >= path.length) return end(index);
924
1000
  const reached = derefStep(nav, here, path[index] as Key);
925
1001
  // A step that reaches nothing fails, and the resolution of the path fails at the same step.
926
- if (reached === undefined) return path;
1002
+ if (reached === undefined) return known ? end(index) : { path };
927
1003
  here = adopt(reached.thing);
928
1004
  }
929
1005
  }
930
1006
 
1007
+ /**
1008
+ * This function resolves the path of a kept read again, for `verifyReads` (SCHED.CLOSURE). It walks the path as
1009
+ * `keptPath` did. A host thing with `known` answers its step. A path without such a step resolves with `readAgain`.
1010
+ *
1011
+ * @param nav - The world and the code of the evaluators.
1012
+ * @param ctx - The new context.
1013
+ * @param path - The path of the kept read.
1014
+ * @returns What the path reaches, or `undefined` when a host does not know a step yet. Then the read changed.
1015
+ */
1016
+ export function keptAgain(nav: Navigator, ctx: Context, path: readonly unknown[]): Result | undefined {
1017
+ const adopt = nav.adopt ?? ((value: unknown): Thing => value);
1018
+ let here = adopt(ctx[path[0] as Root]);
1019
+ let known = false;
1020
+ // Before a step of `known`, each other answer of the walk is the answer of `readAgain`, as before SCHED.CLOSURE.
1021
+ const other = (): Result | undefined => (known ? undefined : readAgain(nav, ctx, path, false).reached);
1022
+ for (let index = 1; index < path.length; index += 1) {
1023
+ if (isHostThing(here)) {
1024
+ if (here.known === undefined) return other();
1025
+ let step: { readonly thing: Thing } | undefined;
1026
+ try {
1027
+ step = here.known(path[index] as Key);
1028
+ } catch (error) {
1029
+ return thrown(error, READ_FAILED);
1030
+ }
1031
+ if (step === undefined) return undefined;
1032
+ known = true;
1033
+ here = adopt(step.thing);
1034
+ continue;
1035
+ }
1036
+ if (nav.plain !== true && isRef(here)) return other();
1037
+ const reached = derefStep(nav, here, path[index] as Key);
1038
+ if (reached === undefined) return other();
1039
+ here = adopt(reached.thing);
1040
+ }
1041
+ if (!known) return other();
1042
+ return nav.plain !== true && isRef(here) ? undefined : ok(here);
1043
+ }
1044
+
931
1045
  /** This function makes the record of the reads of an evaluation, when the host gives a list. */
932
1046
  function readingOf(list: Read[] | undefined, root: Context): Reading | undefined {
933
1047
  return list === undefined ? undefined : { list, root, kept: false, self: false };
@@ -959,8 +1073,11 @@ class Session implements Runtime {
959
1073
  readonly perform: Perform | undefined,
960
1074
  readonly signal: StopSignal | undefined,
961
1075
  effects: Effect[] | undefined,
962
- /** The record of the reads. Without it, a resolution records nothing. */
963
- private readonly reading: Reading | undefined = undefined,
1076
+ /**
1077
+ * The record of the reads. Without it, a resolution records nothing. A nested evaluation of an equation of a tree
1078
+ * shares the record of the evaluation around it (READS.EQUATION).
1079
+ */
1080
+ readonly reading: Reading | undefined = undefined,
964
1081
  ) {
965
1082
  this.list = effects;
966
1083
  this.holder = traced ? holderNode() : undefined;
@@ -1055,14 +1172,15 @@ class Session implements Runtime {
1055
1172
  // A read of the whole root self already holds each path of self.
1056
1173
  if (root === "self" && reading.self) return true;
1057
1174
  // 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;
1175
+ const walk = keptPath(this.machine, reading.root, read.exact ? exact : [root]);
1176
+ if (walk === undefined) return false;
1177
+ const path = walk.path;
1060
1178
  const paths = (reading.paths ??= new Set());
1061
1179
  const text = JSON.stringify(path);
1062
1180
  if (paths.has(text) || paths.has(JSON.stringify([root]))) return true;
1063
1181
  paths.add(text);
1064
1182
  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 });
1183
+ reading.list.push({ path, reached: walk.reached ?? readAgain(this.machine, reading.root, path, false).reached, free: true, kept: true });
1066
1184
  return true;
1067
1185
  }
1068
1186
 
@@ -1086,10 +1204,10 @@ class Session implements Runtime {
1086
1204
  * This function checks the capability of an effect, and records the effect with its address (RUN.ADDRESS). The
1087
1205
  * context keeps only the capabilities of its grant, thus compiled code cannot widen the context of its own effect
1088
1206
  * (CAP.NARROW-CODE). A context of another epoch has no capabilities, thus code cannot keep a context for a later
1089
- * evaluation with fewer capabilities (CAP.NARROW-KEPT).
1207
+ * evaluation with fewer capabilities (CAP.NARROW-KEPT). An equal list of the host is the same epoch (CAP.EPOCH-EQUAL).
1090
1208
  */
1091
1209
  private record(given: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result | Recorded {
1092
- const ctx = (given as Granted)[EPOCH] === this.epoch ? granted(given) : ungranted(given, this.epoch);
1210
+ const ctx = inEpoch(given, this.epoch) ? granted(given) : ungranted(given, this.epoch);
1093
1211
  const keys = typeof kind === "string" ? kind.split(".") : kind;
1094
1212
  const text = keys.join(".");
1095
1213
  if (!this.permitted(ctx, keys)) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
@@ -1180,6 +1298,7 @@ class Session implements Runtime {
1180
1298
  }
1181
1299
  const active: Active[] = [];
1182
1300
  let next = 0;
1301
+ let before = 0;
1183
1302
  const settle = (index: number, result: Result): void => {
1184
1303
  results[index] = result;
1185
1304
  if (failFast && !result.ok) failed = true;
@@ -1231,7 +1350,7 @@ class Session implements Runtime {
1231
1350
  start();
1232
1351
  while (active.length > 0 && !failed) {
1233
1352
  // A replay settles the waits in the recorded order. Each other run takes the first wait that settles.
1234
- const chosen = journal?.next?.(address, active.map((item) => item.index));
1353
+ const chosen = journal?.next?.(address, active.map((item) => item.index), before);
1235
1354
  const pick = chosen === undefined ? undefined : active.find((item) => item.index === chosen);
1236
1355
  const settled = (yield pick === undefined ? Promise.race(active.map((item) => item.pending)) : pick.pending) as {
1237
1356
  readonly item: Active;
@@ -1240,6 +1359,7 @@ class Session implements Runtime {
1240
1359
  readonly threw: boolean;
1241
1360
  };
1242
1361
  journal?.settled?.(address, settled.item.index);
1362
+ before += 1;
1243
1363
  active.splice(active.indexOf(settled.item), 1);
1244
1364
  advance(settled.item.index, settled.item.steps, settled);
1245
1365
  start();
@@ -1402,6 +1522,15 @@ export interface PrepareOptions {
1402
1522
  * evaluates one time, when the plan compiles. Its result is one frozen value for each evaluation (PLAN.FOLD).
1403
1523
  */
1404
1524
  readonly fold?: boolean;
1525
+ /**
1526
+ * The values that the host declares fixed for each evaluation of the plan (PLAN.FIXED, D60). The field `names` gives
1527
+ * a value for each fixed name. A path from a fixed name then resolves one time, when the plan compiles, and a pure
1528
+ * part that reads only fixed names folds. Only a deeply frozen value of plain data fixes its name.
1529
+ *
1530
+ * Each folded part compares the value of its names with one own-key test and `Object.is`. A different value gives
1531
+ * the code without the fold. A plan without folds ignores this option.
1532
+ */
1533
+ readonly fixed?: { readonly names?: Readonly<Record<string, unknown>> };
1405
1534
  }
1406
1535
 
1407
1536
  /** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
@@ -1440,6 +1569,8 @@ interface Compiler {
1440
1569
  readonly noAnswer: Answer;
1441
1570
  /** `true` when closed pure parts fold (PLAN.FOLD). A plan with a trace does not fold. */
1442
1571
  readonly fold: boolean;
1572
+ /** The fixed names and their values, or `undefined` when the plan has none (PLAN.FIXED). */
1573
+ readonly fixed: Readonly<Record<string, unknown>> | undefined;
1443
1574
  /** The runtime of the folds of this compilation. The first fold makes it. */
1444
1575
  probe?: Probe;
1445
1576
  }
@@ -1600,7 +1731,11 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1600
1731
  if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
1601
1732
  const free = freeOf(doc, kind, split.parts, frees);
1602
1733
  if (free === undefined) return done;
1603
- return free.length === 0 ? folded(c, done) : { code: done.code, run: done.run, free };
1734
+ if (free.length === 0) return folded(c, done, undefined);
1735
+ // A part that reads only fixed names folds, and its code compares the values of these names (PLAN.FIXED).
1736
+ const fixed = c.fixed;
1737
+ if (fixed !== undefined && free.every((each) => Object.hasOwn(fixed, each))) return folded(c, done, free);
1738
+ return { code: done.code, run: done.run, free };
1604
1739
  }
1605
1740
 
1606
1741
  /*
@@ -1693,6 +1828,20 @@ class Probe implements Runtime {
1693
1828
  this.root = root;
1694
1829
  }
1695
1830
 
1831
+ /** The context of a part that folds with fixed names: the context of the probe, with the fixed names (PLAN.FIXED). */
1832
+ private withFixed: Context | undefined;
1833
+
1834
+ /**
1835
+ * This function gives the context of a part that folds with fixed names. Its root `names` holds only the fixed
1836
+ * names, thus the probe resolves each fixed name and refuses each other name.
1837
+ *
1838
+ * @param fixed - The fixed names and their values.
1839
+ * @returns The context.
1840
+ */
1841
+ fixedRoot(fixed: Readonly<Record<string, unknown>>): Context {
1842
+ return (this.withFixed ??= { ...this.root, names: fixed });
1843
+ }
1844
+
1696
1845
  resolve(ctx: Context, path: readonly unknown[]): Resolution {
1697
1846
  const name = path[1];
1698
1847
  if (path[0] === "names" && isKey(name) && Object.hasOwn(ctx.names, String(name))) {
@@ -1757,25 +1906,106 @@ function trap(refuse: () => void, copies: boolean): object {
1757
1906
  * one deeply frozen copy, and the code gives that copy at each evaluation (PLAN.SHARED). A fold that the probe cancels
1758
1907
  * gives the code without free names, thus no parent tries the fold again.
1759
1908
  */
1760
- function folded(c: Compiler, done: Compiled): Compiled {
1909
+ function folded(c: Compiler, done: Compiled, fixed: readonly string[] | undefined): Compiled {
1761
1910
  const probe = (c.probe ??= new Probe(c.machine));
1762
1911
  probe.refused = false;
1763
1912
  probe.reads = 0;
1764
1913
  let constant: Result | undefined;
1765
1914
  try {
1766
- const result = done.code(probe.root, probe);
1915
+ const root = fixed === undefined ? probe.root : probe.fixedRoot(c.fixed as Readonly<Record<string, unknown>>);
1916
+ const result = done.code(root, probe);
1767
1917
  if (!probe.refused && (result.ok || result.error.code !== "#DEPTH!")) constant = sharedCopy(result) as Result | undefined;
1768
1918
  } catch {
1769
1919
  constant = undefined;
1770
1920
  }
1771
1921
  if (constant === undefined) return { code: done.code, run: done.run };
1772
1922
  const value = constant;
1923
+ const original = done.code;
1924
+ if (fixed !== undefined) return { code: fixedCode(fixed, c.fixed as Readonly<Record<string, unknown>>, value, original), run: undefined, free: fixed };
1773
1925
  if (probe.reads === 0) return { code: () => value, run: undefined, free: CLOSED };
1774
1926
  // 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
1927
  return { code: (ctx, rt) => (rt instanceof Session && !rt.traced && !rt.recording ? value : original(ctx, rt)), run: undefined, free: CLOSED };
1777
1928
  }
1778
1929
 
1930
+ /**
1931
+ * This function gives the code of a part that folded with fixed names (PLAN.FIXED, D60). The code gives the folded
1932
+ * value when the context has the fixed value of each name, by an own-key test and `Object.is`. A fixed value is
1933
+ * deeply frozen, thus the same object gives the same result. Each other context, a list of reads and another runtime
1934
+ * get the code without the fold. The probe of a parent fold gets the folded value, and it counts a read.
1935
+ */
1936
+ function fixedCode(names: readonly string[], fixed: Readonly<Record<string, unknown>>, value: Result, original: Code): Code {
1937
+ if (names.length === 1) {
1938
+ const name = names[0] as string;
1939
+ const want = fixed[name];
1940
+ return (ctx, rt) => {
1941
+ const given = ctx.names;
1942
+ return Object.hasOwn(given, name) && Object.is(given[name], want) && folds(rt) ? value : original(ctx, rt);
1943
+ };
1944
+ }
1945
+ const wants = names.map((name) => fixed[name]);
1946
+ return (ctx, rt) => {
1947
+ const given = ctx.names;
1948
+ for (let index = 0; index < names.length; index += 1) {
1949
+ const name = names[index] as string;
1950
+ if (!Object.hasOwn(given, name) || !Object.is(given[name], wants[index])) return original(ctx, rt);
1951
+ }
1952
+ return folds(rt) ? value : original(ctx, rt);
1953
+ };
1954
+ }
1955
+
1956
+ /**
1957
+ * This function tells if the code of a part with fixed names can give its folded value in a runtime. The probe of a
1958
+ * fold counts the read of the part, as the code without the fold reads the names.
1959
+ */
1960
+ function folds(rt: Runtime): boolean {
1961
+ if (rt instanceof Session) return !rt.recording;
1962
+ if (!(rt instanceof Probe)) return false;
1963
+ rt.reads += 1;
1964
+ return true;
1965
+ }
1966
+
1967
+ /**
1968
+ * This function gives the fixed names of the options of `prepare` (PLAN.FIXED). A name fixes only when its value is
1969
+ * deeply frozen plain data. Then no change in place can make the folded value stale.
1970
+ *
1971
+ * @returns A record without a prototype, or `undefined` when no name fixes.
1972
+ */
1973
+ function fixedNamesOf(given: Readonly<Record<string, unknown>> | undefined): Readonly<Record<string, unknown>> | undefined {
1974
+ if (given === undefined) return undefined;
1975
+ let out: Record<string, unknown> | undefined;
1976
+ for (const name of Object.keys(given)) {
1977
+ const value = given[name];
1978
+ if (!frozenPlain(value)) continue;
1979
+ out ??= Object.create(null) as Record<string, unknown>;
1980
+ Object.defineProperty(out, name, { value, enumerable: true });
1981
+ }
1982
+ return out;
1983
+ }
1984
+
1985
+ /** This function tells if a value is plain data, as `sharedCopy` reads it, and if each of its objects is frozen. */
1986
+ function frozenPlain(value: unknown): boolean {
1987
+ const seen = new Set<object>();
1988
+ const walk = (here: unknown): boolean => {
1989
+ if (typeof here === "function") return false;
1990
+ if (typeof here !== "object" || here === null || SHARED.has(here) || seen.has(here)) return true;
1991
+ if (!Object.isFrozen(here)) return false;
1992
+ seen.add(here);
1993
+ const proto: unknown = Object.getPrototypeOf(here);
1994
+ if (Array.isArray(here) ? proto !== Array.prototype : proto !== Object.prototype && proto !== null) return false;
1995
+ for (const key of Reflect.ownKeys(here)) {
1996
+ const field = Object.getOwnPropertyDescriptor(here, key);
1997
+ if (typeof key !== "string" || field === undefined || !("value" in field) || !walk(field.value)) return false;
1998
+ }
1999
+ return true;
2000
+ };
2001
+ try {
2002
+ return walk(value);
2003
+ } catch {
2004
+ // A very deep value overflows the stack of the host.
2005
+ return false;
2006
+ }
2007
+ }
2008
+
1779
2009
  /** The objects that a fold made. Each one is deeply frozen, thus a later fold uses it again and does not copy it. */
1780
2010
  const SHARED: WeakSet<object> = /* @__PURE__ */ new WeakSet();
1781
2011
 
@@ -1785,9 +2015,13 @@ const NOT_PLAIN: unique symbol = /* @__PURE__ */ Symbol("not plain");
1785
2015
  /**
1786
2016
  * This function gives a deeply frozen copy of plain data, or `undefined` for other values. Plain data is a primitive or
1787
2017
  * 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.
2018
+ * or no prototype. The copy keeps cycles and shared objects. The objects of the host do not change. The tier `fixed`
2019
+ * of `agree` uses it. The package does not export it.
2020
+ *
2021
+ * @param value - The value.
2022
+ * @returns The frozen copy, or `undefined`.
1789
2023
  */
1790
- function sharedCopy(value: unknown): unknown {
2024
+ export function sharedCopy(value: unknown): unknown {
1791
2025
  try {
1792
2026
  const out = copyPlain(value, new Map());
1793
2027
  return out === NOT_PLAIN ? undefined : out;
@@ -1890,7 +2124,7 @@ function gated(rules: readonly CapsRule[], part: Compiled): Compiled {
1890
2124
  const ctx = granted(given);
1891
2125
  const caps = capsUnder(rules, ctx.caps);
1892
2126
  if (caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0)) return ctx;
1893
- const out: Granted = { ...ctx, caps, [GRANT]: caps };
2127
+ const out: Granted = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx) };
1894
2128
  return out;
1895
2129
  };
1896
2130
  // The steps of an evaluator are `runOf` of its code, thus `runOf` of this code narrows the context the same way.
@@ -1946,6 +2180,36 @@ function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P
1946
2180
  return (items) => (split.parts.length === 0 ? doc.value : split.fill(items));
1947
2181
  }
1948
2182
 
2183
+ /**
2184
+ * The key of the runtime on the input of an evaluator. Only the kernel reads it. The node of a tree evaluates its
2185
+ * equations in the evaluation of this runtime, thus their reads go in its list (READS.EQUATION). A copy of the input
2186
+ * with a spread keeps the key.
2187
+ */
2188
+ export const RUNTIME: unique symbol = /* @__PURE__ */ Symbol("coeval.runtime") as never;
2189
+
2190
+ /** The input of an evaluator, with the runtime of its evaluation. */
2191
+ export interface InputWithRuntime extends EvalInput {
2192
+ readonly [RUNTIME]?: Runtime;
2193
+ }
2194
+
2195
+ /**
2196
+ * This function evaluates a plan inside the evaluation of another runtime, for example an equation of a tree inside
2197
+ * the evaluation of its tree (READS.EQUATION). The plan gets its own runtime, as in `plan.evaluate`. The reads of the
2198
+ * plan go in the record of the reads of `outer`. The context at the root of `outer` tells which reads are free
2199
+ * (READS.FREE). Without such a record, the function gives `plan.evaluate(context)`.
2200
+ *
2201
+ * @param plan - The plan.
2202
+ * @param context - The context of the plan.
2203
+ * @param outer - The runtime of the evaluation around the plan, or `undefined`.
2204
+ * @returns The result.
2205
+ */
2206
+ export function evaluateWithin(plan: Plan, context: Context, outer: Runtime | undefined): Result {
2207
+ const reading = outer instanceof Session ? outer.reading : undefined;
2208
+ if (reading === undefined || !(plan instanceof CompiledPlan)) return plan.evaluate(context);
2209
+ const machine = plan.machine;
2210
+ const ctx = rootContext(machine, context);
2211
+ return plan.code(ctx, new Session(machine, ctx.caps, false, plan.traced, undefined, undefined, undefined, reading));
2212
+ }
1949
2213
 
1950
2214
  function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
1951
2215
  const { kind, typePath, doc, data, place, machine } = input;
@@ -1975,7 +2239,8 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
1975
2239
  resolve: (path, options) => rt.resolve(ctx, path, options),
1976
2240
  effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
1977
2241
  fill: refill,
1978
- });
2242
+ [RUNTIME]: rt,
2243
+ } as InputWithRuntime);
1979
2244
  };
1980
2245
  }
1981
2246
 
@@ -2012,16 +2277,25 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
2012
2277
  /**
2013
2278
  * This function gives the context at the root of an evaluation. It is the context of the host, with the world of
2014
2279
  * the machine when the context has none. The host gives the capabilities, thus the root has the grant of each one
2015
- * (CAP.NARROW-CODE). The list of the host is also the epoch of the grant (CAP.NARROW-KEPT). An evaluation and
2016
- * `verifyReads` share it.
2280
+ * (CAP.NARROW-CODE). The list of the host is also the epoch of the grant (CAP.NARROW-KEPT). The root has no field of
2281
+ * the epoch, thus it has the shape of a context before K120 (K154). An evaluation and `verifyReads` share it.
2282
+ *
2283
+ * The function reads the epoch of the context one time, and one test keeps a context that is already a root (K169).
2017
2284
  *
2018
2285
  * @param machine - The machine.
2019
2286
  * @param ctx - The context of the host.
2020
2287
  * @returns The context at the root.
2021
2288
  */
2022
2289
  export function rootContext(machine: Machine, ctx: Context): Context {
2023
- if (ctx.world !== null && (ctx as Granted)[GRANT] === ctx.caps && (ctx as Granted)[EPOCH] === ctx.caps) return ctx;
2024
- const root: Granted = { ...ctx, world: ctx.world === null ? worldThing(machine.world) : ctx.world, [GRANT]: ctx.caps, [EPOCH]: ctx.caps };
2290
+ const epoch = (ctx as Granted)[EPOCH];
2291
+ if (ctx.world !== null && (ctx as Granted)[GRANT] === ctx.caps && (epoch === undefined || epoch === ctx.caps)) return ctx;
2292
+ const world = ctx.world === null ? worldThing(machine.world) : ctx.world;
2293
+ if (epoch === undefined) {
2294
+ const root: Granted = { ...ctx, world, [GRANT]: ctx.caps };
2295
+ return root;
2296
+ }
2297
+ // A context of the kernel as the context of the host gets the list of the host as its epoch.
2298
+ const root: Granted = { ...ctx, world, [GRANT]: ctx.caps, [EPOCH]: ctx.caps };
2025
2299
  return root;
2026
2300
  }
2027
2301
 
@@ -2042,7 +2316,9 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
2042
2316
  const verb = options.verb ?? "eval";
2043
2317
  const noAnswer: Answer = verb === "eval" ? NO_ANSWER : { verb, error: "no-answer", steps: [] };
2044
2318
  const traced = options.trace === true;
2045
- const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold: !traced && options.fold !== false };
2319
+ const fold = !traced && options.fold !== false;
2320
+ const fixed = fold ? fixedNamesOf(options.fixed?.names) : undefined;
2321
+ const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold, fixed };
2046
2322
  let root: Compiled;
2047
2323
  try {
2048
2324
  root = compileNode(c, doc, [], options.declaredType, 0);