@mark1russell7/coeval 0.20.1 → 0.21.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 +318 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +609 -76
- 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 +31 -5
- package/src/index.ts +4 -0
- package/src/instances.ts +50 -1
- package/src/parts.ts +30 -1
- package/src/providers.ts +209 -29
- package/src/reference.ts +52 -2
- package/src/static.ts +79 -19
- package/src/stdlib.ts +88 -2
- package/src/types.ts +12 -4
- package/src/world.ts +50 -0
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,6 +1,6 @@
|
|
|
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, 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
6
|
import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
|
|
@@ -754,6 +754,8 @@ interface RefTree {
|
|
|
754
754
|
interface RefNode {
|
|
755
755
|
readonly tree: RefTree;
|
|
756
756
|
readonly kind: string;
|
|
757
|
+
/** The binding name from the header of the document of the node (TREE.NAMED). */
|
|
758
|
+
readonly name: string | undefined;
|
|
757
759
|
readonly place: readonly Key[];
|
|
758
760
|
readonly parts: readonly Part[];
|
|
759
761
|
readonly ctx: Context;
|
|
@@ -831,6 +833,52 @@ function foldOf(node: RefNode, name: string, fold: NonNullable<AttributeDeclarat
|
|
|
831
833
|
return acc;
|
|
832
834
|
}
|
|
833
835
|
|
|
836
|
+
/**
|
|
837
|
+
* TREE.COLLECT and TREE.NAMED: the node, then each node of this tree in each list `children`, in the order of the
|
|
838
|
+
* lists. Each node counts one time. The registry `names` maps each binding name to its node.
|
|
839
|
+
*/
|
|
840
|
+
function collectIn(node: RefNode, name: string, declared: unknown): Result {
|
|
841
|
+
const rule = collectOf(declared);
|
|
842
|
+
if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, name)} has a bad declaration collect. ${rule.reason}`);
|
|
843
|
+
const registry: Record<string, unknown> = {};
|
|
844
|
+
const first = new Map<string, RefNode>();
|
|
845
|
+
let acc: Result = rule.names ? ok(registry) : equationOf(node, rule.unit, {});
|
|
846
|
+
if (!acc.ok) return acc;
|
|
847
|
+
const done: RefNode[] = [];
|
|
848
|
+
// The contribution of a node comes before the nodes of its list children. A failure ends the collection.
|
|
849
|
+
const visit = (at: RefNode): Result | undefined => {
|
|
850
|
+
if (done.includes(at)) return undefined;
|
|
851
|
+
done.push(at);
|
|
852
|
+
if (rule.names && at.name !== undefined) {
|
|
853
|
+
const earlier = first.get(at.name);
|
|
854
|
+
if (earlier !== undefined) {
|
|
855
|
+
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(", ")}].`);
|
|
856
|
+
}
|
|
857
|
+
first.set(at.name, at);
|
|
858
|
+
Object.defineProperty(registry, at.name, { value: at.thing, enumerable: true, writable: true, configurable: true });
|
|
859
|
+
}
|
|
860
|
+
const term = rule.names ? undefined : termIn(at.contract?.contributes, name);
|
|
861
|
+
if (!rule.names && term !== undefined) {
|
|
862
|
+
const item = equationOf(at, term, {});
|
|
863
|
+
if (!item.ok) return item;
|
|
864
|
+
acc = equationOf(node, rule.combine, { acc: acc.ok ? acc.value : null, item: item.value });
|
|
865
|
+
if (!acc.ok) return acc;
|
|
866
|
+
}
|
|
867
|
+
const children = attributeOf(at, "children");
|
|
868
|
+
if (children === undefined) return undefined;
|
|
869
|
+
if (!children.ok) return children;
|
|
870
|
+
if (!Array.isArray(children.value)) return fail("#VALUE!", `The collection of ${name} needs a list in children of ${at.kind} at [${at.place.join(", ")}].`);
|
|
871
|
+
for (const item of children.value) {
|
|
872
|
+
const child = refNodeOf(item);
|
|
873
|
+
if (child === undefined || child.tree !== node.tree) continue;
|
|
874
|
+
const failed = visit(child);
|
|
875
|
+
if (failed !== undefined) return failed;
|
|
876
|
+
}
|
|
877
|
+
return undefined;
|
|
878
|
+
};
|
|
879
|
+
return visit(node) ?? acc;
|
|
880
|
+
}
|
|
881
|
+
|
|
834
882
|
/**
|
|
835
883
|
* TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
|
|
836
884
|
* answer, and the next rung then tries.
|
|
@@ -860,6 +908,7 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
|
|
|
860
908
|
(node, name) => {
|
|
861
909
|
const declared = termIn(node.contract?.attributes, name);
|
|
862
910
|
if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
|
|
911
|
+
if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
|
|
863
912
|
return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
|
|
864
913
|
},
|
|
865
914
|
],
|
|
@@ -1065,7 +1114,7 @@ function plainValue(value: unknown, open: readonly object[]): Result {
|
|
|
1065
1114
|
* tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error. The node
|
|
1066
1115
|
* reads `self` only through `resolve` (C4, TREE.SELF).
|
|
1067
1116
|
*/
|
|
1068
|
-
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk }) => {
|
|
1117
|
+
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk, doc }) => {
|
|
1069
1118
|
const reached = walk(["self"]);
|
|
1070
1119
|
const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
|
|
1071
1120
|
const outer = refNodeOf(parent);
|
|
@@ -1076,6 +1125,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1076
1125
|
const node: RefNode = {
|
|
1077
1126
|
tree: nested ? outer.tree : { open: true },
|
|
1078
1127
|
kind,
|
|
1128
|
+
name: doc.name,
|
|
1079
1129
|
place,
|
|
1080
1130
|
parts,
|
|
1081
1131
|
ctx: context,
|
package/src/static.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
|
-
import { type Kind, type World, type WorldEntry, circularOf, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
|
|
3
|
-
import { ask } from "./ladder.ts";
|
|
4
|
-
import { type PartSlot, partsOf, scopesOf } from "./parts.ts";
|
|
2
|
+
import { type Kind, type LazyRule, type World, type WorldEntry, circularOf, collectOf, eachKind, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
|
|
3
|
+
import { answerOf, ask } from "./ladder.ts";
|
|
4
|
+
import { type PartSlot, partsOf, rulesCover, scopesOf } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
|
|
6
6
|
import type { ErrorCode } from "./errors.ts";
|
|
7
7
|
import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
|
|
@@ -405,6 +405,19 @@ export interface Typing {
|
|
|
405
405
|
readonly places: readonly Typed[];
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* This function gives the parts that the code of a runner evaluates itself, without a wait (RUN.DRIVES, D119). The
|
|
410
|
+
* answer of the hook `facts` for the code wins over the contract. The contract describes only the code of its kind for
|
|
411
|
+
* `eval`. Without a declaration, the runner passes each wait of its parts through.
|
|
412
|
+
*/
|
|
413
|
+
function driveRules(machine: Machine, v: DocVisit, kind: Kind, name: string, rung: string | undefined, verb: string): readonly LazyRule[] | undefined {
|
|
414
|
+
if (verb !== "eval") return undefined;
|
|
415
|
+
const hook = machine.facts?.(v.typePath ?? [], name)?.drives;
|
|
416
|
+
if (hook !== undefined) return hook;
|
|
417
|
+
const described = v.declared || rung !== "own" || name === answerOf(machine.world, undefined, kind, verb)?.name;
|
|
418
|
+
return described ? kind.drives : undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
408
421
|
/** The least authority: the capability `[]` permits each effect (CAP.KEYS). */
|
|
409
422
|
const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
|
|
410
423
|
|
|
@@ -412,16 +425,42 @@ const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
|
|
|
412
425
|
const NO_EFFECTS: readonly (readonly Key[])[] = /* @__PURE__ */ Object.freeze([]);
|
|
413
426
|
|
|
414
427
|
/**
|
|
415
|
-
* This function gives the effects that the
|
|
416
|
-
*
|
|
417
|
-
*
|
|
428
|
+
* This function gives the effects that the code of a visit declares for a verb, as keys (FACTS.EFFECTS, VERB.EFFECTS).
|
|
429
|
+
* The text `"type"` gives the type of the document after each alias. A visit without a kind declares no effect.
|
|
430
|
+
* `check`, `factsOf`, `effectsOf` and `checkLatent` read the declarations with this one function (D27).
|
|
431
|
+
*
|
|
432
|
+
* For `eval`, the answer of the hook `facts` of the machine for the code of the visit wins over the contract of the
|
|
433
|
+
* kind (CAP.OWN-EFFECTS, D118). Thus a language gives the effects of an own entry for another evaluator. Without a
|
|
434
|
+
* machine, or without the field `effects` in the answer, the contract gives the effects.
|
|
418
435
|
*
|
|
419
436
|
* @param v - The visit.
|
|
420
437
|
* @param verb - The verb. The default is `eval`. A kind without a declaration for the verb gives no effect.
|
|
421
|
-
* @
|
|
438
|
+
* @param machine - The machine with the hook `facts`, when the caller has it.
|
|
439
|
+
* @param name - The name of the evaluator of the visit, when the caller knows it. Else the ladder gives it.
|
|
440
|
+
* @returns The keys of each declared effect, in the order of the declaration.
|
|
422
441
|
*/
|
|
423
|
-
export function declaredEffects(v: DocVisit, verb = "eval"): readonly (readonly Key[])[] {
|
|
424
|
-
return verbDeclared(v, verb) ?? NO_EFFECTS;
|
|
442
|
+
export function declaredEffects(v: DocVisit, verb = "eval", machine?: Machine, name?: string): readonly (readonly Key[])[] {
|
|
443
|
+
return hookEffects(v, verb, machine, name) ?? verbDeclared(v, verb) ?? NO_EFFECTS;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* This function gives the effects that the hook `facts` gives for the code of a visit, as keys (D118). The result is
|
|
448
|
+
* `undefined` for another verb than `eval`, without the hook, or when the answer has no field `effects`.
|
|
449
|
+
*/
|
|
450
|
+
function hookEffects(v: DocVisit, verb: string, machine: Machine | undefined, name: string | undefined): readonly (readonly Key[])[] | undefined {
|
|
451
|
+
if (verb !== "eval" || machine?.facts === undefined || v.kind === undefined) return undefined;
|
|
452
|
+
const code = name ?? answerOf(machine.world, v.declared ? undefined : v.doc.own, v.kind, "eval")?.name;
|
|
453
|
+
const effects = code === undefined ? undefined : machine.facts(v.typePath ?? [], code)?.effects;
|
|
454
|
+
return effects === undefined ? undefined : effectKeys(v, effects);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* This function gives the keys of a declaration of effects at a visit (D27). The text `"type"` gives the type of the
|
|
459
|
+
* document after each alias.
|
|
460
|
+
*/
|
|
461
|
+
export function effectKeys(v: DocVisit, effects: "type" | readonly string[]): readonly (readonly Key[])[] {
|
|
462
|
+
if (effects.length === 0) return NO_EFFECTS;
|
|
463
|
+
return effects === "type" ? [v.typePath ?? []] : effects.map((kind) => kind.split("."));
|
|
425
464
|
}
|
|
426
465
|
|
|
427
466
|
/**
|
|
@@ -433,8 +472,7 @@ function verbDeclared(v: DocVisit, verb: string): readonly (readonly Key[])[] |
|
|
|
433
472
|
if (v.kind === undefined) return verb === "eval" ? NO_EFFECTS : undefined;
|
|
434
473
|
const effects = effectsOfVerb(v.kind, verb);
|
|
435
474
|
if (effects === undefined) return undefined;
|
|
436
|
-
|
|
437
|
-
return effects === "type" ? [v.typePath ?? []] : effects.map((kind) => kind.split("."));
|
|
475
|
+
return effectKeys(v, effects);
|
|
438
476
|
}
|
|
439
477
|
|
|
440
478
|
/**
|
|
@@ -637,7 +675,8 @@ const FOLD_NAMES: readonly string[] = /* @__PURE__ */ Object.freeze(["acc", "ite
|
|
|
637
675
|
|
|
638
676
|
/**
|
|
639
677
|
* This function gives each equation of the tree contract of a kind, with the names that it sees (D42). An equation of
|
|
640
|
-
* `equations`, a `unit
|
|
678
|
+
* `equations`, a `unit`, a `default` and a contribution see no names. `check` and `factsOf` read the equations with
|
|
679
|
+
* this one function.
|
|
641
680
|
*/
|
|
642
681
|
export function equationsOf(kind: Kind): readonly Equation[] {
|
|
643
682
|
const tree = kind.tree;
|
|
@@ -651,7 +690,14 @@ export function equationsOf(kind: Kind): readonly Equation[] {
|
|
|
651
690
|
out.push({ label: `tree.attributes.${name}.fold.combine`, term: declared.fold.combine, names: FOLD_NAMES, attribute: name, down: false });
|
|
652
691
|
}
|
|
653
692
|
if (declared.default !== undefined) out.push({ label: `tree.attributes.${name}.default`, term: declared.default, names: [], attribute: name, down: false });
|
|
693
|
+
const collect = declared.collect === undefined ? undefined : collectOf(declared.collect);
|
|
694
|
+
if (collect?.ok === true && !collect.names) {
|
|
695
|
+
out.push({ label: `tree.attributes.${name}.collect.unit`, term: collect.unit, names: [], attribute: name, down: false });
|
|
696
|
+
out.push({ label: `tree.attributes.${name}.collect.combine`, term: collect.combine, names: FOLD_NAMES, attribute: name, down: false });
|
|
697
|
+
}
|
|
654
698
|
}
|
|
699
|
+
// A contribution is not the attribute of its node, thus an own part of its name does not hide it (D122).
|
|
700
|
+
for (const [name, term] of Object.entries(tree.contributes ?? {})) out.push({ label: `tree.contributes.${name}`, term, names: [], attribute: name, down: false, contributes: true });
|
|
655
701
|
return out;
|
|
656
702
|
}
|
|
657
703
|
|
|
@@ -665,6 +711,8 @@ export interface Equation {
|
|
|
665
711
|
readonly attribute: string;
|
|
666
712
|
/** `true` for an equation of `down`: it gives the attribute of a child. */
|
|
667
713
|
readonly down: boolean;
|
|
714
|
+
/** `true` for an equation of `contributes`: it gives a contribution to a collection (TREE.COLLECT). */
|
|
715
|
+
readonly contributes?: true;
|
|
668
716
|
}
|
|
669
717
|
|
|
670
718
|
/**
|
|
@@ -837,7 +885,7 @@ export function answered(world: World, node: SeenNode, name: string): boolean {
|
|
|
837
885
|
if (hasEntry(tree?.equations, name)) return true;
|
|
838
886
|
if (node.parent === undefined ? downNames(world).has(name) : hasEntry(node.parent.kind.tree?.down, name)) return true;
|
|
839
887
|
const declared = tree?.attributes !== undefined && Object.hasOwn(tree.attributes, name) ? tree.attributes[name] : undefined;
|
|
840
|
-
return declared !== undefined && (declared.copy === true || declared.fold !== undefined || declared.default !== undefined);
|
|
888
|
+
return declared !== undefined && (declared.copy === true || declared.fold !== undefined || declared.collect !== undefined || declared.default !== undefined);
|
|
841
889
|
}
|
|
842
890
|
|
|
843
891
|
/**
|
|
@@ -853,10 +901,10 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
|
|
|
853
901
|
state.needs.set(kind, needs);
|
|
854
902
|
for (const name of Object.keys(kind.tree?.attributes ?? {})) needs.push({ name, why: "the kind declares it", hidden: undefined });
|
|
855
903
|
const self: SeenNode = { kind, text: kindText, own: undefined, parent: undefined };
|
|
856
|
-
for (const { label, term, names, attribute, down } of equationsOf(kind)) {
|
|
904
|
+
for (const { label, term, names, attribute, down, contributes } of equationsOf(kind)) {
|
|
857
905
|
const inner: CheckOptions = { names, ...(of === undefined ? {} : { declaredType: of }), ...(options.caps === undefined ? {} : { caps: [] }) };
|
|
858
906
|
const read = (name: string): void => {
|
|
859
|
-
needs.push({ name, why: `the equation ${label} reads it`, hidden: down ? undefined : attribute });
|
|
907
|
+
needs.push({ name, why: `the equation ${label} reads it`, hidden: down || contributes === true ? undefined : attribute });
|
|
860
908
|
};
|
|
861
909
|
for (const issue of checkAll(machine, term, inner, undefined, state, { node: self, read })) {
|
|
862
910
|
out.push({ code: issue.code, place: [], reason: `In the equation ${label} of the kind ${kindText}: ${issue.reason}` });
|
|
@@ -868,12 +916,13 @@ function equationIssues(machine: Machine, kind: Kind, kindText: string, options:
|
|
|
868
916
|
/**
|
|
869
917
|
* This function finds the issues of the contract of a tree kind (TREE.CHECK-RESERVED). An attribute with the name of a
|
|
870
918
|
* structural step gets no read. A tree kind of the run side contradicts the evaluation on demand of its attributes. A
|
|
871
|
-
* declaration `circular` without a start value or with a bad limit gives `#VALUE!` (TREE.CIRCULAR-CHECK).
|
|
919
|
+
* declaration `circular` without a start value or with a bad limit gives `#VALUE!` (TREE.CIRCULAR-CHECK). A declaration
|
|
920
|
+
* `collect` without its form gives `#VALUE!` too (TREE.COLLECT).
|
|
872
921
|
*/
|
|
873
922
|
function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
|
|
874
923
|
const out: Issue[] = [];
|
|
875
924
|
const tree = kind.tree;
|
|
876
|
-
for (const table of ["equations", "down", "attributes"] as const) {
|
|
925
|
+
for (const table of ["equations", "down", "attributes", "contributes"] as const) {
|
|
877
926
|
for (const name of Object.keys(tree?.[table] ?? {})) {
|
|
878
927
|
const why = Object.hasOwn(STRUCTURAL, name) ? STRUCTURAL[name] : undefined;
|
|
879
928
|
if (why !== undefined) out.push({ code: "#NAME?", place: [], reason: `The kind ${kindText} gives the reserved attribute tree.${table}.${name}: ${why}.` });
|
|
@@ -883,6 +932,8 @@ function contractIssues(kind: Kind, kindText: string): readonly Issue[] {
|
|
|
883
932
|
for (const [name, declared] of Object.entries(tree?.attributes ?? {})) {
|
|
884
933
|
const rule = declared.circular === undefined ? undefined : circularOf(declared.circular);
|
|
885
934
|
if (rule !== undefined && !rule.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad declaration tree.attributes.${name}.circular. ${rule.reason}` });
|
|
935
|
+
const collect = declared.collect === undefined ? undefined : collectOf(declared.collect);
|
|
936
|
+
if (collect !== undefined && !collect.ok) out.push({ code: "#VALUE!", place: [], reason: `The kind ${kindText} has a bad declaration tree.attributes.${name}.collect. ${collect.reason}` });
|
|
886
937
|
}
|
|
887
938
|
if (kind.side === "run") {
|
|
888
939
|
out.push({ code: "#CALC!", place: [], reason: `The tree kind ${kindText} is on the run side: a node evaluates its attributes on demand, thus in no fixed order.` });
|
|
@@ -971,6 +1022,9 @@ function checkAll(
|
|
|
971
1022
|
// run, and a drive for a runner under an evaluator. A document without code fails before its parts, thus it is not
|
|
972
1023
|
// in the map.
|
|
973
1024
|
const waits = options.run === true ? new Map<DocVisit, Waits>() : undefined;
|
|
1025
|
+
// The parts that the code of a runner evaluates itself, without a wait (RUN.DRIVES). They get no wait, as under an
|
|
1026
|
+
// evaluator. Only a runner that declares `drives` is in the map.
|
|
1027
|
+
const driven = new Map<DocVisit, readonly LazyRule[]>();
|
|
974
1028
|
const drives: Drive[] = [];
|
|
975
1029
|
const maxDepth = machine.maxDepth ?? 1000;
|
|
976
1030
|
const anyName = options.names === "any";
|
|
@@ -1026,7 +1080,7 @@ function checkAll(
|
|
|
1026
1080
|
if (known !== true) push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
|
|
1027
1081
|
}
|
|
1028
1082
|
const caps = v.caps;
|
|
1029
|
-
const stated = verbDeclared(v, verb);
|
|
1083
|
+
const stated = hookEffects(v, verb, machine, name) ?? verbDeclared(v, verb);
|
|
1030
1084
|
const declared = stated ?? NO_EFFECTS;
|
|
1031
1085
|
if (allowed !== undefined && caps !== undefined) {
|
|
1032
1086
|
for (const kind of declared) {
|
|
@@ -1035,7 +1089,9 @@ function checkAll(
|
|
|
1035
1089
|
}
|
|
1036
1090
|
if (waits !== undefined && coded) {
|
|
1037
1091
|
// The parent of the root is the driver of the run, and it can wait. An evaluator gives its parts no wait.
|
|
1038
|
-
|
|
1092
|
+
let up = v.parent === undefined ? true : waits.get(v.parent);
|
|
1093
|
+
const rules = v.parent === undefined ? undefined : driven.get(v.parent);
|
|
1094
|
+
if (up !== false && rules !== undefined && v.parent !== undefined && rulesCover(rules, v.parent.kind?.parts, v.parent.doc.value as Json, v.slot)) up = false;
|
|
1039
1095
|
let chain: Waits = false;
|
|
1040
1096
|
if (runner && up === false) {
|
|
1041
1097
|
// The run drives a runner under an evaluator, and only a real wait fails (RUN.DRIVE-NESTED). Its issue goes in
|
|
@@ -1047,6 +1103,10 @@ function checkAll(
|
|
|
1047
1103
|
chain = up ?? true;
|
|
1048
1104
|
}
|
|
1049
1105
|
waits.set(v, chain);
|
|
1106
|
+
if (runner && v.kind !== undefined) {
|
|
1107
|
+
const own = driveRules(machine, v, v.kind, name, answer.rung, verb);
|
|
1108
|
+
if (own !== undefined && own.length > 0) driven.set(v, own);
|
|
1109
|
+
}
|
|
1050
1110
|
// A runner waits only for an effect that reaches the host (C6). A refused effect gives #CAP! without a wait. A
|
|
1051
1111
|
// runner without a declaration for the verb can cause each effect, thus it can wait.
|
|
1052
1112
|
if (typeof chain === "object" && !chain.wait) {
|