@mark1russell7/coeval 0.16.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. */
@@ -1024,8 +1073,11 @@ class Session implements Runtime {
1024
1073
  readonly perform: Perform | undefined,
1025
1074
  readonly signal: StopSignal | undefined,
1026
1075
  effects: Effect[] | undefined,
1027
- /** The record of the reads. Without it, a resolution records nothing. */
1028
- 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,
1029
1081
  ) {
1030
1082
  this.list = effects;
1031
1083
  this.holder = traced ? holderNode() : undefined;
@@ -1152,10 +1204,10 @@ class Session implements Runtime {
1152
1204
  * This function checks the capability of an effect, and records the effect with its address (RUN.ADDRESS). The
1153
1205
  * context keeps only the capabilities of its grant, thus compiled code cannot widen the context of its own effect
1154
1206
  * (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).
1207
+ * evaluation with fewer capabilities (CAP.NARROW-KEPT). An equal list of the host is the same epoch (CAP.EPOCH-EQUAL).
1156
1208
  */
1157
1209
  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);
1210
+ const ctx = inEpoch(given, this.epoch) ? granted(given) : ungranted(given, this.epoch);
1159
1211
  const keys = typeof kind === "string" ? kind.split(".") : kind;
1160
1212
  const text = keys.join(".");
1161
1213
  if (!this.permitted(ctx, keys)) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
@@ -1246,6 +1298,7 @@ class Session implements Runtime {
1246
1298
  }
1247
1299
  const active: Active[] = [];
1248
1300
  let next = 0;
1301
+ let before = 0;
1249
1302
  const settle = (index: number, result: Result): void => {
1250
1303
  results[index] = result;
1251
1304
  if (failFast && !result.ok) failed = true;
@@ -1297,7 +1350,7 @@ class Session implements Runtime {
1297
1350
  start();
1298
1351
  while (active.length > 0 && !failed) {
1299
1352
  // 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));
1353
+ const chosen = journal?.next?.(address, active.map((item) => item.index), before);
1301
1354
  const pick = chosen === undefined ? undefined : active.find((item) => item.index === chosen);
1302
1355
  const settled = (yield pick === undefined ? Promise.race(active.map((item) => item.pending)) : pick.pending) as {
1303
1356
  readonly item: Active;
@@ -1306,6 +1359,7 @@ class Session implements Runtime {
1306
1359
  readonly threw: boolean;
1307
1360
  };
1308
1361
  journal?.settled?.(address, settled.item.index);
1362
+ before += 1;
1309
1363
  active.splice(active.indexOf(settled.item), 1);
1310
1364
  advance(settled.item.index, settled.item.steps, settled);
1311
1365
  start();
@@ -2070,7 +2124,7 @@ function gated(rules: readonly CapsRule[], part: Compiled): Compiled {
2070
2124
  const ctx = granted(given);
2071
2125
  const caps = capsUnder(rules, ctx.caps);
2072
2126
  if (caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0)) return ctx;
2073
- const out: Granted = { ...ctx, caps, [GRANT]: caps };
2127
+ const out: Granted = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx) };
2074
2128
  return out;
2075
2129
  };
2076
2130
  // The steps of an evaluator are `runOf` of its code, thus `runOf` of this code narrows the context the same way.
@@ -2126,6 +2180,36 @@ function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P
2126
2180
  return (items) => (split.parts.length === 0 ? doc.value : split.fill(items));
2127
2181
  }
2128
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
+ }
2129
2213
 
2130
2214
  function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
2131
2215
  const { kind, typePath, doc, data, place, machine } = input;
@@ -2155,7 +2239,8 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
2155
2239
  resolve: (path, options) => rt.resolve(ctx, path, options),
2156
2240
  effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
2157
2241
  fill: refill,
2158
- });
2242
+ [RUNTIME]: rt,
2243
+ } as InputWithRuntime);
2159
2244
  };
2160
2245
  }
2161
2246
 
@@ -2192,16 +2277,25 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
2192
2277
  /**
2193
2278
  * This function gives the context at the root of an evaluation. It is the context of the host, with the world of
2194
2279
  * 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.
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).
2197
2284
  *
2198
2285
  * @param machine - The machine.
2199
2286
  * @param ctx - The context of the host.
2200
2287
  * @returns The context at the root.
2201
2288
  */
2202
2289
  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 };
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 };
2205
2299
  return root;
2206
2300
  }
2207
2301
 
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
  }
package/src/formula.ts CHANGED
@@ -4,8 +4,8 @@ import { type CompiledPath, type Context, type Resolution, compilePath, valueOfT
4
4
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
5
5
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled, totalCode } from "./evaluate.ts";
6
6
  import { type MachinePart, mergeMachines } from "./merge.ts";
7
- import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeText } from "./types.ts";
8
- import type { TypeInput, TypeRule } from "./typing.ts";
7
+ import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeIssues, typeText } from "./types.ts";
8
+ import type { PartType, TypeInput, TypeRule } from "./typing.ts";
9
9
  import { decodeDomain, domainTypeOf } from "./codec.ts";
10
10
  import { type StaticRead, factsOf } from "./facts.ts";
11
11
 
@@ -33,6 +33,9 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
33
33
  * `ext.domain`.
34
34
  * - `truthy`: the truth of a value for `if`, `and` and `or`. `undefined` is an error `#VALUE!`. The default
35
35
  * accepts only `true` and `false`.
36
+ * - `truthType`: the value type of a truth for the own `truthy`, for a check of types (TYPE.TRUTH, D81). A test of
37
+ * `if` and each term of `and` and `or` must fit it. Without it, the check accepts each type there. A profile without
38
+ * `truthy` ignores it, because the default truth needs `bool`.
36
39
  * - `otherwise`: the value of an `if` without `else`. The default is `null`. A profile with this key gives its value,
37
40
  * also `undefined`.
38
41
  * - `logic`: what `and` and `or` give: a truth value (the default), or the value that decided.
@@ -50,13 +53,15 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
50
53
  * overloads. An operation without a signature takes and gives `unknown` (TYPE.GRADUAL).
51
54
  *
52
55
  * A check of types trusts the signatures. Each operation must give a member of the result of each signature that
53
- * its arguments match. The hook `check` must keep each result in that type.
56
+ * its arguments match. The hook `check` must keep each result in that type. The function `truthy` must give a truth
57
+ * for each member of `truthType`.
54
58
  */
55
59
  export interface FormulaProfile {
56
60
  readonly ops?: Readonly<Record<string, OpFn>> | ((name: string) => OpFn | undefined);
57
61
  readonly lazy?: Readonly<Record<string, LazyOp>>;
58
62
  readonly ext?: Readonly<Record<string, ExtFn>>;
59
63
  readonly truthy?: (value: unknown) => boolean | undefined;
64
+ readonly truthType?: ValueType;
60
65
  readonly otherwise?: unknown;
61
66
  readonly logic?: "boolean" | "value";
62
67
  readonly omit?: (error: EvalError) => boolean;
@@ -266,12 +271,23 @@ function paramsOf(value: unknown): readonly string[] {
266
271
 
267
272
  /**
268
273
  * This function makes the type rules of the formula kit for one profile (TYPE.SYNTH, D25). Each evaluator of the kit
269
- * holds its rule. The rules read the rules of the profile that change a type: `truthy`, `logic`, `otherwise`, `omit`
270
- * and `sigs`.
274
+ * holds its rule. The rules read the rules of the profile that change a type: `truthy`, `truthType`, `logic`,
275
+ * `otherwise`, `omit` and `sigs`.
271
276
  */
272
277
  function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule>> {
273
- // Only the default truth accepts nothing but a truth value. A truth of the profile accepts each value.
274
- const truth: ValueType | undefined = profile.truthy === undefined ? "bool" : undefined;
278
+ // The default truth needs a truth value. An own truth needs its declared type, or nothing without a declaration (D81).
279
+ const declared = profile.truthy !== undefined && profile.truthType !== undefined;
280
+ const truth: ValueType | undefined = profile.truthy === undefined ? "bool" : profile.truthType;
281
+ /**
282
+ * The need of a test or a term. The check examines a declared type of a truth as a contract, one time. A declared
283
+ * type with a problem needs nothing, thus one problem gives one issue.
284
+ */
285
+ const truthPart = (input: TypeInput, what: string): PartType => {
286
+ if (truth === undefined) return {};
287
+ if (!declared) return { expect: truth, what };
288
+ const type = input.contract(truth, "truth type of the profile");
289
+ return typeIssues(type, input.machine.domains).length > 0 ? {} : { expect: type, what };
290
+ };
275
291
  const logicValue = profile.logic === "value";
276
292
  const otherwise = Object.hasOwn(profile, "otherwise") ? valueType(profile.otherwise) : "null";
277
293
  const lookup = (name: string): readonly unknown[] | undefined => {
@@ -307,7 +323,7 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
307
323
  return { sigs, bad: sigs.length !== raw.length };
308
324
  };
309
325
  const logic = (word: "and" | "or"): TypeRule => ({
310
- part: (_input, path) => (truth === undefined ? {} : { expect: truth, what: `term ${Number(path[0]) + 1} of ${word}` }),
326
+ part: (input, path) => truthPart(input, `term ${Number(path[0]) + 1} of ${word}`),
311
327
  synth: (input) => {
312
328
  if (!logicValue) return "bool";
313
329
  // With the logic "value", the result is the term that decided, or the last term.
@@ -438,7 +454,7 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
438
454
 
439
455
  "formula.if": {
440
456
  part: (input, path) => {
441
- if (path[0] === "test") return truth === undefined ? {} : { expect: truth, what: "test of if" };
457
+ if (path[0] === "test") return truthPart(input, "test of if");
442
458
  return isUnknown(input.expected, input.machine.domains) ? {} : { expect: input.expected, what: `${String(path[0])} branch of if` };
443
459
  },
444
460
  synth: (input) => {
@@ -518,15 +534,43 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
518
534
  return { ...kitOf(profile).evaluators };
519
535
  }
520
536
 
537
+ /** The declared truth of a profile: its own `truthy` and its `truthType` (D81). */
538
+ export interface DeclaredTruth {
539
+ readonly truthy: (value: unknown) => boolean | undefined;
540
+ readonly truthType: ValueType;
541
+ }
542
+
543
+ /** The evaluators of the kit that read the truth of a value. */
544
+ const TRUTH_READERS: readonly string[] = ["formula.if", "formula.and", "formula.or"];
545
+
546
+ /** The declared truth of each evaluator of the kit that reads a truth, for `conforms` (D86). */
547
+ const truthByCode = /* @__PURE__ */ new WeakMap<object, DeclaredTruth>();
548
+
549
+ /**
550
+ * This function gives the declared truth of the profile of an evaluator of the kit (D86). Only `formula.if`,
551
+ * `formula.and` and `formula.or` of a profile with an own `truthy` and a `truthType` have one. The kernel does not
552
+ * export the function.
553
+ *
554
+ * @param code - The evaluator or the runner.
555
+ * @returns The declared truth, or `undefined`.
556
+ */
557
+ export function declaredTruthOf(code: unknown): DeclaredTruth | undefined {
558
+ return (typeof code === "object" || typeof code === "function") && code !== null ? truthByCode.get(code) : undefined;
559
+ }
560
+
521
561
  /** This function makes new evaluators of the formula kit for the fields of one profile. */
522
562
  function makeEvaluators(profile: FormulaProfile): Readonly<Record<string, Evaluator>> {
523
563
  const evaluators = formulaCode(profile);
524
564
  const types = formulaTypes(profile);
565
+ const { truthy, truthType } = profile;
566
+ const truth = truthy === undefined || truthType === undefined ? undefined : { truthy, truthType };
525
567
  const out: Record<string, Evaluator> = {};
526
568
  for (const name of Object.keys(evaluators)) {
527
569
  const evaluator = evaluators[name] as CompiledEvaluator;
528
570
  const type = Object.hasOwn(types, name) ? types[name] : undefined;
529
- out[name] = Object.freeze(type === undefined ? evaluator : { compile: evaluator.compile, type });
571
+ const made = Object.freeze(type === undefined ? evaluator : { compile: evaluator.compile, type });
572
+ if (truth !== undefined && TRUTH_READERS.includes(name)) truthByCode.set(made, truth);
573
+ out[name] = made;
530
574
  }
531
575
  return Object.freeze(out);
532
576
  }
@@ -537,6 +581,7 @@ const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
537
581
  "lazy",
538
582
  "ext",
539
583
  "truthy",
584
+ "truthType",
540
585
  "otherwise",
541
586
  "logic",
542
587
  "omit",