@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/README.md +31 -0
- package/build/index.d.ts +399 -100
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1818 -172
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agree.ts +26 -5
- package/src/codec.ts +185 -0
- package/src/conform.ts +2 -3
- package/src/doc.ts +2 -2
- package/src/evaluate.ts +281 -9
- package/src/formula.ts +19 -8
- package/src/index.ts +36 -2
- package/src/parts.ts +70 -1
- package/src/reference.ts +143 -18
- package/src/static.ts +54 -74
- package/src/stdlib.ts +220 -18
- package/src/syntax.ts +846 -0
- package/src/types.ts +28 -1
- package/src/world.ts +50 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
20
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|