@mark1russell7/coeval 0.21.0 → 0.23.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 +23 -5
- package/build/index.d.ts +420 -116
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5669 -3655
- package/build/index.js.map +1 -1
- package/package.json +4 -4
- package/src/codec-laws.ts +152 -0
- package/src/codec.ts +2 -2
- package/src/conform.ts +662 -70
- package/src/deref.ts +17 -3
- package/src/durable.ts +21 -9
- package/src/errors.ts +17 -1
- package/src/evaluate.ts +55 -21
- package/src/formula.ts +82 -22
- package/src/index.ts +16 -0
- package/src/instances.ts +61 -6
- package/src/meaning.ts +43 -10
- package/src/parts.ts +7 -4
- package/src/providers.ts +4 -1
- package/src/reference.ts +134 -9
- package/src/schedule.ts +286 -45
- package/src/static.ts +116 -23
- package/src/stdlib.ts +229 -6
- package/src/syntax.ts +430 -48
- package/src/types.ts +2 -1
- package/src/world.ts +106 -1
package/src/meaning.ts
CHANGED
|
@@ -58,12 +58,14 @@ export interface ScheduleEngine {
|
|
|
58
58
|
read(id: string): Result;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** What a new engine gets beside its machine
|
|
61
|
+
/** What a new engine gets beside its machine. These are the context of the host, the type of a bare root value and the `self` of each node. */
|
|
62
62
|
export interface ScheduleEngineStart {
|
|
63
63
|
/** The first context of the host, without the root `world`. */
|
|
64
64
|
readonly context: Context;
|
|
65
65
|
/** The type of a bare root value, when the options of the harness name one. */
|
|
66
66
|
readonly declaredType?: string;
|
|
67
|
+
/** The `self` of each node, when the options of the harness have `selves`. It has the form of the option of `schedule`. */
|
|
68
|
+
readonly selfOf?: (id: string, host: Context) => unknown;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/**
|
|
@@ -110,6 +112,12 @@ export interface AgreeScheduleOptions {
|
|
|
110
112
|
readonly values?: readonly unknown[];
|
|
111
113
|
/** The contexts of the host at the start of the runs, one after the other. */
|
|
112
114
|
readonly contexts?: readonly Context[];
|
|
115
|
+
/**
|
|
116
|
+
* The values of the `self` of a node (SCHED.NODE-AGREES). With them, the option `selfOf` of each engine gives each
|
|
117
|
+
* node its own `self`. Some nodes then share one document, and a change of data can give a node a new `self`. The
|
|
118
|
+
* value `undefined` keeps the `self` of the host. The default gives each node the context of the host.
|
|
119
|
+
*/
|
|
120
|
+
readonly selves?: readonly unknown[];
|
|
113
121
|
/** The number of runs. The default is 100. */
|
|
114
122
|
readonly count?: number;
|
|
115
123
|
/** The number of commands in one run. The default is 30. */
|
|
@@ -155,6 +163,10 @@ export interface ScheduleAgreementCounts {
|
|
|
155
163
|
readonly same: number;
|
|
156
164
|
/** The new machines. */
|
|
157
165
|
readonly machines: number;
|
|
166
|
+
/** The changes of data with a new `self` of a node, only with the option `selves`. */
|
|
167
|
+
readonly selves: number;
|
|
168
|
+
/** The documents that a node shares with another node, only with the option `selves`. */
|
|
169
|
+
readonly shared: number;
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
/** The outcome of `agreeSchedule`: what the harness reached, or the first disagreement. */
|
|
@@ -266,7 +278,7 @@ function shuffle<T>(items: readonly T[], rng: Random): T[] {
|
|
|
266
278
|
* This function evaluates each node of a graph from scratch, with the meaning of D31. Each node gets a new plan and
|
|
267
279
|
* one value. The algorithm of Tarjan over the demands finds the cycles. The function keeps nothing between calls.
|
|
268
280
|
*/
|
|
269
|
-
function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base: Context, declaredType: string | undefined): ReadonlyMap<string, Result> {
|
|
281
|
+
function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base: Context, declaredType: string | undefined, own: ReadonlyMap<string, unknown> | undefined): ReadonlyMap<string, Result> {
|
|
270
282
|
const declared = declaredType === undefined ? {} : { declaredType };
|
|
271
283
|
const values = new Map<string, Result>();
|
|
272
284
|
const index = new Map<string, number>();
|
|
@@ -298,7 +310,8 @@ function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base:
|
|
|
298
310
|
lowest.set(id, index.get(id) as number);
|
|
299
311
|
component.push(id);
|
|
300
312
|
evaluating.push(id);
|
|
301
|
-
|
|
313
|
+
const self = own?.get(id);
|
|
314
|
+
let result = prepare(machine, docs.get(id), declared).evaluate(self === undefined ? ctx : { ...ctx, self });
|
|
302
315
|
evaluating.pop();
|
|
303
316
|
if ((lowest.get(id) as number) < (index.get(id) as number)) result = cycleOf(id);
|
|
304
317
|
else if (component.splice(component.indexOf(id)).length > 1 || selfReads.has(id)) result = cycleOf(id);
|
|
@@ -340,7 +353,8 @@ function named(result: Result): string {
|
|
|
340
353
|
/**
|
|
341
354
|
* This function examines an engine of a host against an evaluation from scratch (SCHED.AGREES, R4 P4, D58). Each run
|
|
342
355
|
* makes a new engine and a random graph of named nodes from the document generator. Then it gives random commands:
|
|
343
|
-
* edits, new nodes, removed nodes, the same document again, new machines and changes of data.
|
|
356
|
+
* edits, new nodes, removed nodes, the same document again, new machines and changes of data. The option `selves`
|
|
357
|
+
* adds a `self` for each node and documents that nodes share (SCHED.NODE-AGREES).
|
|
344
358
|
*
|
|
345
359
|
* - Each read command evaluates the whole graph from scratch two times, with the meaning of D31.
|
|
346
360
|
* - It then demands each node in a random order, or one node, and compares each answer with `sameResult`.
|
|
@@ -366,6 +380,7 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
|
|
|
366
380
|
const hostNames = options.names ?? ["a", "b", "x", "n"];
|
|
367
381
|
const hostValues = options.values ?? standardValues();
|
|
368
382
|
const contexts = options.contexts ?? standardContexts();
|
|
383
|
+
const selves = options.selves;
|
|
369
384
|
const total = options.count ?? 100;
|
|
370
385
|
const steps = options.steps ?? 30;
|
|
371
386
|
const first = options.seed ?? 9_000_011;
|
|
@@ -376,7 +391,7 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
|
|
|
376
391
|
if (isClosure(a) || isClosure(b)) return isClosure(a) && isClosure(b) && docEquals(closureParams(a), closureParams(b));
|
|
377
392
|
return undefined;
|
|
378
393
|
};
|
|
379
|
-
const counts = { reads: 0, compared: 0, unrepeatable: 0, values: 0, cycles: 0, runs: 0, data: 0, empty: 0, edits: 0, additions: 0, removes: 0, same: 0, machines: 0 };
|
|
394
|
+
const counts = { reads: 0, compared: 0, unrepeatable: 0, values: 0, cycles: 0, runs: 0, data: 0, empty: 0, edits: 0, additions: 0, removes: 0, same: 0, machines: 0, selves: 0, shared: 0 };
|
|
380
395
|
const declared = declaredType === undefined ? {} : { declaredType };
|
|
381
396
|
for (let run = 0; run < total; run += 1) {
|
|
382
397
|
const seed = first + run;
|
|
@@ -385,7 +400,18 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
|
|
|
385
400
|
let at = 0;
|
|
386
401
|
let machine = machines[0] as Machine;
|
|
387
402
|
let made = 0;
|
|
403
|
+
const own = selves === undefined ? undefined : new Map<string, unknown>();
|
|
404
|
+
const pool: unknown[] = [];
|
|
388
405
|
const make = (): unknown => {
|
|
406
|
+
if (selves !== undefined && pool.length > 0 && rng.chance(0.4)) {
|
|
407
|
+
counts.shared += 1;
|
|
408
|
+
return rng.pick(pool);
|
|
409
|
+
}
|
|
410
|
+
const doc = generated();
|
|
411
|
+
pool.push(doc);
|
|
412
|
+
return doc;
|
|
413
|
+
};
|
|
414
|
+
const generated = (): unknown => {
|
|
389
415
|
if (rng.chance(wired)) return wire(rng, node);
|
|
390
416
|
const roll = rng.int(10);
|
|
391
417
|
if (roll === 0) return value(rng);
|
|
@@ -395,15 +421,16 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
|
|
|
395
421
|
return runs && roll === 9 ? { type: "log", value: [doc] } : doc;
|
|
396
422
|
};
|
|
397
423
|
const docs = new Map<string, unknown>(nodes.map((id) => [id, make()]));
|
|
424
|
+
if (own !== undefined && selves !== undefined) for (const id of nodes) own.set(id, rng.pick(selves));
|
|
398
425
|
const start = contexts[run % contexts.length] as Context;
|
|
399
426
|
let base: Context = { ...start, names: { ...start.names } };
|
|
400
|
-
const host = engine(machine, { ...declared, context: base });
|
|
427
|
+
const host = engine(machine, { ...declared, context: base, ...(own === undefined ? {} : { selfOf: (id: string) => own.get(id) }) });
|
|
401
428
|
for (const [id, doc] of docs) host.set(id, doc);
|
|
402
429
|
for (let step = 0; step < steps; step += 1) {
|
|
403
430
|
const what = rng.int(12);
|
|
404
431
|
if (what < 4) {
|
|
405
|
-
const fresh = fromScratch(machine, docs, base, declaredType);
|
|
406
|
-
const again = fromScratch(machine, docs, base, declaredType);
|
|
432
|
+
const fresh = fromScratch(machine, docs, base, declaredType, own);
|
|
433
|
+
const again = fromScratch(machine, docs, base, declaredType, own);
|
|
407
434
|
const ids = shuffle([...docs.keys()], rng);
|
|
408
435
|
// Half of the reads demand each node in a random order, and the other half demand one node.
|
|
409
436
|
for (const id of rng.chance(0.5) ? ids : ids.slice(0, 1)) {
|
|
@@ -428,9 +455,15 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
|
|
|
428
455
|
}
|
|
429
456
|
}
|
|
430
457
|
} else if (what < 6) {
|
|
431
|
-
|
|
458
|
+
if (own !== undefined && selves !== undefined && rng.chance(0.5)) {
|
|
459
|
+
// A new `self` of one node, with the same context object of the host.
|
|
460
|
+
own.set(rng.pick(nodes), rng.pick(selves));
|
|
461
|
+
counts.selves += 1;
|
|
462
|
+
} else {
|
|
463
|
+
base = { ...base, names: change(base, rng, hostNames, hostValues) };
|
|
464
|
+
counts.data += 1;
|
|
465
|
+
}
|
|
432
466
|
host.changeData(base);
|
|
433
|
-
counts.data += 1;
|
|
434
467
|
} else if (what === 6) {
|
|
435
468
|
// A change of data that changes no value: the same context object again.
|
|
436
469
|
host.changeData(base);
|
package/src/parts.ts
CHANGED
|
@@ -41,10 +41,13 @@ export type Split =
|
|
|
41
41
|
| { readonly ok: false; readonly reason: string; readonly at: readonly Key[] };
|
|
42
42
|
|
|
43
43
|
class Bad {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
) {
|
|
44
|
+
readonly reason: string;
|
|
45
|
+
readonly at: readonly Key[];
|
|
46
|
+
|
|
47
|
+
constructor(reason: string, at: readonly Key[]) {
|
|
48
|
+
this.reason = reason;
|
|
49
|
+
this.at = at;
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/** This function sets an own data property. Only the key `__proto__` needs `defineProperty`. */
|
package/src/providers.ts
CHANGED
|
@@ -150,6 +150,7 @@ function unionOf(types: readonly ValueType[]): ValueType {
|
|
|
150
150
|
|
|
151
151
|
/** The state of one reading of a schema: the root, the issues and the definitions that it read. */
|
|
152
152
|
class SchemaReader {
|
|
153
|
+
readonly root: unknown;
|
|
153
154
|
readonly issues: TypeIssue[] = [];
|
|
154
155
|
/** The type of each definition that a reference reached. Each reading of a definition is sound, thus the first stays. */
|
|
155
156
|
readonly defs = new Map<string, ValueType>();
|
|
@@ -158,7 +159,9 @@ class SchemaReader {
|
|
|
158
159
|
/** The number of schemas with an own `$id` around the current schema. A reference inside them has another base. */
|
|
159
160
|
inner = 0;
|
|
160
161
|
|
|
161
|
-
constructor(
|
|
162
|
+
constructor(root: unknown) {
|
|
163
|
+
this.root = root;
|
|
164
|
+
}
|
|
162
165
|
|
|
163
166
|
issue(at: readonly Key[], reason: string): void {
|
|
164
167
|
this.issues.push({ at, reason });
|
package/src/reference.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, docEquals, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import { ask } from "./ladder.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AttributeDeclaration,
|
|
5
|
+
type CapsRule,
|
|
6
|
+
type CircularRule,
|
|
7
|
+
type Kind,
|
|
8
|
+
type TreeContract,
|
|
9
|
+
GENERATION_LIMIT,
|
|
10
|
+
atArgsOf,
|
|
11
|
+
atRuleOf,
|
|
12
|
+
circularOf,
|
|
13
|
+
collectOf,
|
|
14
|
+
documentKey,
|
|
15
|
+
equationType,
|
|
16
|
+
lookupKind,
|
|
17
|
+
} from "./world.ts";
|
|
4
18
|
import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
|
|
5
19
|
import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, ROOTS, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
|
|
6
|
-
import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
|
|
20
|
+
import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
|
|
7
21
|
import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
8
22
|
import {
|
|
9
23
|
type AllOptions,
|
|
@@ -113,6 +127,8 @@ interface Env {
|
|
|
113
127
|
scope: Scope;
|
|
114
128
|
/** The count of the drives without a wait that are active. In a run, an effect under one does not reach the host (RUN.HELD). */
|
|
115
129
|
held: number;
|
|
130
|
+
/** The count of the applications of closures that run at this time: the clock of `call` (FORMULA.CALL-DEPTH, D125). */
|
|
131
|
+
applying: number;
|
|
116
132
|
/** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
|
|
117
133
|
readonly reading: Reading | undefined;
|
|
118
134
|
}
|
|
@@ -674,6 +690,7 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
|
|
|
674
690
|
verb: options.verb ?? "eval",
|
|
675
691
|
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
676
692
|
held: 0,
|
|
693
|
+
applying: 0,
|
|
677
694
|
reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
|
|
678
695
|
};
|
|
679
696
|
}
|
|
@@ -879,6 +896,55 @@ function collectIn(node: RefNode, name: string, declared: unknown): Result {
|
|
|
879
896
|
return visit(node) ?? acc;
|
|
880
897
|
}
|
|
881
898
|
|
|
899
|
+
/** TREE.HIGHER-DEPTH: the depth of each root of a generated document in the reference. */
|
|
900
|
+
const refGenerations = /* @__PURE__ */ new WeakMap<RefNode, number>();
|
|
901
|
+
|
|
902
|
+
/** TREE.HIGHER-DEPTH: the depth of the nearest root of a generated document at or above a node of the reference, or 0. */
|
|
903
|
+
function generationIn(node: RefNode): number {
|
|
904
|
+
for (let at: RefNode | undefined = node; at !== undefined; at = at.up) {
|
|
905
|
+
const depth = refGenerations.get(at);
|
|
906
|
+
if (depth !== undefined) return depth;
|
|
907
|
+
}
|
|
908
|
+
return 0;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* TREE.HIGHER: a higher-order attribute. The equation gives a document as data, and the document evaluates with the
|
|
913
|
+
* node as `self`. The reference compiles each document again, thus a structural key is not a part of the meaning.
|
|
914
|
+
*/
|
|
915
|
+
function higherOf(node: RefNode, name: string, term: Json): Result {
|
|
916
|
+
const value = equationOf(node, term, {});
|
|
917
|
+
if (!value.ok) return value;
|
|
918
|
+
const text = nameOfAttribute(node, name);
|
|
919
|
+
if (documentKey(value.value) === undefined) return fail("#VALUE!", `The equation of the higher-order attribute ${text} gives a value that is not JSON data.`);
|
|
920
|
+
const depth = generationIn(node) + 1;
|
|
921
|
+
if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The higher-order attribute ${text} is under more than ${GENERATION_LIMIT} generated documents.`);
|
|
922
|
+
const root = equationOf(node, value.value as Json, {});
|
|
923
|
+
if (!root.ok) return root;
|
|
924
|
+
const made = refNodeOf(root.value);
|
|
925
|
+
if (made === undefined) return fail("#VALUE!", `The document of the higher-order attribute ${text} gives no node of the tree.`);
|
|
926
|
+
refGenerations.set(made, depth);
|
|
927
|
+
return root;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** TREE.FORWARD: the last rung. The attribute `forwards` gives a node, and that node answers through its own ladder. */
|
|
931
|
+
function forwardOf(node: RefNode, name: string): Result | undefined {
|
|
932
|
+
const target: unknown = node.contract?.forwards;
|
|
933
|
+
if (target === undefined || target === name) return undefined;
|
|
934
|
+
const text = nameOfAttribute(node, name);
|
|
935
|
+
if (typeof target !== "string") return fail("#VALUE!", `The attribute ${text} has a contract with a bad field forwards. It must be a text.`);
|
|
936
|
+
const value = attributeOf(node, target);
|
|
937
|
+
if (value === undefined || !value.ok) return value;
|
|
938
|
+
const to = refNodeOf(value.value);
|
|
939
|
+
if (to === undefined) return fail("#VALUE!", `The attribute ${target} that ${text} forwards to gives no node of a tree.`);
|
|
940
|
+
if (!refGenerations.has(to)) {
|
|
941
|
+
const depth = generationIn(node) + 1;
|
|
942
|
+
if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The attribute ${text} forwards through more than ${GENERATION_LIMIT} generated documents.`);
|
|
943
|
+
refGenerations.set(to, depth);
|
|
944
|
+
}
|
|
945
|
+
return attributeOf(to, name);
|
|
946
|
+
}
|
|
947
|
+
|
|
882
948
|
/**
|
|
883
949
|
* TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
|
|
884
950
|
* answer, and the next rung then tries.
|
|
@@ -909,9 +975,11 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
|
|
|
909
975
|
const declared = termIn(node.contract?.attributes, name);
|
|
910
976
|
if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
|
|
911
977
|
if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
|
|
912
|
-
|
|
978
|
+
if (declared?.default !== undefined) return equationOf(node, declared.default, {});
|
|
979
|
+
return declared?.higher === undefined ? undefined : higherOf(node, name, declared.higher);
|
|
913
980
|
},
|
|
914
981
|
],
|
|
982
|
+
["forward", forwardOf],
|
|
915
983
|
];
|
|
916
984
|
|
|
917
985
|
/** TREE.LADDER: the answer of the first rung that answers, or `undefined`. */
|
|
@@ -1069,6 +1137,53 @@ function attributeOf(node: RefNode, name: string): Result | undefined {
|
|
|
1069
1137
|
return result;
|
|
1070
1138
|
}
|
|
1071
1139
|
|
|
1140
|
+
/** TREE.AT: one instance of an attribute with parameters of a node. Its result is `undefined` while it evaluates. */
|
|
1141
|
+
interface RefInstance {
|
|
1142
|
+
readonly attr: string;
|
|
1143
|
+
readonly names: unknown;
|
|
1144
|
+
result: Result | undefined;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** TREE.AT: the instances of each node of the reference. Two arguments are the same instance by DOC.EQUALS. */
|
|
1148
|
+
const refInstances = /* @__PURE__ */ new WeakMap<RefNode, RefInstance[]>();
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* TREE.AT: the instance of an attribute with parameters, or `undefined` when the kind has no entry in `tree.at`. The
|
|
1152
|
+
* equation sees the arguments as names. A demand of an instance in evaluation gives `#CYCLE!`, also in a fixed point.
|
|
1153
|
+
*/
|
|
1154
|
+
function instanceOf(node: RefNode, attr: string, args: unknown): Result | undefined {
|
|
1155
|
+
const declared = termIn(node.contract?.at, attr);
|
|
1156
|
+
if (declared === undefined) return undefined;
|
|
1157
|
+
const rule = atRuleOf(declared);
|
|
1158
|
+
if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, attr)} has a bad entry tree.at. ${rule.reason}`);
|
|
1159
|
+
const given = atArgsOf(rule.params, args);
|
|
1160
|
+
if (!given.ok) return fail("#VALUE!", `The step tree.at of ${nameOfAttribute(node, attr)} has bad arguments. ${given.reason}`);
|
|
1161
|
+
const name = `${attr} with ${given.key}`;
|
|
1162
|
+
const list = refInstances.get(node) ?? [];
|
|
1163
|
+
refInstances.set(node, list);
|
|
1164
|
+
const found = list.find((item) => item.attr === attr && docEquals(item.names, given.names));
|
|
1165
|
+
if (found?.result !== undefined) return found.result;
|
|
1166
|
+
if (found !== undefined) {
|
|
1167
|
+
const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
|
|
1168
|
+
const steps = inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
|
|
1169
|
+
return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
|
|
1170
|
+
}
|
|
1171
|
+
const item: RefInstance = { attr, names: given.names, result: undefined };
|
|
1172
|
+
list.push(item);
|
|
1173
|
+
const entry = { node, name, circular: false, tentative: false };
|
|
1174
|
+
inEvaluation.push(entry);
|
|
1175
|
+
let result: Result | undefined;
|
|
1176
|
+
try {
|
|
1177
|
+
result = equationOf(node, rule.equation, given.names);
|
|
1178
|
+
} finally {
|
|
1179
|
+
inEvaluation.pop();
|
|
1180
|
+
// A result that read an approximation is not kept (TREE.CIRCULAR).
|
|
1181
|
+
if (result === undefined || entry.tentative) list.splice(list.indexOf(item), 1);
|
|
1182
|
+
else item.result = result;
|
|
1183
|
+
}
|
|
1184
|
+
return result;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1072
1187
|
/** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
|
|
1073
1188
|
function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
|
|
1074
1189
|
const record: Record<string, unknown> = {};
|
|
@@ -1157,6 +1272,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1157
1272
|
};
|
|
1158
1273
|
node.thing = self;
|
|
1159
1274
|
refNodes.set(self, node);
|
|
1275
|
+
AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
|
|
1160
1276
|
if (nested) return ok(self);
|
|
1161
1277
|
try {
|
|
1162
1278
|
return jsonOfNode(node, self, []);
|
|
@@ -1285,13 +1401,22 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
|
|
|
1285
1401
|
}
|
|
1286
1402
|
const names = params as readonly string[];
|
|
1287
1403
|
const body = partNamed(parts, "body") as Part;
|
|
1404
|
+
// FORMULA.CALL-DEPTH: the evaluation that makes the closure counts its applications. A quarter of maxDepth is the limit.
|
|
1405
|
+
const clock = activeEnvs[activeEnvs.length - 1] ?? { applying: 0, maxDepth: 1000 };
|
|
1406
|
+
const limit = Math.ceil(clock.maxDepth / 4);
|
|
1288
1407
|
const closure = (...args: readonly unknown[]): unknown => {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1408
|
+
if (clock.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
|
|
1409
|
+
clock.applying += 1;
|
|
1410
|
+
try {
|
|
1411
|
+
const scope: Record<string, unknown> = {};
|
|
1412
|
+
for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
|
|
1413
|
+
names.forEach((name, index) => setField(scope, name, args[index]));
|
|
1414
|
+
const result = body.evaluate({ ...context, names: scope });
|
|
1415
|
+
if (!result.ok) throw new Failure(result.error);
|
|
1416
|
+
return result.value;
|
|
1417
|
+
} finally {
|
|
1418
|
+
clock.applying -= 1;
|
|
1419
|
+
}
|
|
1295
1420
|
};
|
|
1296
1421
|
Object.defineProperty(closure, CLOSURE, { value: { params: names } });
|
|
1297
1422
|
return ok(closure);
|