@mark1russell7/coeval 0.16.0 → 0.18.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. */
@@ -1004,6 +1053,11 @@ class Session implements Runtime {
1004
1053
  private list: Effect[] | undefined;
1005
1054
  tick = 0;
1006
1055
  nextId = 0;
1056
+ /**
1057
+ * The count of the drives without a wait that are active at this time. In a run, an effect under such a drive does
1058
+ * not reach the host (RUN.HELD).
1059
+ */
1060
+ held = 0;
1007
1061
  current: Building | undefined;
1008
1062
  readonly holder: Building | undefined;
1009
1063
  /**
@@ -1024,8 +1078,11 @@ class Session implements Runtime {
1024
1078
  readonly perform: Perform | undefined,
1025
1079
  readonly signal: StopSignal | undefined,
1026
1080
  effects: Effect[] | undefined,
1027
- /** The record of the reads. Without it, a resolution records nothing. */
1028
- private readonly reading: Reading | undefined = undefined,
1081
+ /**
1082
+ * The record of the reads. Without it, a resolution records nothing. A nested evaluation of an equation of a tree
1083
+ * shares the record of the evaluation around it (READS.EQUATION).
1084
+ */
1085
+ readonly reading: Reading | undefined = undefined,
1029
1086
  ) {
1030
1087
  this.list = effects;
1031
1088
  this.holder = traced ? holderNode() : undefined;
@@ -1152,10 +1209,10 @@ class Session implements Runtime {
1152
1209
  * This function checks the capability of an effect, and records the effect with its address (RUN.ADDRESS). The
1153
1210
  * context keeps only the capabilities of its grant, thus compiled code cannot widen the context of its own effect
1154
1211
  * (CAP.NARROW-CODE). A context of another epoch has no capabilities, thus code cannot keep a context for a later
1155
- * evaluation with fewer capabilities (CAP.NARROW-KEPT).
1212
+ * evaluation with fewer capabilities (CAP.NARROW-KEPT). An equal list of the host is the same epoch (CAP.EPOCH-EQUAL).
1156
1213
  */
1157
1214
  private record(given: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result | Recorded {
1158
- const ctx = (given as Granted)[EPOCH] === this.epoch ? granted(given) : ungranted(given, this.epoch);
1215
+ const ctx = inEpoch(given, this.epoch) ? granted(given) : ungranted(given, this.epoch);
1159
1216
  const keys = typeof kind === "string" ? kind.split(".") : kind;
1160
1217
  const text = keys.join(".");
1161
1218
  if (!this.permitted(ctx, keys)) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
@@ -1213,6 +1270,13 @@ class Session implements Runtime {
1213
1270
  if ("ok" in made) return made;
1214
1271
  const perform = this.perform;
1215
1272
  if (!this.running || perform === undefined) return this.answer(made, place);
1273
+ if (this.held > 0) {
1274
+ // The parent cannot wait for the answer of the host, thus the host does not get the effect (RUN.HELD).
1275
+ made.outcome = fail("#CALC!", heldReason(made.kind), { place });
1276
+ // The drive stops at this wait, and gives the #CALC! of the runner.
1277
+ yield HELD;
1278
+ return made.outcome;
1279
+ }
1216
1280
  let done: unknown;
1217
1281
  try {
1218
1282
  done = perform(made);
@@ -1246,6 +1310,7 @@ class Session implements Runtime {
1246
1310
  }
1247
1311
  const active: Active[] = [];
1248
1312
  let next = 0;
1313
+ let before = 0;
1249
1314
  const settle = (index: number, result: Result): void => {
1250
1315
  results[index] = result;
1251
1316
  if (failFast && !result.ok) failed = true;
@@ -1297,7 +1362,7 @@ class Session implements Runtime {
1297
1362
  start();
1298
1363
  while (active.length > 0 && !failed) {
1299
1364
  // A replay settles the waits in the recorded order. Each other run takes the first wait that settles.
1300
- const chosen = journal?.next?.(address, active.map((item) => item.index));
1365
+ const chosen = journal?.next?.(address, active.map((item) => item.index), before);
1301
1366
  const pick = chosen === undefined ? undefined : active.find((item) => item.index === chosen);
1302
1367
  const settled = (yield pick === undefined ? Promise.race(active.map((item) => item.pending)) : pick.pending) as {
1303
1368
  readonly item: Active;
@@ -1306,6 +1371,7 @@ class Session implements Runtime {
1306
1371
  readonly threw: boolean;
1307
1372
  };
1308
1373
  journal?.settled?.(address, settled.item.index);
1374
+ before += 1;
1309
1375
  active.splice(active.indexOf(settled.item), 1);
1310
1376
  advance(settled.item.index, settled.item.steps, settled);
1311
1377
  start();
@@ -1398,6 +1464,21 @@ export function waitedReason(kindText: string): string {
1398
1464
  return `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`;
1399
1465
  }
1400
1466
 
1467
+ /**
1468
+ * This function gives the reason of an effect that a run does not give to the host: the parent of its runner cannot
1469
+ * wait (RUN.HELD). The plan and the reference give the same reason.
1470
+ *
1471
+ * @param kind - The kind of the effect.
1472
+ * @returns The reason.
1473
+ */
1474
+ export function heldReason(kind: Cap): string {
1475
+ const text = typeof kind === "string" ? kind : kind.join(".");
1476
+ return `The parent of the runner cannot wait, thus the host does not get the effect ${text}.`;
1477
+ }
1478
+
1479
+ /** The wait of an effect that a run does not give to the host. A drive without a wait stops at it (RUN.HELD). */
1480
+ const HELD: Promise<unknown> = /* @__PURE__ */ Promise.resolve(undefined);
1481
+
1401
1482
  /** This function drives steps that cannot wait. A step that waits stops the steps, and gives `#CALC!`. */
1402
1483
  function drive(steps: Steps, kindText: string): Result {
1403
1484
  const step = steps.next();
@@ -2070,7 +2151,7 @@ function gated(rules: readonly CapsRule[], part: Compiled): Compiled {
2070
2151
  const ctx = granted(given);
2071
2152
  const caps = capsUnder(rules, ctx.caps);
2072
2153
  if (caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0)) return ctx;
2073
- const out: Granted = { ...ctx, caps, [GRANT]: caps };
2154
+ const out: Granted = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx) };
2074
2155
  return out;
2075
2156
  };
2076
2157
  // The steps of an evaluator are `runOf` of its code, thus `runOf` of this code narrows the context the same way.
@@ -2094,10 +2175,19 @@ function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode
2094
2175
  /**
2095
2176
  * This function gives the code of a runner under a parent that cannot wait: its steps, driven without a wait. An
2096
2177
  * evaluation and a run both drive them (RUN.DRIVE-NESTED). Only a step that really waits gives `#CALC!`, at the place
2097
- * of the runner, where the error started (RUN.WAIT).
2178
+ * of the runner, where the error started (RUN.WAIT). In a run, an effect under the runner does not reach the host,
2179
+ * because the parent cannot wait for its answer (RUN.HELD).
2098
2180
  */
2099
2181
  function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
2100
- return (ctx, rt) => placed(drive(run(ctx, rt), kindText), place);
2182
+ return (ctx, rt) => {
2183
+ if (!rt.running || !(rt instanceof Session)) return placed(drive(run(ctx, rt), kindText), place);
2184
+ rt.held += 1;
2185
+ try {
2186
+ return placed(drive(run(ctx, rt), kindText), place);
2187
+ } finally {
2188
+ rt.held -= 1;
2189
+ }
2190
+ };
2101
2191
  }
2102
2192
 
2103
2193
  /**
@@ -2126,6 +2216,36 @@ function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P
2126
2216
  return (items) => (split.parts.length === 0 ? doc.value : split.fill(items));
2127
2217
  }
2128
2218
 
2219
+ /**
2220
+ * The key of the runtime on the input of an evaluator. Only the kernel reads it. The node of a tree evaluates its
2221
+ * equations in the evaluation of this runtime, thus their reads go in its list (READS.EQUATION). A copy of the input
2222
+ * with a spread keeps the key.
2223
+ */
2224
+ export const RUNTIME: unique symbol = /* @__PURE__ */ Symbol("coeval.runtime") as never;
2225
+
2226
+ /** The input of an evaluator, with the runtime of its evaluation. */
2227
+ export interface InputWithRuntime extends EvalInput {
2228
+ readonly [RUNTIME]?: Runtime;
2229
+ }
2230
+
2231
+ /**
2232
+ * This function evaluates a plan inside the evaluation of another runtime, for example an equation of a tree inside
2233
+ * the evaluation of its tree (READS.EQUATION). The plan gets its own runtime, as in `plan.evaluate`. The reads of the
2234
+ * plan go in the record of the reads of `outer`. The context at the root of `outer` tells which reads are free
2235
+ * (READS.FREE). Without such a record, the function gives `plan.evaluate(context)`.
2236
+ *
2237
+ * @param plan - The plan.
2238
+ * @param context - The context of the plan.
2239
+ * @param outer - The runtime of the evaluation around the plan, or `undefined`.
2240
+ * @returns The result.
2241
+ */
2242
+ export function evaluateWithin(plan: Plan, context: Context, outer: Runtime | undefined): Result {
2243
+ const reading = outer instanceof Session ? outer.reading : undefined;
2244
+ if (reading === undefined || !(plan instanceof CompiledPlan)) return plan.evaluate(context);
2245
+ const machine = plan.machine;
2246
+ const ctx = rootContext(machine, context);
2247
+ return plan.code(ctx, new Session(machine, ctx.caps, false, plan.traced, undefined, undefined, undefined, reading));
2248
+ }
2129
2249
 
2130
2250
  function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
2131
2251
  const { kind, typePath, doc, data, place, machine } = input;
@@ -2155,7 +2275,8 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
2155
2275
  resolve: (path, options) => rt.resolve(ctx, path, options),
2156
2276
  effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
2157
2277
  fill: refill,
2158
- });
2278
+ [RUNTIME]: rt,
2279
+ } as InputWithRuntime);
2159
2280
  };
2160
2281
  }
2161
2282
 
@@ -2192,16 +2313,25 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
2192
2313
  /**
2193
2314
  * This function gives the context at the root of an evaluation. It is the context of the host, with the world of
2194
2315
  * the machine when the context has none. The host gives the capabilities, thus the root has the grant of each one
2195
- * (CAP.NARROW-CODE). The list of the host is also the epoch of the grant (CAP.NARROW-KEPT). An evaluation and
2196
- * `verifyReads` share it.
2316
+ * (CAP.NARROW-CODE). The list of the host is also the epoch of the grant (CAP.NARROW-KEPT). The root has no field of
2317
+ * the epoch, thus it has the shape of a context before K120 (K154). An evaluation and `verifyReads` share it.
2318
+ *
2319
+ * The function reads the epoch of the context one time, and one test keeps a context that is already a root (K169).
2197
2320
  *
2198
2321
  * @param machine - The machine.
2199
2322
  * @param ctx - The context of the host.
2200
2323
  * @returns The context at the root.
2201
2324
  */
2202
2325
  export function rootContext(machine: Machine, ctx: Context): Context {
2203
- if (ctx.world !== null && (ctx as Granted)[GRANT] === ctx.caps && (ctx as Granted)[EPOCH] === ctx.caps) return ctx;
2204
- const root: Granted = { ...ctx, world: ctx.world === null ? worldThing(machine.world) : ctx.world, [GRANT]: ctx.caps, [EPOCH]: ctx.caps };
2326
+ const epoch = (ctx as Granted)[EPOCH];
2327
+ if (ctx.world !== null && (ctx as Granted)[GRANT] === ctx.caps && (epoch === undefined || epoch === ctx.caps)) return ctx;
2328
+ const world = ctx.world === null ? worldThing(machine.world) : ctx.world;
2329
+ if (epoch === undefined) {
2330
+ const root: Granted = { ...ctx, world, [GRANT]: ctx.caps };
2331
+ return root;
2332
+ }
2333
+ // A context of the kernel as the context of the host gets the list of the host as its epoch.
2334
+ const root: Granted = { ...ctx, world, [GRANT]: ctx.caps, [EPOCH]: ctx.caps };
2205
2335
  return root;
2206
2336
  }
2207
2337
 
package/src/facts.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { type Json, type Key, isEnvelope, isKey } from "./doc.ts";
2
- import { type Kind, type LazyRule, type Side, type World, lookupKind } from "./world.ts";
2
+ import { type Kind, type LazyRule, type Side, type World, equationType, lookupKind } from "./world.ts";
3
3
  import { answerOf } from "./ladder.ts";
4
4
  import { pathMatches, splitParts } from "./parts.ts";
5
5
  import { type Root, ROOTS, isRef } from "./deref.ts";
6
6
  import type { Machine } from "./evaluate.ts";
7
- import { type DocVisit, declaredEffects, walk } from "./static.ts";
7
+ import { type DocVisit, type WalkOptions, declaredEffects, equationsOf, walk } from "./static.ts";
8
8
 
9
9
  /**
10
10
  * One read of a document: a free reference, with its marks (FACTS.READS). A plan resolves the path of the reference
@@ -27,9 +27,12 @@ export interface StaticRead {
27
27
  readonly address: boolean;
28
28
  /** The path of the reference: its root and its steps. */
29
29
  readonly path: readonly Json[];
30
- /** The keys from the root document to the reference document, through the parts. */
30
+ /**
31
+ * The keys from the root document to the reference document, through the parts. A read of an equation has the
32
+ * place of the first document of its tree kind (FACTS.EQUATIONS).
33
+ */
31
34
  readonly place: readonly Key[];
32
- /** The keys from the root JSON value to the reference document. */
35
+ /** The keys from the root JSON value to the reference document, or to the document of the kind for an equation. */
33
36
  readonly at: readonly Key[];
34
37
  }
35
38
 
@@ -151,9 +154,11 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
151
154
  *
152
155
  * - `reads`: each free reference, with the root and the keys of its path. A read under a lazy part has the mark
153
156
  * `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`. A kind that
154
- * declares `reads` adds a read of each of its roots at each document of the kind.
157
+ * declares `reads` adds a read of each of its roots at each document of the kind. The first document of a tree
158
+ * kind also gets the lazy reads of the equations of its kind (FACTS.EQUATIONS). A read from `self` or from a name
159
+ * of a rung is not one of them, because it is not free (READS.EQUATION).
155
160
  * - `side` and `effects`: from the contract of each kind.
156
- * - `pure`: each kind declares `pure`, and the document has no effect and is not volatile.
161
+ * - `pure`: each kind declares `pure`, also in an equation, and the document has no effect and is not volatile.
157
162
  * - `volatile`: a kind, an operation or a step document declares `volatile`, or the host marks a read.
158
163
  *
159
164
  * The hook `facts` of the machine answers for a name that a fallback kind covers, for example an operation. Its
@@ -177,7 +182,6 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
177
182
  export function factsOf(machine: Machine, doc: unknown, options: FactsOptions = {}): DocFacts {
178
183
  const world = machine.world;
179
184
  const hook = machine.facts;
180
- const verb = options.verb ?? "eval";
181
185
  const reads: StaticRead[] = [];
182
186
  const effects: string[] = [];
183
187
  const above = new Map<DocVisit, Above>();
@@ -185,82 +189,113 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
185
189
  let pure = true;
186
190
  let volatile = false;
187
191
  let size = 0;
192
+ // The tree kinds whose equations the facts walked. The first document of each such kind gets their reads (FACTS.EQUATIONS).
193
+ let seen: Set<Kind> | undefined;
188
194
  const note = (isPure: boolean | undefined, isVolatile: boolean | undefined): void => {
189
195
  if (isPure !== true) pure = false;
190
196
  if (isVolatile === true) volatile = true;
191
197
  };
192
- walk(
193
- world,
194
- doc,
195
- (v) => {
196
- size += 1;
197
- const up = v.parent === undefined ? undefined : above.get(v.parent);
198
- const lazy = up !== undefined && v.parent !== undefined && (up.lazy || partLazy(up, v.parent, v.slot));
199
- const kind = v.kind;
200
- if (kind === undefined) {
201
- // A kind that the world does not have gives #KIND?, and the walk does not visit its value.
202
- pure = false;
203
- above.set(v, { lazy, rules: undefined });
204
- return undefined;
205
- }
206
- const code = codeOf(world, v.declared ? undefined : v.doc.own, kind, verb, "eval");
207
- const answer = code.name === undefined ? undefined : hook?.(v.typePath ?? [], code.name);
208
- let roots: readonly Root[] | undefined;
209
- if (code.described) {
210
- note(answer?.pure ?? kind.pure, answer?.volatile ?? kind.volatile);
211
- above.set(v, { lazy, rules: answer?.lazy ?? kind.lazy });
212
- roots = answer?.reads ?? kind.reads;
213
- } else {
214
- // The contract of the kind does not describe this code. Only the hook can.
215
- note(answer?.pure, answer?.volatile);
216
- above.set(v, { lazy, rules: answer?.lazy ?? EACH });
217
- roots = answer?.reads;
218
- }
219
- // The code of the kind resolves each of these roots as a whole, at the place of its document (FACTS.KIND-READS).
220
- for (const root of roots ?? NONE) {
221
- const read: StaticRead = { root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(v.place), at: tight(v.at) };
222
- reads.push(read);
223
- if (options.volatile?.(read) === true) volatile = true;
224
- }
225
- if (kind.side === "run") side = "run";
226
- // check and effectsOf read the declared effects with the same function (D27).
227
- for (const keys of declaredEffects(v)) {
228
- const text = keys.join(".");
229
- if (!effects.includes(text)) effects.push(text);
230
- }
231
- if (!isRef(v.doc)) return undefined;
232
- const path = v.doc.value;
233
- const root = path[0];
234
- if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) return undefined;
235
- let exact = true;
236
- const keys: Key[] = [];
237
- for (let index = 1; index < path.length; index += 1) {
238
- const step = path[index];
239
- if (exact && isKey(step)) {
240
- keys.push(step);
241
- continue;
242
- }
243
- exact = false;
244
- // A step document runs the walker that the ladder finds for the verb step.
245
- const found = isEnvelope(step) ? lookupKind(world, step.type) : undefined;
246
- if (found?.ok !== true) {
198
+ const add = (read: StaticRead): void => {
199
+ reads.push(read);
200
+ if (options.volatile?.(read) === true) volatile = true;
201
+ };
202
+ /**
203
+ * This function walks a document. Without `node`, it walks the document of the facts. With `node`, it walks an
204
+ * equation of a tree kind, and each read gets the place of that node: the first document of the kind.
205
+ */
206
+ const scan = (term: unknown, walkOptions: WalkOptions, verb: string, node: DocVisit | undefined): void => {
207
+ walk(
208
+ world,
209
+ term,
210
+ (v) => {
211
+ if (node === undefined) size += 1;
212
+ const up = v.parent === undefined ? undefined : above.get(v.parent);
213
+ // An equation evaluates only when a demand reaches its attribute, thus each read of an equation is lazy (TREE.LAZY).
214
+ const lazy = node !== undefined || (up !== undefined && v.parent !== undefined && (up.lazy || partLazy(up, v.parent, v.slot)));
215
+ const place = node ?? v;
216
+ const kind = v.kind;
217
+ if (kind === undefined) {
218
+ // A kind that the world does not have gives #KIND?, and the walk does not visit its value.
247
219
  pure = false;
248
- continue;
220
+ above.set(v, { lazy, rules: undefined });
221
+ return undefined;
222
+ }
223
+ const code = codeOf(world, v.declared ? undefined : v.doc.own, kind, verb, "eval");
224
+ const answer = code.name === undefined ? undefined : hook?.(v.typePath ?? [], code.name);
225
+ let roots: readonly Root[] | undefined;
226
+ if (code.described) {
227
+ note(answer?.pure ?? kind.pure, answer?.volatile ?? kind.volatile);
228
+ above.set(v, { lazy, rules: answer?.lazy ?? kind.lazy });
229
+ roots = answer?.reads ?? kind.reads;
230
+ } else {
231
+ // The contract of the kind does not describe this code. Only the hook can.
232
+ note(answer?.pure, answer?.volatile);
233
+ above.set(v, { lazy, rules: answer?.lazy ?? EACH });
234
+ roots = answer?.reads;
249
235
  }
250
- const walker = codeOf(world, (step as { readonly own?: Readonly<Record<string, string>> }).own, found.kind, "step", "step");
251
- const stepAnswer = walker.name === undefined ? undefined : hook?.(found.path, walker.name);
252
- if (walker.described) note(stepAnswer?.pure ?? found.kind.pure, stepAnswer?.volatile ?? found.kind.volatile);
253
- else note(stepAnswer?.pure, stepAnswer?.volatile);
254
- }
255
- const first = path[1];
256
- // A reference to a name that a kind around it binds reads inside the document: it is not a read.
257
- if (root === "names" && isKey(first) && v.names.has(String(first))) return undefined;
258
- const read: StaticRead = { root: root as Root, keys: tight(keys), exact, lazy, address: v.address === true, path, place: tight(v.place), at: tight(v.at) };
259
- reads.push(read);
260
- if (options.volatile?.(read) === true) volatile = true;
261
- return undefined;
262
- },
263
- options.declaredType === undefined ? {} : { declaredType: options.declaredType },
264
- );
236
+ // The code of the kind resolves each of these roots as a whole, at the place of its document (FACTS.KIND-READS).
237
+ for (const root of roots ?? NONE) {
238
+ // In an equation, the root self is the node, thus its read is not free (READS.EQUATION).
239
+ if (node !== undefined && root === "self") continue;
240
+ add({ root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(place.place), at: tight(place.at) });
241
+ }
242
+ // An equation evaluates without capabilities, thus an effect in it never reaches the host. It is still not pure.
243
+ if (kind.side === "run") {
244
+ if (node === undefined) side = "run";
245
+ else pure = false;
246
+ }
247
+ // check and effectsOf read the declared effects with the same function (D27).
248
+ for (const keys of declaredEffects(v)) {
249
+ if (node !== undefined) {
250
+ pure = false;
251
+ continue;
252
+ }
253
+ const text = keys.join(".");
254
+ if (!effects.includes(text)) effects.push(text);
255
+ }
256
+ // The equations of a tree kind are code of the kind. The first document of the kind gets their reads (FACTS.EQUATIONS).
257
+ if (kind.tree !== undefined && !(seen ??= new Set()).has(kind)) {
258
+ seen.add(kind);
259
+ const of = equationType(kind);
260
+ for (const { term: equation, names } of equationsOf(kind)) scan(equation, of === undefined ? { names } : { names, declaredType: of }, "eval", place);
261
+ }
262
+ if (!isRef(v.doc)) return undefined;
263
+ const path = v.doc.value;
264
+ const root = path[0];
265
+ if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) return undefined;
266
+ // In an equation, the root self is the node, thus its read is not free. A step out of the tree reads the outer
267
+ // self through the node, and the read of the whole root self that the tree node makes covers it (TREE.SELF).
268
+ if (node !== undefined && root === "self") return undefined;
269
+ let exact = true;
270
+ const keys: Key[] = [];
271
+ for (let index = 1; index < path.length; index += 1) {
272
+ const step = path[index];
273
+ if (exact && isKey(step)) {
274
+ keys.push(step);
275
+ continue;
276
+ }
277
+ exact = false;
278
+ // A step document runs the walker that the ladder finds for the verb step.
279
+ const found = isEnvelope(step) ? lookupKind(world, step.type) : undefined;
280
+ if (found?.ok !== true) {
281
+ pure = false;
282
+ continue;
283
+ }
284
+ const walker = codeOf(world, (step as { readonly own?: Readonly<Record<string, string>> }).own, found.kind, "step", "step");
285
+ const stepAnswer = walker.name === undefined ? undefined : hook?.(found.path, walker.name);
286
+ if (walker.described) note(stepAnswer?.pure ?? found.kind.pure, stepAnswer?.volatile ?? found.kind.volatile);
287
+ else note(stepAnswer?.pure, stepAnswer?.volatile);
288
+ }
289
+ const first = path[1];
290
+ // A reference to a name that a kind around it binds reads inside the document: it is not a read. The names of
291
+ // an equation are names that its rung binds (TREE.DOWN, TREE.FOLD).
292
+ if (root === "names" && isKey(first) && v.names.has(String(first))) return undefined;
293
+ add({ root: root as Root, keys: tight(keys), exact, lazy, address: v.address === true, path, place: tight(place.place), at: tight(place.at) });
294
+ return undefined;
295
+ },
296
+ walkOptions,
297
+ );
298
+ };
299
+ scan(doc, options.declaredType === undefined ? {} : { declaredType: options.declaredType }, options.verb ?? "eval", undefined);
265
300
  return { reads: tight(reads), side, effects: tight(effects), pure: pure && !volatile && effects.length === 0 && side === "derive", volatile, size };
266
301
  }