@mark1russell7/coeval 0.26.0 → 0.27.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 +25 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +196 -79
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/errors.ts +1 -0
- package/src/evaluate.ts +113 -14
- package/src/formula.ts +7 -4
- package/src/generate.ts +4 -9
- package/src/index.ts +1 -0
- package/src/merge.ts +1 -1
- package/src/reference.ts +93 -38
- package/src/stdlib.ts +39 -14
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.27.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",
|
|
@@ -46,6 +46,9 @@
|
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@mark1russell7/rigor-generators": "^0.1.8"
|
|
51
|
+
},
|
|
49
52
|
"scripts": {
|
|
50
53
|
"build": "tsdown",
|
|
51
54
|
"test": "vitest run",
|
package/src/errors.ts
CHANGED
package/src/evaluate.ts
CHANGED
|
@@ -541,6 +541,25 @@ export function placeText(place: readonly Key[]): string {
|
|
|
541
541
|
/** An effect while the kernel waits for its outcome. */
|
|
542
542
|
type Recorded = { -readonly [K in keyof Effect]: Effect[K] };
|
|
543
543
|
|
|
544
|
+
/**
|
|
545
|
+
* The most work of one evaluation for a machine without its own `maxWork` (WORK.LIMIT, D134). One application of a
|
|
546
|
+
* closure is one point.
|
|
547
|
+
*/
|
|
548
|
+
export const WORK_LIMIT = 200000;
|
|
549
|
+
|
|
550
|
+
/** The work of one node that an equation makes (WORK.NODE, D134). A node costs more than an application. */
|
|
551
|
+
export const NODE_WORK = 8;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* This function gives the error of the limit of work. Each tier gives this error (WORK.AGREES).
|
|
555
|
+
*
|
|
556
|
+
* @param limit - The limit of the machine.
|
|
557
|
+
* @returns The error.
|
|
558
|
+
*/
|
|
559
|
+
export function workFailure(limit: number): EvalError {
|
|
560
|
+
return fail("#WORK!", `The work of one evaluation is more than ${limit} points. The field maxWork of the machine sets the limit.`).error;
|
|
561
|
+
}
|
|
562
|
+
|
|
544
563
|
/**
|
|
545
564
|
* The runtime of one evaluation or run. Compiled code gets it with the context, and asks it to resolve paths and
|
|
546
565
|
* to cause effects.
|
|
@@ -549,6 +568,15 @@ export interface Runtime {
|
|
|
549
568
|
readonly machine: Machine;
|
|
550
569
|
/** `true` in a run: the host does each effect, and a part that waits needs a parent that can wait. */
|
|
551
570
|
readonly running: boolean;
|
|
571
|
+
/** `true` when a node that this runtime makes costs work: in an equation of a tree (WORK.NODE) and in a fold. */
|
|
572
|
+
readonly nested?: boolean;
|
|
573
|
+
/**
|
|
574
|
+
* This optional function adds work to the count of the evaluation (WORK.LIMIT). A runtime without it counts nothing.
|
|
575
|
+
*
|
|
576
|
+
* @param points - The work.
|
|
577
|
+
* @throws {Failure} The error `#WORK!`, when the count is over the limit of the machine.
|
|
578
|
+
*/
|
|
579
|
+
spend?(points: number): void;
|
|
552
580
|
/**
|
|
553
581
|
* This function resolves a path from a context.
|
|
554
582
|
*
|
|
@@ -819,6 +847,11 @@ export interface CompiledEvaluator {
|
|
|
819
847
|
* (TYPE.RULE). Only a check reads it. An evaluator without a rule gives `unknown`.
|
|
820
848
|
*/
|
|
821
849
|
readonly type?: TypeRule;
|
|
850
|
+
/**
|
|
851
|
+
* `true` when a value that the code gives can use its runtime after the end of the evaluation (WORK.KEEPS). The plan
|
|
852
|
+
* then marks that end. A compiled evaluator without this field gives no such value.
|
|
853
|
+
*/
|
|
854
|
+
readonly keeps?: boolean;
|
|
822
855
|
}
|
|
823
856
|
|
|
824
857
|
/** An evaluator: a function of its input, or a compiler. */
|
|
@@ -848,6 +881,7 @@ export type Runner = RunFn | CompiledRunner;
|
|
|
848
881
|
* A machine: a world of kinds, and the code of each evaluator name, of each `deref` and of each walker.
|
|
849
882
|
*
|
|
850
883
|
* - `maxDepth`: the deepest nesting of documents. The default is 1000.
|
|
884
|
+
* - `maxWork`: the most work of one evaluation (WORK.LIMIT, D134). The default is `WORK_LIMIT`.
|
|
851
885
|
* - `permits`: the rule of the host for an effect. The default is the function `permits` on the capabilities. A run,
|
|
852
886
|
* `check` and `effectsOf` ask it (CAP.PERMITS). In a check, its context has only the capabilities and the world.
|
|
853
887
|
* - `known`: the names that the evaluators know, for `check`. Without it, `check` does not check such names.
|
|
@@ -859,6 +893,7 @@ export interface Machine extends Navigator {
|
|
|
859
893
|
/** The runners, by evaluator name. A runner wins over an evaluator of the same name. */
|
|
860
894
|
readonly runners?: Readonly<Record<string, Runner>>;
|
|
861
895
|
readonly maxDepth?: number;
|
|
896
|
+
readonly maxWork?: number;
|
|
862
897
|
readonly permits?: (caps: readonly Cap[], kind: readonly Key[], context: Context) => boolean;
|
|
863
898
|
/**
|
|
864
899
|
* This function tells if an evaluator knows the name in the path of a kind. An example is the operation
|
|
@@ -1157,6 +1192,13 @@ function readingOf(list: Read[] | undefined, root: Context): Reading | undefined
|
|
|
1157
1192
|
|
|
1158
1193
|
/** This class holds the state of one evaluation or run: the machine, the effects, the clock and the trace. */
|
|
1159
1194
|
class Session implements Runtime {
|
|
1195
|
+
/**
|
|
1196
|
+
* The count of the work, on the runtime of the root of the evaluation (WORK.LIMIT). The first count adds the field,
|
|
1197
|
+
* thus an evaluation without a counted point has no field.
|
|
1198
|
+
*/
|
|
1199
|
+
declare work?: number;
|
|
1200
|
+
/** The count of the applications of closures that run at this time. `formula.fn` writes it (FORMULA.CALL-DEPTH). */
|
|
1201
|
+
declare applying?: number;
|
|
1160
1202
|
readonly machine: Machine;
|
|
1161
1203
|
/**
|
|
1162
1204
|
* The epoch of the evaluation: the list of capabilities that the host gave the root (CAP.NARROW-KEPT). A context
|
|
@@ -1166,7 +1208,12 @@ class Session implements Runtime {
|
|
|
1166
1208
|
readonly running: boolean;
|
|
1167
1209
|
readonly traced: boolean;
|
|
1168
1210
|
readonly perform: Perform | undefined;
|
|
1169
|
-
|
|
1211
|
+
/**
|
|
1212
|
+
* The runtime of the root, on the runtime of an equation of a tree (WORK.EQUATION). The runtime of a root holds
|
|
1213
|
+
* itself after the end of its evaluation (WORK.AFTER, WORK.KEEPS), thus the end mark adds no field to the object.
|
|
1214
|
+
* Each runtime has this field, thus a tree and its equations have one object shape.
|
|
1215
|
+
*/
|
|
1216
|
+
top: Session | undefined;
|
|
1170
1217
|
/**
|
|
1171
1218
|
* The record of the reads. Without it, a resolution records nothing. A nested evaluation of an equation of a tree
|
|
1172
1219
|
* shares the record of the evaluation around it (READS.EQUATION).
|
|
@@ -1196,7 +1243,7 @@ class Session implements Runtime {
|
|
|
1196
1243
|
running: boolean,
|
|
1197
1244
|
traced: boolean,
|
|
1198
1245
|
perform: Perform | undefined,
|
|
1199
|
-
|
|
1246
|
+
top: Session | undefined,
|
|
1200
1247
|
effects: Effect[] | undefined,
|
|
1201
1248
|
reading: Reading | undefined = undefined,
|
|
1202
1249
|
) {
|
|
@@ -1205,13 +1252,36 @@ class Session implements Runtime {
|
|
|
1205
1252
|
this.running = running;
|
|
1206
1253
|
this.traced = traced;
|
|
1207
1254
|
this.perform = perform;
|
|
1208
|
-
this.
|
|
1255
|
+
this.top = top;
|
|
1209
1256
|
this.reading = reading;
|
|
1210
1257
|
this.list = effects;
|
|
1211
1258
|
this.holder = traced ? holderNode() : undefined;
|
|
1212
1259
|
this.current = this.holder;
|
|
1213
1260
|
}
|
|
1214
1261
|
|
|
1262
|
+
/** This getter gives `true` for the runtime of an equation of a tree (WORK.NODE). */
|
|
1263
|
+
get nested(): boolean {
|
|
1264
|
+
const top = this.top;
|
|
1265
|
+
return top !== undefined && top !== this;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* This function adds work to the count of the evaluation (WORK.LIMIT). One count is on the runtime of the root, thus
|
|
1270
|
+
* each equation of a tree adds to it. After the end of the evaluation, each entry from the host starts a new count
|
|
1271
|
+
* (WORK.AFTER).
|
|
1272
|
+
*
|
|
1273
|
+
* @param points - The work.
|
|
1274
|
+
* @throws {Failure} The error `#WORK!`, when the count is over the limit of the machine.
|
|
1275
|
+
*/
|
|
1276
|
+
spend(points: number): void {
|
|
1277
|
+
const root = this.top ?? this;
|
|
1278
|
+
const entry = root.top === root && (this.applying ?? 0) === 0;
|
|
1279
|
+
const work = (entry ? 0 : (root.work ?? 0)) + points;
|
|
1280
|
+
root.work = work;
|
|
1281
|
+
const limit = this.machine.maxWork ?? WORK_LIMIT;
|
|
1282
|
+
if (work > limit) throw new Failure(workFailure(limit));
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1215
1285
|
/** The effects of the evaluation, in their order. */
|
|
1216
1286
|
get effects(): Effect[] {
|
|
1217
1287
|
return (this.list ??= []);
|
|
@@ -1753,6 +1823,8 @@ interface Compiler {
|
|
|
1753
1823
|
readonly guards: ReadonlyMap<string, ValueType> | undefined;
|
|
1754
1824
|
/** The runtime of the folds of this compilation. The first fold makes it. */
|
|
1755
1825
|
probe?: Probe;
|
|
1826
|
+
/** `true` when a document of this compilation can use its runtime after the end of the evaluation (WORK.KEEPS). */
|
|
1827
|
+
keeps?: boolean;
|
|
1756
1828
|
}
|
|
1757
1829
|
|
|
1758
1830
|
function failing(c: Compiler, meta: Meta, place: readonly Key[], code: ErrorCode, reason: string): Compiled {
|
|
@@ -1890,6 +1962,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1890
1962
|
};
|
|
1891
1963
|
// A compiler that throws fails its own document when that document evaluates, with its place (PLAN.FAIL-LATE).
|
|
1892
1964
|
if (runner !== undefined) {
|
|
1965
|
+
c.keeps = true;
|
|
1893
1966
|
let inner: RunCode;
|
|
1894
1967
|
try {
|
|
1895
1968
|
inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill, kind?.caps?.some((rule) => rule.rule === "grant") === true);
|
|
@@ -1901,6 +1974,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1901
1974
|
return finish(c, meta, runnerCode(run, place, kindText), run);
|
|
1902
1975
|
}
|
|
1903
1976
|
|
|
1977
|
+
if (!isCompiledEvaluator(evaluator as Evaluator) || (evaluator as CompiledEvaluator).keeps === true) c.keeps = true;
|
|
1904
1978
|
let inner: Code;
|
|
1905
1979
|
try {
|
|
1906
1980
|
inner = isCompiledEvaluator(evaluator as Evaluator) ? (evaluator as CompiledEvaluator).compile(input) : interpret(evaluator as EvalFn, input, split.fill);
|
|
@@ -1990,6 +2064,8 @@ class Probe implements Runtime {
|
|
|
1990
2064
|
readonly running = false;
|
|
1991
2065
|
/** `true` after the part asked for something that a fold cannot give. */
|
|
1992
2066
|
refused = false;
|
|
2067
|
+
/** A node in a fold costs work, thus a tree stops the fold before it evaluates an equation (WORK.PREPARE). */
|
|
2068
|
+
readonly nested = true;
|
|
1993
2069
|
/** The count of the names that the part read. */
|
|
1994
2070
|
reads = 0;
|
|
1995
2071
|
/**
|
|
@@ -2058,6 +2134,12 @@ class Probe implements Runtime {
|
|
|
2058
2134
|
keepsContext(): void {
|
|
2059
2135
|
this.refused = true;
|
|
2060
2136
|
}
|
|
2137
|
+
|
|
2138
|
+
/** This function stops the fold at the first point of work, thus `prepare` does no such work (WORK.PREPARE). */
|
|
2139
|
+
spend(): void {
|
|
2140
|
+
this.refused = true;
|
|
2141
|
+
throw new Failure(workFailure(0));
|
|
2142
|
+
}
|
|
2061
2143
|
}
|
|
2062
2144
|
|
|
2063
2145
|
/**
|
|
@@ -2418,8 +2500,8 @@ export interface InputWithRuntime extends EvalInput {
|
|
|
2418
2500
|
/**
|
|
2419
2501
|
* This function evaluates a plan inside the evaluation of another runtime, for example an equation of a tree inside
|
|
2420
2502
|
* the evaluation of its tree (READS.EQUATION). The plan gets its own runtime, as in `plan.evaluate`. The reads of the
|
|
2421
|
-
* plan go in the record of the reads of `outer
|
|
2422
|
-
* (READS.FREE). Without
|
|
2503
|
+
* plan go in the record of the reads of `outer`, and its work goes in the count of `outer` (WORK.EQUATION). The context
|
|
2504
|
+
* at the root of `outer` tells which reads are free (READS.FREE). Without a session, the function gives `plan.evaluate(context)`.
|
|
2423
2505
|
*
|
|
2424
2506
|
* @param plan - The plan.
|
|
2425
2507
|
* @param context - The context of the plan.
|
|
@@ -2427,11 +2509,10 @@ export interface InputWithRuntime extends EvalInput {
|
|
|
2427
2509
|
* @returns The result.
|
|
2428
2510
|
*/
|
|
2429
2511
|
export function evaluateWithin(plan: Plan, context: Context, outer: Runtime | undefined): Result {
|
|
2430
|
-
|
|
2431
|
-
if (reading === undefined || !(plan instanceof CompiledPlan)) return plan.evaluate(context);
|
|
2512
|
+
if (!(outer instanceof Session) || !(plan instanceof CompiledPlan)) return plan.evaluate(context);
|
|
2432
2513
|
const machine = plan.machine;
|
|
2433
2514
|
const ctx = rootContext(machine, context);
|
|
2434
|
-
return plan.code(ctx, new Session(machine, ctx.caps, false, plan.traced, undefined,
|
|
2515
|
+
return plan.code(ctx, new Session(machine, ctx.caps, false, plan.traced, undefined, outer.top ?? outer, undefined, outer.reading));
|
|
2435
2516
|
}
|
|
2436
2517
|
|
|
2437
2518
|
function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
|
|
@@ -2570,7 +2651,23 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
|
|
|
2570
2651
|
const result = placed(thrown(error, "the compilation"), []);
|
|
2571
2652
|
root = { code: () => result, run: undefined };
|
|
2572
2653
|
}
|
|
2573
|
-
return new CompiledPlan(machine, c.traced, root.code, root.run);
|
|
2654
|
+
return new CompiledPlan(machine, c.traced, c.keeps === true ? endingCode(root.code) : root.code, root.run);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
/**
|
|
2658
|
+
* This function gives the code of a plan with a document that can use its runtime after the end of the evaluation. The
|
|
2659
|
+
* code marks that end (WORK.KEEPS). Each other plan has no such step, thus the count adds nothing to a read.
|
|
2660
|
+
*
|
|
2661
|
+
* @param code - The code of the root.
|
|
2662
|
+
* @returns The code that marks the end.
|
|
2663
|
+
*/
|
|
2664
|
+
function endingCode(code: Code): Code {
|
|
2665
|
+
return (ctx, rt) => {
|
|
2666
|
+
const result = code(ctx, rt);
|
|
2667
|
+
const session = rt as Session;
|
|
2668
|
+
session.top ??= session;
|
|
2669
|
+
return result;
|
|
2670
|
+
};
|
|
2574
2671
|
}
|
|
2575
2672
|
|
|
2576
2673
|
/**
|
|
@@ -2619,8 +2716,10 @@ class CompiledPlan implements Plan {
|
|
|
2619
2716
|
run(context: Context = makeContext(), runOptions: PlanRunOptions = {}): Promise<Result> {
|
|
2620
2717
|
const machine = this.machine;
|
|
2621
2718
|
const ctx = rootContext(machine, context);
|
|
2622
|
-
const s = new Session(machine, ctx.caps, true, this.traced, runOptions.perform,
|
|
2623
|
-
return driveWaiting(this.runCode()(ctx, s), runOptions.signal)
|
|
2719
|
+
const s = new Session(machine, ctx.caps, true, this.traced, runOptions.perform, undefined, undefined, readingOf(runOptions.reads, ctx));
|
|
2720
|
+
return driveWaiting(this.runCode()(ctx, s), runOptions.signal).finally(() => {
|
|
2721
|
+
s.top = s;
|
|
2722
|
+
});
|
|
2624
2723
|
}
|
|
2625
2724
|
}
|
|
2626
2725
|
|
|
@@ -2689,8 +2788,7 @@ export function evaluate(machine: Machine, doc: unknown, context: Context = make
|
|
|
2689
2788
|
});
|
|
2690
2789
|
const ctx = rootContext(machine, context);
|
|
2691
2790
|
const s = new Session(machine, ctx.caps, false, true, given.perform, undefined, undefined, readingOf(given.reads, ctx));
|
|
2692
|
-
|
|
2693
|
-
return record(s, result);
|
|
2791
|
+
return record(s, (plan as CompiledPlan).code(ctx, s));
|
|
2694
2792
|
}
|
|
2695
2793
|
|
|
2696
2794
|
/**
|
|
@@ -2712,8 +2810,9 @@ export async function run(machine: Machine, doc: unknown, context: Context = mak
|
|
|
2712
2810
|
...(options.verb === undefined ? {} : { verb: options.verb }),
|
|
2713
2811
|
});
|
|
2714
2812
|
const ctx = rootContext(machine, context);
|
|
2715
|
-
const s = new Session(machine, ctx.caps, true, true, options.perform,
|
|
2813
|
+
const s = new Session(machine, ctx.caps, true, true, options.perform, undefined, undefined, readingOf(options.reads, ctx));
|
|
2716
2814
|
const result = await driveWaiting((plan as CompiledPlan).runCode()(ctx, s), options.signal);
|
|
2815
|
+
s.top = s;
|
|
2717
2816
|
return record(s, result);
|
|
2718
2817
|
}
|
|
2719
2818
|
|
package/src/formula.ts
CHANGED
|
@@ -175,8 +175,8 @@ function setOwn(target: Record<string, unknown>, key: string, value: unknown): v
|
|
|
175
175
|
else target[key] = value;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
function compiled(compile: (input: CompileInput) => Code): CompiledEvaluator {
|
|
179
|
-
return { compile };
|
|
178
|
+
function compiled(compile: (input: CompileInput) => Code, keeps?: true): CompiledEvaluator {
|
|
179
|
+
return keeps === undefined ? { compile } : { compile, keeps };
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/** This function evaluates parts in order. It gives the value of each part, or the first error. */
|
|
@@ -756,7 +756,7 @@ function makeEvaluators(profile: FormulaProfile): Readonly<Record<string, Evalua
|
|
|
756
756
|
for (const name of Object.keys(evaluators)) {
|
|
757
757
|
const evaluator = evaluators[name] as CompiledEvaluator;
|
|
758
758
|
const type = Object.hasOwn(types, name) ? types[name] : undefined;
|
|
759
|
-
const made = Object.freeze(type === undefined ? evaluator : {
|
|
759
|
+
const made = Object.freeze(type === undefined ? evaluator : { ...evaluator, type });
|
|
760
760
|
if (truth !== undefined && TRUTH_READERS.includes(name)) truthByCode.set(made, truth);
|
|
761
761
|
if (PROFILE_READERS.includes(name)) readerByCode.set(made, { name, profile });
|
|
762
762
|
out[name] = made;
|
|
@@ -1045,6 +1045,8 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
|
|
|
1045
1045
|
// Each application counts one level on the runtime of its evaluation, and the count goes down after it (FORMULA.CALL-DEPTH).
|
|
1046
1046
|
const level = (clock.applying ?? 0) + 1;
|
|
1047
1047
|
if (level > limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
|
|
1048
|
+
// WORK.LIMIT: an application is a place where work can multiply, thus each one counts one point (D134).
|
|
1049
|
+
rt.spend?.(1);
|
|
1048
1050
|
clock.applying = level;
|
|
1049
1051
|
try {
|
|
1050
1052
|
const scope: Record<string, unknown> = { ...ctx.names };
|
|
@@ -1059,7 +1061,8 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
|
|
|
1059
1061
|
Object.defineProperty(closure, CLOSURE, { value: { params: names } });
|
|
1060
1062
|
return ok(closure);
|
|
1061
1063
|
};
|
|
1062
|
-
|
|
1064
|
+
// A closure uses its runtime after the end of the evaluation (WORK.KEEPS).
|
|
1065
|
+
}, true),
|
|
1063
1066
|
|
|
1064
1067
|
"formula.call": compiled(({ value }) => {
|
|
1065
1068
|
const record = partsOf(value);
|
package/src/generate.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Json, type Key, docEquals, isEnvelope, typePath } from "./doc.ts";
|
|
2
2
|
import { type Kind, type ScopeRule, type Shape, type World, type WorldEntry, isAlias, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
3
3
|
import { pathMatches } from "./parts.ts";
|
|
4
|
+
import { mulberry32 } from "@mark1russell7/rigor-generators";
|
|
4
5
|
import { ROOTS, type Root } from "./deref.ts";
|
|
5
6
|
|
|
6
7
|
/** A source of random numbers with a seed. The same seed gives the same numbers. */
|
|
@@ -16,20 +17,14 @@ export interface Random {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* This function makes a source of random numbers with a seed
|
|
20
|
+
* This function makes a source of random numbers with a seed. The numbers come from `mulberry32` of
|
|
21
|
+
* `@mark1russell7/rigor-generators` (K248, D191). The draws `int`, `chance` and `pick` keep the rules of the core.
|
|
20
22
|
*
|
|
21
23
|
* @param seed - The seed: a whole number.
|
|
22
24
|
* @returns The source.
|
|
23
25
|
*/
|
|
24
26
|
export function random(seed: number): Random {
|
|
25
|
-
|
|
26
|
-
const next = (): number => {
|
|
27
|
-
state = (state + 0x6d2b79f5) >>> 0;
|
|
28
|
-
let t = state;
|
|
29
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
30
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
31
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
32
|
-
};
|
|
27
|
+
const next = mulberry32(seed);
|
|
33
28
|
const int = (below: number): number => Math.floor(next() * below);
|
|
34
29
|
return { next, int, chance: (p) => next() < p, pick: (items) => items[int(items.length)] as never };
|
|
35
30
|
}
|
package/src/index.ts
CHANGED
package/src/merge.ts
CHANGED
|
@@ -581,7 +581,7 @@ function checkHidden(m: Merger, draft: MachineDraft): void {
|
|
|
581
581
|
* - A runner hides the evaluator of its name. Thus one part must give both, or the merge gives a conflict.
|
|
582
582
|
* - The hooks `known` compose in the order of the parts. A hook does not answer for a name that a later part replaces.
|
|
583
583
|
* The hooks `facts` compose the same way, and the first answer that is not `undefined` wins.
|
|
584
|
-
* - Each other field, for example `permits`, `adopt`, `roots`, `plain` or `
|
|
584
|
+
* - Each other field, for example `permits`, `adopt`, `roots`, `plain`, `maxDepth` or `maxWork`, is one value. Two different
|
|
585
585
|
* values are a conflict. An absent field has no value.
|
|
586
586
|
*
|
|
587
587
|
* A conflict stops the merge, unless the later part names its path in `replaces`. A part can be a function of the
|
package/src/reference.ts
CHANGED
|
@@ -36,12 +36,15 @@ import {
|
|
|
36
36
|
type Steps,
|
|
37
37
|
type StopSignal,
|
|
38
38
|
JOURNAL,
|
|
39
|
+
NODE_WORK,
|
|
40
|
+
WORK_LIMIT,
|
|
39
41
|
heldReason,
|
|
40
42
|
makeContext,
|
|
41
43
|
permits,
|
|
42
44
|
placeText,
|
|
43
45
|
reachedOf,
|
|
44
46
|
waitedReason,
|
|
47
|
+
workFailure,
|
|
45
48
|
} from "./evaluate.ts";
|
|
46
49
|
import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
|
|
47
50
|
|
|
@@ -111,6 +114,13 @@ interface Reading {
|
|
|
111
114
|
readonly root: Context;
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
/** The count of the work of one evaluation of the reference (WORK.LIMIT). */
|
|
118
|
+
interface Meter {
|
|
119
|
+
spent: number;
|
|
120
|
+
/** `true` after the end of the evaluation. Then each entry from the host starts a new count (WORK.AFTER). */
|
|
121
|
+
ended: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
/** The state of one evaluation of the reference. */
|
|
115
125
|
interface Env {
|
|
116
126
|
readonly machine: Machine;
|
|
@@ -130,6 +140,12 @@ interface Env {
|
|
|
130
140
|
held: number;
|
|
131
141
|
/** The count of the applications of closures that run at this time: the clock of `call` (FORMULA.CALL-DEPTH, D125). */
|
|
132
142
|
applying: number;
|
|
143
|
+
/** The most work of one evaluation (WORK.LIMIT, D134). */
|
|
144
|
+
readonly maxWork: number;
|
|
145
|
+
/** The count of the work. An equation of a tree shares the count around it (WORK.EQUATION). */
|
|
146
|
+
readonly meter: Meter;
|
|
147
|
+
/** `true` for an equation of a tree (WORK.NODE). */
|
|
148
|
+
readonly nested: boolean;
|
|
133
149
|
/** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
|
|
134
150
|
readonly reading: Reading | undefined;
|
|
135
151
|
}
|
|
@@ -689,23 +705,37 @@ function* together(env: Env, list: readonly RunPart[], options: AllOptions = {})
|
|
|
689
705
|
* @returns The result.
|
|
690
706
|
*/
|
|
691
707
|
export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
|
|
692
|
-
return evaluateRoot(machine, doc, context, options, undefined);
|
|
708
|
+
return evaluateRoot(machine, doc, context, options, undefined, undefined);
|
|
693
709
|
}
|
|
694
710
|
|
|
695
711
|
/**
|
|
696
|
-
* This function evaluates a document from its root. An equation of a tree gives the record of its tree
|
|
697
|
-
*
|
|
712
|
+
* This function evaluates a document from its root. An equation of a tree gives the record of its tree and the count
|
|
713
|
+
* of its work. Thus its reads go in the list of the outer evaluation (REF.READS), and its work in the outer count
|
|
714
|
+
* (WORK.EQUATION).
|
|
698
715
|
*/
|
|
699
|
-
function evaluateRoot(machine: Machine, doc: unknown, context: Context, options: ReferenceOptions, outer: Reading | undefined): Result {
|
|
716
|
+
function evaluateRoot(machine: Machine, doc: unknown, context: Context, options: ReferenceOptions, outer: Reading | undefined, meter: Meter | undefined): Result {
|
|
700
717
|
const at = rootAt(machine, doc, context, options);
|
|
701
|
-
const env = envOf(machine, options, false, at.ctx, outer);
|
|
718
|
+
const env = envOf(machine, options, false, at.ctx, outer, meter);
|
|
702
719
|
try {
|
|
703
720
|
return within(env, () => evaluateAt(env, at));
|
|
704
721
|
} catch (error) {
|
|
705
722
|
return placed(thrown(error, "the reference"), []);
|
|
723
|
+
} finally {
|
|
724
|
+
if (!env.nested) env.meter.ended = true;
|
|
706
725
|
}
|
|
707
726
|
}
|
|
708
727
|
|
|
728
|
+
/**
|
|
729
|
+
* WORK.LIMIT: this function adds work to the count of an evaluation. After the end of the evaluation, each entry from
|
|
730
|
+
* the host starts a new count (WORK.AFTER). A count over the limit throws `#WORK!`.
|
|
731
|
+
*/
|
|
732
|
+
function spend(env: Pick<Env, "maxWork" | "meter" | "applying">, points: number): void {
|
|
733
|
+
const meter = env.meter;
|
|
734
|
+
if (meter.ended && env.applying === 0) meter.spent = 0;
|
|
735
|
+
meter.spent += points;
|
|
736
|
+
if (meter.spent > env.maxWork) throw new Failure(workFailure(env.maxWork));
|
|
737
|
+
}
|
|
738
|
+
|
|
709
739
|
/**
|
|
710
740
|
* This function starts a run of a document by the rules, without a compilation (REF.RUN). The host does each effect of a runner,
|
|
711
741
|
* and a step waits when the host waits (RUN.SEQ). `all` settles the waits in the order of the host, or in the order
|
|
@@ -723,16 +753,20 @@ function evaluateRoot(machine: Machine, doc: unknown, context: Context, options:
|
|
|
723
753
|
*/
|
|
724
754
|
export function referenceRun(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceRunOptions = {}): Promise<Result> {
|
|
725
755
|
const at = rootAt(machine, doc, context, options);
|
|
726
|
-
const env = envOf(machine, options, true, at.ctx, undefined);
|
|
756
|
+
const env = envOf(machine, options, true, at.ctx, undefined, undefined);
|
|
727
757
|
const steps = stepsAt(env, at);
|
|
728
|
-
return driveRun(env, steps, options.signal)
|
|
758
|
+
return driveRun(env, steps, options.signal)
|
|
759
|
+
.catch((error: unknown) => placed(thrown(error, "the reference"), []))
|
|
760
|
+
.finally(() => {
|
|
761
|
+
env.meter.ended = true;
|
|
762
|
+
});
|
|
729
763
|
}
|
|
730
764
|
|
|
731
765
|
/**
|
|
732
766
|
* This function makes the state of the reference for one evaluation or one run. The record of the reads comes from
|
|
733
767
|
* the evaluation around it, or from the list of the options.
|
|
734
768
|
*/
|
|
735
|
-
function envOf(machine: Machine, options: ReferenceOptions, running: boolean, root: Context, outer: Reading | undefined): Env {
|
|
769
|
+
function envOf(machine: Machine, options: ReferenceOptions, running: boolean, root: Context, outer: Reading | undefined, meter: Meter | undefined): Env {
|
|
736
770
|
const perform = options.perform;
|
|
737
771
|
return {
|
|
738
772
|
machine,
|
|
@@ -746,6 +780,9 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
|
|
|
746
780
|
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
747
781
|
held: 0,
|
|
748
782
|
applying: 0,
|
|
783
|
+
maxWork: machine.maxWork ?? WORK_LIMIT,
|
|
784
|
+
meter: meter ?? { spent: 0, ended: false },
|
|
785
|
+
nested: meter !== undefined,
|
|
749
786
|
reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
|
|
750
787
|
};
|
|
751
788
|
}
|
|
@@ -869,7 +906,7 @@ function equationOf(node: RefNode, term: Json, names: Readonly<Record<string, un
|
|
|
869
906
|
const env = node.env;
|
|
870
907
|
if (env === undefined) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
|
|
871
908
|
const ctx: Context = { ...node.ctx, names, caps: [], self: node.thing };
|
|
872
|
-
return evaluateRoot(env.machine, term, ctx, { definitions: env.definitions, ...(node.of === undefined ? {} : { declaredType: node.of }) }, env.reading);
|
|
909
|
+
return evaluateRoot(env.machine, term, ctx, { definitions: env.definitions, ...(node.of === undefined ? {} : { declaredType: node.of }) }, env.reading, env.meter);
|
|
873
910
|
}
|
|
874
911
|
|
|
875
912
|
function termIn<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
|
|
@@ -1240,43 +1277,56 @@ function instanceOf(node: RefNode, attr: string, args: unknown): Result | undefi
|
|
|
1240
1277
|
}
|
|
1241
1278
|
|
|
1242
1279
|
/** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
|
|
1243
|
-
function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1280
|
+
function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[], seen: Set<object>): Result {
|
|
1281
|
+
try {
|
|
1282
|
+
const record: Record<string, unknown> = {};
|
|
1283
|
+
for (const part of node.parts) {
|
|
1284
|
+
const result = attributeOf(node, part.key) as Result;
|
|
1285
|
+
if (!result.ok) return result;
|
|
1286
|
+
const value = plainValue(node, result.value, [...open, self], seen);
|
|
1287
|
+
if (!value.ok) return value;
|
|
1288
|
+
setField(record, part.key, value.value);
|
|
1289
|
+
}
|
|
1290
|
+
return ok(record);
|
|
1291
|
+
} finally {
|
|
1292
|
+
seen.add(self);
|
|
1251
1293
|
}
|
|
1252
|
-
return ok(record);
|
|
1253
1294
|
}
|
|
1254
1295
|
|
|
1255
|
-
/**
|
|
1256
|
-
function
|
|
1296
|
+
/**
|
|
1297
|
+
* This function gives a copy of a value with the JSON of each node in it. The set `seen` holds each node, list and
|
|
1298
|
+
* record that the walk left. A second visit of one of them counts one point on the evaluation of `owner` (WORK.SHARED).
|
|
1299
|
+
*/
|
|
1300
|
+
function plainValue(owner: RefNode, value: unknown, open: readonly object[], seen: Set<object>): Result {
|
|
1257
1301
|
const node = refNodeOf(value);
|
|
1258
1302
|
if (node !== undefined) {
|
|
1259
1303
|
if (open.includes(value as object)) return fail("#CYCLE!", `The JSON of the node ${node.kind} at [${node.place.join(", ")}] holds the node itself.`);
|
|
1260
|
-
|
|
1304
|
+
if (seen.has(value as object) && owner.env !== undefined) spend(owner.env, 1);
|
|
1305
|
+
return jsonOfNode(node, value as HostThing, open, seen);
|
|
1261
1306
|
}
|
|
1262
1307
|
if (typeof value !== "object" || value === null || isHostThing(value) || open.includes(value)) return ok(value);
|
|
1263
|
-
if (Array.isArray(value))
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1308
|
+
if (!Array.isArray(value) && !isRecord(value)) return ok(value);
|
|
1309
|
+
if (seen.has(value) && owner.env !== undefined) spend(owner.env, 1);
|
|
1310
|
+
try {
|
|
1311
|
+
if (Array.isArray(value)) {
|
|
1312
|
+
const items: unknown[] = [];
|
|
1313
|
+
for (const item of value) {
|
|
1314
|
+
const plain = plainValue(owner, item, [...open, value], seen);
|
|
1315
|
+
if (!plain.ok) return plain;
|
|
1316
|
+
items.push(plain.value);
|
|
1317
|
+
}
|
|
1318
|
+
return ok(items);
|
|
1319
|
+
}
|
|
1320
|
+
const copy: Record<string, unknown> = {};
|
|
1321
|
+
for (const key of Object.keys(value)) {
|
|
1322
|
+
const plain = plainValue(owner, value[key], [...open, value], seen);
|
|
1267
1323
|
if (!plain.ok) return plain;
|
|
1268
|
-
|
|
1324
|
+
setField(copy, key, plain.value);
|
|
1269
1325
|
}
|
|
1270
|
-
return ok(
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const copy: Record<string, unknown> = {};
|
|
1274
|
-
for (const key of Object.keys(value)) {
|
|
1275
|
-
const plain = plainValue(value[key], [...open, value]);
|
|
1276
|
-
if (!plain.ok) return plain;
|
|
1277
|
-
setField(copy, key, plain.value);
|
|
1326
|
+
return ok(copy);
|
|
1327
|
+
} finally {
|
|
1328
|
+
seen.add(value);
|
|
1278
1329
|
}
|
|
1279
|
-
return ok(copy);
|
|
1280
1330
|
}
|
|
1281
1331
|
|
|
1282
1332
|
/**
|
|
@@ -1285,6 +1335,9 @@ function plainValue(value: unknown, open: readonly object[]): Result {
|
|
|
1285
1335
|
* reads `self` only through `resolve` (C4, TREE.SELF).
|
|
1286
1336
|
*/
|
|
1287
1337
|
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk, doc }) => {
|
|
1338
|
+
// WORK.NODE: a node that an equation makes counts. A node of the text of the document does not.
|
|
1339
|
+
const maker = activeEnvs[activeEnvs.length - 1];
|
|
1340
|
+
if (maker?.nested === true) spend(maker, NODE_WORK);
|
|
1288
1341
|
const reached = walk(["self"]);
|
|
1289
1342
|
const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
|
|
1290
1343
|
const outer = refNodeOf(parent);
|
|
@@ -1320,7 +1373,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1320
1373
|
},
|
|
1321
1374
|
has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)) || node.values.has(String(key)),
|
|
1322
1375
|
toJson: () => {
|
|
1323
|
-
const result = jsonOfNode(node, self, []);
|
|
1376
|
+
const result = jsonOfNode(node, self, [], new Set());
|
|
1324
1377
|
if (result.ok) return result.value;
|
|
1325
1378
|
throw new Failure(result.error);
|
|
1326
1379
|
},
|
|
@@ -1330,7 +1383,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1330
1383
|
AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
|
|
1331
1384
|
if (nested) return ok(self);
|
|
1332
1385
|
try {
|
|
1333
|
-
return jsonOfNode(node, self, []);
|
|
1386
|
+
return jsonOfNode(node, self, [], new Set());
|
|
1334
1387
|
} finally {
|
|
1335
1388
|
node.tree.open = false;
|
|
1336
1389
|
}
|
|
@@ -1468,10 +1521,12 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
|
|
|
1468
1521
|
const names = params as readonly string[];
|
|
1469
1522
|
const body = partNamed(parts, "body") as Part;
|
|
1470
1523
|
// FORMULA.CALL-DEPTH: the evaluation that makes the closure counts its applications. A quarter of maxDepth is the limit.
|
|
1471
|
-
const clock = activeEnvs[activeEnvs.length - 1] ?? { applying: 0, maxDepth: 1000 };
|
|
1524
|
+
const clock = activeEnvs[activeEnvs.length - 1] ?? { applying: 0, maxDepth: 1000, maxWork: WORK_LIMIT, meter: { spent: 0, ended: false } };
|
|
1472
1525
|
const limit = Math.ceil(clock.maxDepth / 4);
|
|
1473
1526
|
const closure = (...args: readonly unknown[]): unknown => {
|
|
1474
1527
|
if (clock.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
|
|
1528
|
+
// WORK.LIMIT: each application counts one point, after the clock of its depth.
|
|
1529
|
+
spend(clock, 1);
|
|
1475
1530
|
clock.applying += 1;
|
|
1476
1531
|
try {
|
|
1477
1532
|
const scope: Record<string, unknown> = {};
|