@mark1russell7/coeval 0.26.1 → 0.27.1
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 +42 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1560 -1363
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +1 -0
- package/src/evaluate.ts +118 -14
- package/src/formula.ts +13 -5
- package/src/generate.ts +6 -1
- package/src/index.ts +2 -1
- package/src/language.ts +51 -1
- package/src/merge.ts +1 -1
- package/src/reference.ts +102 -39
- package/src/stdlib.ts +39 -14
- package/src/stepper.ts +49 -6
package/src/reference.ts
CHANGED
|
@@ -36,12 +36,15 @@ import {
|
|
|
36
36
|
type Steps,
|
|
37
37
|
type StopSignal,
|
|
38
38
|
JOURNAL,
|
|
39
|
+
NODE_WORK,
|
|
40
|
+
WORK_LIMIT,
|
|
39
41
|
heldReason,
|
|
40
42
|
makeContext,
|
|
41
43
|
permits,
|
|
42
44
|
placeText,
|
|
43
45
|
reachedOf,
|
|
44
46
|
waitedReason,
|
|
47
|
+
workFailure,
|
|
45
48
|
} from "./evaluate.ts";
|
|
46
49
|
import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
|
|
47
50
|
|
|
@@ -111,6 +114,15 @@ interface Reading {
|
|
|
111
114
|
readonly root: Context;
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
/** The count of the work of one evaluation of the reference (WORK.LIMIT). */
|
|
118
|
+
interface Meter {
|
|
119
|
+
spent: number;
|
|
120
|
+
/** `true` after the end of the evaluation. Then each entry from the host starts a new count (WORK.AFTER). */
|
|
121
|
+
ended: boolean;
|
|
122
|
+
/** The count of the applications of closures that run at this time, in each state of the evaluation (WORK.AFTER). */
|
|
123
|
+
active: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
/** The state of one evaluation of the reference. */
|
|
115
127
|
interface Env {
|
|
116
128
|
readonly machine: Machine;
|
|
@@ -130,6 +142,12 @@ interface Env {
|
|
|
130
142
|
held: number;
|
|
131
143
|
/** The count of the applications of closures that run at this time: the clock of `call` (FORMULA.CALL-DEPTH, D125). */
|
|
132
144
|
applying: number;
|
|
145
|
+
/** The most work of one evaluation (WORK.LIMIT, D134). */
|
|
146
|
+
readonly maxWork: number;
|
|
147
|
+
/** The count of the work. An equation of a tree shares the count around it (WORK.EQUATION). */
|
|
148
|
+
readonly meter: Meter;
|
|
149
|
+
/** `true` for an equation of a tree (WORK.NODE). */
|
|
150
|
+
readonly nested: boolean;
|
|
133
151
|
/** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
|
|
134
152
|
readonly reading: Reading | undefined;
|
|
135
153
|
}
|
|
@@ -689,23 +707,37 @@ function* together(env: Env, list: readonly RunPart[], options: AllOptions = {})
|
|
|
689
707
|
* @returns The result.
|
|
690
708
|
*/
|
|
691
709
|
export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
|
|
692
|
-
return evaluateRoot(machine, doc, context, options, undefined);
|
|
710
|
+
return evaluateRoot(machine, doc, context, options, undefined, undefined);
|
|
693
711
|
}
|
|
694
712
|
|
|
695
713
|
/**
|
|
696
|
-
* This function evaluates a document from its root. An equation of a tree gives the record of its tree
|
|
697
|
-
*
|
|
714
|
+
* This function evaluates a document from its root. An equation of a tree gives the record of its tree and the count
|
|
715
|
+
* of its work. Thus its reads go in the list of the outer evaluation (REF.READS), and its work in the outer count
|
|
716
|
+
* (WORK.EQUATION).
|
|
698
717
|
*/
|
|
699
|
-
function evaluateRoot(machine: Machine, doc: unknown, context: Context, options: ReferenceOptions, outer: Reading | undefined): Result {
|
|
718
|
+
function evaluateRoot(machine: Machine, doc: unknown, context: Context, options: ReferenceOptions, outer: Reading | undefined, meter: Meter | undefined): Result {
|
|
700
719
|
const at = rootAt(machine, doc, context, options);
|
|
701
|
-
const env = envOf(machine, options, false, at.ctx, outer);
|
|
720
|
+
const env = envOf(machine, options, false, at.ctx, outer, meter);
|
|
702
721
|
try {
|
|
703
722
|
return within(env, () => evaluateAt(env, at));
|
|
704
723
|
} catch (error) {
|
|
705
724
|
return placed(thrown(error, "the reference"), []);
|
|
725
|
+
} finally {
|
|
726
|
+
if (!env.nested) env.meter.ended = true;
|
|
706
727
|
}
|
|
707
728
|
}
|
|
708
729
|
|
|
730
|
+
/**
|
|
731
|
+
* WORK.LIMIT: this function adds work to the count of an evaluation. After the end of the evaluation, each entry from
|
|
732
|
+
* the host starts a new count (WORK.AFTER). A count over the limit throws `#WORK!`.
|
|
733
|
+
*/
|
|
734
|
+
function spend(env: Pick<Env, "maxWork" | "meter" | "applying">, points: number): void {
|
|
735
|
+
const meter = env.meter;
|
|
736
|
+
if (meter.ended && meter.active === 0) meter.spent = 0;
|
|
737
|
+
meter.spent += points;
|
|
738
|
+
if (meter.spent > env.maxWork) throw new Failure(workFailure(env.maxWork));
|
|
739
|
+
}
|
|
740
|
+
|
|
709
741
|
/**
|
|
710
742
|
* This function starts a run of a document by the rules, without a compilation (REF.RUN). The host does each effect of a runner,
|
|
711
743
|
* and a step waits when the host waits (RUN.SEQ). `all` settles the waits in the order of the host, or in the order
|
|
@@ -723,16 +755,20 @@ function evaluateRoot(machine: Machine, doc: unknown, context: Context, options:
|
|
|
723
755
|
*/
|
|
724
756
|
export function referenceRun(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceRunOptions = {}): Promise<Result> {
|
|
725
757
|
const at = rootAt(machine, doc, context, options);
|
|
726
|
-
const env = envOf(machine, options, true, at.ctx, undefined);
|
|
758
|
+
const env = envOf(machine, options, true, at.ctx, undefined, undefined);
|
|
727
759
|
const steps = stepsAt(env, at);
|
|
728
|
-
return driveRun(env, steps, options.signal)
|
|
760
|
+
return driveRun(env, steps, options.signal)
|
|
761
|
+
.catch((error: unknown) => placed(thrown(error, "the reference"), []))
|
|
762
|
+
.finally(() => {
|
|
763
|
+
env.meter.ended = true;
|
|
764
|
+
});
|
|
729
765
|
}
|
|
730
766
|
|
|
731
767
|
/**
|
|
732
768
|
* This function makes the state of the reference for one evaluation or one run. The record of the reads comes from
|
|
733
769
|
* the evaluation around it, or from the list of the options.
|
|
734
770
|
*/
|
|
735
|
-
function envOf(machine: Machine, options: ReferenceOptions, running: boolean, root: Context, outer: Reading | undefined): Env {
|
|
771
|
+
function envOf(machine: Machine, options: ReferenceOptions, running: boolean, root: Context, outer: Reading | undefined, meter: Meter | undefined): Env {
|
|
736
772
|
const perform = options.perform;
|
|
737
773
|
return {
|
|
738
774
|
machine,
|
|
@@ -746,6 +782,9 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
|
|
|
746
782
|
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
747
783
|
held: 0,
|
|
748
784
|
applying: 0,
|
|
785
|
+
maxWork: machine.maxWork ?? WORK_LIMIT,
|
|
786
|
+
meter: meter ?? { spent: 0, ended: false, active: 0 },
|
|
787
|
+
nested: meter !== undefined,
|
|
749
788
|
reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
|
|
750
789
|
};
|
|
751
790
|
}
|
|
@@ -869,7 +908,7 @@ function equationOf(node: RefNode, term: Json, names: Readonly<Record<string, un
|
|
|
869
908
|
const env = node.env;
|
|
870
909
|
if (env === undefined) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
|
|
871
910
|
const ctx: Context = { ...node.ctx, names, caps: [], self: node.thing };
|
|
872
|
-
return evaluateRoot(env.machine, term, ctx, { definitions: env.definitions, ...(node.of === undefined ? {} : { declaredType: node.of }) }, env.reading);
|
|
911
|
+
return evaluateRoot(env.machine, term, ctx, { definitions: env.definitions, ...(node.of === undefined ? {} : { declaredType: node.of }) }, env.reading, env.meter);
|
|
873
912
|
}
|
|
874
913
|
|
|
875
914
|
function termIn<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
|
|
@@ -1240,43 +1279,56 @@ function instanceOf(node: RefNode, attr: string, args: unknown): Result | undefi
|
|
|
1240
1279
|
}
|
|
1241
1280
|
|
|
1242
1281
|
/** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
|
|
1243
|
-
function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1282
|
+
function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[], seen: Set<object>): Result {
|
|
1283
|
+
try {
|
|
1284
|
+
const record: Record<string, unknown> = {};
|
|
1285
|
+
for (const part of node.parts) {
|
|
1286
|
+
const result = attributeOf(node, part.key) as Result;
|
|
1287
|
+
if (!result.ok) return result;
|
|
1288
|
+
const value = plainValue(node, result.value, [...open, self], seen);
|
|
1289
|
+
if (!value.ok) return value;
|
|
1290
|
+
setField(record, part.key, value.value);
|
|
1291
|
+
}
|
|
1292
|
+
return ok(record);
|
|
1293
|
+
} finally {
|
|
1294
|
+
seen.add(self);
|
|
1251
1295
|
}
|
|
1252
|
-
return ok(record);
|
|
1253
1296
|
}
|
|
1254
1297
|
|
|
1255
|
-
/**
|
|
1256
|
-
function
|
|
1298
|
+
/**
|
|
1299
|
+
* This function gives a copy of a value with the JSON of each node in it. The set `seen` holds each node, list and
|
|
1300
|
+
* record that the walk left. A second visit of one of them counts one point on the evaluation of `owner` (WORK.SHARED).
|
|
1301
|
+
*/
|
|
1302
|
+
function plainValue(owner: RefNode, value: unknown, open: readonly object[], seen: Set<object>): Result {
|
|
1257
1303
|
const node = refNodeOf(value);
|
|
1258
1304
|
if (node !== undefined) {
|
|
1259
1305
|
if (open.includes(value as object)) return fail("#CYCLE!", `The JSON of the node ${node.kind} at [${node.place.join(", ")}] holds the node itself.`);
|
|
1260
|
-
|
|
1306
|
+
if (seen.has(value as object) && owner.env !== undefined) spend(owner.env, 1);
|
|
1307
|
+
return jsonOfNode(node, value as HostThing, open, seen);
|
|
1261
1308
|
}
|
|
1262
1309
|
if (typeof value !== "object" || value === null || isHostThing(value) || open.includes(value)) return ok(value);
|
|
1263
|
-
if (Array.isArray(value))
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1310
|
+
if (!Array.isArray(value) && !isRecord(value)) return ok(value);
|
|
1311
|
+
if (seen.has(value) && owner.env !== undefined) spend(owner.env, 1);
|
|
1312
|
+
try {
|
|
1313
|
+
if (Array.isArray(value)) {
|
|
1314
|
+
const items: unknown[] = [];
|
|
1315
|
+
for (const item of value) {
|
|
1316
|
+
const plain = plainValue(owner, item, [...open, value], seen);
|
|
1317
|
+
if (!plain.ok) return plain;
|
|
1318
|
+
items.push(plain.value);
|
|
1319
|
+
}
|
|
1320
|
+
return ok(items);
|
|
1321
|
+
}
|
|
1322
|
+
const copy: Record<string, unknown> = {};
|
|
1323
|
+
for (const key of Object.keys(value)) {
|
|
1324
|
+
const plain = plainValue(owner, value[key], [...open, value], seen);
|
|
1267
1325
|
if (!plain.ok) return plain;
|
|
1268
|
-
|
|
1326
|
+
setField(copy, key, plain.value);
|
|
1269
1327
|
}
|
|
1270
|
-
return ok(
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const copy: Record<string, unknown> = {};
|
|
1274
|
-
for (const key of Object.keys(value)) {
|
|
1275
|
-
const plain = plainValue(value[key], [...open, value]);
|
|
1276
|
-
if (!plain.ok) return plain;
|
|
1277
|
-
setField(copy, key, plain.value);
|
|
1328
|
+
return ok(copy);
|
|
1329
|
+
} finally {
|
|
1330
|
+
seen.add(value);
|
|
1278
1331
|
}
|
|
1279
|
-
return ok(copy);
|
|
1280
1332
|
}
|
|
1281
1333
|
|
|
1282
1334
|
/**
|
|
@@ -1285,6 +1337,9 @@ function plainValue(value: unknown, open: readonly object[]): Result {
|
|
|
1285
1337
|
* reads `self` only through `resolve` (C4, TREE.SELF).
|
|
1286
1338
|
*/
|
|
1287
1339
|
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk, doc }) => {
|
|
1340
|
+
// WORK.NODE: a node that an equation makes counts. A node of the text of the document does not.
|
|
1341
|
+
const maker = activeEnvs[activeEnvs.length - 1];
|
|
1342
|
+
if (maker?.nested === true) spend(maker, NODE_WORK);
|
|
1288
1343
|
const reached = walk(["self"]);
|
|
1289
1344
|
const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
|
|
1290
1345
|
const outer = refNodeOf(parent);
|
|
@@ -1320,7 +1375,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1320
1375
|
},
|
|
1321
1376
|
has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)) || node.values.has(String(key)),
|
|
1322
1377
|
toJson: () => {
|
|
1323
|
-
const result = jsonOfNode(node, self, []);
|
|
1378
|
+
const result = jsonOfNode(node, self, [], new Set());
|
|
1324
1379
|
if (result.ok) return result.value;
|
|
1325
1380
|
throw new Failure(result.error);
|
|
1326
1381
|
},
|
|
@@ -1330,7 +1385,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1330
1385
|
AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
|
|
1331
1386
|
if (nested) return ok(self);
|
|
1332
1387
|
try {
|
|
1333
|
-
return jsonOfNode(node, self, []);
|
|
1388
|
+
return jsonOfNode(node, self, [], new Set());
|
|
1334
1389
|
} finally {
|
|
1335
1390
|
node.tree.open = false;
|
|
1336
1391
|
}
|
|
@@ -1468,20 +1523,28 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
|
|
|
1468
1523
|
const names = params as readonly string[];
|
|
1469
1524
|
const body = partNamed(parts, "body") as Part;
|
|
1470
1525
|
// FORMULA.CALL-DEPTH: the evaluation that makes the closure counts its applications. A quarter of maxDepth is the limit.
|
|
1471
|
-
const
|
|
1526
|
+
const maker = activeEnvs[activeEnvs.length - 1];
|
|
1527
|
+
const clock = maker ?? { applying: 0, maxDepth: 1000, maxWork: WORK_LIMIT, meter: { spent: 0, ended: false, active: 0 } };
|
|
1472
1528
|
const limit = Math.ceil(clock.maxDepth / 4);
|
|
1473
1529
|
const closure = (...args: readonly unknown[]): unknown => {
|
|
1474
1530
|
if (clock.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
|
|
1531
|
+
// WORK.LIMIT: each application counts one point, after the clock of its depth.
|
|
1532
|
+
spend(clock, 1);
|
|
1475
1533
|
clock.applying += 1;
|
|
1534
|
+
clock.meter.active += 1;
|
|
1476
1535
|
try {
|
|
1477
1536
|
const scope: Record<string, unknown> = {};
|
|
1478
1537
|
for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
|
|
1479
1538
|
names.forEach((name, index) => setField(scope, name, args[index]));
|
|
1480
|
-
|
|
1539
|
+
// The body evaluates in the state of the evaluation that made the closure, also for a host after the end. Thus a
|
|
1540
|
+
// node of a tree in the body has the machine for its equations (WORK.AFTER).
|
|
1541
|
+
const scoped: Context = { ...context, names: scope };
|
|
1542
|
+
const result = maker === undefined ? body.evaluate(scoped) : within(maker, () => body.evaluate(scoped));
|
|
1481
1543
|
if (!result.ok) throw new Failure(result.error);
|
|
1482
1544
|
return result.value;
|
|
1483
1545
|
} finally {
|
|
1484
1546
|
clock.applying -= 1;
|
|
1547
|
+
clock.meter.active -= 1;
|
|
1485
1548
|
}
|
|
1486
1549
|
};
|
|
1487
1550
|
Object.defineProperty(closure, CLOSURE, { value: { params: names } });
|
package/src/stdlib.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
type Result,
|
|
10
10
|
type RunPart,
|
|
11
11
|
type Runner,
|
|
12
|
+
type Runtime,
|
|
12
13
|
type Steps,
|
|
14
|
+
NODE_WORK,
|
|
13
15
|
RUNTIME,
|
|
14
16
|
evaluateWithin,
|
|
15
17
|
fail,
|
|
@@ -245,11 +247,20 @@ function isNumber(value: unknown): value is number {
|
|
|
245
247
|
function allValues(results: readonly Result[]): { readonly values: readonly unknown[] } | { readonly error: Result } {
|
|
246
248
|
const errors = results.filter((result) => !result.ok);
|
|
247
249
|
const first = errors[0];
|
|
250
|
+
// D190: the test of the errors comes before the values. With the values first, a case of the bench took 1.2 to 1.4
|
|
251
|
+
// times the time in the Maglev tier of V8, in probe 4 of K216.
|
|
248
252
|
if (first !== undefined && errors.length === 1) return { error: first };
|
|
249
|
-
if (errors.length > 1) return { error:
|
|
253
|
+
if (errors.length > 1) return { error: partsFailed(errors) };
|
|
250
254
|
return { values: results.map((result) => (result.ok ? result.value : null)) };
|
|
251
255
|
}
|
|
252
256
|
|
|
257
|
+
/** This function gives the error of more than one failed part. */
|
|
258
|
+
function partsFailed(errors: readonly Result[]): Result {
|
|
259
|
+
// WORK.LIMIT: after the limit each counted part fails, and the code of the limit stays the code of the result.
|
|
260
|
+
const stopped = errors.find((result) => !result.ok && result.error.code === "#WORK!");
|
|
261
|
+
return stopped ?? fail("#ARGS", `${errors.length} parts failed.`);
|
|
262
|
+
}
|
|
263
|
+
|
|
253
264
|
function numbers(values: readonly unknown[], kind: string): readonly number[] | Result {
|
|
254
265
|
const bad = values.findIndex((value) => !isNumber(value));
|
|
255
266
|
if (bad !== -1) return fail("#VALUE!", `Part ${bad} of ${kind} is not a number.`);
|
|
@@ -379,7 +390,7 @@ interface NodeState extends LoopNode {
|
|
|
379
390
|
* @param path - The nodes and the containers whose JSON is in progress. A node on it gives `#CYCLE!`.
|
|
380
391
|
* @returns The JSON, or the first error.
|
|
381
392
|
*/
|
|
382
|
-
json(path:
|
|
393
|
+
json(path: JsonWalk): Result;
|
|
383
394
|
}
|
|
384
395
|
|
|
385
396
|
/** The answer of the ladder for one attribute of one node (TREE.CELL, D108). */
|
|
@@ -887,24 +898,35 @@ function putField(target: Record<string, unknown>, key: string, value: unknown):
|
|
|
887
898
|
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
888
899
|
}
|
|
889
900
|
|
|
901
|
+
/**
|
|
902
|
+
* The record of a walk for JSON. A node, list or record has `true` while its JSON is in progress, and `false` after
|
|
903
|
+
* that. Thus a value in the map with `false` is a second visit (WORK.SHARED).
|
|
904
|
+
*/
|
|
905
|
+
type JsonWalk = Map<object, boolean>;
|
|
906
|
+
|
|
890
907
|
/**
|
|
891
908
|
* This function gives a value with the JSON of each node of a tree in it. A list or a plain record is new only when
|
|
892
909
|
* it holds a node. A container that holds itself stays as it is.
|
|
893
910
|
*/
|
|
894
|
-
function plainOf(value: unknown, path:
|
|
911
|
+
function plainOf(value: unknown, path: JsonWalk, runtime: Runtime | undefined): Result {
|
|
912
|
+
if (typeof value !== "object" || value === null) return ok(value);
|
|
895
913
|
const state = stateOf(value);
|
|
914
|
+
const open = path.get(value);
|
|
896
915
|
if (state !== undefined) {
|
|
897
|
-
if (
|
|
916
|
+
if (open === true) return fail("#CYCLE!", `The JSON of the node ${state.type} at [${state.place.join(", ")}] holds the node itself.`);
|
|
917
|
+
if (open === false) runtime?.spend?.(1);
|
|
898
918
|
return state.json(path);
|
|
899
919
|
}
|
|
900
|
-
if (
|
|
920
|
+
if (open === true || isHostThing(value)) return ok(value);
|
|
901
921
|
if (!Array.isArray(value) && !isRecord(value)) return ok(value);
|
|
902
|
-
|
|
922
|
+
// WORK.SHARED: a second visit of a container counts one point, because a chain of shared containers doubles the walk.
|
|
923
|
+
if (open === false) runtime?.spend?.(1);
|
|
924
|
+
path.set(value, true);
|
|
903
925
|
try {
|
|
904
926
|
if (Array.isArray(value)) {
|
|
905
927
|
let list: unknown[] | undefined;
|
|
906
928
|
for (let index = 0; index < value.length; index += 1) {
|
|
907
|
-
const item = plainOf(value[index], path);
|
|
929
|
+
const item = plainOf(value[index], path, runtime);
|
|
908
930
|
if (!item.ok) return item;
|
|
909
931
|
if (item.value !== value[index]) (list ??= [...(value as readonly unknown[])])[index] = item.value;
|
|
910
932
|
}
|
|
@@ -913,7 +935,7 @@ function plainOf(value: unknown, path: Set<object>): Result {
|
|
|
913
935
|
const record = value as Readonly<Record<string, unknown>>;
|
|
914
936
|
let out: Record<string, unknown> | undefined;
|
|
915
937
|
for (const key of Object.keys(record)) {
|
|
916
|
-
const item = plainOf(record[key], path);
|
|
938
|
+
const item = plainOf(record[key], path, runtime);
|
|
917
939
|
if (!item.ok) return item;
|
|
918
940
|
if (item.value === record[key]) continue;
|
|
919
941
|
if (out === undefined) {
|
|
@@ -924,7 +946,7 @@ function plainOf(value: unknown, path: Set<object>): Result {
|
|
|
924
946
|
}
|
|
925
947
|
return ok(out ?? value);
|
|
926
948
|
} finally {
|
|
927
|
-
path.
|
|
949
|
+
path.set(value, false);
|
|
928
950
|
}
|
|
929
951
|
}
|
|
930
952
|
|
|
@@ -1323,19 +1345,19 @@ function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, ou
|
|
|
1323
1345
|
return cell.by === undefined ? { attribute: key, rungs } : { attribute: key, rungs, by: cell.by };
|
|
1324
1346
|
},
|
|
1325
1347
|
json(path) {
|
|
1326
|
-
path.
|
|
1348
|
+
path.set(node, true);
|
|
1327
1349
|
try {
|
|
1328
1350
|
const out: Record<string, unknown> = {};
|
|
1329
1351
|
for (const part of parts) {
|
|
1330
1352
|
const result = read(part.key) as Result;
|
|
1331
1353
|
if (!result.ok) return result;
|
|
1332
|
-
const value = plainOf(result.value, path);
|
|
1354
|
+
const value = plainOf(result.value, path, runtime);
|
|
1333
1355
|
if (!value.ok) return value;
|
|
1334
1356
|
putField(out, part.key, value.value);
|
|
1335
1357
|
}
|
|
1336
1358
|
return ok(out);
|
|
1337
1359
|
} finally {
|
|
1338
|
-
path.
|
|
1360
|
+
path.set(node, false);
|
|
1339
1361
|
}
|
|
1340
1362
|
},
|
|
1341
1363
|
};
|
|
@@ -1359,7 +1381,7 @@ function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, ou
|
|
|
1359
1381
|
return cell !== undefined && !cell.busy && cell.result !== undefined;
|
|
1360
1382
|
},
|
|
1361
1383
|
toJson(): unknown {
|
|
1362
|
-
const result = state.json(new
|
|
1384
|
+
const result = state.json(new Map());
|
|
1363
1385
|
if (!result.ok) throw new Failure(result.error);
|
|
1364
1386
|
return result.value;
|
|
1365
1387
|
},
|
|
@@ -1404,6 +1426,9 @@ const NO_HOPS = /* @__PURE__ */ Object.freeze({ hops: false });
|
|
|
1404
1426
|
* The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of each node.
|
|
1405
1427
|
*/
|
|
1406
1428
|
function treeNode(input: EvalInput, host: AttributeCell | undefined): Result {
|
|
1429
|
+
const runtime = (input as InputWithRuntime)[RUNTIME];
|
|
1430
|
+
// WORK.NODE: a node that an equation makes counts. A node of the text of the document does not.
|
|
1431
|
+
if (runtime?.nested === true) runtime.spend?.(NODE_WORK);
|
|
1407
1432
|
const found = input.resolve(SELF_PATH, NO_HOPS);
|
|
1408
1433
|
const outer = found.ok ? found.thing : undefined;
|
|
1409
1434
|
const above = stateOf(outer);
|
|
@@ -1415,7 +1440,7 @@ function treeNode(input: EvalInput, host: AttributeCell | undefined): Result {
|
|
|
1415
1440
|
build.open = true;
|
|
1416
1441
|
const root = treeThing(input, place, build, outer, host);
|
|
1417
1442
|
try {
|
|
1418
|
-
return (stateOf(root) as NodeState).json(new
|
|
1443
|
+
return (stateOf(root) as NodeState).json(new Map());
|
|
1419
1444
|
} finally {
|
|
1420
1445
|
build.open = false;
|
|
1421
1446
|
}
|
package/src/stepper.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, type TypeRef, docEquals, field, isEnvelope, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import { type Cap, type Context, type Resolution, type ResolveOptions, type Thing, isHostThing, worldThing } from "./deref.ts";
|
|
3
3
|
import { type EvalError, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
4
|
-
import { type Effect, type EvalInput, type Machine, type Part, type Perform, intersectCaps, makeContext, permits, placeText } from "./evaluate.ts";
|
|
4
|
+
import { type Effect, type EvalInput, type Machine, type Part, type Perform, WORK_LIMIT, intersectCaps, makeContext, permits, placeText, workFailure } from "./evaluate.ts";
|
|
5
5
|
import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
|
|
6
6
|
import type { World } from "./ladder.ts";
|
|
7
7
|
import { pathMatches } from "./parts.ts";
|
|
8
|
-
import type
|
|
8
|
+
import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
|
|
9
|
+
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
9
10
|
import { type CapsRule, type Kind, type Shape, type WorldEntry, isAlias, isKind, isNamespace } from "./world.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -111,6 +112,12 @@ interface Shared {
|
|
|
111
112
|
readonly applying: number;
|
|
112
113
|
/** `true` after a closure or a held error goes in a value. Only then does a value need a change for the host. */
|
|
113
114
|
readonly marked: boolean;
|
|
115
|
+
/** The work of the evaluation after the last call of `stepperNext` (WORK.LIMIT). A configuration without it has done no work. */
|
|
116
|
+
readonly work?: number;
|
|
117
|
+
/** The number of earlier effects at each place, after the last call of `stepperNext` (RUN.ADDRESS). */
|
|
118
|
+
readonly counts?: Readonly<Record<string, number>>;
|
|
119
|
+
/** The number of effects of the evaluation, after the last call of `stepperNext`. */
|
|
120
|
+
readonly effected?: number;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
/**
|
|
@@ -164,8 +171,16 @@ interface Program {
|
|
|
164
171
|
readonly perform: Perform | undefined;
|
|
165
172
|
/** The number of earlier effects at each place, by the text of the place (RUN.ADDRESS). */
|
|
166
173
|
readonly counts: Map<string, number>;
|
|
174
|
+
/** The number of effects of the evaluation before the list `effects`. */
|
|
175
|
+
before: number;
|
|
167
176
|
/** The count of the applications at the last call of host code. */
|
|
168
177
|
applying: number;
|
|
178
|
+
/** The most work of one evaluation (WORK.LIMIT). */
|
|
179
|
+
readonly maxWork: number;
|
|
180
|
+
/** The work of the evaluation: one point for each application of a closure (WORK.LIMIT). */
|
|
181
|
+
work: number;
|
|
182
|
+
/** `true` after the end of the evaluation. Then each application from the host starts a new count (WORK.AFTER). */
|
|
183
|
+
ended: boolean;
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
const CLOSURE_MARK = "coeval.closure";
|
|
@@ -555,6 +570,9 @@ function apply(p: Program, c: Configuration, target: unknown, args: readonly unk
|
|
|
555
570
|
}
|
|
556
571
|
const limit = Math.ceil(p.maxDepth / 4);
|
|
557
572
|
if (c.applying >= limit) return back(c, fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`));
|
|
573
|
+
// WORK.LIMIT: each application counts one point, after the clock of its depth.
|
|
574
|
+
p.work += 1;
|
|
575
|
+
if (p.work > p.maxWork) return back(c, { ok: false, error: workFailure(p.maxWork) });
|
|
558
576
|
const frame: Frame = { f: "return", place: closure.place.concat(closure.body.path) };
|
|
559
577
|
pushed(c, frame);
|
|
560
578
|
const start = enter(c, { at: NO_KEYS }, closure.site, closure.body, scopeOf(closure, args));
|
|
@@ -576,6 +594,11 @@ function hostClosure(p: Program, value: object, closure: Closure): unknown {
|
|
|
576
594
|
const fn = (...args: readonly unknown[]): unknown => {
|
|
577
595
|
const limit = Math.ceil(p.maxDepth / 4);
|
|
578
596
|
if (p.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
|
|
597
|
+
// WORK.AFTER: after the end of the evaluation, each application from the host starts a new count.
|
|
598
|
+
p.work = (p.ended && p.applying === 0 ? 0 : p.work) + 1;
|
|
599
|
+
if (p.work > p.maxWork) throw new Failure(workFailure(p.maxWork));
|
|
600
|
+
// Host code in the nested stepper writes the count of the applications, thus the call puts back the count of its start.
|
|
601
|
+
const before = p.applying;
|
|
579
602
|
p.applying += 1;
|
|
580
603
|
try {
|
|
581
604
|
const place = closure.place.concat(closure.body.path);
|
|
@@ -584,7 +607,7 @@ function hostClosure(p: Program, value: object, closure: Closure): unknown {
|
|
|
584
607
|
if (!end.result.ok) throw new Failure(end.result.error);
|
|
585
608
|
return forHost(p, end, end.result.value);
|
|
586
609
|
} finally {
|
|
587
|
-
p.applying
|
|
610
|
+
p.applying = before;
|
|
588
611
|
}
|
|
589
612
|
};
|
|
590
613
|
Object.defineProperty(fn, CLOSURE, { value: { params: closure.params } });
|
|
@@ -856,7 +879,7 @@ function cause(p: Program, ctx: Context, kind: Cap, payload: unknown, place: rea
|
|
|
856
879
|
const count = counts.get(at) ?? 0;
|
|
857
880
|
counts.set(at, count + 1);
|
|
858
881
|
const effect: { -readonly [K in keyof Effect]: Effect[K] } = {
|
|
859
|
-
index: effects.length,
|
|
882
|
+
index: p.before + effects.length,
|
|
860
883
|
kind: text,
|
|
861
884
|
path: keys,
|
|
862
885
|
payload,
|
|
@@ -948,7 +971,9 @@ function evaluateDoc(p: Program, c: Evaluating): Configuration {
|
|
|
948
971
|
return back(c, thrown(error, `the evaluator ${name}`));
|
|
949
972
|
}
|
|
950
973
|
}
|
|
951
|
-
|
|
974
|
+
// A standard evaluator gets the definition of the reference, unless the options give another definition (REF.TREE).
|
|
975
|
+
const standard = code === own(STD_EVALUATORS, name) ? own(STD_DEFINITIONS.evaluators, name) : undefined;
|
|
976
|
+
const defined = own(p.definitions.evaluators, name) ?? standard;
|
|
952
977
|
if (defined !== undefined) return hostEvaluator(p, c, name, defined, doc, keys, read.data, read.slots, site);
|
|
953
978
|
return back(c, fail("#NOEVAL", `The stepper has no definition of the compiled evaluator ${name}.`));
|
|
954
979
|
}
|
|
@@ -1087,7 +1112,11 @@ function programOf(machine: Machine, options: StepperOptions): Program {
|
|
|
1087
1112
|
effects: options.effects ?? [],
|
|
1088
1113
|
perform: options.perform,
|
|
1089
1114
|
counts: new Map(),
|
|
1115
|
+
before: 0,
|
|
1090
1116
|
applying: 0,
|
|
1117
|
+
maxWork: machine.maxWork ?? WORK_LIMIT,
|
|
1118
|
+
work: 0,
|
|
1119
|
+
ended: false,
|
|
1091
1120
|
};
|
|
1092
1121
|
}
|
|
1093
1122
|
|
|
@@ -1124,7 +1153,20 @@ export function stepperNext(machine: Machine, configuration: Configuration, opti
|
|
|
1124
1153
|
else if (f === "let") stack[stack.length - 1] = { ...top, names: { ...top.names } };
|
|
1125
1154
|
else stack[stack.length - 1] = { ...top };
|
|
1126
1155
|
}
|
|
1127
|
-
|
|
1156
|
+
const p = programOf(machine, options);
|
|
1157
|
+
// Each call has a new program. Thus the configuration holds the state of the evaluation that a later step needs.
|
|
1158
|
+
p.work = configuration.work ?? 0;
|
|
1159
|
+
for (const [at, count] of Object.entries(configuration.counts ?? {})) p.counts.set(at, count);
|
|
1160
|
+
// A list of effects from the host holds the earlier effects. Without it, the configuration holds their number.
|
|
1161
|
+
p.before = options.effects === undefined ? (configuration.effected ?? 0) : 0;
|
|
1162
|
+
const next = advance(p, { ...configuration, stack });
|
|
1163
|
+
const effected = p.before + p.effects.length;
|
|
1164
|
+
return {
|
|
1165
|
+
...next,
|
|
1166
|
+
...(p.work === 0 ? {} : { work: p.work }),
|
|
1167
|
+
...(p.counts.size === 0 ? {} : { counts: Object.fromEntries(p.counts) }),
|
|
1168
|
+
...(effected === 0 ? {} : { effected }),
|
|
1169
|
+
};
|
|
1128
1170
|
}
|
|
1129
1171
|
|
|
1130
1172
|
/**
|
|
@@ -1143,6 +1185,7 @@ export function stepper(machine: Machine, doc: unknown, context: Context = makeC
|
|
|
1143
1185
|
const p = programOf(machine, options);
|
|
1144
1186
|
try {
|
|
1145
1187
|
const end = drive(p, stepperStart(machine, doc, context, options));
|
|
1188
|
+
p.ended = true;
|
|
1146
1189
|
return end.result.ok ? ok(forHost(p, end, end.result.value)) : end.result;
|
|
1147
1190
|
} catch (error) {
|
|
1148
1191
|
return placed(thrown(error, "the stepper"), []);
|