@mark1russell7/coeval 0.18.0 → 0.20.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 +4 -4
- package/build/index.d.ts +307 -20
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3362 -1537
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/codec.ts +30 -10
- package/src/durable.ts +17 -5
- package/src/facts.ts +55 -19
- package/src/formula.ts +45 -1
- package/src/index.ts +20 -2
- package/src/instances.ts +665 -0
- package/src/reads.ts +186 -2
- package/src/reference.ts +142 -12
- package/src/static.ts +363 -50
- package/src/stdlib.ts +480 -15
- package/src/types.ts +6 -2
- package/src/typing.ts +143 -3
- package/src/world.ts +108 -0
package/src/reads.ts
CHANGED
|
@@ -22,8 +22,41 @@ export type ReadsCheck =
|
|
|
22
22
|
readonly read: Read;
|
|
23
23
|
/** What the path of that read reaches in the new context. */
|
|
24
24
|
readonly now: Result;
|
|
25
|
+
/**
|
|
26
|
+
* `true` when the read reaches the same thing by `sameResult`, but the print of that thing changed
|
|
27
|
+
* (READS.IN-PLACE). Only the option `prints` gives this mark.
|
|
28
|
+
*/
|
|
29
|
+
readonly inPlace?: true;
|
|
25
30
|
};
|
|
26
31
|
|
|
32
|
+
/** The options of `printReads` (READS.IN-PLACE, D112). */
|
|
33
|
+
export interface PrintOptions {
|
|
34
|
+
/**
|
|
35
|
+
* The most entries in the print of one value. Each value, each field name and each mark counts as one entry. A
|
|
36
|
+
* larger value gets no print, and `verifyReads` then gives its read as changed. The default is 10000.
|
|
37
|
+
*/
|
|
38
|
+
readonly limit?: number;
|
|
39
|
+
/**
|
|
40
|
+
* The stamp of the host for an object, for example the version of a host thing. A stamp other than `undefined`
|
|
41
|
+
* takes the place of the fields of that object in the print. A stamp compares with `Object.is`.
|
|
42
|
+
*
|
|
43
|
+
* @param value - An object of the value of a read.
|
|
44
|
+
* @returns The stamp, or `undefined` for the walk of the fields.
|
|
45
|
+
*/
|
|
46
|
+
readonly stamp?: (value: object) => unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The prints of a list of reads, from `printReads`. `verifyReads` takes them with the option `prints`. The prints
|
|
51
|
+
* keep their options, thus the verification walks each value with the same rules.
|
|
52
|
+
*/
|
|
53
|
+
export interface ReadPrints {
|
|
54
|
+
/** The print of each read, at the index of the read. A read without an object in its value has no print. */
|
|
55
|
+
readonly prints: readonly (readonly unknown[] | undefined)[];
|
|
56
|
+
/** The options of the prints. */
|
|
57
|
+
readonly options: PrintOptions;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
/** The options of `verifyReads`. */
|
|
28
61
|
export interface VerifyOptions {
|
|
29
62
|
/**
|
|
@@ -35,11 +68,154 @@ export interface VerifyOptions {
|
|
|
35
68
|
* @returns `true` when the two things are the same, or `undefined` for the rules of `sameResult`.
|
|
36
69
|
*/
|
|
37
70
|
readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The prints of the reads, from `printReads` after the evaluation (READS.IN-PLACE). When the prints are given, a
|
|
73
|
+
* read that reaches the same thing also compares its print. Thus a change in place of a host value is a changed
|
|
74
|
+
* read. Without the prints, the verification does no work for them.
|
|
75
|
+
*/
|
|
76
|
+
readonly prints?: ReadPrints;
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
/** One answer serves each list whose reads are all the same. */
|
|
41
80
|
const SAME: ReadsCheck = /* @__PURE__ */ Object.freeze({ same: true });
|
|
42
81
|
|
|
82
|
+
/** The default limit of the entries in the print of one value. */
|
|
83
|
+
const PRINT_LIMIT = 10_000;
|
|
84
|
+
|
|
85
|
+
/** The marks of a print. Each mark is a private object, thus no value of a host is the same as a mark. */
|
|
86
|
+
const OBJECT = /* @__PURE__ */ Object.freeze({ mark: "object" });
|
|
87
|
+
const LIST = /* @__PURE__ */ Object.freeze({ mark: "list" });
|
|
88
|
+
const DATE = /* @__PURE__ */ Object.freeze({ mark: "date" });
|
|
89
|
+
const MAP = /* @__PURE__ */ Object.freeze({ mark: "map" });
|
|
90
|
+
const SET = /* @__PURE__ */ Object.freeze({ mark: "set" });
|
|
91
|
+
const SEEN = /* @__PURE__ */ Object.freeze({ mark: "seen" });
|
|
92
|
+
const STAMP = /* @__PURE__ */ Object.freeze({ mark: "stamp" });
|
|
93
|
+
|
|
94
|
+
/** The print of a value that the walk cannot finish: it is too large, or the host threw. It is not the same as any print. */
|
|
95
|
+
const NO_PRINT: readonly unknown[] = /* @__PURE__ */ Object.freeze([]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* This function gives the print of a value: a flat list of its parts, in the order of a walk (D112). Two prints are
|
|
99
|
+
* the same when they have the same length, and each entry is the same by `Object.is`.
|
|
100
|
+
*
|
|
101
|
+
* - An object starts with a mark and its prototype. A list adds its length, a date its time, and a map or a set
|
|
102
|
+
* its size and its entries. Then come the count of the own enumerable fields, each name and the kind of each field.
|
|
103
|
+
* - A field with a getter gives the getter function. The walk does not start host code of a field.
|
|
104
|
+
* - A primitive value and a function are entries. A function compares by its identity.
|
|
105
|
+
* - An object that the walk already visited gives the mark `SEEN` and its number. Thus a cycle ends.
|
|
106
|
+
* - A value with more than `limit` entries, or an exception of the host, gives `NO_PRINT`.
|
|
107
|
+
*
|
|
108
|
+
* @param value - The value of a read.
|
|
109
|
+
* @param options - The limit and the stamp of the host.
|
|
110
|
+
* @returns The print.
|
|
111
|
+
*/
|
|
112
|
+
function printOf(value: unknown, options: PrintOptions): readonly unknown[] {
|
|
113
|
+
const limit = options.limit ?? PRINT_LIMIT;
|
|
114
|
+
const stamp = options.stamp;
|
|
115
|
+
const out: unknown[] = [];
|
|
116
|
+
const ids = new Map<object, number>();
|
|
117
|
+
const stack: unknown[] = [value];
|
|
118
|
+
try {
|
|
119
|
+
while (stack.length > 0) {
|
|
120
|
+
if (out.length > limit) return NO_PRINT;
|
|
121
|
+
const x = stack.pop();
|
|
122
|
+
if (typeof x !== "object" || x === null) {
|
|
123
|
+
out.push(x);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const id = ids.get(x);
|
|
127
|
+
if (id !== undefined) {
|
|
128
|
+
out.push(SEEN, id);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
ids.set(x, ids.size);
|
|
132
|
+
const stamped = stamp?.(x);
|
|
133
|
+
if (stamped !== undefined) {
|
|
134
|
+
out.push(STAMP, stamped);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const children: unknown[] = [];
|
|
138
|
+
out.push(OBJECT, Object.getPrototypeOf(x));
|
|
139
|
+
if (Array.isArray(x)) out.push(LIST, x.length);
|
|
140
|
+
else if (x instanceof Date) out.push(DATE, Date.prototype.getTime.call(x));
|
|
141
|
+
else if (x instanceof Map) {
|
|
142
|
+
out.push(MAP, x.size);
|
|
143
|
+
if (out.length + 2 * x.size > limit) return NO_PRINT;
|
|
144
|
+
for (const [key, item] of Map.prototype.entries.call(x) as IterableIterator<[unknown, unknown]>) children.push(key, item);
|
|
145
|
+
} else if (x instanceof Set) {
|
|
146
|
+
out.push(SET, x.size);
|
|
147
|
+
if (out.length + x.size > limit) return NO_PRINT;
|
|
148
|
+
for (const item of Set.prototype.values.call(x) as IterableIterator<unknown>) children.push(item);
|
|
149
|
+
}
|
|
150
|
+
const keys = Object.keys(x);
|
|
151
|
+
if (out.length + 3 * keys.length > limit) return NO_PRINT;
|
|
152
|
+
out.push(keys.length);
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
const field = Object.getOwnPropertyDescriptor(x, key);
|
|
155
|
+
// A field that a proxy hides between `keys` and its descriptor reads as a field without a value.
|
|
156
|
+
const data = field === undefined || "value" in field;
|
|
157
|
+
out.push(key, data);
|
|
158
|
+
children.push(field === undefined ? undefined : "value" in field ? field.value : field.get);
|
|
159
|
+
}
|
|
160
|
+
// The stack gives the last entry first, thus the children go on it in the reverse order.
|
|
161
|
+
for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
return NO_PRINT;
|
|
165
|
+
}
|
|
166
|
+
return out.length > limit ? NO_PRINT : out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** This function tells if two prints are the same. `NO_PRINT` is not the same as any print, also not as itself. */
|
|
170
|
+
function samePrint(a: readonly unknown[], b: readonly unknown[]): boolean {
|
|
171
|
+
if (a === NO_PRINT || b === NO_PRINT || a.length !== b.length) return false;
|
|
172
|
+
for (let index = 0; index < a.length; index += 1) if (!Object.is(a[index], b[index])) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** This function gives the print of the value of a result, or `undefined` when the value holds no object. */
|
|
177
|
+
function printOfResult(result: Result, options: PrintOptions, cache: Map<object, readonly unknown[]>): readonly unknown[] | undefined {
|
|
178
|
+
if (!result.ok || typeof result.value !== "object" || result.value === null) return undefined;
|
|
179
|
+
const value = result.value;
|
|
180
|
+
let print = cache.get(value);
|
|
181
|
+
if (print === undefined) {
|
|
182
|
+
print = printOf(value, options);
|
|
183
|
+
cache.set(value, print);
|
|
184
|
+
}
|
|
185
|
+
return print;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* This function gives the prints of the values of a list of reads (READS.IN-PLACE, D112). A host starts it after
|
|
190
|
+
* the evaluation, and it gives the prints to `verifyReads` with the option `prints`.
|
|
191
|
+
*
|
|
192
|
+
* - The print of a value walks its lists, its plain records and the own enumerable fields of each other object. A
|
|
193
|
+
* date adds its time, and a map or a set adds its entries.
|
|
194
|
+
* - A read that is not free, a read that reached an error and a read with a primitive value get no print.
|
|
195
|
+
* - A cycle ends, because the walk visits each object one time.
|
|
196
|
+
* - A value with more entries than the limit gets no print. `verifyReads` then gives that read as changed.
|
|
197
|
+
*
|
|
198
|
+
* The walk does not see the state in a closure or a private field. It does not see a field with a symbol name, a
|
|
199
|
+
* field that is not enumerable or the value of a getter. A host gives a stamp for such an object, for example its version.
|
|
200
|
+
*
|
|
201
|
+
* @param reads - The reads of the evaluation, from the option `reads`.
|
|
202
|
+
* @param options - The limit and the stamp of the host.
|
|
203
|
+
* @returns The prints, for the option `prints` of `verifyReads`.
|
|
204
|
+
*/
|
|
205
|
+
export function printReads(reads: readonly Read[], options: PrintOptions = {}): ReadPrints {
|
|
206
|
+
const cache = new Map<object, readonly unknown[]>();
|
|
207
|
+
const prints = reads.map((read) => (read.free ? printOfResult(read.reached, options, cache) : undefined));
|
|
208
|
+
return { prints, options };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** This function tells if the print of a read changed, when the read has a print. */
|
|
212
|
+
function changedInPlace(prints: ReadPrints, index: number, now: Result, cache: Map<object, readonly unknown[]>): boolean {
|
|
213
|
+
const old = prints.prints[index];
|
|
214
|
+
if (old === undefined) return false;
|
|
215
|
+
const print = printOfResult(now, prints.options, cache);
|
|
216
|
+
return print === undefined || !samePrint(old, print);
|
|
217
|
+
}
|
|
218
|
+
|
|
43
219
|
/** This function tells if two lists of hops are the same: the same steps, rungs and things, in order. */
|
|
44
220
|
function sameHops(a: readonly Hop[], b: readonly Hop[] | undefined, equal: VerifyOptions["equal"]): boolean {
|
|
45
221
|
if (b === undefined || a.length !== b.length) return false;
|
|
@@ -78,18 +254,22 @@ function heldStop(root: Context, path: readonly unknown[]): Result | undefined {
|
|
|
78
254
|
* extension of a profile can read outside `resolve`. The schedule asks `factsOf` first.
|
|
79
255
|
*
|
|
80
256
|
* The values of the context must not change in place, as PLAN.CACHE says for the documents. A value that changed in
|
|
81
|
-
* place is the same object, thus
|
|
257
|
+
* place is the same object, thus `sameResult` cannot see the change. The option `prints` finds such a change
|
|
258
|
+
* (READS.IN-PLACE). The prints come from `printReads` after the evaluation, and the answer then has the mark `inPlace`.
|
|
82
259
|
*
|
|
83
260
|
* @param target - The machine of the evaluation, or its plan.
|
|
84
261
|
* @param reads - The reads of the evaluation, from the option `reads`.
|
|
85
262
|
* @param context - The new context.
|
|
86
|
-
* @param options - The rule of the host for two things.
|
|
263
|
+
* @param options - The rule of the host for two things, and the prints of the reads.
|
|
87
264
|
* @returns `{ same: true }` when each free read reaches the same thing, or else the first changed read.
|
|
88
265
|
*/
|
|
89
266
|
export function verifyReads(target: Machine | Plan, reads: readonly Read[], context: Context, options: VerifyOptions = {}): ReadsCheck {
|
|
90
267
|
const machine = "evaluators" in target ? target : target.machine;
|
|
91
268
|
const root = rootContext(machine, context);
|
|
92
269
|
const equal = options.equal;
|
|
270
|
+
const prints = options.prints;
|
|
271
|
+
// The prints of the new values, made only with the option `prints`. Two reads of one object share its print.
|
|
272
|
+
let printed: Map<object, readonly unknown[]> | undefined;
|
|
93
273
|
for (let index = 0; index < reads.length; index += 1) {
|
|
94
274
|
const read = reads[index] as Read;
|
|
95
275
|
if (!read.free) continue;
|
|
@@ -100,6 +280,7 @@ export function verifyReads(target: Machine | Plan, reads: readonly Read[], cont
|
|
|
100
280
|
const now = keptAgain(machine, root, read.path);
|
|
101
281
|
if (now === undefined) return { same: false, index, read, now: fail("#N/A", "The host does not know the value of the kept read yet.") };
|
|
102
282
|
if (!sameResult(read.reached, now, equal)) return { same: false, index, read, now };
|
|
283
|
+
if (prints !== undefined && changedInPlace(prints, index, now, (printed ??= new Map()))) return { same: false, index, read, now, inPlace: true };
|
|
103
284
|
continue;
|
|
104
285
|
}
|
|
105
286
|
const asked = read.hops !== undefined;
|
|
@@ -107,6 +288,9 @@ export function verifyReads(target: Machine | Plan, reads: readonly Read[], cont
|
|
|
107
288
|
if (!sameResult(read.reached, now.reached, equal) || (read.hops !== undefined && !sameHops(read.hops, now.hops, equal))) {
|
|
108
289
|
return { same: false, index, read, now: now.reached };
|
|
109
290
|
}
|
|
291
|
+
if (prints !== undefined && changedInPlace(prints, index, now.reached, (printed ??= new Map()))) {
|
|
292
|
+
return { same: false, index, read, now: now.reached, inPlace: true };
|
|
293
|
+
}
|
|
110
294
|
}
|
|
111
295
|
return SAME;
|
|
112
296
|
}
|
package/src/reference.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
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 { type AttributeDeclaration, type CapsRule, type Kind, type TreeContract, equationType, lookupKind } from "./world.ts";
|
|
3
|
+
import { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, circularOf, equationType, lookupKind } from "./world.ts";
|
|
4
4
|
import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, ROOTS, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
|
|
6
6
|
import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
|
|
@@ -776,7 +776,7 @@ interface RefNode {
|
|
|
776
776
|
const refNodes = /* @__PURE__ */ new WeakMap<object, RefNode>();
|
|
777
777
|
|
|
778
778
|
/** The attributes in evaluation in the reference, in the order of their demands (TREE.CYCLE). */
|
|
779
|
-
const inEvaluation: { readonly node: RefNode; readonly name: string }[] = [];
|
|
779
|
+
const inEvaluation: { readonly node: RefNode; readonly name: string; readonly circular: boolean; tentative: boolean }[] = [];
|
|
780
780
|
|
|
781
781
|
function nameOfAttribute(node: RefNode, name: string): string {
|
|
782
782
|
return `${name} of ${node.kind} at [${node.place.join(", ")}]`;
|
|
@@ -865,29 +865,159 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
|
|
|
865
865
|
],
|
|
866
866
|
];
|
|
867
867
|
|
|
868
|
-
/** TREE.
|
|
868
|
+
/** TREE.LADDER: the answer of the first rung that answers, or `undefined`. */
|
|
869
|
+
function ladderOf(node: RefNode, name: string): Result | undefined {
|
|
870
|
+
for (const [, rung] of REF_RUNGS) {
|
|
871
|
+
const result = rung(node, name);
|
|
872
|
+
if (result !== undefined) return result;
|
|
873
|
+
}
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** One approximation of a circular attribute in the reference, with its round (TREE.CIRCULAR). */
|
|
878
|
+
interface RefApproximation {
|
|
879
|
+
readonly result: Result | undefined;
|
|
880
|
+
readonly round: number;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* TREE.CIRCULAR: a fixed point in evaluation in the reference. The first circular attribute of a demand leads it, and
|
|
885
|
+
* each circular attribute that a demand reaches during its rounds joins it.
|
|
886
|
+
*/
|
|
887
|
+
interface RefPoint {
|
|
888
|
+
/** The index of the leader in `inEvaluation`. */
|
|
889
|
+
readonly depth: number;
|
|
890
|
+
round: number;
|
|
891
|
+
changed: boolean;
|
|
892
|
+
readonly values: Map<RefNode, Map<string, RefApproximation>>;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** The open fixed point of the reference, or `undefined`. */
|
|
896
|
+
let refPoint: RefPoint | undefined;
|
|
897
|
+
|
|
898
|
+
/** TREE.CIRCULAR-CHECK: the declaration `circular` of an attribute of a node, or `undefined`. */
|
|
899
|
+
function circularRuleOf(node: RefNode, name: string): CircularRule | undefined {
|
|
900
|
+
const declared = termIn(node.contract?.attributes, name)?.circular;
|
|
901
|
+
return declared === undefined ? undefined : circularOf(declared);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** TREE.CIRCULAR: two approximations are the same when two values are equal (DOC.EQUALS) or two errors have one code and reason. */
|
|
905
|
+
function sameApproximation(a: Result | undefined, b: Result | undefined): boolean {
|
|
906
|
+
if (a === undefined || b === undefined) return a === b;
|
|
907
|
+
if (a.ok && b.ok) return docEquals(a.value, b.value);
|
|
908
|
+
if (!a.ok && !b.ok) return a.error.code === b.error.code && a.error.reason === b.error.reason;
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** TREE.CIRCULAR: the attributes above the leader read an approximation, thus their results are not final. */
|
|
913
|
+
function readApproximation(point: RefPoint): void {
|
|
914
|
+
for (const entry of inEvaluation.slice(point.depth + 1)) entry.tentative = true;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function approximationsOf(point: RefPoint, node: RefNode): Map<string, RefApproximation> {
|
|
918
|
+
const known = point.values.get(node);
|
|
919
|
+
if (known !== undefined) return known;
|
|
920
|
+
const made = new Map<string, RefApproximation>();
|
|
921
|
+
point.values.set(node, made);
|
|
922
|
+
return made;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* TREE.CIRCULAR and TREE.CIRCULAR-LIMIT: the rounds of the leader of a fixed point. A round without a new value ends
|
|
927
|
+
* the rounds, and each circular attribute of that round keeps its value. An error ends the rounds at once. The last
|
|
928
|
+
* round of the limit gives `#CYCLE!` at the place of the node of the leader.
|
|
929
|
+
*/
|
|
930
|
+
function fixedPointOf(node: RefNode, name: string, rule: CircularRule & { readonly ok: true }): Result | undefined {
|
|
931
|
+
const point: RefPoint = { depth: inEvaluation.length - 1, round: 0, changed: false, values: new Map() };
|
|
932
|
+
refPoint = point;
|
|
933
|
+
try {
|
|
934
|
+
let before: Result | undefined = ok(rule.start);
|
|
935
|
+
for (;;) {
|
|
936
|
+
point.round += 1;
|
|
937
|
+
point.changed = false;
|
|
938
|
+
const result = ladderOf(node, name);
|
|
939
|
+
if (result !== undefined && !result.ok) return result;
|
|
940
|
+
if (!sameApproximation(before, result)) point.changed = true;
|
|
941
|
+
before = result;
|
|
942
|
+
approximationsOf(point, node).set(name, { result, round: point.round });
|
|
943
|
+
if (!point.changed) {
|
|
944
|
+
for (const [member, values] of point.values) {
|
|
945
|
+
for (const [key, value] of values) if (value.round === point.round && value.result !== undefined) member.values.set(key, value.result);
|
|
946
|
+
}
|
|
947
|
+
return result;
|
|
948
|
+
}
|
|
949
|
+
if (point.round >= rule.limit) {
|
|
950
|
+
return fail("#CYCLE!", `The cycle of the circular attribute ${nameOfAttribute(node, name)} does not become stable within its limit of ${rule.limit} ${rule.limit === 1 ? "round" : "rounds"}.`, {
|
|
951
|
+
place: node.place,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} finally {
|
|
956
|
+
refPoint = undefined;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** TREE.CIRCULAR: a circular attribute in the open fixed point of another leader evaluates one time in each round. */
|
|
961
|
+
function roundOf(point: RefPoint, node: RefNode, name: string, rule: CircularRule & { readonly ok: true }): Result | undefined {
|
|
962
|
+
let value = point.values.get(node)?.get(name);
|
|
963
|
+
if (value === undefined || value.round !== point.round) {
|
|
964
|
+
node.pending.add(name);
|
|
965
|
+
inEvaluation.push({ node, name, circular: true, tentative: false });
|
|
966
|
+
let result: Result | undefined;
|
|
967
|
+
try {
|
|
968
|
+
result = ladderOf(node, name);
|
|
969
|
+
} finally {
|
|
970
|
+
node.pending.delete(name);
|
|
971
|
+
inEvaluation.pop();
|
|
972
|
+
}
|
|
973
|
+
if (!sameApproximation(value === undefined ? ok(rule.start) : value.result, result)) point.changed = true;
|
|
974
|
+
value = { result, round: point.round };
|
|
975
|
+
approximationsOf(point, node).set(name, value);
|
|
976
|
+
}
|
|
977
|
+
readApproximation(point);
|
|
978
|
+
return value.result;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* TREE.ONCE, TREE.MEMO, TREE.CYCLE and TREE.CIRCULAR: the result of one attribute, or `undefined` when no rung of its
|
|
983
|
+
* ladder answers. A result that read an approximation of an open fixed point is not kept.
|
|
984
|
+
*/
|
|
869
985
|
function attributeOf(node: RefNode, name: string): Result | undefined {
|
|
870
986
|
const done = node.values.get(name);
|
|
871
987
|
if (done !== undefined) return done;
|
|
872
988
|
if (node.pending.has(name)) {
|
|
873
989
|
const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
|
|
990
|
+
const point = refPoint;
|
|
991
|
+
if (point !== undefined && from >= point.depth && inEvaluation.slice(from).every((entry) => entry.circular)) {
|
|
992
|
+
readApproximation(point);
|
|
993
|
+
const kept = point.values.get(node)?.get(name);
|
|
994
|
+
if (kept !== undefined) return kept.result;
|
|
995
|
+
const rule = circularRuleOf(node, name);
|
|
996
|
+
return ok(rule?.ok === true ? rule.start : null);
|
|
997
|
+
}
|
|
874
998
|
const steps = from < 0 ? [nameOfAttribute(node, name)] : inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
|
|
875
999
|
return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
|
|
876
1000
|
}
|
|
1001
|
+
const rule = circularRuleOf(node, name);
|
|
1002
|
+
if (rule !== undefined && !rule.ok) {
|
|
1003
|
+
const bad = fail("#VALUE!", `The attribute ${nameOfAttribute(node, name)} has a bad declaration circular. ${rule.reason}`);
|
|
1004
|
+
node.values.set(name, bad);
|
|
1005
|
+
return bad;
|
|
1006
|
+
}
|
|
1007
|
+
const point = refPoint;
|
|
1008
|
+
if (point !== undefined && rule !== undefined) return roundOf(point, node, name, rule);
|
|
1009
|
+
const entry = { node, name, circular: rule !== undefined, tentative: false };
|
|
877
1010
|
node.pending.add(name);
|
|
878
|
-
inEvaluation.push(
|
|
1011
|
+
inEvaluation.push(entry);
|
|
1012
|
+
let result: Result | undefined;
|
|
879
1013
|
try {
|
|
880
|
-
|
|
881
|
-
const result = rung(node, name);
|
|
882
|
-
if (result === undefined) continue;
|
|
883
|
-
node.values.set(name, result);
|
|
884
|
-
return result;
|
|
885
|
-
}
|
|
886
|
-
return undefined;
|
|
1014
|
+
result = rule === undefined ? ladderOf(node, name) : fixedPointOf(node, name, rule);
|
|
887
1015
|
} finally {
|
|
888
1016
|
node.pending.delete(name);
|
|
889
1017
|
inEvaluation.pop();
|
|
890
1018
|
}
|
|
1019
|
+
if (!entry.tentative && result !== undefined) node.values.set(name, result);
|
|
1020
|
+
return result;
|
|
891
1021
|
}
|
|
892
1022
|
|
|
893
1023
|
/** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
|