@mark1russell7/coeval 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +379 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1017 -98
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/conform.ts +13 -4
- package/src/deref.ts +32 -2
- package/src/effects.ts +4 -2
- package/src/evaluate.ts +2 -0
- package/src/facts.ts +3 -2
- package/src/formula.ts +53 -10
- package/src/index.ts +10 -0
- package/src/instances.ts +94 -5
- package/src/parts.ts +30 -1
- package/src/providers.ts +209 -29
- package/src/reference.ts +119 -9
- package/src/static.ts +144 -23
- package/src/stdlib.ts +189 -2
- package/src/types.ts +12 -4
- package/src/world.ts +135 -1
package/src/providers.ts
CHANGED
|
@@ -33,13 +33,106 @@ const NOTES: ReadonlySet<string> = /* @__PURE__ */ new Set([
|
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
35
|
/** The keywords that the reading reads. Each other keyword gives an issue and tests nothing. */
|
|
36
|
-
const READ: ReadonlySet<string> = /* @__PURE__ */ new Set([
|
|
36
|
+
const READ: ReadonlySet<string> = /* @__PURE__ */ new Set([
|
|
37
|
+
"type",
|
|
38
|
+
"properties",
|
|
39
|
+
"required",
|
|
40
|
+
"additionalProperties",
|
|
41
|
+
"items",
|
|
42
|
+
"enum",
|
|
43
|
+
"const",
|
|
44
|
+
"anyOf",
|
|
45
|
+
"oneOf",
|
|
46
|
+
"allOf",
|
|
47
|
+
"$ref",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The keywords that can permit more values than the other keywords permit. A dynamic reference can reach a schema
|
|
52
|
+
* outside this schema, and `nullable` of OpenAPI 3.0 permits `null`. Thus a schema with one of them reads as `unknown`.
|
|
53
|
+
*/
|
|
54
|
+
const WIDENING: ReadonlySet<string> = /* @__PURE__ */ new Set(["$dynamicRef", "$recursiveRef", "nullable"]);
|
|
55
|
+
|
|
56
|
+
/** The keywords of an object without the keyword `type`. They test only the values that are records. */
|
|
57
|
+
const OBJECT_KEYS: readonly string[] = ["properties", "required", "additionalProperties"];
|
|
58
|
+
|
|
59
|
+
/** The form of a local reference: a definition in `$defs` or `definitions` of the root schema. */
|
|
60
|
+
const LOCAL_REF = /^#\/(\$defs|definitions)\/([^/]*)$/;
|
|
61
|
+
|
|
62
|
+
/** The most pairs of members that the meet of two unions builds. A larger meet keeps the first type, which is sound. */
|
|
63
|
+
const MAX_MEET_PAIRS = 64;
|
|
64
|
+
|
|
65
|
+
/** The keywords of one JSON type without the keyword `type`: they test only the records or only the lists. */
|
|
66
|
+
interface Condition {
|
|
67
|
+
readonly on: "record" | "list";
|
|
68
|
+
readonly type: ValueType;
|
|
69
|
+
readonly at: readonly Key[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A record type of the reading. */
|
|
73
|
+
type RecordType = Extract<ValueType, { readonly record: unknown }>;
|
|
74
|
+
|
|
75
|
+
/** This function tells if a field of a record type can be absent. */
|
|
76
|
+
function isOptional(type: RecordType, key: string): boolean {
|
|
77
|
+
return type.optional?.includes(key) === true;
|
|
78
|
+
}
|
|
37
79
|
|
|
38
80
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
81
|
+
* This function gives the meet of two types of a reading: a type that holds each value of both types. A schema is
|
|
82
|
+
* the conjunction of its keywords, thus its type is the meet of their types. The meet is exact for the forms that a
|
|
83
|
+
* reading makes: base types, lists, records, unions and `one`. A meet of two different forms is `never`.
|
|
41
84
|
*/
|
|
42
|
-
|
|
85
|
+
function meet(a: ValueType, b: ValueType, depth: number): ValueType {
|
|
86
|
+
if (depth > MAX_SCHEMA_DEPTH) return a;
|
|
87
|
+
if (isUnknown(a, undefined)) return b;
|
|
88
|
+
if (isUnknown(b, undefined)) return a;
|
|
89
|
+
if (a === "never" || b === "never") return "never";
|
|
90
|
+
if (typeof a !== "string" && "one" in a) return { one: a.one.filter((value) => member(value, b)) };
|
|
91
|
+
if (typeof b !== "string" && "one" in b) return { one: b.one.filter((value) => member(value, a)) };
|
|
92
|
+
if (typeof b !== "string" && "union" in b && !(typeof a !== "string" && "union" in a)) return meet(b, a, depth);
|
|
93
|
+
if (typeof a !== "string" && "union" in a) {
|
|
94
|
+
const others = typeof b !== "string" && "union" in b ? b.union : [b];
|
|
95
|
+
if (a.union.length * others.length > MAX_MEET_PAIRS) return a;
|
|
96
|
+
return unionOf(a.union.flatMap((item) => others.map((other) => meet(item, other, depth + 1))));
|
|
97
|
+
}
|
|
98
|
+
if (typeof a === "string" || typeof b === "string") return a === b ? a : "never";
|
|
99
|
+
if ("list" in a && "list" in b) return { list: meet(a.list, b.list, depth + 1) };
|
|
100
|
+
if ("record" in a && "record" in b) return meetRecords(a, b, depth + 1);
|
|
101
|
+
// A list and a record have no common value. Each other pair keeps the first type, which is sound.
|
|
102
|
+
return ("list" in a || "record" in a) && ("list" in b || "record" in b) ? "never" : a;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* This function gives the meet of two record types. A field of both records gets the meet of its types, and it is
|
|
107
|
+
* optional only when both records permit its absence. A closed record has no other field, thus a field of only the
|
|
108
|
+
* other record is absent. When the other record needs that field, no value fits both records.
|
|
109
|
+
*/
|
|
110
|
+
function meetRecords(a: RecordType, b: RecordType, depth: number): ValueType {
|
|
111
|
+
const fields: Record<string, ValueType> = {};
|
|
112
|
+
const optional: string[] = [];
|
|
113
|
+
const keys = [...Object.keys(a.record), ...Object.keys(b.record).filter((key) => !Object.hasOwn(a.record, key))];
|
|
114
|
+
for (const key of keys) {
|
|
115
|
+
const inA = Object.hasOwn(a.record, key);
|
|
116
|
+
const inB = Object.hasOwn(b.record, key);
|
|
117
|
+
if ((!inA && a.closed === true) || (!inB && b.closed === true)) {
|
|
118
|
+
if ((inA && !isOptional(a, key)) || (inB && !isOptional(b, key))) return "never";
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const type = inA && inB ? meet(a.record[key] as ValueType, b.record[key] as ValueType, depth) : ((inA ? a.record[key] : b.record[key]) as ValueType);
|
|
122
|
+
setOwn(fields, key, type);
|
|
123
|
+
if ((!inA || isOptional(a, key)) && (!inB || isOptional(b, key))) optional.push(key);
|
|
124
|
+
}
|
|
125
|
+
const closed = a.closed === true || b.closed === true;
|
|
126
|
+
return { record: fields, ...(optional.length === 0 ? {} : { optional }), ...(closed ? { closed: true } : {}) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** This function applies a condition to a type: each member of the JSON type of the condition meets its type. */
|
|
130
|
+
function narrow(type: ValueType, condition: Condition): ValueType {
|
|
131
|
+
if (typeof type === "string") return type;
|
|
132
|
+
if ("union" in type) return unionOf(type.union.map((item) => narrow(item, condition)));
|
|
133
|
+
if ("one" in type) return { one: type.one.filter((value) => !(condition.on === "list" ? Array.isArray(value) : isRecord(value)) || member(value, condition.type)) };
|
|
134
|
+
return condition.on in type ? meet(type, condition.type, 0) : type;
|
|
135
|
+
}
|
|
43
136
|
|
|
44
137
|
/** The base type of each name of the keyword `type`. */
|
|
45
138
|
const BASES: Readonly<Record<string, ValueType>> = { null: "null", boolean: "bool", number: "number", integer: "number", string: "text" };
|
|
@@ -55,25 +148,55 @@ function unionOf(types: readonly ValueType[]): ValueType {
|
|
|
55
148
|
return types.reduce<ValueType>((all, type) => join(all, type), "never");
|
|
56
149
|
}
|
|
57
150
|
|
|
58
|
-
/** The state of one reading of a schema: the issues that it
|
|
151
|
+
/** The state of one reading of a schema: the root, the issues and the definitions that it read. */
|
|
59
152
|
class SchemaReader {
|
|
60
153
|
readonly issues: TypeIssue[] = [];
|
|
154
|
+
/** The type of each definition that a reference reached. Each reading of a definition is sound, thus the first stays. */
|
|
155
|
+
readonly defs = new Map<string, ValueType>();
|
|
156
|
+
/** The definitions that the reading has not finished. A reference to one of them is a cycle. */
|
|
157
|
+
readonly open = new Set<string>();
|
|
158
|
+
/** The number of schemas with an own `$id` around the current schema. A reference inside them has another base. */
|
|
159
|
+
inner = 0;
|
|
160
|
+
|
|
161
|
+
constructor(readonly root: unknown) {}
|
|
61
162
|
|
|
62
163
|
issue(at: readonly Key[], reason: string): void {
|
|
63
164
|
this.issues.push({ at, reason });
|
|
64
165
|
}
|
|
65
166
|
|
|
66
|
-
/** This method reads one schema. */
|
|
167
|
+
/** This method reads one schema: the meet of the types of its keywords, then its conditions. */
|
|
67
168
|
read(schema: unknown, at: readonly Key[], depth: number): ValueType {
|
|
169
|
+
const types: ValueType[] = [];
|
|
170
|
+
const conditions: Condition[] = [];
|
|
171
|
+
this.parts(schema, at, depth, types, conditions);
|
|
172
|
+
let type = types.reduce<ValueType>((all, item) => meet(all, item, 0), "unknown");
|
|
173
|
+
for (const condition of conditions) {
|
|
174
|
+
if (isUnknown(type, undefined)) {
|
|
175
|
+
this.issue(condition.at, "The schema has no keyword type. Thus a value of each JSON type fits it, and the type reads it as unknown.");
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
type = narrow(type, condition);
|
|
179
|
+
}
|
|
180
|
+
return type;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* This method adds the type of each keyword of a schema to `types`, and its conditions to `conditions`. Each type
|
|
185
|
+
* holds each value that the schema accepts. The parts of `allOf` add their keywords too.
|
|
186
|
+
*/
|
|
187
|
+
parts(schema: unknown, at: readonly Key[], depth: number, types: ValueType[], conditions: Condition[]): void {
|
|
68
188
|
if (depth > MAX_SCHEMA_DEPTH) {
|
|
69
189
|
this.issue(at, `The schema is deeper than ${MAX_SCHEMA_DEPTH} levels. The type reads it as unknown.`);
|
|
70
|
-
return
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (schema === true) return;
|
|
193
|
+
if (schema === false) {
|
|
194
|
+
types.push("never");
|
|
195
|
+
return;
|
|
71
196
|
}
|
|
72
|
-
if (schema === true) return "unknown";
|
|
73
|
-
if (schema === false) return "never";
|
|
74
197
|
if (!isRecord(schema)) {
|
|
75
198
|
this.issue(at, "A schema is a record or a truth value. The type reads this value as unknown.");
|
|
76
|
-
return
|
|
199
|
+
return;
|
|
77
200
|
}
|
|
78
201
|
let widens = false;
|
|
79
202
|
for (const key of Object.keys(schema)) {
|
|
@@ -85,31 +208,85 @@ class SchemaReader {
|
|
|
85
208
|
this.issue([...at, key], `The type does not read the keyword ${key}. It does not test what the keyword tests.`);
|
|
86
209
|
}
|
|
87
210
|
}
|
|
88
|
-
if (widens) return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
211
|
+
if (widens) return;
|
|
212
|
+
const nested = schema !== this.root && Object.hasOwn(schema, "$id");
|
|
213
|
+
if (nested) this.inner += 1;
|
|
214
|
+
try {
|
|
215
|
+
this.keywords(schema, at, depth, types, conditions);
|
|
216
|
+
} finally {
|
|
217
|
+
if (nested) this.inner -= 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** This method reads the keywords of a schema that does not permit more values (WIDENING). */
|
|
222
|
+
keywords(schema: Readonly<Record<string, unknown>>, at: readonly Key[], depth: number, types: ValueType[], conditions: Condition[]): void {
|
|
223
|
+
if (Object.hasOwn(schema, "$ref")) {
|
|
224
|
+
// Older drafts ignore each keyword beside $ref. The target alone holds each value in each draft.
|
|
225
|
+
for (const key of Object.keys(schema)) if (READ.has(key) && key !== "$ref") this.issue([...at, key], `The type reads only the target of $ref. It does not test the keyword ${key} beside it.`);
|
|
226
|
+
const type = this.target(schema["$ref"], [...at, "$ref"], depth);
|
|
227
|
+
if (type !== undefined) types.push(type);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (Object.hasOwn(schema, "type")) types.push(this.byType(schema, at, depth));
|
|
231
|
+
else {
|
|
232
|
+
if (OBJECT_KEYS.some((key) => Object.hasOwn(schema, key))) conditions.push({ on: "record", type: this.object(schema, at, depth), at });
|
|
233
|
+
if (Object.hasOwn(schema, "items")) conditions.push({ on: "list", type: this.array(schema, at, depth), at });
|
|
94
234
|
}
|
|
95
|
-
for (const key of ["anyOf", "oneOf"]) {
|
|
235
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
96
236
|
if (!Object.hasOwn(schema, key)) continue;
|
|
97
237
|
const items = schema[key];
|
|
98
238
|
if (!Array.isArray(items)) {
|
|
99
239
|
this.issue([...at, key], `The value of ${key} is a list of schemas.`);
|
|
100
240
|
continue;
|
|
101
241
|
}
|
|
242
|
+
// Each part of allOf holds each value of the schema, thus its keywords join the keywords of the schema.
|
|
243
|
+
if (key === "allOf") for (const [index, item] of items.entries()) this.parts(item, [...at, key, index], depth + 1, types, conditions);
|
|
102
244
|
// A union holds each value of oneOf too. It does not test that only one schema fits.
|
|
103
|
-
|
|
245
|
+
else types.push(unionOf(items.map((item, index) => this.read(item, [...at, key, index], depth + 1))));
|
|
104
246
|
}
|
|
105
|
-
// A finite set of values meets each other keyword exactly: the type keeps each value that each side holds.
|
|
106
247
|
const values = this.values(schema, at);
|
|
107
|
-
if (values !== undefined)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
248
|
+
if (values !== undefined) types.push({ one: values });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** This method reads the target of a reference: a definition of the root schema, or `undefined` for each value. */
|
|
252
|
+
target(ref: unknown, at: readonly Key[], depth: number): ValueType | undefined {
|
|
253
|
+
const match = typeof ref === "string" ? LOCAL_REF.exec(ref) : null;
|
|
254
|
+
let name: string | undefined;
|
|
255
|
+
try {
|
|
256
|
+
// The name is a step of a JSON Pointer in a URI fragment.
|
|
257
|
+
name = match === null ? undefined : decodeURIComponent(match[2] ?? "").replace(/~1/g, "/").replace(/~0/g, "~");
|
|
258
|
+
} catch {
|
|
259
|
+
name = undefined;
|
|
260
|
+
}
|
|
261
|
+
if (match === null || name === undefined) {
|
|
262
|
+
this.issue(at, "The type reads only a $ref to #/$defs/<name> or #/definitions/<name> of the same schema. Thus it reads the schema as unknown.");
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
if (this.inner > 0) {
|
|
266
|
+
this.issue(at, "A $ref inside a schema with its own $id has another base. Thus the type reads the schema as unknown.");
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
const group = match[1] as string;
|
|
270
|
+
const defs = isRecord(this.root) ? this.root[group] : undefined;
|
|
271
|
+
if (!isRecord(defs) || !Object.hasOwn(defs, name)) {
|
|
272
|
+
this.issue(at, `The schema has no definition ${String(ref)}. Thus the type reads the reference as unknown.`);
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const key = `${group}/${name}`;
|
|
276
|
+
const known = this.defs.get(key);
|
|
277
|
+
if (known !== undefined) return known;
|
|
278
|
+
if (this.open.has(key)) {
|
|
279
|
+
this.issue(at, `The reference ${String(ref)} is in a cycle. The type reads this reference as unknown.`);
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
this.open.add(key);
|
|
283
|
+
try {
|
|
284
|
+
const type = this.read(defs[name], [group, name], depth + 1);
|
|
285
|
+
this.defs.set(key, type);
|
|
286
|
+
return type;
|
|
287
|
+
} finally {
|
|
288
|
+
this.open.delete(key);
|
|
289
|
+
}
|
|
113
290
|
}
|
|
114
291
|
|
|
115
292
|
/** This method reads `enum` and `const`: the values that the schema permits, or `undefined` without them. */
|
|
@@ -196,17 +373,20 @@ class SchemaReader {
|
|
|
196
373
|
* - `properties`, `required` and `additionalProperties` give a record. `false` closes it, and a property that is not
|
|
197
374
|
* in `required` is optional.
|
|
198
375
|
* - `items` gives a list. `enum` and `const` give `one`. `anyOf` and `oneOf` give a union.
|
|
376
|
+
* - `$ref` reads a definition in `$defs` or `definitions` of the same schema (TYPE.SCHEMA-REF). Another reference, a
|
|
377
|
+
* missing definition and a reference in a cycle read as `unknown`.
|
|
378
|
+
* - `allOf` and the other keywords of a schema give the meet of their types (TYPE.SCHEMA-ALLOF). Records merge their
|
|
379
|
+
* fields, and a closed part keeps the merge closed.
|
|
199
380
|
*
|
|
200
381
|
* The type holds each value that the schema accepts. Each keyword that the function does not read tests nothing, as
|
|
201
|
-
* `unknown`, and the issues name it. Examples are `minimum
|
|
202
|
-
* reads as `unknown`, because these keywords can permit more values.
|
|
203
|
-
* and `oneOf` gives the type of the first, and an issue for each other. The function does not throw.
|
|
382
|
+
* `unknown`, and the issues name it. Examples are `minimum` and `pattern`. A schema with `nullable` or `$dynamicRef`
|
|
383
|
+
* reads as `unknown`, because these keywords can permit more values. The function does not throw.
|
|
204
384
|
*
|
|
205
385
|
* @param schema - The schema, as JSON.
|
|
206
386
|
* @returns The type, and the problems of the reading with their keys in the schema.
|
|
207
387
|
*/
|
|
208
388
|
export function typeFromSchema(schema: unknown): TypeReading {
|
|
209
|
-
const reader = new SchemaReader();
|
|
389
|
+
const reader = new SchemaReader(schema);
|
|
210
390
|
try {
|
|
211
391
|
return { type: reader.read(schema, [], 0), issues: reader.issues };
|
|
212
392
|
} catch (error) {
|
package/src/reference.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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 { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, circularOf, equationType, lookupKind } from "./world.ts";
|
|
3
|
+
import { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, atArgsOf, atRuleOf, circularOf, collectOf, 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
|
-
import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
|
|
6
|
+
import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
|
|
7
7
|
import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
8
8
|
import {
|
|
9
9
|
type AllOptions,
|
|
@@ -113,6 +113,8 @@ interface Env {
|
|
|
113
113
|
scope: Scope;
|
|
114
114
|
/** 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
115
|
held: number;
|
|
116
|
+
/** The count of the applications of closures that run at this time: the clock of `call` (FORMULA.CALL-DEPTH, D125). */
|
|
117
|
+
applying: number;
|
|
116
118
|
/** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
|
|
117
119
|
readonly reading: Reading | undefined;
|
|
118
120
|
}
|
|
@@ -674,6 +676,7 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
|
|
|
674
676
|
verb: options.verb ?? "eval",
|
|
675
677
|
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
676
678
|
held: 0,
|
|
679
|
+
applying: 0,
|
|
677
680
|
reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
|
|
678
681
|
};
|
|
679
682
|
}
|
|
@@ -754,6 +757,8 @@ interface RefTree {
|
|
|
754
757
|
interface RefNode {
|
|
755
758
|
readonly tree: RefTree;
|
|
756
759
|
readonly kind: string;
|
|
760
|
+
/** The binding name from the header of the document of the node (TREE.NAMED). */
|
|
761
|
+
readonly name: string | undefined;
|
|
757
762
|
readonly place: readonly Key[];
|
|
758
763
|
readonly parts: readonly Part[];
|
|
759
764
|
readonly ctx: Context;
|
|
@@ -831,6 +836,52 @@ function foldOf(node: RefNode, name: string, fold: NonNullable<AttributeDeclarat
|
|
|
831
836
|
return acc;
|
|
832
837
|
}
|
|
833
838
|
|
|
839
|
+
/**
|
|
840
|
+
* TREE.COLLECT and TREE.NAMED: the node, then each node of this tree in each list `children`, in the order of the
|
|
841
|
+
* lists. Each node counts one time. The registry `names` maps each binding name to its node.
|
|
842
|
+
*/
|
|
843
|
+
function collectIn(node: RefNode, name: string, declared: unknown): Result {
|
|
844
|
+
const rule = collectOf(declared);
|
|
845
|
+
if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, name)} has a bad declaration collect. ${rule.reason}`);
|
|
846
|
+
const registry: Record<string, unknown> = {};
|
|
847
|
+
const first = new Map<string, RefNode>();
|
|
848
|
+
let acc: Result = rule.names ? ok(registry) : equationOf(node, rule.unit, {});
|
|
849
|
+
if (!acc.ok) return acc;
|
|
850
|
+
const done: RefNode[] = [];
|
|
851
|
+
// The contribution of a node comes before the nodes of its list children. A failure ends the collection.
|
|
852
|
+
const visit = (at: RefNode): Result | undefined => {
|
|
853
|
+
if (done.includes(at)) return undefined;
|
|
854
|
+
done.push(at);
|
|
855
|
+
if (rule.names && at.name !== undefined) {
|
|
856
|
+
const earlier = first.get(at.name);
|
|
857
|
+
if (earlier !== undefined) {
|
|
858
|
+
return fail("#VALUE!", `Two nodes have the name ${at.name} in the registry ${nameOfAttribute(node, name)}: ${earlier.kind} at [${earlier.place.join(", ")}] and ${at.kind} at [${at.place.join(", ")}].`);
|
|
859
|
+
}
|
|
860
|
+
first.set(at.name, at);
|
|
861
|
+
Object.defineProperty(registry, at.name, { value: at.thing, enumerable: true, writable: true, configurable: true });
|
|
862
|
+
}
|
|
863
|
+
const term = rule.names ? undefined : termIn(at.contract?.contributes, name);
|
|
864
|
+
if (!rule.names && term !== undefined) {
|
|
865
|
+
const item = equationOf(at, term, {});
|
|
866
|
+
if (!item.ok) return item;
|
|
867
|
+
acc = equationOf(node, rule.combine, { acc: acc.ok ? acc.value : null, item: item.value });
|
|
868
|
+
if (!acc.ok) return acc;
|
|
869
|
+
}
|
|
870
|
+
const children = attributeOf(at, "children");
|
|
871
|
+
if (children === undefined) return undefined;
|
|
872
|
+
if (!children.ok) return children;
|
|
873
|
+
if (!Array.isArray(children.value)) return fail("#VALUE!", `The collection of ${name} needs a list in children of ${at.kind} at [${at.place.join(", ")}].`);
|
|
874
|
+
for (const item of children.value) {
|
|
875
|
+
const child = refNodeOf(item);
|
|
876
|
+
if (child === undefined || child.tree !== node.tree) continue;
|
|
877
|
+
const failed = visit(child);
|
|
878
|
+
if (failed !== undefined) return failed;
|
|
879
|
+
}
|
|
880
|
+
return undefined;
|
|
881
|
+
};
|
|
882
|
+
return visit(node) ?? acc;
|
|
883
|
+
}
|
|
884
|
+
|
|
834
885
|
/**
|
|
835
886
|
* TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
|
|
836
887
|
* answer, and the next rung then tries.
|
|
@@ -860,6 +911,7 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
|
|
|
860
911
|
(node, name) => {
|
|
861
912
|
const declared = termIn(node.contract?.attributes, name);
|
|
862
913
|
if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
|
|
914
|
+
if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
|
|
863
915
|
return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
|
|
864
916
|
},
|
|
865
917
|
],
|
|
@@ -1020,6 +1072,53 @@ function attributeOf(node: RefNode, name: string): Result | undefined {
|
|
|
1020
1072
|
return result;
|
|
1021
1073
|
}
|
|
1022
1074
|
|
|
1075
|
+
/** TREE.AT: one instance of an attribute with parameters of a node. Its result is `undefined` while it evaluates. */
|
|
1076
|
+
interface RefInstance {
|
|
1077
|
+
readonly attr: string;
|
|
1078
|
+
readonly names: unknown;
|
|
1079
|
+
result: Result | undefined;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** TREE.AT: the instances of each node of the reference. Two arguments are the same instance by DOC.EQUALS. */
|
|
1083
|
+
const refInstances = /* @__PURE__ */ new WeakMap<RefNode, RefInstance[]>();
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* TREE.AT: the instance of an attribute with parameters, or `undefined` when the kind has no entry in `tree.at`. The
|
|
1087
|
+
* equation sees the arguments as names. A demand of an instance in evaluation gives `#CYCLE!`, also in a fixed point.
|
|
1088
|
+
*/
|
|
1089
|
+
function instanceOf(node: RefNode, attr: string, args: unknown): Result | undefined {
|
|
1090
|
+
const declared = termIn(node.contract?.at, attr);
|
|
1091
|
+
if (declared === undefined) return undefined;
|
|
1092
|
+
const rule = atRuleOf(declared);
|
|
1093
|
+
if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, attr)} has a bad entry tree.at. ${rule.reason}`);
|
|
1094
|
+
const given = atArgsOf(rule.params, args);
|
|
1095
|
+
if (!given.ok) return fail("#VALUE!", `The step tree.at of ${nameOfAttribute(node, attr)} has bad arguments. ${given.reason}`);
|
|
1096
|
+
const name = `${attr} with ${given.key}`;
|
|
1097
|
+
const list = refInstances.get(node) ?? [];
|
|
1098
|
+
refInstances.set(node, list);
|
|
1099
|
+
const found = list.find((item) => item.attr === attr && docEquals(item.names, given.names));
|
|
1100
|
+
if (found?.result !== undefined) return found.result;
|
|
1101
|
+
if (found !== undefined) {
|
|
1102
|
+
const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
|
|
1103
|
+
const steps = inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
|
|
1104
|
+
return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
|
|
1105
|
+
}
|
|
1106
|
+
const item: RefInstance = { attr, names: given.names, result: undefined };
|
|
1107
|
+
list.push(item);
|
|
1108
|
+
const entry = { node, name, circular: false, tentative: false };
|
|
1109
|
+
inEvaluation.push(entry);
|
|
1110
|
+
let result: Result | undefined;
|
|
1111
|
+
try {
|
|
1112
|
+
result = equationOf(node, rule.equation, given.names);
|
|
1113
|
+
} finally {
|
|
1114
|
+
inEvaluation.pop();
|
|
1115
|
+
// A result that read an approximation is not kept (TREE.CIRCULAR).
|
|
1116
|
+
if (result === undefined || entry.tentative) list.splice(list.indexOf(item), 1);
|
|
1117
|
+
else item.result = result;
|
|
1118
|
+
}
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1023
1122
|
/** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
|
|
1024
1123
|
function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
|
|
1025
1124
|
const record: Record<string, unknown> = {};
|
|
@@ -1065,7 +1164,7 @@ function plainValue(value: unknown, open: readonly object[]): Result {
|
|
|
1065
1164
|
* tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error. The node
|
|
1066
1165
|
* reads `self` only through `resolve` (C4, TREE.SELF).
|
|
1067
1166
|
*/
|
|
1068
|
-
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk }) => {
|
|
1167
|
+
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk, doc }) => {
|
|
1069
1168
|
const reached = walk(["self"]);
|
|
1070
1169
|
const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
|
|
1071
1170
|
const outer = refNodeOf(parent);
|
|
@@ -1076,6 +1175,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1076
1175
|
const node: RefNode = {
|
|
1077
1176
|
tree: nested ? outer.tree : { open: true },
|
|
1078
1177
|
kind,
|
|
1178
|
+
name: doc.name,
|
|
1079
1179
|
place,
|
|
1080
1180
|
parts,
|
|
1081
1181
|
ctx: context,
|
|
@@ -1107,6 +1207,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1107
1207
|
};
|
|
1108
1208
|
node.thing = self;
|
|
1109
1209
|
refNodes.set(self, node);
|
|
1210
|
+
AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
|
|
1110
1211
|
if (nested) return ok(self);
|
|
1111
1212
|
try {
|
|
1112
1213
|
return jsonOfNode(node, self, []);
|
|
@@ -1235,13 +1336,22 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
|
|
|
1235
1336
|
}
|
|
1236
1337
|
const names = params as readonly string[];
|
|
1237
1338
|
const body = partNamed(parts, "body") as Part;
|
|
1339
|
+
// FORMULA.CALL-DEPTH: the evaluation that makes the closure counts its applications. A quarter of maxDepth is the limit.
|
|
1340
|
+
const clock = activeEnvs[activeEnvs.length - 1] ?? { applying: 0, maxDepth: 1000 };
|
|
1341
|
+
const limit = Math.ceil(clock.maxDepth / 4);
|
|
1238
1342
|
const closure = (...args: readonly unknown[]): unknown => {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1343
|
+
if (clock.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
|
|
1344
|
+
clock.applying += 1;
|
|
1345
|
+
try {
|
|
1346
|
+
const scope: Record<string, unknown> = {};
|
|
1347
|
+
for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
|
|
1348
|
+
names.forEach((name, index) => setField(scope, name, args[index]));
|
|
1349
|
+
const result = body.evaluate({ ...context, names: scope });
|
|
1350
|
+
if (!result.ok) throw new Failure(result.error);
|
|
1351
|
+
return result.value;
|
|
1352
|
+
} finally {
|
|
1353
|
+
clock.applying -= 1;
|
|
1354
|
+
}
|
|
1245
1355
|
};
|
|
1246
1356
|
Object.defineProperty(closure, CLOSURE, { value: { params: names } });
|
|
1247
1357
|
return ok(closure);
|