@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/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.26.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
@@ -16,6 +16,7 @@ export type KernelCode =
16
16
  | "#CAP!"
17
17
  | "#CALC!"
18
18
  | "#DEPTH!"
19
+ | "#WORK!"
19
20
  | "#ABORT"
20
21
  | "#REPLAY!"
21
22
  | "#TYPE!";
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
- readonly signal: StopSignal | undefined;
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
- signal: StopSignal | undefined,
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.signal = signal;
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`. The context at the root of `outer` tells which reads are free
2422
- * (READS.FREE). Without such a record, the function gives `plan.evaluate(context)`.
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
- const reading = outer instanceof Session ? outer.reading : undefined;
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, undefined, undefined, reading));
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, runOptions.signal, undefined, readingOf(runOptions.reads, ctx));
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
- const result = (plan as CompiledPlan).code(ctx, s);
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, options.signal, undefined, readingOf(options.reads, ctx));
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 : { compile: evaluator.compile, type });
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 (the algorithm mulberry32). It needs no package.
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
- let state = seed >>> 0;
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
@@ -186,6 +186,7 @@ export {
186
186
  prepare,
187
187
  resolveCompiled,
188
188
  run,
189
+ WORK_LIMIT,
189
190
  } from "./evaluate.ts";
190
191
  export {
191
192
  type AttributeAnswer,
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 `maxDepth`, is one value. Two different
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. Thus the
697
- * reads of the equation go in the list of the outer evaluation (REF.READS).
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).catch((error: unknown) => placed(thrown(error, "the reference"), []));
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
- const record: Record<string, unknown> = {};
1245
- for (const part of node.parts) {
1246
- const result = attributeOf(node, part.key) as Result;
1247
- if (!result.ok) return result;
1248
- const value = plainValue(result.value, [...open, self]);
1249
- if (!value.ok) return value;
1250
- setField(record, part.key, value.value);
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
- /** This function gives a copy of a value with the JSON of each node in it. */
1256
- function plainValue(value: unknown, open: readonly object[]): Result {
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
- return jsonOfNode(node, value as HostThing, open);
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
- const items: unknown[] = [];
1265
- for (const item of value) {
1266
- const plain = plainValue(item, [...open, value]);
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
- items.push(plain.value);
1324
+ setField(copy, key, plain.value);
1269
1325
  }
1270
- return ok(items);
1271
- }
1272
- if (!isRecord(value)) return ok(value);
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> = {};