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