@mark1russell7/coeval 0.7.0 → 0.8.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/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";
@@ -523,6 +524,8 @@ export interface EvalInput {
523
524
  readonly context: Context;
524
525
  /** The keys from the root document to this document. */
525
526
  readonly place: readonly Key[];
527
+ /** The machine of the evaluation, for example for its domains (D40). The kernel sets it. */
528
+ readonly machine?: Machine;
526
529
  /** `true` when the position of the document declares an address. */
527
530
  readonly address?: boolean;
528
531
  /**
@@ -580,6 +583,8 @@ export interface RunInput {
580
583
  readonly parts: readonly RunPart[];
581
584
  readonly context: Context;
582
585
  readonly place: readonly Key[];
586
+ /** The machine of the evaluation, for example for its domains (D40). The kernel sets it. */
587
+ readonly machine?: Machine;
583
588
  resolve(path: readonly unknown[], options?: ResolveOptions): Resolution;
584
589
  /**
585
590
  * This function causes an effect. In a run, the host does the effect, and the steps wait when the host waits. In
@@ -882,6 +887,11 @@ class Session implements Runtime {
882
887
  return (this.list ??= []);
883
888
  }
884
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
+
885
895
  resolve(ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution {
886
896
  return this.read(ctx, path, undefined, options);
887
897
  }
@@ -1261,6 +1271,11 @@ export interface PrepareOptions {
1261
1271
  * The default is `eval` (VERB.PREPARE).
1262
1272
  */
1263
1273
  readonly verb?: string;
1274
+ /**
1275
+ * `false`: no part of the plan folds. The default is `true`. Then a closed pure part of a plan without a trace
1276
+ * evaluates one time, when the plan compiles. Its result is one frozen value for each evaluation (PLAN.FOLD).
1277
+ */
1278
+ readonly fold?: boolean;
1264
1279
  }
1265
1280
 
1266
1281
  /** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
@@ -1297,6 +1312,10 @@ interface Compiler {
1297
1312
  readonly verb: string;
1298
1313
  /** The answer of a document that the ladder did not see, in a trace. */
1299
1314
  readonly noAnswer: Answer;
1315
+ /** `true` when closed pure parts fold (PLAN.FOLD). A plan with a trace does not fold. */
1316
+ readonly fold: boolean;
1317
+ /** The runtime of the folds of this compilation. The first fold makes it. */
1318
+ probe?: Probe;
1300
1319
  }
1301
1320
 
1302
1321
  function failing(c: Compiler, meta: Meta, place: readonly Key[], code: ErrorCode, reason: string): Compiled {
@@ -1381,6 +1400,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1381
1400
  let meta = meta0;
1382
1401
  let name: string | undefined;
1383
1402
  let unknown: boolean;
1403
+ let quick: ReturnType<typeof answerOf>;
1384
1404
  if (c.traced) {
1385
1405
  const answer = ask(world, envelope ?? {}, c.verb, declared === undefined ? {} : { declaredType: declared });
1386
1406
  meta = { ...meta0, answer, side: kind?.side ?? "derive" };
@@ -1388,7 +1408,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1388
1408
  unknown = answer.error === "unknown-kind";
1389
1409
  } else {
1390
1410
  // A document has a type here, thus a kind that the read did not find is a kind that the world does not have.
1391
- const quick = answerOf(world, envelope?.own, kind ?? null, c.verb);
1411
+ quick = answerOf(world, envelope?.own, kind ?? null, c.verb);
1392
1412
  name = quick?.name;
1393
1413
  unknown = quick === undefined && kind === undefined;
1394
1414
  }
@@ -1402,10 +1422,13 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1402
1422
  }
1403
1423
 
1404
1424
  if (!split.ok) return failing(c, meta, place, "#VALUE!", split.reason);
1425
+ // The free names of each part, for the fold of this document (PLAN.FOLD). A plan without folds does not collect them.
1426
+ const frees: (readonly string[] | undefined)[] | undefined = c.fold ? [] : undefined;
1405
1427
  const parts: CompiledPart[] = split.parts.map((slot) => {
1406
1428
  const item = slot.item;
1407
1429
  // The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
1408
1430
  const own = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
1431
+ frees?.push(own.free);
1409
1432
  const rules = rulesFor(kind?.caps, slot.path);
1410
1433
  const compiled = rules.length === 0 ? own : gated(rules, own);
1411
1434
  // The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
@@ -1446,7 +1469,248 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1446
1469
  } catch (error) {
1447
1470
  return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
1448
1471
  }
1449
- return finish(c, meta, placedCode(inner, place, name), undefined);
1472
+ const done = finish(c, meta, placedCode(inner, place, name), undefined);
1473
+ if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
1474
+ const free = freeOf(doc, kind, split.parts, frees);
1475
+ if (free === undefined) return done;
1476
+ return free.length === 0 ? folded(c, done) : { code: done.code, run: done.run, free };
1477
+ }
1478
+
1479
+ /*
1480
+ * The fold of a closed pure part (PLAN.FOLD, D43). A part folds when it evaluates one time at compile time, and its
1481
+ * code then gives that one result. These conditions make the result the same as at each evaluation:
1482
+ *
1483
+ * - The code of the part and of each part in it is pure, and the kind declares no effect and no run side.
1484
+ * - The part reads no name that it does not bind (STATIC.REFS). A reference is the only document that reads a name.
1485
+ * - The evaluation at compile time gets an empty context and a probe runtime. The probe resolves only the names that
1486
+ * the part binds. It refuses each other read, each effect and each closure that keeps its context.
1487
+ *
1488
+ * A refusal, an exception, the error #DEPTH! and a result that is not plain data cancel the fold.
1489
+ */
1490
+
1491
+ /** The free names of a closed part. */
1492
+ const CLOSED: readonly string[] = /* @__PURE__ */ Object.freeze([]);
1493
+
1494
+ /**
1495
+ * This function tells if the code of a document is pure, by the facts of D24 (FACTS.PURE). It reads the facts as
1496
+ * `factsOf` reads them. A kind with a run side, with declared effects or with volatile code is not pure here.
1497
+ */
1498
+ function pureCode(c: Compiler, envelope: Envelope | undefined, kind: Kind, typePath: readonly Key[], name: string, quick: ReturnType<typeof answerOf>): boolean {
1499
+ if (kind.side === "run" || (kind.effects !== undefined && kind.effects.length > 0)) return false;
1500
+ let answer: KindFacts | undefined;
1501
+ try {
1502
+ answer = c.machine.facts?.(typePath, name);
1503
+ } catch {
1504
+ return false;
1505
+ }
1506
+ // The contract of the kind describes the code of its own name, or an own entry that names the same code (D24).
1507
+ const described = c.verb === "eval" && (envelope === undefined || quick?.rung !== "own" || name === answerOf(c.machine.world, undefined, kind, c.verb)?.name);
1508
+ const pure = described ? (answer?.pure ?? kind.pure) : answer?.pure;
1509
+ const volatile = described ? (answer?.volatile ?? kind.volatile) : answer?.volatile;
1510
+ return pure === true && volatile !== true;
1511
+ }
1512
+
1513
+ /**
1514
+ * This function gives the names that a document reads and does not bind, or `undefined` when the document can read
1515
+ * other roots. A reference reads the name of its path. Each other document reads the free names of its parts, less
1516
+ * the names that its scope rules give each part (STATIC.REFS).
1517
+ */
1518
+ function freeOf(doc: Envelope, kind: Kind, slots: readonly PartSlot[], frees: readonly (readonly string[] | undefined)[]): readonly string[] | undefined {
1519
+ if (isRef(doc)) {
1520
+ const path = doc.value;
1521
+ if (path.length < 2 || path[0] !== "names" || !path.every((step, index) => index === 0 || isKey(step))) return undefined;
1522
+ return [String(path[1])];
1523
+ }
1524
+ let sees: readonly (readonly string[])[] | undefined;
1525
+ let out: string[] | undefined;
1526
+ for (let index = 0; index < frees.length; index += 1) {
1527
+ const free = frees[index];
1528
+ if (free === undefined) return undefined;
1529
+ if (free.length === 0) continue;
1530
+ if (sees === undefined) sees = kind.scope === undefined || kind.scope.length === 0 ? [] : scopesOf(kind.scope, doc.value, slots).sees;
1531
+ const bound = sees[index] ?? CLOSED;
1532
+ for (const name of free) {
1533
+ if (bound.includes(name)) continue;
1534
+ out ??= [];
1535
+ if (!out.includes(name)) out.push(name);
1536
+ }
1537
+ }
1538
+ return out ?? CLOSED;
1539
+ }
1540
+
1541
+ /** This class is the runtime of a fold. It reads only the names that the folded part binds, and it causes no effect. */
1542
+ class Probe implements Runtime {
1543
+ readonly running = false;
1544
+ /** `true` after the part asked for something that a fold cannot give. */
1545
+ refused = false;
1546
+ /** The count of the names that the part read. */
1547
+ reads = 0;
1548
+ /**
1549
+ * The context of the folded part. Its roots `self`, `event` and `names` are traps: a direct read of a field
1550
+ * refuses the fold, as a resolution of these roots does. A copy of the record of the names reads no field.
1551
+ */
1552
+ readonly root: Context;
1553
+
1554
+ constructor(readonly machine: Machine) {
1555
+ const refuse = (): void => {
1556
+ this.refused = true;
1557
+ };
1558
+ const root: Granted = {
1559
+ self: trap(refuse, false),
1560
+ event: trap(refuse, false),
1561
+ names: trap(refuse, true) as Readonly<Record<string, unknown>>,
1562
+ world: worldThing(machine.world),
1563
+ caps: NO_CAPS,
1564
+ [GRANT]: NO_CAPS,
1565
+ };
1566
+ this.root = root;
1567
+ }
1568
+
1569
+ resolve(ctx: Context, path: readonly unknown[]): Resolution {
1570
+ const name = path[1];
1571
+ if (path[0] === "names" && isKey(name) && Object.hasOwn(ctx.names, String(name))) {
1572
+ this.reads += 1;
1573
+ const found = resolve(this.machine, ctx, path, WITH_HOPS);
1574
+ // A reference in the data resolves in the context of its reader, thus it can read a free name (DEREF.READER).
1575
+ if (found.hops.some((hop) => hop.by === "collapse" || isRef(hop.thing))) this.refused = true;
1576
+ return found;
1577
+ }
1578
+ this.refused = true;
1579
+ return { ok: false, code: "#REF!", reason: "A folded part reads only the names that it binds.", hops: [] };
1580
+ }
1581
+
1582
+ effect(): Result {
1583
+ this.refused = true;
1584
+ return fail("#CAP!", "A folded part causes no effect.");
1585
+ }
1586
+
1587
+ *runEffect(): Steps {
1588
+ return this.effect();
1589
+ }
1590
+
1591
+ *all(): Generator<Promise<unknown>, readonly Result[], unknown> {
1592
+ this.refused = true;
1593
+ return [];
1594
+ }
1595
+
1596
+ keepsContext(): void {
1597
+ this.refused = true;
1598
+ }
1599
+ }
1600
+
1601
+ /**
1602
+ * 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
1603
+ * names gives no keys, thus a copy of the names reads no field.
1604
+ */
1605
+ function trap(refuse: () => void, copies: boolean): object {
1606
+ const no = (): undefined => {
1607
+ refuse();
1608
+ return undefined;
1609
+ };
1610
+ return new Proxy(
1611
+ {},
1612
+ {
1613
+ get: no,
1614
+ getOwnPropertyDescriptor: no,
1615
+ has: () => (refuse(), false),
1616
+ ownKeys: () => {
1617
+ if (!copies) refuse();
1618
+ return [];
1619
+ },
1620
+ getPrototypeOf: () => (refuse(), null),
1621
+ set: () => (refuse(), false),
1622
+ defineProperty: () => (refuse(), false),
1623
+ deleteProperty: () => (refuse(), false),
1624
+ },
1625
+ );
1626
+ }
1627
+
1628
+ /**
1629
+ * This function folds the code of a closed pure part. The part evaluates one time with the probe. Its result becomes
1630
+ * one deeply frozen copy, and the code gives that copy at each evaluation (PLAN.SHARED). A fold that the probe cancels
1631
+ * gives the code without free names, thus no parent tries the fold again.
1632
+ */
1633
+ function folded(c: Compiler, done: Compiled): Compiled {
1634
+ const probe = (c.probe ??= new Probe(c.machine));
1635
+ probe.refused = false;
1636
+ probe.reads = 0;
1637
+ let constant: Result | undefined;
1638
+ try {
1639
+ const result = done.code(probe.root, probe);
1640
+ if (!probe.refused && (result.ok || result.error.code !== "#DEPTH!")) constant = sharedCopy(result) as Result | undefined;
1641
+ } catch {
1642
+ constant = undefined;
1643
+ }
1644
+ if (constant === undefined) return { code: done.code, run: done.run };
1645
+ const value = constant;
1646
+ if (probe.reads === 0) return { code: () => value, run: undefined, free: CLOSED };
1647
+ // The part reads its own names. A list of reads gets each read, thus the code evaluates again (READS.RECORD).
1648
+ const original = done.code;
1649
+ return { code: (ctx, rt) => (rt instanceof Session && !rt.traced && !rt.recording ? value : original(ctx, rt)), run: undefined, free: CLOSED };
1650
+ }
1651
+
1652
+ /** The objects that a fold made. Each one is deeply frozen, thus a later fold uses it again and does not copy it. */
1653
+ const SHARED: WeakSet<object> = /* @__PURE__ */ new WeakSet();
1654
+
1655
+ /** The mark of a value that is not plain data. */
1656
+ const NOT_PLAIN: unique symbol = /* @__PURE__ */ Symbol("not plain");
1657
+
1658
+ /**
1659
+ * This function gives a deeply frozen copy of plain data, or `undefined` for other values. Plain data is a primitive or
1660
+ * an array without holes and extra keys. It is also a record of enumerable data fields with the prototype of an object
1661
+ * or no prototype. The copy keeps cycles and shared objects. The objects of the host do not change.
1662
+ */
1663
+ function sharedCopy(value: unknown): unknown {
1664
+ try {
1665
+ const out = copyPlain(value, new Map());
1666
+ return out === NOT_PLAIN ? undefined : out;
1667
+ } catch {
1668
+ // A very deep value overflows the stack of the host.
1669
+ return undefined;
1670
+ }
1671
+ }
1672
+
1673
+ /** This function copies one value for `sharedCopy`. The map holds the copy of each object that the copy saw. */
1674
+ function copyPlain(value: unknown, memo: Map<object, unknown>): unknown {
1675
+ if (typeof value === "function") return NOT_PLAIN;
1676
+ if (typeof value !== "object" || value === null) return value;
1677
+ if (SHARED.has(value)) return value;
1678
+ const seen = memo.get(value);
1679
+ if (seen !== undefined) return seen;
1680
+ const proto: unknown = Object.getPrototypeOf(value);
1681
+ const keys = Reflect.ownKeys(value);
1682
+ let out: Record<string, unknown> | unknown[];
1683
+ if (Array.isArray(value)) {
1684
+ if (proto !== Array.prototype || keys.length !== value.length + 1) return NOT_PLAIN;
1685
+ const list: unknown[] = [];
1686
+ memo.set(value, list);
1687
+ for (let index = 0; index < value.length; index += 1) {
1688
+ const field = Object.getOwnPropertyDescriptor(value, index);
1689
+ if (field === undefined || !("value" in field) || field.enumerable !== true) return NOT_PLAIN;
1690
+ const item = copyPlain(field.value, memo);
1691
+ if (item === NOT_PLAIN) return NOT_PLAIN;
1692
+ list.push(item);
1693
+ }
1694
+ out = list;
1695
+ } else {
1696
+ if (proto !== Object.prototype && proto !== null) return NOT_PLAIN;
1697
+ const record: Record<string, unknown> = proto === null ? (Object.create(null) as Record<string, unknown>) : {};
1698
+ memo.set(value, record);
1699
+ for (const key of keys) {
1700
+ if (typeof key !== "string") return NOT_PLAIN;
1701
+ const field = Object.getOwnPropertyDescriptor(value, key);
1702
+ if (field === undefined || !("value" in field) || field.enumerable !== true) return NOT_PLAIN;
1703
+ const item = copyPlain(field.value, memo);
1704
+ if (item === NOT_PLAIN) return NOT_PLAIN;
1705
+ // The key __proto__ is a field of the data, thus it does not set the prototype of the copy.
1706
+ if (key === "__proto__") Object.defineProperty(record, key, { value: item, enumerable: true, writable: true, configurable: true });
1707
+ else record[key] = item;
1708
+ }
1709
+ out = record;
1710
+ }
1711
+ Object.freeze(out);
1712
+ SHARED.add(out);
1713
+ return out;
1450
1714
  }
1451
1715
 
1452
1716
  /*
@@ -1518,6 +1782,11 @@ function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code
1518
1782
  interface Compiled {
1519
1783
  readonly code: Code;
1520
1784
  readonly run: RunCode | undefined;
1785
+ /**
1786
+ * The names that the document reads and does not bind, when it is pure. An empty list marks a folded document.
1787
+ * `undefined` marks a document that cannot fold, or a plan without folds (PLAN.FOLD).
1788
+ */
1789
+ readonly free?: readonly string[];
1521
1790
  }
1522
1791
 
1523
1792
  /** This function gives the steps of a run of an evaluator. The code runs at the first step, and it does not wait. */
@@ -1534,7 +1803,7 @@ function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P
1534
1803
 
1535
1804
 
1536
1805
  function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
1537
- const { kind, typePath, doc, data, place } = input;
1806
+ const { kind, typePath, doc, data, place, machine } = input;
1538
1807
  const address = input.address === true ? { address: true } : {};
1539
1808
  // The code keeps the fill and not the whole input, thus it does not keep the compiled value of the document (PLAN.LEAN).
1540
1809
  const refill = input.fill;
@@ -1556,6 +1825,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
1556
1825
  parts,
1557
1826
  context: ctx,
1558
1827
  place,
1828
+ machine,
1559
1829
  ...address,
1560
1830
  resolve: (path, options) => rt.resolve(ctx, path, options),
1561
1831
  effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
@@ -1565,7 +1835,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
1565
1835
  }
1566
1836
 
1567
1837
  function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): RunCode {
1568
- const { kind, typePath, doc, data, place } = input;
1838
+ const { kind, typePath, doc, data, place, machine } = input;
1569
1839
  const refill = input.fill;
1570
1840
  const compiled = input.parts;
1571
1841
  const hasParts = compiled.length > 0;
@@ -1585,6 +1855,7 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
1585
1855
  parts,
1586
1856
  context: ctx,
1587
1857
  place,
1858
+ machine,
1588
1859
  resolve: (path, options) => rt.resolve(ctx, path, options),
1589
1860
  effect: (effectKind, payload) => rt.runEffect(ctx, effectKind, payload, place),
1590
1861
  all: (list, options) => rt.all(list.map((part) => () => part.evaluate()), options),
@@ -1624,7 +1895,8 @@ export function rootContext(machine: Machine, ctx: Context): Context {
1624
1895
  export function prepare(machine: Machine, doc: unknown, options: PrepareOptions = {}): Plan {
1625
1896
  const verb = options.verb ?? "eval";
1626
1897
  const noAnswer: Answer = verb === "eval" ? NO_ANSWER : { verb, error: "no-answer", steps: [] };
1627
- const c: Compiler = { machine, traced: options.trace === true, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer };
1898
+ const traced = options.trace === true;
1899
+ const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold: !traced && options.fold !== false };
1628
1900
  let root: Compiled;
1629
1901
  try {
1630
1902
  root = compileNode(c, doc, [], options.declaredType, 0);
@@ -1731,8 +2003,8 @@ function record(s: Session, result: Result): Run {
1731
2003
  * tells where its parts are. The evaluator gets each part sealed: it can evaluate the part, and it cannot read its
1732
2004
  * value.
1733
2005
  *
1734
- * A failure gives an error value, and the function does not throw. An effect is recorded in its order, and the host
1735
- * does not do it. A mock can answer each effect at once (RUN.MOCK).
2006
+ * A failure gives an error value, and the function does not throw. The function records each effect in its order,
2007
+ * and the host does not do it. A mock can answer each effect at once (RUN.MOCK).
1736
2008
  *
1737
2009
  * @param machine - The world and the code of the evaluators.
1738
2010
  * @param doc - The document, as JSON.
package/src/formula.ts CHANGED
@@ -4,8 +4,9 @@ import { type CompiledPath, type Context, type Resolution, compilePath, valueOfT
4
4
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
5
5
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled } from "./evaluate.ts";
6
6
  import { type MachinePart, mergeMachines } from "./merge.ts";
7
- import { type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, stepType, typeText } from "./types.ts";
7
+ import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, stepType, typeText } from "./types.ts";
8
8
  import type { TypeInput, TypeRule } from "./typing.ts";
9
+ import { decodeDomain, domainTypeOf } from "./codec.ts";
9
10
 
10
11
  /** An operation of a formula language: a function of the values of its arguments. */
11
12
  export type OpFn = (...args: never[]) => unknown;
@@ -16,15 +17,19 @@ export type OpFn = (...args: never[]) => unknown;
16
17
  */
17
18
  export type LazyOp = (args: readonly (() => Result)[]) => Result;
18
19
 
19
- /** The code of an extension: it gives the value of the data of an extension document, in the context of the document. */
20
- export type ExtFn = (data: Json, context: Context) => Result;
20
+ /**
21
+ * The code of an extension: it gives the value of the data of an extension document, in the context of the document.
22
+ * It also gets the domains of the machine, for the codecs of `ext.domain` (D40).
23
+ */
24
+ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<string, Domain>>) => Result;
21
25
 
22
26
  /**
23
27
  * The rules of one formula language: its operations, and the rules that differ between languages.
24
28
  *
25
29
  * - `ops`: the operations of the namespace `ops`, by their name (the keys after `ops`, with dots between them).
26
30
  * - `lazy`: the operations that evaluate their arguments themselves.
27
- * - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined` and `ext.number`.
31
+ * - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined`, `ext.number` and
32
+ * `ext.domain`.
28
33
  * - `truthy`: the truth of a value for `if`, `and` and `or`. `undefined` is an error `#VALUE!`. The default
29
34
  * accepts only `true` and `false`.
30
35
  * - `otherwise`: the value of an `if` without `else`. The default is `null`. A profile with this key gives its value,
@@ -97,7 +102,10 @@ export function isClosure(value: unknown): value is Closure {
97
102
  return typeof value === "function" && (value as Partial<Closure>)[CLOSURE] !== undefined;
98
103
  }
99
104
 
100
- /** The standard extensions: the values that JSON cannot write. */
105
+ /**
106
+ * The standard extensions: the values that JSON cannot write. `ext.domain` reads a member of a domain through the
107
+ * codec of the domain (EXT.DOMAIN, D40).
108
+ */
101
109
  export const STD_EXT: Readonly<Record<string, ExtFn>> = {
102
110
  undefined: () => ok(undefined),
103
111
  number: (data) => {
@@ -107,6 +115,7 @@ export const STD_EXT: Readonly<Record<string, ExtFn>> = {
107
115
  if (data === "-0") return ok(-0);
108
116
  return fail("#VALUE!", "The value of ext.number is NaN, Infinity, -Infinity or -0.");
109
117
  },
118
+ domain: (data, _context, domains) => decodeDomain(data, domains),
110
119
  };
111
120
 
112
121
  /** The truth of a value in the rules of JavaScript: `0`, `""`, `null`, `undefined` and `NaN` are false. */
@@ -477,7 +486,9 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
477
486
  synth: (input) => {
478
487
  const name = nameOf(input);
479
488
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
480
- return name === "number" && !own ? "number" : "unknown";
489
+ if (own) return "unknown";
490
+ if (name === "number") return "number";
491
+ return name === "domain" ? domainTypeOf(input.doc.value, input.machine.domains) : "unknown";
481
492
  },
482
493
  },
483
494
  };
@@ -811,7 +822,7 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
811
822
  };
812
823
  }),
813
824
 
814
- "formula.ext": compiled(({ typePath, doc }) => {
825
+ "formula.ext": compiled(({ typePath, doc, machine }) => {
815
826
  const name = typePath.slice(1).join(".");
816
827
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name) ? profile.ext[name] : undefined;
817
828
  const handler = own ?? (Object.hasOwn(STD_EXT, name) ? STD_EXT[name] : undefined);
@@ -821,7 +832,7 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
821
832
  }
822
833
  return (ctx) => {
823
834
  try {
824
- return handler(doc.value, ctx);
835
+ return handler(doc.value, ctx, machine.domains);
825
836
  } catch (error) {
826
837
  return thrown(error, `the extension ${name}`);
827
838
  }
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export {
36
36
  } from "./errors.ts";
37
37
  export {
38
38
  type Alias,
39
+ type AttributeDeclaration,
39
40
  type CapsRule,
40
41
  type Kind,
41
42
  type KindFacts,
@@ -47,9 +48,11 @@ export {
47
48
  type ScopeRule,
48
49
  type Shape as PartShape,
49
50
  type Side,
51
+ type TreeContract,
50
52
  type World,
51
53
  type WorldEntry,
52
54
  entriesAsJson,
55
+ equationType,
53
56
  isAlias,
54
57
  isKind,
55
58
  isNamespace,
@@ -157,7 +160,20 @@ export {
157
160
  prepare,
158
161
  run,
159
162
  } from "./evaluate.ts";
160
- export { type NearestOptions, STD_CAPS, STD_EVALUATORS, STD_MACHINE, STD_RUNNERS, STD_WALKERS, STD_WORLD, nearestWalker } from "./stdlib.ts";
163
+ export {
164
+ type AttributeRung,
165
+ type AttributeTrace,
166
+ type NearestOptions,
167
+ ATTRIBUTE_RUNGS,
168
+ STD_CAPS,
169
+ STD_EVALUATORS,
170
+ STD_MACHINE,
171
+ STD_RUNNERS,
172
+ STD_WALKERS,
173
+ STD_WORLD,
174
+ attributeTrace,
175
+ nearestWalker,
176
+ } from "./stdlib.ts";
161
177
  export {
162
178
  type Edge,
163
179
  type OrderProblem,
@@ -211,6 +227,24 @@ export {
211
227
  isClosure,
212
228
  jsTruthy,
213
229
  } from "./formula.ts";
230
+ export { decodeDomain, decodeValue, encodeValue } from "./codec.ts";
231
+ export {
232
+ type ParseResult,
233
+ type SyntaxAssoc,
234
+ type SyntaxCalls,
235
+ type SyntaxFixity,
236
+ type SyntaxForm,
237
+ type SyntaxIssue,
238
+ type SyntaxLiterals,
239
+ type SyntaxOperator,
240
+ type SyntaxProfile,
241
+ type SyntaxRefPattern,
242
+ type SyntaxRefs,
243
+ STD_SYNTAX,
244
+ parse,
245
+ print,
246
+ syntaxFormat,
247
+ } from "./syntax.ts";
214
248
  export { type Definitions, type ReferenceOptions, STD_DEFINITIONS, formulaDefinitions, reference } from "./reference.ts";
215
249
  export { type AgreeOptions, type Agreement, type Tier, type TierInput, type TierResult, agree, sameValue } from "./agree.ts";
216
250
  export { type ConformOptions, type ConformProbe, type Finding, type Obligation, conforms } from "./conform.ts";
@@ -230,6 +264,6 @@ export {
230
264
  export { type GraphCycle, type GraphEdge, type GraphNode, type GraphOptions, type NodeGraph, components, cycles, heights, necessary, nodeLayers } from "./graph.ts";
231
265
  export { type Recorder, type ReplayEntry, type ReplayLog, type ReplayOptions, type ReplaySettle, recorder, replayer } from "./replay.ts";
232
266
  export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
233
- export { type Domain, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
267
+ export { type Domain, type DomainCodec, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
234
268
  export { type PartType, type RootTypes, type TypeInput, type TypeProblem, type TypeRule, type Typed, CAPS_TYPE } from "./typing.ts";
235
269
  export { type SampleOptions, type TypeReading, typeFromSamples, typeFromSchema } from "./providers.ts";
package/src/parts.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
2
- import { type Kind, type Shape, type World, lookupKind } from "./world.ts";
2
+ import { type Kind, type NamesSource, type ScopeRule, type Shape, type World, lookupKind } from "./world.ts";
3
3
  import type { ValueType } from "./types.ts";
4
4
 
5
5
  /** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
@@ -268,3 +268,72 @@ export function pathMatches(path: readonly Key[], pattern: readonly Key[]): bool
268
268
  }
269
269
  return true;
270
270
  }
271
+
272
+ function valueAt(value: unknown, path: readonly Key[]): unknown {
273
+ let here: unknown = value;
274
+ for (const key of path) {
275
+ if (Array.isArray(here)) here = here[Number(key)];
276
+ else if (isRecord(here) && Object.hasOwn(here, String(key))) here = here[String(key)];
277
+ else return undefined;
278
+ }
279
+ return here;
280
+ }
281
+
282
+ function headerName(item: Json): string | undefined {
283
+ return isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
284
+ }
285
+
286
+ function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot[]): readonly string[] {
287
+ if ("fixed" in source) return source.fixed;
288
+ if ("keysAt" in source) {
289
+ const at = valueAt(value, source.keysAt);
290
+ return isRecord(at) ? Object.keys(at) : [];
291
+ }
292
+ if ("listAt" in source) {
293
+ const at = valueAt(value, source.listAt);
294
+ return Array.isArray(at) ? (at as readonly unknown[]).filter((item): item is string => typeof item === "string") : [];
295
+ }
296
+ if ("textAt" in source) {
297
+ const at = valueAt(value, source.textAt);
298
+ if (typeof at === "string") return [at];
299
+ return source.otherwise === undefined ? [] : [source.otherwise];
300
+ }
301
+ return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
302
+ }
303
+
304
+ /**
305
+ * This function gives the names that the scope rules of a kind give each part, and the names that only later parts
306
+ * see. `check`, `conforms` and the plan read the scope rules with it.
307
+ *
308
+ * @param rules - The scope rules of the kind.
309
+ * @param value - The value of the document.
310
+ * @param slots - The parts of the value, in order.
311
+ * @returns The names that each part sees, and the names that only the later parts see, by the index of the part.
312
+ */
313
+ export function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
314
+ const sees: string[][] = slots.map(() => []);
315
+ const later: string[][] = slots.map(() => []);
316
+ for (const rule of rules ?? []) {
317
+ const covered = slots.map((slot, index) => ({ slot, index })).filter(({ slot }) => rule.in.some((pattern) => pathMatches(slot.path, pattern)));
318
+ if (rule.sequential !== true) {
319
+ const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
320
+ for (const { index } of covered) sees[index]?.push(...names);
321
+ continue;
322
+ }
323
+ if ("headers" in rule.names) {
324
+ covered.forEach(({ index }, position) => {
325
+ const before = covered.slice(0, position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
326
+ const after = covered.slice(position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
327
+ sees[index]?.push(...before);
328
+ later[index]?.push(...after);
329
+ });
330
+ continue;
331
+ }
332
+ const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
333
+ covered.forEach(({ index }, position) => {
334
+ if (position > 0) sees[index]?.push(...names);
335
+ else later[index]?.push(...names);
336
+ });
337
+ }
338
+ return { sees, later };
339
+ }