@mark1russell7/coeval 0.7.1 → 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/build/index.d.ts +175 -96
- package/build/index.d.ts.map +1 -1
- package/build/index.js +762 -127
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agree.ts +26 -5
- package/src/conform.ts +1 -2
- package/src/doc.ts +2 -2
- package/src/evaluate.ts +273 -7
- package/src/index.ts +17 -1
- package/src/parts.ts +70 -1
- package/src/reference.ts +139 -16
- package/src/static.ts +54 -74
- package/src/stdlib.ts +220 -18
- package/src/world.ts +50 -0
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.
|
|
4
|
+
"version": "0.8.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": "
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
24
|
-
* not
|
|
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
|
}
|
|
@@ -1265,6 +1271,11 @@ export interface PrepareOptions {
|
|
|
1265
1271
|
* The default is `eval` (VERB.PREPARE).
|
|
1266
1272
|
*/
|
|
1267
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;
|
|
1268
1279
|
}
|
|
1269
1280
|
|
|
1270
1281
|
/** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
|
|
@@ -1301,6 +1312,10 @@ interface Compiler {
|
|
|
1301
1312
|
readonly verb: string;
|
|
1302
1313
|
/** The answer of a document that the ladder did not see, in a trace. */
|
|
1303
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;
|
|
1304
1319
|
}
|
|
1305
1320
|
|
|
1306
1321
|
function failing(c: Compiler, meta: Meta, place: readonly Key[], code: ErrorCode, reason: string): Compiled {
|
|
@@ -1385,6 +1400,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1385
1400
|
let meta = meta0;
|
|
1386
1401
|
let name: string | undefined;
|
|
1387
1402
|
let unknown: boolean;
|
|
1403
|
+
let quick: ReturnType<typeof answerOf>;
|
|
1388
1404
|
if (c.traced) {
|
|
1389
1405
|
const answer = ask(world, envelope ?? {}, c.verb, declared === undefined ? {} : { declaredType: declared });
|
|
1390
1406
|
meta = { ...meta0, answer, side: kind?.side ?? "derive" };
|
|
@@ -1392,7 +1408,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1392
1408
|
unknown = answer.error === "unknown-kind";
|
|
1393
1409
|
} else {
|
|
1394
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.
|
|
1395
|
-
|
|
1411
|
+
quick = answerOf(world, envelope?.own, kind ?? null, c.verb);
|
|
1396
1412
|
name = quick?.name;
|
|
1397
1413
|
unknown = quick === undefined && kind === undefined;
|
|
1398
1414
|
}
|
|
@@ -1406,10 +1422,13 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1406
1422
|
}
|
|
1407
1423
|
|
|
1408
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;
|
|
1409
1427
|
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
1410
1428
|
const item = slot.item;
|
|
1411
1429
|
// The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
|
|
1412
1430
|
const own = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
|
|
1431
|
+
frees?.push(own.free);
|
|
1413
1432
|
const rules = rulesFor(kind?.caps, slot.path);
|
|
1414
1433
|
const compiled = rules.length === 0 ? own : gated(rules, own);
|
|
1415
1434
|
// The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
|
|
@@ -1450,7 +1469,248 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1450
1469
|
} catch (error) {
|
|
1451
1470
|
return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
|
|
1452
1471
|
}
|
|
1453
|
-
|
|
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;
|
|
1454
1714
|
}
|
|
1455
1715
|
|
|
1456
1716
|
/*
|
|
@@ -1522,6 +1782,11 @@ function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code
|
|
|
1522
1782
|
interface Compiled {
|
|
1523
1783
|
readonly code: Code;
|
|
1524
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[];
|
|
1525
1790
|
}
|
|
1526
1791
|
|
|
1527
1792
|
/** 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 +1895,8 @@ export function rootContext(machine: Machine, ctx: Context): Context {
|
|
|
1630
1895
|
export function prepare(machine: Machine, doc: unknown, options: PrepareOptions = {}): Plan {
|
|
1631
1896
|
const verb = options.verb ?? "eval";
|
|
1632
1897
|
const noAnswer: Answer = verb === "eval" ? NO_ANSWER : { verb, error: "no-answer", steps: [] };
|
|
1633
|
-
const
|
|
1898
|
+
const traced = options.trace === true;
|
|
1899
|
+
const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold: !traced && options.fold !== false };
|
|
1634
1900
|
let root: Compiled;
|
|
1635
1901
|
try {
|
|
1636
1902
|
root = compileNode(c, doc, [], options.declaredType, 0);
|
|
@@ -1737,8 +2003,8 @@ function record(s: Session, result: Result): Run {
|
|
|
1737
2003
|
* tells where its parts are. The evaluator gets each part sealed: it can evaluate the part, and it cannot read its
|
|
1738
2004
|
* value.
|
|
1739
2005
|
*
|
|
1740
|
-
* A failure gives an error value, and the function does not throw.
|
|
1741
|
-
* 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).
|
|
1742
2008
|
*
|
|
1743
2009
|
* @param machine - The world and the code of the evaluators.
|
|
1744
2010
|
* @param doc - The document, as JSON.
|
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,
|
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
|
+
}
|