@mark1russell7/coeval 0.26.1 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/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,15 @@ 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
+ /** The count of the applications of closures that run at this time, in each state of the evaluation (WORK.AFTER). */
123
+ active: number;
124
+ }
125
+
114
126
  /** The state of one evaluation of the reference. */
115
127
  interface Env {
116
128
  readonly machine: Machine;
@@ -130,6 +142,12 @@ interface Env {
130
142
  held: number;
131
143
  /** The count of the applications of closures that run at this time: the clock of `call` (FORMULA.CALL-DEPTH, D125). */
132
144
  applying: number;
145
+ /** The most work of one evaluation (WORK.LIMIT, D134). */
146
+ readonly maxWork: number;
147
+ /** The count of the work. An equation of a tree shares the count around it (WORK.EQUATION). */
148
+ readonly meter: Meter;
149
+ /** `true` for an equation of a tree (WORK.NODE). */
150
+ readonly nested: boolean;
133
151
  /** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
134
152
  readonly reading: Reading | undefined;
135
153
  }
@@ -689,23 +707,37 @@ function* together(env: Env, list: readonly RunPart[], options: AllOptions = {})
689
707
  * @returns The result.
690
708
  */
691
709
  export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
692
- return evaluateRoot(machine, doc, context, options, undefined);
710
+ return evaluateRoot(machine, doc, context, options, undefined, undefined);
693
711
  }
694
712
 
695
713
  /**
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).
714
+ * This function evaluates a document from its root. An equation of a tree gives the record of its tree and the count
715
+ * of its work. Thus its reads go in the list of the outer evaluation (REF.READS), and its work in the outer count
716
+ * (WORK.EQUATION).
698
717
  */
699
- function evaluateRoot(machine: Machine, doc: unknown, context: Context, options: ReferenceOptions, outer: Reading | undefined): Result {
718
+ function evaluateRoot(machine: Machine, doc: unknown, context: Context, options: ReferenceOptions, outer: Reading | undefined, meter: Meter | undefined): Result {
700
719
  const at = rootAt(machine, doc, context, options);
701
- const env = envOf(machine, options, false, at.ctx, outer);
720
+ const env = envOf(machine, options, false, at.ctx, outer, meter);
702
721
  try {
703
722
  return within(env, () => evaluateAt(env, at));
704
723
  } catch (error) {
705
724
  return placed(thrown(error, "the reference"), []);
725
+ } finally {
726
+ if (!env.nested) env.meter.ended = true;
706
727
  }
707
728
  }
708
729
 
730
+ /**
731
+ * WORK.LIMIT: this function adds work to the count of an evaluation. After the end of the evaluation, each entry from
732
+ * the host starts a new count (WORK.AFTER). A count over the limit throws `#WORK!`.
733
+ */
734
+ function spend(env: Pick<Env, "maxWork" | "meter" | "applying">, points: number): void {
735
+ const meter = env.meter;
736
+ if (meter.ended && meter.active === 0) meter.spent = 0;
737
+ meter.spent += points;
738
+ if (meter.spent > env.maxWork) throw new Failure(workFailure(env.maxWork));
739
+ }
740
+
709
741
  /**
710
742
  * 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
743
  * 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 +755,20 @@ function evaluateRoot(machine: Machine, doc: unknown, context: Context, options:
723
755
  */
724
756
  export function referenceRun(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceRunOptions = {}): Promise<Result> {
725
757
  const at = rootAt(machine, doc, context, options);
726
- const env = envOf(machine, options, true, at.ctx, undefined);
758
+ const env = envOf(machine, options, true, at.ctx, undefined, undefined);
727
759
  const steps = stepsAt(env, at);
728
- return driveRun(env, steps, options.signal).catch((error: unknown) => placed(thrown(error, "the reference"), []));
760
+ return driveRun(env, steps, options.signal)
761
+ .catch((error: unknown) => placed(thrown(error, "the reference"), []))
762
+ .finally(() => {
763
+ env.meter.ended = true;
764
+ });
729
765
  }
730
766
 
731
767
  /**
732
768
  * This function makes the state of the reference for one evaluation or one run. The record of the reads comes from
733
769
  * the evaluation around it, or from the list of the options.
734
770
  */
735
- function envOf(machine: Machine, options: ReferenceOptions, running: boolean, root: Context, outer: Reading | undefined): Env {
771
+ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, root: Context, outer: Reading | undefined, meter: Meter | undefined): Env {
736
772
  const perform = options.perform;
737
773
  return {
738
774
  machine,
@@ -746,6 +782,9 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
746
782
  scope: { prefix: "", counts: new Map(), alls: 0 },
747
783
  held: 0,
748
784
  applying: 0,
785
+ maxWork: machine.maxWork ?? WORK_LIMIT,
786
+ meter: meter ?? { spent: 0, ended: false, active: 0 },
787
+ nested: meter !== undefined,
749
788
  reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
750
789
  };
751
790
  }
@@ -869,7 +908,7 @@ function equationOf(node: RefNode, term: Json, names: Readonly<Record<string, un
869
908
  const env = node.env;
870
909
  if (env === undefined) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
871
910
  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);
911
+ return evaluateRoot(env.machine, term, ctx, { definitions: env.definitions, ...(node.of === undefined ? {} : { declaredType: node.of }) }, env.reading, env.meter);
873
912
  }
874
913
 
875
914
  function termIn<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
@@ -1240,43 +1279,56 @@ function instanceOf(node: RefNode, attr: string, args: unknown): Result | undefi
1240
1279
  }
1241
1280
 
1242
1281
  /** 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);
1282
+ function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[], seen: Set<object>): Result {
1283
+ try {
1284
+ const record: Record<string, unknown> = {};
1285
+ for (const part of node.parts) {
1286
+ const result = attributeOf(node, part.key) as Result;
1287
+ if (!result.ok) return result;
1288
+ const value = plainValue(node, result.value, [...open, self], seen);
1289
+ if (!value.ok) return value;
1290
+ setField(record, part.key, value.value);
1291
+ }
1292
+ return ok(record);
1293
+ } finally {
1294
+ seen.add(self);
1251
1295
  }
1252
- return ok(record);
1253
1296
  }
1254
1297
 
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 {
1298
+ /**
1299
+ * This function gives a copy of a value with the JSON of each node in it. The set `seen` holds each node, list and
1300
+ * record that the walk left. A second visit of one of them counts one point on the evaluation of `owner` (WORK.SHARED).
1301
+ */
1302
+ function plainValue(owner: RefNode, value: unknown, open: readonly object[], seen: Set<object>): Result {
1257
1303
  const node = refNodeOf(value);
1258
1304
  if (node !== undefined) {
1259
1305
  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);
1306
+ if (seen.has(value as object) && owner.env !== undefined) spend(owner.env, 1);
1307
+ return jsonOfNode(node, value as HostThing, open, seen);
1261
1308
  }
1262
1309
  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]);
1310
+ if (!Array.isArray(value) && !isRecord(value)) return ok(value);
1311
+ if (seen.has(value) && owner.env !== undefined) spend(owner.env, 1);
1312
+ try {
1313
+ if (Array.isArray(value)) {
1314
+ const items: unknown[] = [];
1315
+ for (const item of value) {
1316
+ const plain = plainValue(owner, item, [...open, value], seen);
1317
+ if (!plain.ok) return plain;
1318
+ items.push(plain.value);
1319
+ }
1320
+ return ok(items);
1321
+ }
1322
+ const copy: Record<string, unknown> = {};
1323
+ for (const key of Object.keys(value)) {
1324
+ const plain = plainValue(owner, value[key], [...open, value], seen);
1267
1325
  if (!plain.ok) return plain;
1268
- items.push(plain.value);
1326
+ setField(copy, key, plain.value);
1269
1327
  }
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);
1328
+ return ok(copy);
1329
+ } finally {
1330
+ seen.add(value);
1278
1331
  }
1279
- return ok(copy);
1280
1332
  }
1281
1333
 
1282
1334
  /**
@@ -1285,6 +1337,9 @@ function plainValue(value: unknown, open: readonly object[]): Result {
1285
1337
  * reads `self` only through `resolve` (C4, TREE.SELF).
1286
1338
  */
1287
1339
  const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk, doc }) => {
1340
+ // WORK.NODE: a node that an equation makes counts. A node of the text of the document does not.
1341
+ const maker = activeEnvs[activeEnvs.length - 1];
1342
+ if (maker?.nested === true) spend(maker, NODE_WORK);
1288
1343
  const reached = walk(["self"]);
1289
1344
  const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
1290
1345
  const outer = refNodeOf(parent);
@@ -1320,7 +1375,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1320
1375
  },
1321
1376
  has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)) || node.values.has(String(key)),
1322
1377
  toJson: () => {
1323
- const result = jsonOfNode(node, self, []);
1378
+ const result = jsonOfNode(node, self, [], new Set());
1324
1379
  if (result.ok) return result.value;
1325
1380
  throw new Failure(result.error);
1326
1381
  },
@@ -1330,7 +1385,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1330
1385
  AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
1331
1386
  if (nested) return ok(self);
1332
1387
  try {
1333
- return jsonOfNode(node, self, []);
1388
+ return jsonOfNode(node, self, [], new Set());
1334
1389
  } finally {
1335
1390
  node.tree.open = false;
1336
1391
  }
@@ -1468,20 +1523,28 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
1468
1523
  const names = params as readonly string[];
1469
1524
  const body = partNamed(parts, "body") as Part;
1470
1525
  // 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 };
1526
+ const maker = activeEnvs[activeEnvs.length - 1];
1527
+ const clock = maker ?? { applying: 0, maxDepth: 1000, maxWork: WORK_LIMIT, meter: { spent: 0, ended: false, active: 0 } };
1472
1528
  const limit = Math.ceil(clock.maxDepth / 4);
1473
1529
  const closure = (...args: readonly unknown[]): unknown => {
1474
1530
  if (clock.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
1531
+ // WORK.LIMIT: each application counts one point, after the clock of its depth.
1532
+ spend(clock, 1);
1475
1533
  clock.applying += 1;
1534
+ clock.meter.active += 1;
1476
1535
  try {
1477
1536
  const scope: Record<string, unknown> = {};
1478
1537
  for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
1479
1538
  names.forEach((name, index) => setField(scope, name, args[index]));
1480
- const result = body.evaluate({ ...context, names: scope });
1539
+ // The body evaluates in the state of the evaluation that made the closure, also for a host after the end. Thus a
1540
+ // node of a tree in the body has the machine for its equations (WORK.AFTER).
1541
+ const scoped: Context = { ...context, names: scope };
1542
+ const result = maker === undefined ? body.evaluate(scoped) : within(maker, () => body.evaluate(scoped));
1481
1543
  if (!result.ok) throw new Failure(result.error);
1482
1544
  return result.value;
1483
1545
  } finally {
1484
1546
  clock.applying -= 1;
1547
+ clock.meter.active -= 1;
1485
1548
  }
1486
1549
  };
1487
1550
  Object.defineProperty(closure, CLOSURE, { value: { params: names } });
package/src/stdlib.ts CHANGED
@@ -9,7 +9,9 @@ import {
9
9
  type Result,
10
10
  type RunPart,
11
11
  type Runner,
12
+ type Runtime,
12
13
  type Steps,
14
+ NODE_WORK,
13
15
  RUNTIME,
14
16
  evaluateWithin,
15
17
  fail,
@@ -245,11 +247,20 @@ function isNumber(value: unknown): value is number {
245
247
  function allValues(results: readonly Result[]): { readonly values: readonly unknown[] } | { readonly error: Result } {
246
248
  const errors = results.filter((result) => !result.ok);
247
249
  const first = errors[0];
250
+ // D190: the test of the errors comes before the values. With the values first, a case of the bench took 1.2 to 1.4
251
+ // times the time in the Maglev tier of V8, in probe 4 of K216.
248
252
  if (first !== undefined && errors.length === 1) return { error: first };
249
- if (errors.length > 1) return { error: fail("#ARGS", `${errors.length} parts failed.`) };
253
+ if (errors.length > 1) return { error: partsFailed(errors) };
250
254
  return { values: results.map((result) => (result.ok ? result.value : null)) };
251
255
  }
252
256
 
257
+ /** This function gives the error of more than one failed part. */
258
+ function partsFailed(errors: readonly Result[]): Result {
259
+ // WORK.LIMIT: after the limit each counted part fails, and the code of the limit stays the code of the result.
260
+ const stopped = errors.find((result) => !result.ok && result.error.code === "#WORK!");
261
+ return stopped ?? fail("#ARGS", `${errors.length} parts failed.`);
262
+ }
263
+
253
264
  function numbers(values: readonly unknown[], kind: string): readonly number[] | Result {
254
265
  const bad = values.findIndex((value) => !isNumber(value));
255
266
  if (bad !== -1) return fail("#VALUE!", `Part ${bad} of ${kind} is not a number.`);
@@ -379,7 +390,7 @@ interface NodeState extends LoopNode {
379
390
  * @param path - The nodes and the containers whose JSON is in progress. A node on it gives `#CYCLE!`.
380
391
  * @returns The JSON, or the first error.
381
392
  */
382
- json(path: Set<object>): Result;
393
+ json(path: JsonWalk): Result;
383
394
  }
384
395
 
385
396
  /** The answer of the ladder for one attribute of one node (TREE.CELL, D108). */
@@ -887,24 +898,35 @@ function putField(target: Record<string, unknown>, key: string, value: unknown):
887
898
  Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
888
899
  }
889
900
 
901
+ /**
902
+ * The record of a walk for JSON. A node, list or record has `true` while its JSON is in progress, and `false` after
903
+ * that. Thus a value in the map with `false` is a second visit (WORK.SHARED).
904
+ */
905
+ type JsonWalk = Map<object, boolean>;
906
+
890
907
  /**
891
908
  * This function gives a value with the JSON of each node of a tree in it. A list or a plain record is new only when
892
909
  * it holds a node. A container that holds itself stays as it is.
893
910
  */
894
- function plainOf(value: unknown, path: Set<object>): Result {
911
+ function plainOf(value: unknown, path: JsonWalk, runtime: Runtime | undefined): Result {
912
+ if (typeof value !== "object" || value === null) return ok(value);
895
913
  const state = stateOf(value);
914
+ const open = path.get(value);
896
915
  if (state !== undefined) {
897
- if (path.has(value as object)) return fail("#CYCLE!", `The JSON of the node ${state.type} at [${state.place.join(", ")}] holds the node itself.`);
916
+ if (open === true) return fail("#CYCLE!", `The JSON of the node ${state.type} at [${state.place.join(", ")}] holds the node itself.`);
917
+ if (open === false) runtime?.spend?.(1);
898
918
  return state.json(path);
899
919
  }
900
- if (typeof value !== "object" || value === null || path.has(value) || isHostThing(value)) return ok(value);
920
+ if (open === true || isHostThing(value)) return ok(value);
901
921
  if (!Array.isArray(value) && !isRecord(value)) return ok(value);
902
- path.add(value);
922
+ // WORK.SHARED: a second visit of a container counts one point, because a chain of shared containers doubles the walk.
923
+ if (open === false) runtime?.spend?.(1);
924
+ path.set(value, true);
903
925
  try {
904
926
  if (Array.isArray(value)) {
905
927
  let list: unknown[] | undefined;
906
928
  for (let index = 0; index < value.length; index += 1) {
907
- const item = plainOf(value[index], path);
929
+ const item = plainOf(value[index], path, runtime);
908
930
  if (!item.ok) return item;
909
931
  if (item.value !== value[index]) (list ??= [...(value as readonly unknown[])])[index] = item.value;
910
932
  }
@@ -913,7 +935,7 @@ function plainOf(value: unknown, path: Set<object>): Result {
913
935
  const record = value as Readonly<Record<string, unknown>>;
914
936
  let out: Record<string, unknown> | undefined;
915
937
  for (const key of Object.keys(record)) {
916
- const item = plainOf(record[key], path);
938
+ const item = plainOf(record[key], path, runtime);
917
939
  if (!item.ok) return item;
918
940
  if (item.value === record[key]) continue;
919
941
  if (out === undefined) {
@@ -924,7 +946,7 @@ function plainOf(value: unknown, path: Set<object>): Result {
924
946
  }
925
947
  return ok(out ?? value);
926
948
  } finally {
927
- path.delete(value);
949
+ path.set(value, false);
928
950
  }
929
951
  }
930
952
 
@@ -1323,19 +1345,19 @@ function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, ou
1323
1345
  return cell.by === undefined ? { attribute: key, rungs } : { attribute: key, rungs, by: cell.by };
1324
1346
  },
1325
1347
  json(path) {
1326
- path.add(node);
1348
+ path.set(node, true);
1327
1349
  try {
1328
1350
  const out: Record<string, unknown> = {};
1329
1351
  for (const part of parts) {
1330
1352
  const result = read(part.key) as Result;
1331
1353
  if (!result.ok) return result;
1332
- const value = plainOf(result.value, path);
1354
+ const value = plainOf(result.value, path, runtime);
1333
1355
  if (!value.ok) return value;
1334
1356
  putField(out, part.key, value.value);
1335
1357
  }
1336
1358
  return ok(out);
1337
1359
  } finally {
1338
- path.delete(node);
1360
+ path.set(node, false);
1339
1361
  }
1340
1362
  },
1341
1363
  };
@@ -1359,7 +1381,7 @@ function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, ou
1359
1381
  return cell !== undefined && !cell.busy && cell.result !== undefined;
1360
1382
  },
1361
1383
  toJson(): unknown {
1362
- const result = state.json(new Set());
1384
+ const result = state.json(new Map());
1363
1385
  if (!result.ok) throw new Failure(result.error);
1364
1386
  return result.value;
1365
1387
  },
@@ -1404,6 +1426,9 @@ const NO_HOPS = /* @__PURE__ */ Object.freeze({ hops: false });
1404
1426
  * The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of each node.
1405
1427
  */
1406
1428
  function treeNode(input: EvalInput, host: AttributeCell | undefined): Result {
1429
+ const runtime = (input as InputWithRuntime)[RUNTIME];
1430
+ // WORK.NODE: a node that an equation makes counts. A node of the text of the document does not.
1431
+ if (runtime?.nested === true) runtime.spend?.(NODE_WORK);
1407
1432
  const found = input.resolve(SELF_PATH, NO_HOPS);
1408
1433
  const outer = found.ok ? found.thing : undefined;
1409
1434
  const above = stateOf(outer);
@@ -1415,7 +1440,7 @@ function treeNode(input: EvalInput, host: AttributeCell | undefined): Result {
1415
1440
  build.open = true;
1416
1441
  const root = treeThing(input, place, build, outer, host);
1417
1442
  try {
1418
- return (stateOf(root) as NodeState).json(new Set());
1443
+ return (stateOf(root) as NodeState).json(new Map());
1419
1444
  } finally {
1420
1445
  build.open = false;
1421
1446
  }
package/src/stepper.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, docEquals, field, isEnvelope, isRecord, typeName } from "./doc.ts";
2
2
  import { type Cap, type Context, type Resolution, type ResolveOptions, type Thing, isHostThing, worldThing } from "./deref.ts";
3
3
  import { type EvalError, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
4
- import { type Effect, type EvalInput, type Machine, type Part, type Perform, intersectCaps, makeContext, permits, placeText } from "./evaluate.ts";
4
+ import { type Effect, type EvalInput, type Machine, type Part, type Perform, WORK_LIMIT, intersectCaps, makeContext, permits, placeText, workFailure } from "./evaluate.ts";
5
5
  import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
6
6
  import type { World } from "./ladder.ts";
7
7
  import { pathMatches } from "./parts.ts";
8
- import type { Definitions } from "./reference.ts";
8
+ import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
9
+ import { STD_EVALUATORS } from "./stdlib.ts";
9
10
  import { type CapsRule, type Kind, type Shape, type WorldEntry, isAlias, isKind, isNamespace } from "./world.ts";
10
11
 
11
12
  /**
@@ -111,6 +112,12 @@ interface Shared {
111
112
  readonly applying: number;
112
113
  /** `true` after a closure or a held error goes in a value. Only then does a value need a change for the host. */
113
114
  readonly marked: boolean;
115
+ /** The work of the evaluation after the last call of `stepperNext` (WORK.LIMIT). A configuration without it has done no work. */
116
+ readonly work?: number;
117
+ /** The number of earlier effects at each place, after the last call of `stepperNext` (RUN.ADDRESS). */
118
+ readonly counts?: Readonly<Record<string, number>>;
119
+ /** The number of effects of the evaluation, after the last call of `stepperNext`. */
120
+ readonly effected?: number;
114
121
  }
115
122
 
116
123
  /**
@@ -164,8 +171,16 @@ interface Program {
164
171
  readonly perform: Perform | undefined;
165
172
  /** The number of earlier effects at each place, by the text of the place (RUN.ADDRESS). */
166
173
  readonly counts: Map<string, number>;
174
+ /** The number of effects of the evaluation before the list `effects`. */
175
+ before: number;
167
176
  /** The count of the applications at the last call of host code. */
168
177
  applying: number;
178
+ /** The most work of one evaluation (WORK.LIMIT). */
179
+ readonly maxWork: number;
180
+ /** The work of the evaluation: one point for each application of a closure (WORK.LIMIT). */
181
+ work: number;
182
+ /** `true` after the end of the evaluation. Then each application from the host starts a new count (WORK.AFTER). */
183
+ ended: boolean;
169
184
  }
170
185
 
171
186
  const CLOSURE_MARK = "coeval.closure";
@@ -555,6 +570,9 @@ function apply(p: Program, c: Configuration, target: unknown, args: readonly unk
555
570
  }
556
571
  const limit = Math.ceil(p.maxDepth / 4);
557
572
  if (c.applying >= limit) return back(c, fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`));
573
+ // WORK.LIMIT: each application counts one point, after the clock of its depth.
574
+ p.work += 1;
575
+ if (p.work > p.maxWork) return back(c, { ok: false, error: workFailure(p.maxWork) });
558
576
  const frame: Frame = { f: "return", place: closure.place.concat(closure.body.path) };
559
577
  pushed(c, frame);
560
578
  const start = enter(c, { at: NO_KEYS }, closure.site, closure.body, scopeOf(closure, args));
@@ -576,6 +594,11 @@ function hostClosure(p: Program, value: object, closure: Closure): unknown {
576
594
  const fn = (...args: readonly unknown[]): unknown => {
577
595
  const limit = Math.ceil(p.maxDepth / 4);
578
596
  if (p.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
597
+ // WORK.AFTER: after the end of the evaluation, each application from the host starts a new count.
598
+ p.work = (p.ended && p.applying === 0 ? 0 : p.work) + 1;
599
+ if (p.work > p.maxWork) throw new Failure(workFailure(p.maxWork));
600
+ // Host code in the nested stepper writes the count of the applications, thus the call puts back the count of its start.
601
+ const before = p.applying;
579
602
  p.applying += 1;
580
603
  try {
581
604
  const place = closure.place.concat(closure.body.path);
@@ -584,7 +607,7 @@ function hostClosure(p: Program, value: object, closure: Closure): unknown {
584
607
  if (!end.result.ok) throw new Failure(end.result.error);
585
608
  return forHost(p, end, end.result.value);
586
609
  } finally {
587
- p.applying -= 1;
610
+ p.applying = before;
588
611
  }
589
612
  };
590
613
  Object.defineProperty(fn, CLOSURE, { value: { params: closure.params } });
@@ -856,7 +879,7 @@ function cause(p: Program, ctx: Context, kind: Cap, payload: unknown, place: rea
856
879
  const count = counts.get(at) ?? 0;
857
880
  counts.set(at, count + 1);
858
881
  const effect: { -readonly [K in keyof Effect]: Effect[K] } = {
859
- index: effects.length,
882
+ index: p.before + effects.length,
860
883
  kind: text,
861
884
  path: keys,
862
885
  payload,
@@ -948,7 +971,9 @@ function evaluateDoc(p: Program, c: Evaluating): Configuration {
948
971
  return back(c, thrown(error, `the evaluator ${name}`));
949
972
  }
950
973
  }
951
- const defined = own(p.definitions.evaluators, name);
974
+ // A standard evaluator gets the definition of the reference, unless the options give another definition (REF.TREE).
975
+ const standard = code === own(STD_EVALUATORS, name) ? own(STD_DEFINITIONS.evaluators, name) : undefined;
976
+ const defined = own(p.definitions.evaluators, name) ?? standard;
952
977
  if (defined !== undefined) return hostEvaluator(p, c, name, defined, doc, keys, read.data, read.slots, site);
953
978
  return back(c, fail("#NOEVAL", `The stepper has no definition of the compiled evaluator ${name}.`));
954
979
  }
@@ -1087,7 +1112,11 @@ function programOf(machine: Machine, options: StepperOptions): Program {
1087
1112
  effects: options.effects ?? [],
1088
1113
  perform: options.perform,
1089
1114
  counts: new Map(),
1115
+ before: 0,
1090
1116
  applying: 0,
1117
+ maxWork: machine.maxWork ?? WORK_LIMIT,
1118
+ work: 0,
1119
+ ended: false,
1091
1120
  };
1092
1121
  }
1093
1122
 
@@ -1124,7 +1153,20 @@ export function stepperNext(machine: Machine, configuration: Configuration, opti
1124
1153
  else if (f === "let") stack[stack.length - 1] = { ...top, names: { ...top.names } };
1125
1154
  else stack[stack.length - 1] = { ...top };
1126
1155
  }
1127
- return advance(programOf(machine, options), { ...configuration, stack });
1156
+ const p = programOf(machine, options);
1157
+ // Each call has a new program. Thus the configuration holds the state of the evaluation that a later step needs.
1158
+ p.work = configuration.work ?? 0;
1159
+ for (const [at, count] of Object.entries(configuration.counts ?? {})) p.counts.set(at, count);
1160
+ // A list of effects from the host holds the earlier effects. Without it, the configuration holds their number.
1161
+ p.before = options.effects === undefined ? (configuration.effected ?? 0) : 0;
1162
+ const next = advance(p, { ...configuration, stack });
1163
+ const effected = p.before + p.effects.length;
1164
+ return {
1165
+ ...next,
1166
+ ...(p.work === 0 ? {} : { work: p.work }),
1167
+ ...(p.counts.size === 0 ? {} : { counts: Object.fromEntries(p.counts) }),
1168
+ ...(effected === 0 ? {} : { effected }),
1169
+ };
1128
1170
  }
1129
1171
 
1130
1172
  /**
@@ -1143,6 +1185,7 @@ export function stepper(machine: Machine, doc: unknown, context: Context = makeC
1143
1185
  const p = programOf(machine, options);
1144
1186
  try {
1145
1187
  const end = drive(p, stepperStart(machine, doc, context, options));
1188
+ p.ended = true;
1146
1189
  return end.result.ok ? ok(forHost(p, end, end.result.value)) : end.result;
1147
1190
  } catch (error) {
1148
1191
  return placed(thrown(error, "the stepper"), []);