@mark1russell7/coeval 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +315 -87
- package/build/index.d.ts.map +1 -1
- package/build/index.js +728 -32
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +66 -6
- package/src/deref.ts +27 -9
- package/src/evaluate.ts +198 -16
- package/src/index.ts +10 -0
- package/src/meaning.ts +472 -0
- package/src/reads.ts +9 -2
- package/src/schedule.ts +14 -4
- package/src/stdlib.ts +26 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/coeval",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./build/index.js",
|
package/src/agree.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Key, docEquals, equalWith } from "./doc.ts";
|
|
2
2
|
import type { Context } from "./deref.ts";
|
|
3
3
|
import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
|
|
4
|
-
import { type Effect, type Journal, type Machine, type Perform, JOURNAL, evaluate, makeContext, prepare, run } from "./evaluate.ts";
|
|
4
|
+
import { type Effect, type Journal, type Machine, type Perform, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
|
|
5
5
|
import { Held, isClosure } from "./formula.ts";
|
|
6
6
|
import { type Definitions, reference, referenceRun } from "./reference.ts";
|
|
7
7
|
import { type ReplaySettle, recorder, replayer } from "./replay.ts";
|
|
@@ -14,9 +14,11 @@ import { type ReplaySettle, recorder, replayer } from "./replay.ts";
|
|
|
14
14
|
* - `again`: a second evaluation of the same plan.
|
|
15
15
|
* - `folds`: a second plan with the other choice of folds (PLAN.FOLD). It has no folds when the plan has them, and
|
|
16
16
|
* it has folds when the option `observe` stops the folds of the plan.
|
|
17
|
+
* - `fixed`: a plan that declares each name of plain data fixed (PLAN.FIXED). It evaluates with a frozen copy of
|
|
18
|
+
* each such name, and then with the context of `agree`. `agreeRun` does not have this tier.
|
|
17
19
|
* - `other`: the second tier of a language, from the option `other`.
|
|
18
20
|
*/
|
|
19
|
-
export type Tier = "reference" | "traced" | "again" | "folds" | "other";
|
|
21
|
+
export type Tier = "reference" | "traced" | "again" | "folds" | "fixed" | "other";
|
|
20
22
|
|
|
21
23
|
/** What a second tier gives: its result, and its effects when it records them. */
|
|
22
24
|
export interface TierResult {
|
|
@@ -45,8 +47,8 @@ export interface AgreeOptions {
|
|
|
45
47
|
/** The definitional code of the compiled evaluators of the machine, for the reference. */
|
|
46
48
|
readonly definitions?: Definitions;
|
|
47
49
|
/**
|
|
48
|
-
* The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation
|
|
49
|
-
*
|
|
50
|
+
* The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation, the
|
|
51
|
+
* plan with the other choice of folds and the plan with fixed names.
|
|
50
52
|
*/
|
|
51
53
|
readonly tiers?: readonly Exclude<Tier, "other">[];
|
|
52
54
|
/**
|
|
@@ -85,7 +87,7 @@ export interface AgreeOptions {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** The tiers that `agree` compares when its options name none. */
|
|
88
|
-
const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.freeze(["reference", "traced", "again", "folds"]);
|
|
90
|
+
const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.freeze(["reference", "traced", "again", "folds", "fixed"]);
|
|
89
91
|
|
|
90
92
|
/** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
|
|
91
93
|
export type Agreement =
|
|
@@ -132,7 +134,8 @@ function sameInTiers(x: unknown, y: unknown): boolean | undefined {
|
|
|
132
134
|
|
|
133
135
|
const CLOSURE = /* @__PURE__ */ Symbol.for("coeval.closure");
|
|
134
136
|
|
|
135
|
-
function
|
|
137
|
+
/** This function gives the parameters of a closure of `fn`, or `undefined` for another function. */
|
|
138
|
+
export function closureParams(fn: object): unknown {
|
|
136
139
|
return (fn as { readonly [CLOSURE]?: { readonly params: readonly string[] } })[CLOSURE]?.params;
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -184,6 +187,57 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
|
|
|
184
187
|
return undefined;
|
|
185
188
|
}
|
|
186
189
|
|
|
190
|
+
/**
|
|
191
|
+
* This function compares the plan with the tier `fixed` (PLAN.FIXED). The tier declares each name of plain data fixed,
|
|
192
|
+
* with a frozen copy of its value. It evaluates with these copies, thus the folds with fixed names give the result.
|
|
193
|
+
* Then it evaluates with the context of `agree`, thus the code without the fold gives the result. A fold moves the
|
|
194
|
+
* calls of the host to the compilation, thus only the result and the effects compare.
|
|
195
|
+
*/
|
|
196
|
+
function fixedDiffers(
|
|
197
|
+
machine: Machine,
|
|
198
|
+
doc: unknown,
|
|
199
|
+
context: Context,
|
|
200
|
+
result: Result,
|
|
201
|
+
effects: readonly Effect[],
|
|
202
|
+
given: { readonly declaredType?: string; readonly verb?: string; readonly perform?: Perform },
|
|
203
|
+
options: AgreeOptions,
|
|
204
|
+
): { readonly other: Result; readonly reason: string } | undefined {
|
|
205
|
+
const { perform, ...declared } = given;
|
|
206
|
+
const held = fixedContext(context);
|
|
207
|
+
const plan = prepare(machine, doc, { ...declared, fixed: { names: held.fixed } });
|
|
208
|
+
for (const each of [held.context, context]) {
|
|
209
|
+
const tierEffects: Effect[] = [];
|
|
210
|
+
const other = plan.evaluate(each, { effects: tierEffects, ...(perform === undefined ? {} : { perform }) });
|
|
211
|
+
options.observe?.();
|
|
212
|
+
if (overflowed(result) || overflowed(other)) continue;
|
|
213
|
+
const reason = resultDiffers(result, other, options) ?? effectsDiffer(effects, tierEffects, options);
|
|
214
|
+
if (reason !== undefined) return { other, reason };
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* This function gives the context of the tier `fixed`. Each name of plain data gets a deeply frozen copy of its value,
|
|
221
|
+
* and the record of the fixed names holds the same copy. Each other name keeps its value.
|
|
222
|
+
*/
|
|
223
|
+
function fixedContext(context: Context): { readonly context: Context; readonly fixed: Readonly<Record<string, unknown>> } {
|
|
224
|
+
const given = context.names;
|
|
225
|
+
const names = Object.create(Object.getPrototypeOf(given) as object | null) as Record<string, unknown>;
|
|
226
|
+
const fixed = Object.create(null) as Record<string, unknown>;
|
|
227
|
+
for (const key of Reflect.ownKeys(given)) {
|
|
228
|
+
let field = Object.getOwnPropertyDescriptor(given, key) as PropertyDescriptor;
|
|
229
|
+
if (typeof key === "string" && "value" in field) {
|
|
230
|
+
const copy = sharedCopy(field.value);
|
|
231
|
+
if (copy !== undefined || field.value === undefined) {
|
|
232
|
+
Object.defineProperty(fixed, key, { value: copy, enumerable: true });
|
|
233
|
+
field = { ...field, value: copy };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
Object.defineProperty(names, key, field);
|
|
237
|
+
}
|
|
238
|
+
return { context: { ...context, names }, fixed };
|
|
239
|
+
}
|
|
240
|
+
|
|
187
241
|
/**
|
|
188
242
|
* This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
|
|
189
243
|
* results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
|
|
@@ -228,6 +282,10 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
228
282
|
const traced = evaluate(machine, doc, context, { ...declared, ...mock });
|
|
229
283
|
other = traced.result;
|
|
230
284
|
otherEffects = [...traced.effects];
|
|
285
|
+
} else if (tier === "fixed") {
|
|
286
|
+
const why = fixedDiffers(machine, doc, context, result, effects, { ...declared, ...mock }, options);
|
|
287
|
+
if (why !== undefined) return { ok: false, tier, plan: result, other: why.other, reason: why.reason };
|
|
288
|
+
continue;
|
|
231
289
|
} else if (tier === "folds") {
|
|
232
290
|
other = prepare(machine, doc, { ...declared, fold: !folds }).evaluate(context, { effects: otherEffects, ...mock });
|
|
233
291
|
if (observe !== undefined) {
|
|
@@ -340,6 +398,8 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
|
|
|
340
398
|
const log = recording.log();
|
|
341
399
|
|
|
342
400
|
for (const tier of options.tiers ?? DEFAULT_TIERS) {
|
|
401
|
+
// A run has no tier with fixed names (PLAN.FIXED).
|
|
402
|
+
if (tier === "fixed") continue;
|
|
343
403
|
const replay = watched(replayer(log));
|
|
344
404
|
const host = { perform: replay.perform };
|
|
345
405
|
let other: Result;
|
package/src/deref.ts
CHANGED
|
@@ -48,6 +48,15 @@ export interface HostThing {
|
|
|
48
48
|
* @returns What the keys reach, or `undefined` when they reach nothing. `{ thing: undefined }` tells that the keys reach a field that holds `undefined`.
|
|
49
49
|
*/
|
|
50
50
|
resolve?(keys: readonly Key[]): { readonly thing: Thing } | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* This optional function answers one step only when the host has its value without new work (SCHED.CLOSURE). A
|
|
53
|
+
* kept read of a closure uses it, because the evaluation did not ask for that read. A schedule gives the value of
|
|
54
|
+
* a node that has its result in the data epoch. A throw gives the error of the step.
|
|
55
|
+
*
|
|
56
|
+
* @param key - The key of the step.
|
|
57
|
+
* @returns What the step reaches, or `undefined` when the host does not know it yet. Then the kept read ends at the thing.
|
|
58
|
+
*/
|
|
59
|
+
known?(key: Key): { readonly thing: Thing } | undefined;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
/** Something that a path can reach: any value of the host, or a host thing. */
|
|
@@ -116,6 +125,14 @@ export interface WalkInput {
|
|
|
116
125
|
* @returns What the step reaches, or `undefined`.
|
|
117
126
|
*/
|
|
118
127
|
deref(thing: Thing, key: Key): Thing | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* This optional function records one hop inside the walk (HOP.EACH-STEP). The resolution gives it only when it
|
|
130
|
+
* records hops. The hop of the whole walk comes after these hops, as before.
|
|
131
|
+
*
|
|
132
|
+
* @param key - The key that the walk followed.
|
|
133
|
+
* @param thing - The thing that the key reached.
|
|
134
|
+
*/
|
|
135
|
+
readonly visit?: (key: Key, thing: Thing) => void;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
/** A walker: the code of the verb `step` of a kind of step documents. */
|
|
@@ -651,15 +668,16 @@ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], ho
|
|
|
651
668
|
const walker = given === undefined ? walkerOf(nav, step) : (given ?? undefined);
|
|
652
669
|
if (walker === undefined) return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
|
|
653
670
|
const envelope = step as Envelope;
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
671
|
+
const deref = (thing: Thing, key: Key): Thing | undefined => {
|
|
672
|
+
const reached = derefStep(nav, thing, key);
|
|
673
|
+
return reached === undefined ? undefined : adopt(reached.thing);
|
|
674
|
+
};
|
|
675
|
+
// A resolution without a record of hops gives no `visit`, thus a walker records nothing (HOP.EACH-STEP).
|
|
676
|
+
const result = walker(
|
|
677
|
+
hops === undefined
|
|
678
|
+
? { data: envelope.value, here, context: ctx, deref }
|
|
679
|
+
: { data: envelope.value, here, context: ctx, deref, visit: (key, thing) => hops.push({ step: key, thing, by: "step" }) },
|
|
680
|
+
);
|
|
663
681
|
if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
|
|
664
682
|
here = adopt(result.thing);
|
|
665
683
|
hops?.push({ step: envelope as Json, thing: here, by: "step" });
|
package/src/evaluate.ts
CHANGED
|
@@ -904,30 +904,95 @@ interface Reading {
|
|
|
904
904
|
paths?: Set<string>;
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
+
/** The walk of a kept path: the path, and what it reached when a host thing answered a step with `known`. */
|
|
908
|
+
interface KeptWalk {
|
|
909
|
+
readonly path: readonly unknown[];
|
|
910
|
+
/** What the path reached. Without it, the read resolves its path with `readAgain`. */
|
|
911
|
+
readonly reached?: Result;
|
|
912
|
+
}
|
|
913
|
+
|
|
907
914
|
/**
|
|
908
915
|
* This function gives the path of a kept read (READS.CLOSURE). The evaluation did not ask for the read. Thus the read
|
|
909
|
-
* does not step into a host thing
|
|
916
|
+
* does not step into a host thing with `deref`. A host thing with `known` answers a step without new work
|
|
917
|
+
* (SCHED.CLOSURE). Each other host thing ends the path, and a host thing that does not know the step ends it too.
|
|
910
918
|
* A reference in the data reads the context of its reader, thus the function then gives `undefined`.
|
|
911
919
|
*
|
|
912
920
|
* @param nav - The world and the code of the evaluators.
|
|
913
921
|
* @param root - The context at the root of the evaluation.
|
|
914
922
|
* @param path - The root and the keys of the static read.
|
|
915
|
-
* @returns The path of the kept read
|
|
923
|
+
* @returns The path of the kept read with what it reached after a step of `known`, or `undefined` for a reference on the way.
|
|
916
924
|
*/
|
|
917
|
-
function keptPath(nav: Navigator, root: Context, path: readonly unknown[]):
|
|
925
|
+
function keptPath(nav: Navigator, root: Context, path: readonly unknown[]): KeptWalk | undefined {
|
|
918
926
|
const adopt = nav.adopt ?? ((value: unknown): Thing => value);
|
|
919
927
|
let here = adopt(root[path[0] as Root]);
|
|
928
|
+
// This mark is `true` after a step of `known`. Then the walk gives what the path reached, and no resolution follows.
|
|
929
|
+
let known = false;
|
|
930
|
+
const end = (index: number): KeptWalk => {
|
|
931
|
+
const kept = index >= path.length ? path : path.slice(0, index);
|
|
932
|
+
return known ? { path: kept, reached: ok(here) } : { path: kept };
|
|
933
|
+
};
|
|
920
934
|
for (let index = 1; ; index += 1) {
|
|
921
|
-
if (isHostThing(here))
|
|
935
|
+
if (isHostThing(here)) {
|
|
936
|
+
if (index >= path.length || here.known === undefined) return end(index);
|
|
937
|
+
let step: { readonly thing: Thing } | undefined;
|
|
938
|
+
try {
|
|
939
|
+
step = here.known(path[index] as Key);
|
|
940
|
+
} catch (error) {
|
|
941
|
+
// A failed value gives its error to the read, as an exception in `readAgain` does.
|
|
942
|
+
return { path: path.slice(0, index + 1), reached: thrown(error, READ_FAILED) };
|
|
943
|
+
}
|
|
944
|
+
if (step === undefined) return end(index);
|
|
945
|
+
known = true;
|
|
946
|
+
here = adopt(step.thing);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
922
949
|
if (nav.plain !== true && isRef(here)) return undefined;
|
|
923
|
-
if (index >= path.length) return
|
|
950
|
+
if (index >= path.length) return end(index);
|
|
924
951
|
const reached = derefStep(nav, here, path[index] as Key);
|
|
925
952
|
// A step that reaches nothing fails, and the resolution of the path fails at the same step.
|
|
926
|
-
if (reached === undefined) return path;
|
|
953
|
+
if (reached === undefined) return known ? end(index) : { path };
|
|
927
954
|
here = adopt(reached.thing);
|
|
928
955
|
}
|
|
929
956
|
}
|
|
930
957
|
|
|
958
|
+
/**
|
|
959
|
+
* This function resolves the path of a kept read again, for `verifyReads` (SCHED.CLOSURE). It walks the path as
|
|
960
|
+
* `keptPath` did. A host thing with `known` answers its step. A path without such a step resolves with `readAgain`.
|
|
961
|
+
*
|
|
962
|
+
* @param nav - The world and the code of the evaluators.
|
|
963
|
+
* @param ctx - The new context.
|
|
964
|
+
* @param path - The path of the kept read.
|
|
965
|
+
* @returns What the path reaches, or `undefined` when a host does not know a step yet. Then the read changed.
|
|
966
|
+
*/
|
|
967
|
+
export function keptAgain(nav: Navigator, ctx: Context, path: readonly unknown[]): Result | undefined {
|
|
968
|
+
const adopt = nav.adopt ?? ((value: unknown): Thing => value);
|
|
969
|
+
let here = adopt(ctx[path[0] as Root]);
|
|
970
|
+
let known = false;
|
|
971
|
+
// Before a step of `known`, each other answer of the walk is the answer of `readAgain`, as before SCHED.CLOSURE.
|
|
972
|
+
const other = (): Result | undefined => (known ? undefined : readAgain(nav, ctx, path, false).reached);
|
|
973
|
+
for (let index = 1; index < path.length; index += 1) {
|
|
974
|
+
if (isHostThing(here)) {
|
|
975
|
+
if (here.known === undefined) return other();
|
|
976
|
+
let step: { readonly thing: Thing } | undefined;
|
|
977
|
+
try {
|
|
978
|
+
step = here.known(path[index] as Key);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
return thrown(error, READ_FAILED);
|
|
981
|
+
}
|
|
982
|
+
if (step === undefined) return undefined;
|
|
983
|
+
known = true;
|
|
984
|
+
here = adopt(step.thing);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (nav.plain !== true && isRef(here)) return other();
|
|
988
|
+
const reached = derefStep(nav, here, path[index] as Key);
|
|
989
|
+
if (reached === undefined) return other();
|
|
990
|
+
here = adopt(reached.thing);
|
|
991
|
+
}
|
|
992
|
+
if (!known) return other();
|
|
993
|
+
return nav.plain !== true && isRef(here) ? undefined : ok(here);
|
|
994
|
+
}
|
|
995
|
+
|
|
931
996
|
/** This function makes the record of the reads of an evaluation, when the host gives a list. */
|
|
932
997
|
function readingOf(list: Read[] | undefined, root: Context): Reading | undefined {
|
|
933
998
|
return list === undefined ? undefined : { list, root, kept: false, self: false };
|
|
@@ -1055,14 +1120,15 @@ class Session implements Runtime {
|
|
|
1055
1120
|
// A read of the whole root self already holds each path of self.
|
|
1056
1121
|
if (root === "self" && reading.self) return true;
|
|
1057
1122
|
// A step document can walk outside the keys of its path, thus such a read keeps the whole root.
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1123
|
+
const walk = keptPath(this.machine, reading.root, read.exact ? exact : [root]);
|
|
1124
|
+
if (walk === undefined) return false;
|
|
1125
|
+
const path = walk.path;
|
|
1060
1126
|
const paths = (reading.paths ??= new Set());
|
|
1061
1127
|
const text = JSON.stringify(path);
|
|
1062
1128
|
if (paths.has(text) || paths.has(JSON.stringify([root]))) return true;
|
|
1063
1129
|
paths.add(text);
|
|
1064
1130
|
if (path.length === 1 && root === "self") reading.self = true;
|
|
1065
|
-
reading.list.push({ path, reached: readAgain(this.machine, reading.root, path, false).reached, free: true, kept: true });
|
|
1131
|
+
reading.list.push({ path, reached: walk.reached ?? readAgain(this.machine, reading.root, path, false).reached, free: true, kept: true });
|
|
1066
1132
|
return true;
|
|
1067
1133
|
}
|
|
1068
1134
|
|
|
@@ -1402,6 +1468,15 @@ export interface PrepareOptions {
|
|
|
1402
1468
|
* evaluates one time, when the plan compiles. Its result is one frozen value for each evaluation (PLAN.FOLD).
|
|
1403
1469
|
*/
|
|
1404
1470
|
readonly fold?: boolean;
|
|
1471
|
+
/**
|
|
1472
|
+
* The values that the host declares fixed for each evaluation of the plan (PLAN.FIXED, D60). The field `names` gives
|
|
1473
|
+
* a value for each fixed name. A path from a fixed name then resolves one time, when the plan compiles, and a pure
|
|
1474
|
+
* part that reads only fixed names folds. Only a deeply frozen value of plain data fixes its name.
|
|
1475
|
+
*
|
|
1476
|
+
* Each folded part compares the value of its names with one own-key test and `Object.is`. A different value gives
|
|
1477
|
+
* the code without the fold. A plan without folds ignores this option.
|
|
1478
|
+
*/
|
|
1479
|
+
readonly fixed?: { readonly names?: Readonly<Record<string, unknown>> };
|
|
1405
1480
|
}
|
|
1406
1481
|
|
|
1407
1482
|
/** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
|
|
@@ -1440,6 +1515,8 @@ interface Compiler {
|
|
|
1440
1515
|
readonly noAnswer: Answer;
|
|
1441
1516
|
/** `true` when closed pure parts fold (PLAN.FOLD). A plan with a trace does not fold. */
|
|
1442
1517
|
readonly fold: boolean;
|
|
1518
|
+
/** The fixed names and their values, or `undefined` when the plan has none (PLAN.FIXED). */
|
|
1519
|
+
readonly fixed: Readonly<Record<string, unknown>> | undefined;
|
|
1443
1520
|
/** The runtime of the folds of this compilation. The first fold makes it. */
|
|
1444
1521
|
probe?: Probe;
|
|
1445
1522
|
}
|
|
@@ -1600,7 +1677,11 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1600
1677
|
if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
|
|
1601
1678
|
const free = freeOf(doc, kind, split.parts, frees);
|
|
1602
1679
|
if (free === undefined) return done;
|
|
1603
|
-
|
|
1680
|
+
if (free.length === 0) return folded(c, done, undefined);
|
|
1681
|
+
// A part that reads only fixed names folds, and its code compares the values of these names (PLAN.FIXED).
|
|
1682
|
+
const fixed = c.fixed;
|
|
1683
|
+
if (fixed !== undefined && free.every((each) => Object.hasOwn(fixed, each))) return folded(c, done, free);
|
|
1684
|
+
return { code: done.code, run: done.run, free };
|
|
1604
1685
|
}
|
|
1605
1686
|
|
|
1606
1687
|
/*
|
|
@@ -1693,6 +1774,20 @@ class Probe implements Runtime {
|
|
|
1693
1774
|
this.root = root;
|
|
1694
1775
|
}
|
|
1695
1776
|
|
|
1777
|
+
/** The context of a part that folds with fixed names: the context of the probe, with the fixed names (PLAN.FIXED). */
|
|
1778
|
+
private withFixed: Context | undefined;
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* This function gives the context of a part that folds with fixed names. Its root `names` holds only the fixed
|
|
1782
|
+
* names, thus the probe resolves each fixed name and refuses each other name.
|
|
1783
|
+
*
|
|
1784
|
+
* @param fixed - The fixed names and their values.
|
|
1785
|
+
* @returns The context.
|
|
1786
|
+
*/
|
|
1787
|
+
fixedRoot(fixed: Readonly<Record<string, unknown>>): Context {
|
|
1788
|
+
return (this.withFixed ??= { ...this.root, names: fixed });
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1696
1791
|
resolve(ctx: Context, path: readonly unknown[]): Resolution {
|
|
1697
1792
|
const name = path[1];
|
|
1698
1793
|
if (path[0] === "names" && isKey(name) && Object.hasOwn(ctx.names, String(name))) {
|
|
@@ -1757,25 +1852,106 @@ function trap(refuse: () => void, copies: boolean): object {
|
|
|
1757
1852
|
* one deeply frozen copy, and the code gives that copy at each evaluation (PLAN.SHARED). A fold that the probe cancels
|
|
1758
1853
|
* gives the code without free names, thus no parent tries the fold again.
|
|
1759
1854
|
*/
|
|
1760
|
-
function folded(c: Compiler, done: Compiled): Compiled {
|
|
1855
|
+
function folded(c: Compiler, done: Compiled, fixed: readonly string[] | undefined): Compiled {
|
|
1761
1856
|
const probe = (c.probe ??= new Probe(c.machine));
|
|
1762
1857
|
probe.refused = false;
|
|
1763
1858
|
probe.reads = 0;
|
|
1764
1859
|
let constant: Result | undefined;
|
|
1765
1860
|
try {
|
|
1766
|
-
const
|
|
1861
|
+
const root = fixed === undefined ? probe.root : probe.fixedRoot(c.fixed as Readonly<Record<string, unknown>>);
|
|
1862
|
+
const result = done.code(root, probe);
|
|
1767
1863
|
if (!probe.refused && (result.ok || result.error.code !== "#DEPTH!")) constant = sharedCopy(result) as Result | undefined;
|
|
1768
1864
|
} catch {
|
|
1769
1865
|
constant = undefined;
|
|
1770
1866
|
}
|
|
1771
1867
|
if (constant === undefined) return { code: done.code, run: done.run };
|
|
1772
1868
|
const value = constant;
|
|
1869
|
+
const original = done.code;
|
|
1870
|
+
if (fixed !== undefined) return { code: fixedCode(fixed, c.fixed as Readonly<Record<string, unknown>>, value, original), run: undefined, free: fixed };
|
|
1773
1871
|
if (probe.reads === 0) return { code: () => value, run: undefined, free: CLOSED };
|
|
1774
1872
|
// The part reads its own names. A list of reads gets each read, thus the code evaluates again (READS.RECORD).
|
|
1775
|
-
const original = done.code;
|
|
1776
1873
|
return { code: (ctx, rt) => (rt instanceof Session && !rt.traced && !rt.recording ? value : original(ctx, rt)), run: undefined, free: CLOSED };
|
|
1777
1874
|
}
|
|
1778
1875
|
|
|
1876
|
+
/**
|
|
1877
|
+
* This function gives the code of a part that folded with fixed names (PLAN.FIXED, D60). The code gives the folded
|
|
1878
|
+
* value when the context has the fixed value of each name, by an own-key test and `Object.is`. A fixed value is
|
|
1879
|
+
* deeply frozen, thus the same object gives the same result. Each other context, a list of reads and another runtime
|
|
1880
|
+
* get the code without the fold. The probe of a parent fold gets the folded value, and it counts a read.
|
|
1881
|
+
*/
|
|
1882
|
+
function fixedCode(names: readonly string[], fixed: Readonly<Record<string, unknown>>, value: Result, original: Code): Code {
|
|
1883
|
+
if (names.length === 1) {
|
|
1884
|
+
const name = names[0] as string;
|
|
1885
|
+
const want = fixed[name];
|
|
1886
|
+
return (ctx, rt) => {
|
|
1887
|
+
const given = ctx.names;
|
|
1888
|
+
return Object.hasOwn(given, name) && Object.is(given[name], want) && folds(rt) ? value : original(ctx, rt);
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
const wants = names.map((name) => fixed[name]);
|
|
1892
|
+
return (ctx, rt) => {
|
|
1893
|
+
const given = ctx.names;
|
|
1894
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
1895
|
+
const name = names[index] as string;
|
|
1896
|
+
if (!Object.hasOwn(given, name) || !Object.is(given[name], wants[index])) return original(ctx, rt);
|
|
1897
|
+
}
|
|
1898
|
+
return folds(rt) ? value : original(ctx, rt);
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* This function tells if the code of a part with fixed names can give its folded value in a runtime. The probe of a
|
|
1904
|
+
* fold counts the read of the part, as the code without the fold reads the names.
|
|
1905
|
+
*/
|
|
1906
|
+
function folds(rt: Runtime): boolean {
|
|
1907
|
+
if (rt instanceof Session) return !rt.recording;
|
|
1908
|
+
if (!(rt instanceof Probe)) return false;
|
|
1909
|
+
rt.reads += 1;
|
|
1910
|
+
return true;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/**
|
|
1914
|
+
* This function gives the fixed names of the options of `prepare` (PLAN.FIXED). A name fixes only when its value is
|
|
1915
|
+
* deeply frozen plain data. Then no change in place can make the folded value stale.
|
|
1916
|
+
*
|
|
1917
|
+
* @returns A record without a prototype, or `undefined` when no name fixes.
|
|
1918
|
+
*/
|
|
1919
|
+
function fixedNamesOf(given: Readonly<Record<string, unknown>> | undefined): Readonly<Record<string, unknown>> | undefined {
|
|
1920
|
+
if (given === undefined) return undefined;
|
|
1921
|
+
let out: Record<string, unknown> | undefined;
|
|
1922
|
+
for (const name of Object.keys(given)) {
|
|
1923
|
+
const value = given[name];
|
|
1924
|
+
if (!frozenPlain(value)) continue;
|
|
1925
|
+
out ??= Object.create(null) as Record<string, unknown>;
|
|
1926
|
+
Object.defineProperty(out, name, { value, enumerable: true });
|
|
1927
|
+
}
|
|
1928
|
+
return out;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/** This function tells if a value is plain data, as `sharedCopy` reads it, and if each of its objects is frozen. */
|
|
1932
|
+
function frozenPlain(value: unknown): boolean {
|
|
1933
|
+
const seen = new Set<object>();
|
|
1934
|
+
const walk = (here: unknown): boolean => {
|
|
1935
|
+
if (typeof here === "function") return false;
|
|
1936
|
+
if (typeof here !== "object" || here === null || SHARED.has(here) || seen.has(here)) return true;
|
|
1937
|
+
if (!Object.isFrozen(here)) return false;
|
|
1938
|
+
seen.add(here);
|
|
1939
|
+
const proto: unknown = Object.getPrototypeOf(here);
|
|
1940
|
+
if (Array.isArray(here) ? proto !== Array.prototype : proto !== Object.prototype && proto !== null) return false;
|
|
1941
|
+
for (const key of Reflect.ownKeys(here)) {
|
|
1942
|
+
const field = Object.getOwnPropertyDescriptor(here, key);
|
|
1943
|
+
if (typeof key !== "string" || field === undefined || !("value" in field) || !walk(field.value)) return false;
|
|
1944
|
+
}
|
|
1945
|
+
return true;
|
|
1946
|
+
};
|
|
1947
|
+
try {
|
|
1948
|
+
return walk(value);
|
|
1949
|
+
} catch {
|
|
1950
|
+
// A very deep value overflows the stack of the host.
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1779
1955
|
/** The objects that a fold made. Each one is deeply frozen, thus a later fold uses it again and does not copy it. */
|
|
1780
1956
|
const SHARED: WeakSet<object> = /* @__PURE__ */ new WeakSet();
|
|
1781
1957
|
|
|
@@ -1785,9 +1961,13 @@ const NOT_PLAIN: unique symbol = /* @__PURE__ */ Symbol("not plain");
|
|
|
1785
1961
|
/**
|
|
1786
1962
|
* This function gives a deeply frozen copy of plain data, or `undefined` for other values. Plain data is a primitive or
|
|
1787
1963
|
* an array without holes and extra keys. It is also a record of enumerable data fields with the prototype of an object
|
|
1788
|
-
* or no prototype. The copy keeps cycles and shared objects. The objects of the host do not change.
|
|
1964
|
+
* or no prototype. The copy keeps cycles and shared objects. The objects of the host do not change. The tier `fixed`
|
|
1965
|
+
* of `agree` uses it. The package does not export it.
|
|
1966
|
+
*
|
|
1967
|
+
* @param value - The value.
|
|
1968
|
+
* @returns The frozen copy, or `undefined`.
|
|
1789
1969
|
*/
|
|
1790
|
-
function sharedCopy(value: unknown): unknown {
|
|
1970
|
+
export function sharedCopy(value: unknown): unknown {
|
|
1791
1971
|
try {
|
|
1792
1972
|
const out = copyPlain(value, new Map());
|
|
1793
1973
|
return out === NOT_PLAIN ? undefined : out;
|
|
@@ -2042,7 +2222,9 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
|
|
|
2042
2222
|
const verb = options.verb ?? "eval";
|
|
2043
2223
|
const noAnswer: Answer = verb === "eval" ? NO_ANSWER : { verb, error: "no-answer", steps: [] };
|
|
2044
2224
|
const traced = options.trace === true;
|
|
2045
|
-
const
|
|
2225
|
+
const fold = !traced && options.fold !== false;
|
|
2226
|
+
const fixed = fold ? fixedNamesOf(options.fixed?.names) : undefined;
|
|
2227
|
+
const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold, fixed };
|
|
2046
2228
|
let root: Compiled;
|
|
2047
2229
|
try {
|
|
2048
2230
|
root = compileNode(c, doc, [], options.declaredType, 0);
|
package/src/index.ts
CHANGED
|
@@ -176,6 +176,7 @@ export {
|
|
|
176
176
|
STD_WORLD,
|
|
177
177
|
attributeTrace,
|
|
178
178
|
nearestWalker,
|
|
179
|
+
placeOf,
|
|
179
180
|
} from "./stdlib.ts";
|
|
180
181
|
export {
|
|
181
182
|
type Edge,
|
|
@@ -266,6 +267,15 @@ export {
|
|
|
266
267
|
edgesOf,
|
|
267
268
|
schedule,
|
|
268
269
|
} from "./schedule.ts";
|
|
270
|
+
export {
|
|
271
|
+
type AgreeScheduleOptions,
|
|
272
|
+
type ScheduleAgreement,
|
|
273
|
+
type ScheduleAgreementCounts,
|
|
274
|
+
type ScheduleEngine,
|
|
275
|
+
type ScheduleEngineFactory,
|
|
276
|
+
type ScheduleEngineStart,
|
|
277
|
+
agreeSchedule,
|
|
278
|
+
} from "./meaning.ts";
|
|
269
279
|
export { type GraphCycle, type GraphEdge, type GraphNode, type GraphOptions, type NodeGraph, components, cycles, heights, necessary, nodeLayers } from "./graph.ts";
|
|
270
280
|
export { type Recorder, type ReplayEntry, type ReplayLog, type ReplayOptions, type ReplaySettle, recorder, replayer } from "./replay.ts";
|
|
271
281
|
export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
|