@mark1russell7/coeval 0.7.1 → 0.9.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.7.1",
4
+ "version": "0.9.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",
@@ -34,7 +34,7 @@
34
34
  "json"
35
35
  ],
36
36
  "devDependencies": {
37
- "@mark1russell7/cue": "github:mark1russell7/cue",
37
+ "@mark1russell7/cue": "^1.0.2",
38
38
  "@types/node": "^26.6.4",
39
39
  "esbuild": "^0.28.2",
40
40
  "typescript": "^7.0.2",
package/src/agree.ts CHANGED
@@ -11,9 +11,11 @@ import { type Definitions, reference } from "./reference.ts";
11
11
  * - `reference`: the reference interpreter, which compiles nothing.
12
12
  * - `traced`: the traced evaluation, `evaluate`.
13
13
  * - `again`: a second evaluation of the same plan.
14
+ * - `folds`: a second plan with the other choice of folds (PLAN.FOLD). It has no folds when the plan has them, and
15
+ * it has folds when the option `observe` stops the folds of the plan.
14
16
  * - `other`: the second tier of a language, from the option `other`.
15
17
  */
16
- export type Tier = "reference" | "traced" | "again" | "other";
18
+ export type Tier = "reference" | "traced" | "again" | "folds" | "other";
17
19
 
18
20
  /** What a second tier gives: its result, and its effects when it records them. */
19
21
  export interface TierResult {
@@ -41,7 +43,10 @@ export interface AgreeOptions {
41
43
  readonly verb?: string;
42
44
  /** The definitional code of the compiled evaluators of the machine, for the reference. */
43
45
  readonly definitions?: Definitions;
44
- /** The tiers to compare with the plan. The default is the reference, the traced evaluation and a second evaluation. */
46
+ /**
47
+ * The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation and
48
+ * the plan with the other choice of folds.
49
+ */
45
50
  readonly tiers?: readonly Exclude<Tier, "other">[];
46
51
  /**
47
52
  * This function gives the result of the second tier of a language, for example a compiler to loops.
@@ -70,13 +75,17 @@ export interface AgreeOptions {
70
75
  /**
71
76
  * This function gives what the host saw since its last call, for example the calls of an operation. `agree` uses
72
77
  * it before the plan and after each tier, and compares the results. Thus a tier that evaluates a part that the plan
73
- * does not evaluate fails.
78
+ * does not evaluate fails. A fold moves the calls of a part to the compilation, thus with this option the plan has
79
+ * no folds. The tier `folds` then has folds, and `agree` does not compare its calls.
74
80
  *
75
81
  * @returns What the host saw.
76
82
  */
77
83
  readonly observe?: () => unknown;
78
84
  }
79
85
 
86
+ /** The tiers that `agree` compares when its options name none. */
87
+ const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.freeze(["reference", "traced", "again", "folds"]);
88
+
80
89
  /** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
81
90
  export type Agreement =
82
91
  | { readonly ok: true; readonly result: Result; readonly effects: readonly Effect[] }
@@ -195,7 +204,9 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
195
204
  const tierInput: TierInput = { ...mock, ...(options.verb === undefined ? {} : { verb: options.verb }) };
196
205
  const observe = options.observe;
197
206
  observe?.();
198
- const plan = prepare(machine, doc, declared);
207
+ // A fold moves host calls to the compilation, thus a plan whose calls the host compares has no folds (PLAN.FOLD).
208
+ const folds = observe === undefined;
209
+ const plan = prepare(machine, doc, { ...declared, fold: folds });
199
210
  const effects: Effect[] = [];
200
211
  const result = plan.evaluate(context, { effects, ...mock });
201
212
  const saw = observe?.();
@@ -207,7 +218,7 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
207
218
  return sameValue(saw, observe(), options.equal) ? undefined : "The host saw other calls in the tier.";
208
219
  };
209
220
 
210
- for (const tier of options.tiers ?? ["reference", "traced", "again"]) {
221
+ for (const tier of options.tiers ?? DEFAULT_TIERS) {
211
222
  let other: Result;
212
223
  let otherEffects: Effect[] = [];
213
224
  if (tier === "reference") {
@@ -216,6 +227,16 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
216
227
  const traced = evaluate(machine, doc, context, { ...declared, ...mock });
217
228
  other = traced.result;
218
229
  otherEffects = [...traced.effects];
230
+ } else if (tier === "folds") {
231
+ other = prepare(machine, doc, { ...declared, fold: !folds }).evaluate(context, { effects: otherEffects, ...mock });
232
+ if (observe !== undefined) {
233
+ // The folds of this tier call the host when it compiles. Thus only the result and the effects compare.
234
+ observe();
235
+ if (overflowed(result) || overflowed(other)) continue;
236
+ const why = resultDiffers(result, other, options) ?? effectsDiffer(effects, otherEffects, options);
237
+ if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
238
+ continue;
239
+ }
219
240
  } else {
220
241
  other = plan.evaluate(context, { effects: otherEffects, ...mock });
221
242
  }
package/src/conform.ts CHANGED
@@ -27,12 +27,11 @@ import {
27
27
  permits,
28
28
  rootContext,
29
29
  } from "./evaluate.ts";
30
- import { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
30
+ import { type PartSlot, type Split, pathMatches, scopesOf, splitParts } from "./parts.ts";
31
31
  import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
32
32
  import { STD_EVALUATORS } from "./stdlib.ts";
33
33
  import { sameValue } from "./agree.ts";
34
34
  import { type Random, random } from "./generate.ts";
35
- import { scopesOf } from "./static.ts";
36
35
  import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, isKind, isNamespace, lookupKind } from "./world.ts";
37
36
 
38
37
  /*
package/src/doc.ts CHANGED
@@ -20,8 +20,8 @@ export type Key = string | number;
20
20
  export type TypeRef = string | readonly Key[] | { readonly type: "ref"; readonly value: readonly Json[] };
21
21
 
22
22
  /**
23
- * A typed document: an envelope with a header and a value. The header names the type of the value. The value is
24
- * not changed by the envelope at any time. Thus any JSON value can have a type: a number, a list, a text or a record.
23
+ * A typed document: an envelope with a header and a value. The header names the type of the value. The envelope
24
+ * does not change the value at any time. Thus any JSON value can have a type: a number, a list, a text or a record.
25
25
  */
26
26
  export type Envelope = {
27
27
  /** The type: a reference to a kind of the world. */
package/src/evaluate.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, isKey, typeName } from "./doc.ts";
2
- import { type Split, partsOf, pathMatches } from "./parts.ts";
2
+ import { type PartSlot, type Split, partsOf, pathMatches, scopesOf } from "./parts.ts";
3
3
  import { answerOf, ask, type Answer } from "./ladder.ts";
4
- import type { CapsRule, KindFacts, Side } from "./world.ts";
4
+ import type { CapsRule, Kind, KindFacts, Side } from "./world.ts";
5
5
  import type { Domain } from "./types.ts";
6
6
  import type { TypeRule } from "./typing.ts";
7
7
  import { type ErrorCode, type EvalError, type Result, Failure, fail, ok, placed, thrown, describe } from "./errors.ts";
@@ -18,6 +18,7 @@ import {
18
18
  ROOTS,
19
19
  context as makeRootContext,
20
20
  isHostThing,
21
+ isRef,
21
22
  resolve,
22
23
  worldThing,
23
24
  } from "./deref.ts";
@@ -886,6 +887,11 @@ class Session implements Runtime {
886
887
  return (this.list ??= []);
887
888
  }
888
889
 
890
+ /** `true` when the evaluation records its reads. Then folded code evaluates again, thus each read goes in the list (PLAN.FOLD). */
891
+ get recording(): boolean {
892
+ return this.reading !== undefined;
893
+ }
894
+
889
895
  resolve(ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution {
890
896
  return this.read(ctx, path, undefined, options);
891
897
  }
@@ -907,6 +913,8 @@ class Session implements Runtime {
907
913
  const free = isFree(reading.root, ctx, path);
908
914
  // A kind that sets another self can give the old self on, as the parent of a node of a tree does (READS.KEPT).
909
915
  if (!free && path[0] === "self" && !reading.self) this.keep(reading, ["self"]);
916
+ // A free read of the whole root self already holds what a kept read of self holds, as the root of a tree reads it.
917
+ else if (free && path.length === 1 && path[0] === "self") reading.self = true;
910
918
  let found: Resolution;
911
919
  try {
912
920
  found = this.find(ctx, path, compiled, hops);
@@ -1265,6 +1273,11 @@ export interface PrepareOptions {
1265
1273
  * The default is `eval` (VERB.PREPARE).
1266
1274
  */
1267
1275
  readonly verb?: string;
1276
+ /**
1277
+ * `false`: no part of the plan folds. The default is `true`. Then a closed pure part of a plan without a trace
1278
+ * evaluates one time, when the plan compiles. Its result is one frozen value for each evaluation (PLAN.FOLD).
1279
+ */
1280
+ readonly fold?: boolean;
1268
1281
  }
1269
1282
 
1270
1283
  /** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
@@ -1301,6 +1314,10 @@ interface Compiler {
1301
1314
  readonly verb: string;
1302
1315
  /** The answer of a document that the ladder did not see, in a trace. */
1303
1316
  readonly noAnswer: Answer;
1317
+ /** `true` when closed pure parts fold (PLAN.FOLD). A plan with a trace does not fold. */
1318
+ readonly fold: boolean;
1319
+ /** The runtime of the folds of this compilation. The first fold makes it. */
1320
+ probe?: Probe;
1304
1321
  }
1305
1322
 
1306
1323
  function failing(c: Compiler, meta: Meta, place: readonly Key[], code: ErrorCode, reason: string): Compiled {
@@ -1385,6 +1402,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1385
1402
  let meta = meta0;
1386
1403
  let name: string | undefined;
1387
1404
  let unknown: boolean;
1405
+ let quick: ReturnType<typeof answerOf>;
1388
1406
  if (c.traced) {
1389
1407
  const answer = ask(world, envelope ?? {}, c.verb, declared === undefined ? {} : { declaredType: declared });
1390
1408
  meta = { ...meta0, answer, side: kind?.side ?? "derive" };
@@ -1392,7 +1410,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1392
1410
  unknown = answer.error === "unknown-kind";
1393
1411
  } else {
1394
1412
  // A document has a type here, thus a kind that the read did not find is a kind that the world does not have.
1395
- const quick = answerOf(world, envelope?.own, kind ?? null, c.verb);
1413
+ quick = answerOf(world, envelope?.own, kind ?? null, c.verb);
1396
1414
  name = quick?.name;
1397
1415
  unknown = quick === undefined && kind === undefined;
1398
1416
  }
@@ -1406,10 +1424,13 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1406
1424
  }
1407
1425
 
1408
1426
  if (!split.ok) return failing(c, meta, place, "#VALUE!", split.reason);
1427
+ // The free names of each part, for the fold of this document (PLAN.FOLD). A plan without folds does not collect them.
1428
+ const frees: (readonly string[] | undefined)[] | undefined = c.fold ? [] : undefined;
1409
1429
  const parts: CompiledPart[] = split.parts.map((slot) => {
1410
1430
  const item = slot.item;
1411
1431
  // The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
1412
1432
  const own = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
1433
+ frees?.push(own.free);
1413
1434
  const rules = rulesFor(kind?.caps, slot.path);
1414
1435
  const compiled = rules.length === 0 ? own : gated(rules, own);
1415
1436
  // The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
@@ -1450,7 +1471,248 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1450
1471
  } catch (error) {
1451
1472
  return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
1452
1473
  }
1453
- return finish(c, meta, placedCode(inner, place, name), undefined);
1474
+ const done = finish(c, meta, placedCode(inner, place, name), undefined);
1475
+ if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
1476
+ const free = freeOf(doc, kind, split.parts, frees);
1477
+ if (free === undefined) return done;
1478
+ return free.length === 0 ? folded(c, done) : { code: done.code, run: done.run, free };
1479
+ }
1480
+
1481
+ /*
1482
+ * The fold of a closed pure part (PLAN.FOLD, D43). A part folds when it evaluates one time at compile time, and its
1483
+ * code then gives that one result. These conditions make the result the same as at each evaluation:
1484
+ *
1485
+ * - The code of the part and of each part in it is pure, and the kind declares no effect and no run side.
1486
+ * - The part reads no name that it does not bind (STATIC.REFS). A reference is the only document that reads a name.
1487
+ * - The evaluation at compile time gets an empty context and a probe runtime. The probe resolves only the names that
1488
+ * the part binds. It refuses each other read, each effect and each closure that keeps its context.
1489
+ *
1490
+ * A refusal, an exception, the error #DEPTH! and a result that is not plain data cancel the fold.
1491
+ */
1492
+
1493
+ /** The free names of a closed part. */
1494
+ const CLOSED: readonly string[] = /* @__PURE__ */ Object.freeze([]);
1495
+
1496
+ /**
1497
+ * This function tells if the code of a document is pure, by the facts of D24 (FACTS.PURE). It reads the facts as
1498
+ * `factsOf` reads them. A kind with a run side, with declared effects or with volatile code is not pure here.
1499
+ */
1500
+ function pureCode(c: Compiler, envelope: Envelope | undefined, kind: Kind, typePath: readonly Key[], name: string, quick: ReturnType<typeof answerOf>): boolean {
1501
+ if (kind.side === "run" || (kind.effects !== undefined && kind.effects.length > 0)) return false;
1502
+ let answer: KindFacts | undefined;
1503
+ try {
1504
+ answer = c.machine.facts?.(typePath, name);
1505
+ } catch {
1506
+ return false;
1507
+ }
1508
+ // The contract of the kind describes the code of its own name, or an own entry that names the same code (D24).
1509
+ const described = c.verb === "eval" && (envelope === undefined || quick?.rung !== "own" || name === answerOf(c.machine.world, undefined, kind, c.verb)?.name);
1510
+ const pure = described ? (answer?.pure ?? kind.pure) : answer?.pure;
1511
+ const volatile = described ? (answer?.volatile ?? kind.volatile) : answer?.volatile;
1512
+ return pure === true && volatile !== true;
1513
+ }
1514
+
1515
+ /**
1516
+ * This function gives the names that a document reads and does not bind, or `undefined` when the document can read
1517
+ * other roots. A reference reads the name of its path. Each other document reads the free names of its parts, less
1518
+ * the names that its scope rules give each part (STATIC.REFS).
1519
+ */
1520
+ function freeOf(doc: Envelope, kind: Kind, slots: readonly PartSlot[], frees: readonly (readonly string[] | undefined)[]): readonly string[] | undefined {
1521
+ if (isRef(doc)) {
1522
+ const path = doc.value;
1523
+ if (path.length < 2 || path[0] !== "names" || !path.every((step, index) => index === 0 || isKey(step))) return undefined;
1524
+ return [String(path[1])];
1525
+ }
1526
+ let sees: readonly (readonly string[])[] | undefined;
1527
+ let out: string[] | undefined;
1528
+ for (let index = 0; index < frees.length; index += 1) {
1529
+ const free = frees[index];
1530
+ if (free === undefined) return undefined;
1531
+ if (free.length === 0) continue;
1532
+ if (sees === undefined) sees = kind.scope === undefined || kind.scope.length === 0 ? [] : scopesOf(kind.scope, doc.value, slots).sees;
1533
+ const bound = sees[index] ?? CLOSED;
1534
+ for (const name of free) {
1535
+ if (bound.includes(name)) continue;
1536
+ out ??= [];
1537
+ if (!out.includes(name)) out.push(name);
1538
+ }
1539
+ }
1540
+ return out ?? CLOSED;
1541
+ }
1542
+
1543
+ /** This class is the runtime of a fold. It reads only the names that the folded part binds, and it causes no effect. */
1544
+ class Probe implements Runtime {
1545
+ readonly running = false;
1546
+ /** `true` after the part asked for something that a fold cannot give. */
1547
+ refused = false;
1548
+ /** The count of the names that the part read. */
1549
+ reads = 0;
1550
+ /**
1551
+ * The context of the folded part. Its roots `self`, `event` and `names` are traps: a direct read of a field
1552
+ * refuses the fold, as a resolution of these roots does. A copy of the record of the names reads no field.
1553
+ */
1554
+ readonly root: Context;
1555
+
1556
+ constructor(readonly machine: Machine) {
1557
+ const refuse = (): void => {
1558
+ this.refused = true;
1559
+ };
1560
+ const root: Granted = {
1561
+ self: trap(refuse, false),
1562
+ event: trap(refuse, false),
1563
+ names: trap(refuse, true) as Readonly<Record<string, unknown>>,
1564
+ world: worldThing(machine.world),
1565
+ caps: NO_CAPS,
1566
+ [GRANT]: NO_CAPS,
1567
+ };
1568
+ this.root = root;
1569
+ }
1570
+
1571
+ resolve(ctx: Context, path: readonly unknown[]): Resolution {
1572
+ const name = path[1];
1573
+ if (path[0] === "names" && isKey(name) && Object.hasOwn(ctx.names, String(name))) {
1574
+ this.reads += 1;
1575
+ const found = resolve(this.machine, ctx, path, WITH_HOPS);
1576
+ // A reference in the data resolves in the context of its reader, thus it can read a free name (DEREF.READER).
1577
+ if (found.hops.some((hop) => hop.by === "collapse" || isRef(hop.thing))) this.refused = true;
1578
+ return found;
1579
+ }
1580
+ this.refused = true;
1581
+ return { ok: false, code: "#REF!", reason: "A folded part reads only the names that it binds.", hops: [] };
1582
+ }
1583
+
1584
+ effect(): Result {
1585
+ this.refused = true;
1586
+ return fail("#CAP!", "A folded part causes no effect.");
1587
+ }
1588
+
1589
+ *runEffect(): Steps {
1590
+ return this.effect();
1591
+ }
1592
+
1593
+ *all(): Generator<Promise<unknown>, readonly Result[], unknown> {
1594
+ this.refused = true;
1595
+ return [];
1596
+ }
1597
+
1598
+ keepsContext(): void {
1599
+ this.refused = true;
1600
+ }
1601
+ }
1602
+
1603
+ /**
1604
+ * This function makes a trap for a root of the context of a fold. Each read of a field calls `refuse`. A trap of the
1605
+ * names gives no keys, thus a copy of the names reads no field.
1606
+ */
1607
+ function trap(refuse: () => void, copies: boolean): object {
1608
+ const no = (): undefined => {
1609
+ refuse();
1610
+ return undefined;
1611
+ };
1612
+ return new Proxy(
1613
+ {},
1614
+ {
1615
+ get: no,
1616
+ getOwnPropertyDescriptor: no,
1617
+ has: () => (refuse(), false),
1618
+ ownKeys: () => {
1619
+ if (!copies) refuse();
1620
+ return [];
1621
+ },
1622
+ getPrototypeOf: () => (refuse(), null),
1623
+ set: () => (refuse(), false),
1624
+ defineProperty: () => (refuse(), false),
1625
+ deleteProperty: () => (refuse(), false),
1626
+ },
1627
+ );
1628
+ }
1629
+
1630
+ /**
1631
+ * This function folds the code of a closed pure part. The part evaluates one time with the probe. Its result becomes
1632
+ * one deeply frozen copy, and the code gives that copy at each evaluation (PLAN.SHARED). A fold that the probe cancels
1633
+ * gives the code without free names, thus no parent tries the fold again.
1634
+ */
1635
+ function folded(c: Compiler, done: Compiled): Compiled {
1636
+ const probe = (c.probe ??= new Probe(c.machine));
1637
+ probe.refused = false;
1638
+ probe.reads = 0;
1639
+ let constant: Result | undefined;
1640
+ try {
1641
+ const result = done.code(probe.root, probe);
1642
+ if (!probe.refused && (result.ok || result.error.code !== "#DEPTH!")) constant = sharedCopy(result) as Result | undefined;
1643
+ } catch {
1644
+ constant = undefined;
1645
+ }
1646
+ if (constant === undefined) return { code: done.code, run: done.run };
1647
+ const value = constant;
1648
+ if (probe.reads === 0) return { code: () => value, run: undefined, free: CLOSED };
1649
+ // The part reads its own names. A list of reads gets each read, thus the code evaluates again (READS.RECORD).
1650
+ const original = done.code;
1651
+ return { code: (ctx, rt) => (rt instanceof Session && !rt.traced && !rt.recording ? value : original(ctx, rt)), run: undefined, free: CLOSED };
1652
+ }
1653
+
1654
+ /** The objects that a fold made. Each one is deeply frozen, thus a later fold uses it again and does not copy it. */
1655
+ const SHARED: WeakSet<object> = /* @__PURE__ */ new WeakSet();
1656
+
1657
+ /** The mark of a value that is not plain data. */
1658
+ const NOT_PLAIN: unique symbol = /* @__PURE__ */ Symbol("not plain");
1659
+
1660
+ /**
1661
+ * This function gives a deeply frozen copy of plain data, or `undefined` for other values. Plain data is a primitive or
1662
+ * an array without holes and extra keys. It is also a record of enumerable data fields with the prototype of an object
1663
+ * or no prototype. The copy keeps cycles and shared objects. The objects of the host do not change.
1664
+ */
1665
+ function sharedCopy(value: unknown): unknown {
1666
+ try {
1667
+ const out = copyPlain(value, new Map());
1668
+ return out === NOT_PLAIN ? undefined : out;
1669
+ } catch {
1670
+ // A very deep value overflows the stack of the host.
1671
+ return undefined;
1672
+ }
1673
+ }
1674
+
1675
+ /** This function copies one value for `sharedCopy`. The map holds the copy of each object that the copy saw. */
1676
+ function copyPlain(value: unknown, memo: Map<object, unknown>): unknown {
1677
+ if (typeof value === "function") return NOT_PLAIN;
1678
+ if (typeof value !== "object" || value === null) return value;
1679
+ if (SHARED.has(value)) return value;
1680
+ const seen = memo.get(value);
1681
+ if (seen !== undefined) return seen;
1682
+ const proto: unknown = Object.getPrototypeOf(value);
1683
+ const keys = Reflect.ownKeys(value);
1684
+ let out: Record<string, unknown> | unknown[];
1685
+ if (Array.isArray(value)) {
1686
+ if (proto !== Array.prototype || keys.length !== value.length + 1) return NOT_PLAIN;
1687
+ const list: unknown[] = [];
1688
+ memo.set(value, list);
1689
+ for (let index = 0; index < value.length; index += 1) {
1690
+ const field = Object.getOwnPropertyDescriptor(value, index);
1691
+ if (field === undefined || !("value" in field) || field.enumerable !== true) return NOT_PLAIN;
1692
+ const item = copyPlain(field.value, memo);
1693
+ if (item === NOT_PLAIN) return NOT_PLAIN;
1694
+ list.push(item);
1695
+ }
1696
+ out = list;
1697
+ } else {
1698
+ if (proto !== Object.prototype && proto !== null) return NOT_PLAIN;
1699
+ const record: Record<string, unknown> = proto === null ? (Object.create(null) as Record<string, unknown>) : {};
1700
+ memo.set(value, record);
1701
+ for (const key of keys) {
1702
+ if (typeof key !== "string") return NOT_PLAIN;
1703
+ const field = Object.getOwnPropertyDescriptor(value, key);
1704
+ if (field === undefined || !("value" in field) || field.enumerable !== true) return NOT_PLAIN;
1705
+ const item = copyPlain(field.value, memo);
1706
+ if (item === NOT_PLAIN) return NOT_PLAIN;
1707
+ // The key __proto__ is a field of the data, thus it does not set the prototype of the copy.
1708
+ if (key === "__proto__") Object.defineProperty(record, key, { value: item, enumerable: true, writable: true, configurable: true });
1709
+ else record[key] = item;
1710
+ }
1711
+ out = record;
1712
+ }
1713
+ Object.freeze(out);
1714
+ SHARED.add(out);
1715
+ return out;
1454
1716
  }
1455
1717
 
1456
1718
  /*
@@ -1522,6 +1784,11 @@ function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code
1522
1784
  interface Compiled {
1523
1785
  readonly code: Code;
1524
1786
  readonly run: RunCode | undefined;
1787
+ /**
1788
+ * The names that the document reads and does not bind, when it is pure. An empty list marks a folded document.
1789
+ * `undefined` marks a document that cannot fold, or a plan without folds (PLAN.FOLD).
1790
+ */
1791
+ readonly free?: readonly string[];
1525
1792
  }
1526
1793
 
1527
1794
  /** This function gives the steps of a run of an evaluator. The code runs at the first step, and it does not wait. */
@@ -1630,7 +1897,8 @@ export function rootContext(machine: Machine, ctx: Context): Context {
1630
1897
  export function prepare(machine: Machine, doc: unknown, options: PrepareOptions = {}): Plan {
1631
1898
  const verb = options.verb ?? "eval";
1632
1899
  const noAnswer: Answer = verb === "eval" ? NO_ANSWER : { verb, error: "no-answer", steps: [] };
1633
- const c: Compiler = { machine, traced: options.trace === true, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer };
1900
+ const traced = options.trace === true;
1901
+ const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold: !traced && options.fold !== false };
1634
1902
  let root: Compiled;
1635
1903
  try {
1636
1904
  root = compileNode(c, doc, [], options.declaredType, 0);
@@ -1737,8 +2005,8 @@ function record(s: Session, result: Result): Run {
1737
2005
  * tells where its parts are. The evaluator gets each part sealed: it can evaluate the part, and it cannot read its
1738
2006
  * value.
1739
2007
  *
1740
- * A failure gives an error value, and the function does not throw. An effect is recorded in its order, and the host
1741
- * does not do it. A mock can answer each effect at once (RUN.MOCK).
2008
+ * A failure gives an error value, and the function does not throw. The function records each effect in its order,
2009
+ * and the host does not do it. A mock can answer each effect at once (RUN.MOCK).
1742
2010
  *
1743
2011
  * @param machine - The world and the code of the evaluators.
1744
2012
  * @param doc - The document, as JSON.
package/src/facts.ts CHANGED
@@ -8,7 +8,8 @@ import { type DocVisit, declaredEffects, walk } from "./static.ts";
8
8
 
9
9
  /**
10
10
  * One read of a document: a free reference, with its marks (FACTS.READS). A plan resolves the path of the reference
11
- * when it evaluates the reference. The static read covers each path that the plan resolves there (FACTS.SOUND).
11
+ * when it evaluates the reference. The static read covers each path that the plan resolves there (FACTS.SOUND). A
12
+ * root that a kind declares in `reads` is a read too, with the path of the root alone.
12
13
  */
13
14
  export interface StaticRead {
14
15
  /** The root of the path. */
@@ -34,7 +35,10 @@ export interface StaticRead {
34
35
 
35
36
  /** The static facts of one document (D24). They depend only on the document and the machine, not on a context. */
36
37
  export interface DocFacts {
37
- /** The free references, in the order of the document (FACTS.READS). A reference to a name that a kind binds is not here. */
38
+ /**
39
+ * The free references, in the order of the document (FACTS.READS). A reference to a name that a kind binds is not
40
+ * here. The roots that a kind declares in `reads` are here too, each one at the place of its document.
41
+ */
38
42
  readonly reads: readonly StaticRead[];
39
43
  /** `"run"` when one kind of the document is on the run side (FACTS.SIDE). */
40
44
  readonly side: Side;
@@ -146,7 +150,8 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
146
150
  * by the contracts of its kinds:
147
151
  *
148
152
  * - `reads`: each free reference, with the root and the keys of its path. A read under a lazy part has the mark
149
- * `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`.
153
+ * `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`. A kind that
154
+ * declares `reads` adds a read of each of its roots at each document of the kind.
150
155
  * - `side` and `effects`: from the contract of each kind.
151
156
  * - `pure`: each kind declares `pure`, and the document has no effect and is not volatile.
152
157
  * - `volatile`: a kind, an operation or a step document declares `volatile`, or the host marks a read.
@@ -200,13 +205,22 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
200
205
  }
201
206
  const code = codeOf(world, v.declared ? undefined : v.doc.own, kind, verb, "eval");
202
207
  const answer = code.name === undefined ? undefined : hook?.(v.typePath ?? [], code.name);
208
+ let roots: readonly Root[] | undefined;
203
209
  if (code.described) {
204
210
  note(answer?.pure ?? kind.pure, answer?.volatile ?? kind.volatile);
205
211
  above.set(v, { lazy, rules: answer?.lazy ?? kind.lazy });
212
+ roots = answer?.reads ?? kind.reads;
206
213
  } else {
207
214
  // The contract of the kind does not describe this code. Only the hook can.
208
215
  note(answer?.pure, answer?.volatile);
209
216
  above.set(v, { lazy, rules: answer?.lazy ?? EACH });
217
+ roots = answer?.reads;
218
+ }
219
+ // The code of the kind resolves each of these roots as a whole, at the place of its document (FACTS.KIND-READS).
220
+ for (const root of roots ?? NONE) {
221
+ const read: StaticRead = { root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(v.place), at: tight(v.at) };
222
+ reads.push(read);
223
+ if (options.volatile?.(read) === true) volatile = true;
210
224
  }
211
225
  if (kind.side === "run") side = "run";
212
226
  // check and effectsOf read the declared effects with the same function (D27).