@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/README.md +9 -4
- package/build/index.d.ts +522 -171
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2352 -296
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +128 -16
- package/src/conform.ts +49 -5
- package/src/deref.ts +76 -11
- package/src/evaluate.ts +113 -19
- package/src/facts.ts +113 -78
- package/src/formula.ts +55 -10
- package/src/index.ts +23 -1
- package/src/merge.ts +46 -14
- package/src/reference.ts +90 -14
- package/src/replay.ts +964 -18
- package/src/static.ts +2 -2
- package/src/stdlib.ts +55 -2
- package/src/syntax.ts +357 -52
- package/src/types.ts +443 -16
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
|
-
*
|
|
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
|
-
|
|
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
|
|
211
|
-
if ((ctx as Granted)[GRANT] === ctx.caps && (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
|
-
/**
|
|
1028
|
-
|
|
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
|
|
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).
|
|
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
|
-
|
|
2204
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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`, `
|
|
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
|
-
//
|
|
274
|
-
const
|
|
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: (
|
|
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
|
|
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
|
-
|
|
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",
|