@mark1russell7/coeval 0.6.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts ADDED
@@ -0,0 +1,768 @@
1
+ import { type Json, type Key, docEquals, isEnvelope, isRecord } from "./doc.ts";
2
+
3
+ /*
4
+ * Value types and value domains (R5 P2, D22). A value type is a set of values that a result can have. It is a JSON
5
+ * term that only a check reads. It is not a kind, and it does not go into an envelope (D2, D6). A text names a base
6
+ * type. The set of base types is open: a language adds its own domains, each with a membership test.
7
+ */
8
+
9
+ /**
10
+ * A value type: a set of values that a result can have, as a JSON term.
11
+ *
12
+ * - A text names a base type: `unknown`, `never`, a domain of the kernel (`null`, `bool`, `number`, `text`), or a
13
+ * domain of a language.
14
+ * - `list`: a list whose items have the type.
15
+ * - `record`: a plain record with the named fields. `optional` names the fields that can be absent. A `closed`
16
+ * record has no other fields. The other fields of an open record can hold each value.
17
+ * - `fn`: a function with the types of its parameters, and the type of its result (`to`).
18
+ * - `union`: a value of one of the types. An empty union holds no value.
19
+ * - `one`: one of the JSON values of the list.
20
+ *
21
+ * A term that is not in this list reads as `unknown` (TYPE.UNKNOWN). Thus a later version can add a term.
22
+ */
23
+ export type ValueType =
24
+ | string
25
+ | { readonly list: ValueType }
26
+ | { readonly record: Readonly<Record<string, ValueType>>; readonly optional?: readonly string[]; readonly closed?: boolean }
27
+ | { readonly fn: readonly ValueType[]; readonly to: ValueType }
28
+ | { readonly union: readonly ValueType[] }
29
+ | { readonly one: readonly Json[] };
30
+
31
+ /**
32
+ * A value domain: a base type of a language, with its membership test. A machine holds its domains by their names
33
+ * (`Machine.domains`). A value is a member of a domain only when the test of the domain and the test of each parent
34
+ * accept it (TYPE.MEMBER).
35
+ *
36
+ * A path does not follow a member of a domain as a reference (DEREF.COLLAPSE). Thus the test `is` must refuse each
37
+ * reference document.
38
+ */
39
+ export interface Domain {
40
+ /**
41
+ * This function tells if a value is a member of the domain. It must be pure. A test that throws gives `false`.
42
+ *
43
+ * @param value - The value.
44
+ * @returns `true` for a member.
45
+ */
46
+ readonly is: (value: unknown) => boolean;
47
+ /** The names of the parents: the base types that hold each member of the domain. A base type fits each parent. */
48
+ readonly under?: readonly string[];
49
+ /** One sentence that tells what the domain holds. */
50
+ readonly summary?: string;
51
+ /**
52
+ * The value type of each step into a member, by its key, for example the field `x` of a point (TYPE.STEPS). The
53
+ * check of a path reads the steps of the domain first, then the steps of each parent. The language keeps them true:
54
+ * a step with the key into a member reaches a member of the type. A key without a type gives `unknown`.
55
+ */
56
+ readonly steps?: Readonly<Record<string, ValueType>>;
57
+ /**
58
+ * The codec of the members: it writes a member as JSON and reads it again (D40). A document holds a member as the
59
+ * standard extension `ext.domain`. A domain without a codec has no such document.
60
+ */
61
+ readonly codec?: DomainCodec;
62
+ }
63
+
64
+ /**
65
+ * The codec of a domain: it writes a member of the domain as JSON, and it reads the JSON again (D40). Both functions
66
+ * must be pure. `decode(encode(v))` must equal `v` by `sameValue` (EXT.ROUNDTRIP). The codec must give one JSON
67
+ * value for each member, thus the same value has the same document in each language (D12).
68
+ */
69
+ export interface DomainCodec {
70
+ /**
71
+ * This function writes a member of the domain as JSON.
72
+ *
73
+ * @param value - A member of the domain.
74
+ * @returns The JSON of the member.
75
+ */
76
+ readonly encode: (value: unknown) => Json;
77
+ /**
78
+ * This function reads the JSON of a member again. It throws for JSON that does not write a member.
79
+ *
80
+ * @param data - The JSON that `encode` gave.
81
+ * @returns The member.
82
+ */
83
+ readonly decode: (data: Json) => unknown;
84
+ }
85
+
86
+ /** A problem of a value type: the keys from the root of the term to the problem, and the reason. */
87
+ export interface TypeIssue {
88
+ readonly at: readonly Key[];
89
+ readonly reason: string;
90
+ }
91
+
92
+ /**
93
+ * The domains of the kernel. `unknown` and `never` are not domains: they are the top and the bottom of the types.
94
+ * A machine that does not name one of these domains gets it from this table.
95
+ */
96
+ export const STD_DOMAINS: Readonly<Record<string, Domain>> = {
97
+ null: { is: (value) => value === null, summary: "The value null." },
98
+ bool: { is: (value) => typeof value === "boolean", summary: "A truth value: true or false." },
99
+ number: { is: (value) => typeof value === "number", summary: "A number of the host, also NaN, an infinity and -0." },
100
+ text: { is: (value) => typeof value === "string", summary: "A text." },
101
+ };
102
+
103
+ /** The deepest term that a value type can have. A deeper term reads as `unknown`, thus a check stays total. */
104
+ const MAX_TYPE_DEPTH = 256;
105
+
106
+ /** The keys that each term permits beside its own key. */
107
+ const TERM_KEYS: Readonly<Record<string, readonly string[]>> = {
108
+ list: ["list"],
109
+ record: ["record", "optional", "closed"],
110
+ fn: ["fn", "to"],
111
+ union: ["union"],
112
+ one: ["one"],
113
+ };
114
+
115
+ const TERMS = /* @__PURE__ */ Object.keys(TERM_KEYS);
116
+
117
+ /** The form of a value type that the rules read. `top` is `unknown`, and also each term that is not a value type. */
118
+ type Form =
119
+ | { readonly t: "top" }
120
+ | { readonly t: "bottom" }
121
+ | { readonly t: "base"; readonly name: string }
122
+ | { readonly t: "list"; readonly item: ValueType }
123
+ | { readonly t: "record"; readonly fields: Readonly<Record<string, ValueType>>; readonly optional: ReadonlySet<string>; readonly closed: boolean }
124
+ | { readonly t: "fn"; readonly params: readonly ValueType[]; readonly to: ValueType }
125
+ | { readonly t: "union"; readonly members: readonly ValueType[] }
126
+ | { readonly t: "one"; readonly values: readonly Json[] };
127
+
128
+ const TOP: Form = { t: "top" };
129
+ const BOTTOM: Form = { t: "bottom" };
130
+
131
+ /** This function gives the domain of a base type: the domain of the machine, or else the domain of the kernel. */
132
+ export function domainOf(name: string, domains: Readonly<Record<string, Domain>> | undefined): Domain | undefined {
133
+ // The top and the bottom are not domains. A table entry with their names has no effect.
134
+ if (name === "unknown" || name === "never") return undefined;
135
+ const found = domains !== undefined && Object.hasOwn(domains, name) ? domains[name] : Object.hasOwn(STD_DOMAINS, name) ? STD_DOMAINS[name] : undefined;
136
+ // A domain can be an instance of a class, thus the test reads only its function `is`.
137
+ return typeof found === "object" && found !== null && typeof found.is === "function" ? found : undefined;
138
+ }
139
+
140
+ /** This function gives the term key of a record, when the record has exactly one term key and no other key. */
141
+ function termOf(term: Readonly<Record<string, unknown>>): string | undefined {
142
+ const keys = Object.keys(term);
143
+ const own = keys.filter((key) => TERMS.includes(key));
144
+ if (own.length !== 1) return undefined;
145
+ const allowed = TERM_KEYS[own[0] as string] as readonly string[];
146
+ return keys.every((key) => allowed.includes(key)) ? own[0] : undefined;
147
+ }
148
+
149
+ /** This function reads one level of a value type. A part of a term reads only when a rule asks for it. */
150
+ function formOf(type: unknown, domains: Readonly<Record<string, Domain>> | undefined): Form {
151
+ if (typeof type === "string") {
152
+ if (type === "unknown") return TOP;
153
+ if (type === "never") return BOTTOM;
154
+ return domainOf(type, domains) === undefined ? TOP : { t: "base", name: type };
155
+ }
156
+ if (!isRecord(type)) return TOP;
157
+ const term = termOf(type);
158
+ if (term === "list") return { t: "list", item: type["list"] as ValueType };
159
+ if (term === "fn") {
160
+ const params = type["fn"];
161
+ return Array.isArray(params) && Object.hasOwn(type, "to") ? { t: "fn", params: params as readonly ValueType[], to: type["to"] as ValueType } : TOP;
162
+ }
163
+ if (term === "union") return Array.isArray(type["union"]) ? { t: "union", members: type["union"] as readonly ValueType[] } : TOP;
164
+ if (term === "one") return Array.isArray(type["one"]) ? { t: "one", values: type["one"] as readonly Json[] } : TOP;
165
+ if (term !== "record") return TOP;
166
+ const fields = type["record"];
167
+ const optional = type["optional"] ?? [];
168
+ const closed = type["closed"] ?? false;
169
+ if (!isRecord(fields) || !Array.isArray(optional) || typeof closed !== "boolean") return TOP;
170
+ const names = (optional as readonly unknown[]).filter((name): name is string => typeof name === "string" && Object.hasOwn(fields, name));
171
+ return { t: "record", fields: fields as Readonly<Record<string, ValueType>>, optional: new Set(names), closed };
172
+ }
173
+
174
+ /** This function gives each base type that holds the members of a base type: the type itself and each parent, through each level. */
175
+ function lineOf(name: string, domains: Readonly<Record<string, Domain>> | undefined): ReadonlySet<string> {
176
+ const seen = new Set<string>([name]);
177
+ const queue = [name];
178
+ while (queue.length > 0) {
179
+ const under = domainOf(queue.pop() as string, domains)?.under;
180
+ if (!Array.isArray(under)) continue;
181
+ for (const parent of under) {
182
+ if (typeof parent !== "string" || seen.has(parent)) continue;
183
+ seen.add(parent);
184
+ queue.push(parent);
185
+ }
186
+ }
187
+ return seen;
188
+ }
189
+
190
+ /** This function tells if a value is a member of a base type: each test of the type and of its parents accepts it. */
191
+ function inDomain(value: unknown, name: string, domains: Readonly<Record<string, Domain>> | undefined): boolean {
192
+ for (const each of lineOf(name, domains)) {
193
+ if (each === "never") return false;
194
+ const domain = domainOf(each, domains);
195
+ if (domain === undefined) continue;
196
+ try {
197
+ if (domain.is(value) !== true) return false;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+ return true;
203
+ }
204
+
205
+ /** This function is the membership test at one depth of a type. Below the deepest term, each value is a member. */
206
+ function memberAt(value: unknown, type: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number): boolean {
207
+ if (depth > MAX_TYPE_DEPTH) return true;
208
+ const form = formOf(type, domains);
209
+ const next = depth + 1;
210
+ switch (form.t) {
211
+ case "top":
212
+ return true;
213
+ case "bottom":
214
+ return false;
215
+ case "base":
216
+ return inDomain(value, form.name, domains);
217
+ case "list": {
218
+ if (!Array.isArray(value)) return false;
219
+ // A hole of a list holds undefined, thus a loop by index tests it too.
220
+ for (let index = 0; index < value.length; index += 1) if (!memberAt(value[index], form.item, domains, next)) return false;
221
+ return true;
222
+ }
223
+ case "record": {
224
+ if (!isRecord(value)) return false;
225
+ for (const key of Object.keys(form.fields)) {
226
+ if (Object.hasOwn(value, key)) {
227
+ if (!memberAt(value[key], form.fields[key], domains, next)) return false;
228
+ } else if (!form.optional.has(key)) {
229
+ return false;
230
+ }
231
+ }
232
+ return !form.closed || Object.keys(value).every((key) => Object.hasOwn(form.fields, key));
233
+ }
234
+ case "fn":
235
+ return typeof value === "function";
236
+ case "union":
237
+ return form.members.some((item) => memberAt(value, item, domains, next));
238
+ case "one":
239
+ return form.values.some((item) => docEquals(item, value));
240
+ }
241
+ }
242
+
243
+ /**
244
+ * This function tells if a value is a member of a value type (TYPE.MEMBER). It is the membership test that a cast
245
+ * at run time uses.
246
+ *
247
+ * - Each value is a member of `unknown`, and no value is a member of `never`.
248
+ * - A value is a member of a domain when the test `is` of the domain and of each parent accepts it.
249
+ * - A list, a record, a union and `one` test their values at each depth.
250
+ * - A function type tests only that the value is a function. Its parameters need a test at each call.
251
+ *
252
+ * The function does not throw. A term that is not a value type, or a base type without a domain, holds each value.
253
+ *
254
+ * @param value - The value.
255
+ * @param type - The value type.
256
+ * @param domains - The domains of the machine. A domain of the kernel that the table does not name comes from `STD_DOMAINS`.
257
+ * @returns `true` when the value is a member of the type.
258
+ */
259
+ export function member(value: unknown, type: ValueType, domains?: Readonly<Record<string, Domain>>): boolean {
260
+ return memberAt(value, type, domains, 0);
261
+ }
262
+
263
+ /** This function tells if the record type `a` fits the record type `b`: width, closed records and optional fields. */
264
+ function recordFits(
265
+ a: Extract<Form, { readonly t: "record" }>,
266
+ b: Extract<Form, { readonly t: "record" }>,
267
+ domains: Readonly<Record<string, Domain>> | undefined,
268
+ depth: number,
269
+ gradual: boolean,
270
+ ): boolean {
271
+ if (b.closed && (!a.closed || Object.keys(a.fields).some((key) => !Object.hasOwn(b.fields, key)))) return false;
272
+ for (const key of Object.keys(b.fields)) {
273
+ const want = b.fields[key];
274
+ const needed = !b.optional.has(key);
275
+ if (Object.hasOwn(a.fields, key)) {
276
+ if (needed && a.optional.has(key)) return false;
277
+ if (!fitsAt(a.fields[key], want, domains, depth, gradual)) return false;
278
+ } else if (needed) {
279
+ return false;
280
+ } else if (!a.closed && formOf(want, domains).t !== "top") {
281
+ // The other fields of an open record can hold each value, thus only `unknown` accepts such a field.
282
+ return false;
283
+ }
284
+ }
285
+ return true;
286
+ }
287
+
288
+ /**
289
+ * This function is the relation `fits` at one depth of two types. The order of its tests is the order of the rules of
290
+ * `fits`. Without `gradual`, `unknown` is only the top: it fits only `unknown`. The join uses this strict form.
291
+ */
292
+ function fitsAt(found: unknown, expected: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number, gradual = true): boolean {
293
+ if (depth > MAX_TYPE_DEPTH) return true;
294
+ const a = formOf(found, domains);
295
+ const b = formOf(expected, domains);
296
+ const next = depth + 1;
297
+ if (b.t === "top" || a.t === "bottom") return true;
298
+ if (a.t === "top") return gradual;
299
+ if (a.t === "union") return a.members.every((item) => fitsAt(item, expected, domains, next, gradual));
300
+ if (a.t === "one") return a.values.every((item) => memberAt(item, expected, domains, next));
301
+ if (b.t === "bottom") return false;
302
+ if (b.t === "union") return b.members.some((item) => fitsAt(found, item, domains, next, gradual));
303
+ if (a.t === "base") return b.t === "base" && lineOf(a.name, domains).has(b.name);
304
+ if (a.t === "list") return b.t === "list" && fitsAt(a.item, b.item, domains, next, gradual);
305
+ if (a.t === "record") return b.t === "record" && recordFits(a, b, domains, next, gradual);
306
+ if (b.t !== "fn" || a.params.length > b.params.length) return false;
307
+ // A function fits when it accepts each argument of the expected type, and when its result fits.
308
+ return a.params.every((param, index) => fitsAt(b.params[index], param, domains, next, gradual)) && fitsAt(a.to, b.to, domains, next, gradual);
309
+ }
310
+
311
+ /**
312
+ * This function tells if each value of the type `found` can be where the type `expected` must be (TYPE.FITS). Each
313
+ * check of a type uses it.
314
+ *
315
+ * - `unknown` fits each type, and each type fits `unknown` (TYPE.UNKNOWN). This is the gradual top.
316
+ * - `never` fits each type. Only `never`, an empty union and an empty `one` fit `never`.
317
+ * - A union fits when each of its types fits. A type fits a union when it fits one of its types.
318
+ * - `one` fits when each of its values is a member of `expected`.
319
+ * - A base type fits itself and each parent of its domain, through each level (TYPE.DOMAIN).
320
+ * - A list fits a list when its items fit.
321
+ * - A record fits a record with fewer fields. A field that `expected` needs is not optional in `found`, and each field
322
+ * fits. Only a closed record fits a closed record. An open record without an optional field fits only when that
323
+ * field is `unknown`.
324
+ * - A function fits when it has the same or fewer parameters, each expected parameter fits its parameter, and its
325
+ * result fits.
326
+ *
327
+ * The relation is sound: a member of `found` is a member of `expected` when no `unknown` is in the types. It is not
328
+ * complete. For example, `bool` does not fit `{ "one": [true, false] }`. The function does not throw.
329
+ *
330
+ * @param found - The type that a part has.
331
+ * @param expected - The type that the position needs.
332
+ * @param domains - The domains of the machine. A domain of the kernel that the table does not name comes from `STD_DOMAINS`.
333
+ * @returns `true` when `found` fits `expected`.
334
+ */
335
+ export function fits(found: ValueType, expected: ValueType, domains?: Readonly<Record<string, Domain>>): boolean {
336
+ return fitsAt(found, expected, domains, 0);
337
+ }
338
+
339
+ /** This function sets an own field of a new record. Only the key `__proto__` needs `defineProperty`. */
340
+ function setOwn(target: Record<string, unknown>, key: string, value: unknown): void {
341
+ if (key === "__proto__") Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
342
+ else target[key] = value;
343
+ }
344
+
345
+ /** This function is the join of two record types. A field that only one record has is optional, or absent from an open record. */
346
+ function joinRecords(
347
+ a: Extract<Form, { readonly t: "record" }>,
348
+ b: Extract<Form, { readonly t: "record" }>,
349
+ domains: Readonly<Record<string, Domain>> | undefined,
350
+ depth: number,
351
+ ): ValueType {
352
+ const closed = a.closed && b.closed;
353
+ const fields: Record<string, ValueType> = {};
354
+ const optional: string[] = [];
355
+ const keys = [...Object.keys(a.fields), ...Object.keys(b.fields).filter((key) => !Object.hasOwn(a.fields, key))];
356
+ for (const key of keys) {
357
+ const inA = Object.hasOwn(a.fields, key);
358
+ const inB = Object.hasOwn(b.fields, key);
359
+ if (inA && inB) {
360
+ setOwn(fields, key, joinAt(a.fields[key], b.fields[key], domains, depth));
361
+ if (a.optional.has(key) || b.optional.has(key)) optional.push(key);
362
+ } else if (closed) {
363
+ // The other record is closed, thus the field can be absent.
364
+ setOwn(fields, key, inA ? a.fields[key] : b.fields[key]);
365
+ optional.push(key);
366
+ }
367
+ // In an open record, a field of one side can hold each value on the other side. The open join leaves it out.
368
+ }
369
+ return { record: fields, ...(optional.length === 0 ? {} : { optional }), ...(closed ? { closed: true } : {}) };
370
+ }
371
+
372
+ /**
373
+ * This function makes a union of types. It puts the members of each inner union and each value of `one` together. It
374
+ * leaves out each member that fits another member, by the strict form of `fits`.
375
+ */
376
+ function unionOf(types: readonly unknown[], domains: Readonly<Record<string, Domain>> | undefined, depth: number): ValueType {
377
+ const members: unknown[] = [];
378
+ const values: Json[] = [];
379
+ let valuesAt = -1;
380
+ const add = (type: unknown, level: number): void => {
381
+ const form = formOf(type, domains);
382
+ if (form.t === "bottom") return;
383
+ if (form.t === "union" && level <= MAX_TYPE_DEPTH) {
384
+ for (const item of form.members) add(item, level + 1);
385
+ return;
386
+ }
387
+ if (form.t === "one") {
388
+ if (valuesAt === -1) {
389
+ valuesAt = members.length;
390
+ members.push(undefined);
391
+ }
392
+ for (const value of form.values) if (!values.some((item) => docEquals(item, value))) values.push(value);
393
+ return;
394
+ }
395
+ members.push(type);
396
+ };
397
+ for (const type of types) add(type, depth);
398
+ if (valuesAt !== -1) members[valuesAt] = { one: values };
399
+ const kept: unknown[] = [];
400
+ for (const member of members) {
401
+ if (kept.some((item) => fitsAt(member, item, domains, depth, false))) continue;
402
+ for (let index = kept.length - 1; index >= 0; index -= 1) if (fitsAt(kept[index], member, domains, depth, false)) kept.splice(index, 1);
403
+ kept.push(member);
404
+ }
405
+ if (kept.length === 0) return "never";
406
+ return kept.length === 1 ? (kept[0] as ValueType) : { union: kept as ValueType[] };
407
+ }
408
+
409
+ /** This function is the join at one depth of two types. */
410
+ function joinAt(a: unknown, b: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number): ValueType {
411
+ if (depth > MAX_TYPE_DEPTH) return "unknown";
412
+ const fa = formOf(a, domains);
413
+ const fb = formOf(b, domains);
414
+ if (fa.t === "top" || fb.t === "top") return "unknown";
415
+ if (fa.t === "bottom") return b as ValueType;
416
+ if (fb.t === "bottom") return a as ValueType;
417
+ // The strict form keeps the join sound: a type with `unknown` inside is not under a type without it.
418
+ if (fitsAt(a, b, domains, depth, false)) return b as ValueType;
419
+ if (fitsAt(b, a, domains, depth, false)) return a as ValueType;
420
+ const next = depth + 1;
421
+ if (fa.t === "list" && fb.t === "list") return { list: joinAt(fa.item, fb.item, domains, next) };
422
+ if (fa.t === "record" && fb.t === "record") return joinRecords(fa, fb, domains, next);
423
+ return unionOf([a, b], domains, next);
424
+ }
425
+
426
+ /**
427
+ * This function gives the join of two value types (TYPE.JOIN): a type that holds each member of both types. The check
428
+ * of a type uses it for the branches of an `if` and for the items of a list.
429
+ *
430
+ * - `unknown` with each type gives `unknown`, and `never` with a type gives that type.
431
+ * - When one type fits the other by the strict form of `fits`, the join is the wider type. The strict form reads
432
+ * `unknown` only as the top.
433
+ * - Two lists give a list of the join of their items.
434
+ * - Two records give a record of the join of their common fields. A field of one closed record is optional in the
435
+ * join of two closed records. The join of an open record leaves out the fields that only one record has.
436
+ * - Each other pair gives a union. The union puts the values of `one` together and leaves out each member that fits
437
+ * another member.
438
+ *
439
+ * The join is sound: a member of `a` or of `b` is a member of the join. It is not always the least such type. The
440
+ * function does not throw.
441
+ *
442
+ * @param a - The first type.
443
+ * @param b - The second type.
444
+ * @param domains - The domains of the machine. A domain of the kernel that the table does not name comes from `STD_DOMAINS`.
445
+ * @returns The join.
446
+ */
447
+ export function join(a: ValueType, b: ValueType, domains?: Readonly<Record<string, Domain>>): ValueType {
448
+ return joinAt(a, b, domains, 0);
449
+ }
450
+
451
+ /** The most values of `one` that the text of a type shows. */
452
+ const MAX_TEXT_VALUES = 8;
453
+ /** The most characters of one JSON value in the text of a type. */
454
+ const MAX_TEXT_VALUE = 40;
455
+
456
+ /** This function gives a short text of a JSON value for the text of a type. */
457
+ function valueText(value: unknown): string {
458
+ let text: string;
459
+ try {
460
+ text = JSON.stringify(value) ?? String(value);
461
+ } catch {
462
+ text = String(value);
463
+ }
464
+ return text.length > MAX_TEXT_VALUE ? `${text.slice(0, MAX_TEXT_VALUE - 1)}…` : text;
465
+ }
466
+
467
+ /** This function gives the text of a type at one depth. `wrap` puts a union or a function type in brackets. */
468
+ function textAt(type: unknown, depth: number, wrap: boolean): string {
469
+ if (depth > MAX_TYPE_DEPTH) return "unknown";
470
+ if (typeof type === "string") return type;
471
+ const term = isRecord(type) ? termOf(type) : undefined;
472
+ if (!isRecord(type) || term === undefined) return "unknown";
473
+ const next = depth + 1;
474
+ const group = (items: readonly string[]): string => (items.length === 0 ? "never" : items.length > 1 && wrap ? `(${items.join(" | ")})` : items.join(" | "));
475
+ if (term === "list") return `list<${textAt(type["list"], next, false)}>`;
476
+ if (term === "union") return Array.isArray(type["union"]) ? group((type["union"] as readonly unknown[]).map((item) => textAt(item, next, true))) : "unknown";
477
+ if (term === "one") {
478
+ if (!Array.isArray(type["one"])) return "unknown";
479
+ const values = type["one"] as readonly unknown[];
480
+ const shown = values.slice(0, MAX_TEXT_VALUES).map(valueText);
481
+ return group(values.length > MAX_TEXT_VALUES ? [...shown, `… ${values.length - MAX_TEXT_VALUES} more`] : shown);
482
+ }
483
+ if (term === "fn") {
484
+ if (!Array.isArray(type["fn"])) return "unknown";
485
+ const text = `(${(type["fn"] as readonly unknown[]).map((item) => textAt(item, next, false)).join(", ")}) => ${textAt(type["to"], next, true)}`;
486
+ return wrap ? `(${text})` : text;
487
+ }
488
+ const fields = type["record"];
489
+ if (!isRecord(fields)) return "unknown";
490
+ const optional = Array.isArray(type["optional"]) ? (type["optional"] as readonly unknown[]) : [];
491
+ const items = Object.keys(fields).map((key) => {
492
+ const name = /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
493
+ return `${name}${optional.includes(key) ? "?" : ""}: ${textAt(fields[key], next, false)}`;
494
+ });
495
+ if (type["closed"] !== true) items.push("...");
496
+ return `{${items.join(", ")}}`;
497
+ }
498
+
499
+ /**
500
+ * This function gives the text of a value type (TYPE.TEXT), for the reason of an issue. The function does not throw.
501
+ *
502
+ * - A base type gives its name.
503
+ * - `list<T>` is a list, `{a: T, b?: U}` is a closed record, and `{a: T, ...}` is an open record.
504
+ * - `(T, U) => V` is a function, and `T | U` is a union.
505
+ * - `one` gives its values as JSON, and an empty union gives `never`.
506
+ * - A term that is not a value type gives `unknown`.
507
+ *
508
+ * The text shows at most 8 values of `one`, and it cuts a long value.
509
+ *
510
+ * @param type - The value type.
511
+ * @returns The text.
512
+ */
513
+ export function typeText(type: ValueType): string {
514
+ return textAt(type, 0, false);
515
+ }
516
+
517
+ /*
518
+ * The functions below read one level of a type for the type rules of the kit. The package does not export them.
519
+ */
520
+
521
+ /** This function tells if a type reads as `unknown`. Such a type is the top, a term that is not a value type, or a base type without a domain. */
522
+ export function isUnknown(type: unknown, domains: Readonly<Record<string, Domain>> | undefined): boolean {
523
+ return formOf(type, domains).t === "top";
524
+ }
525
+
526
+ /** This function gives the type of the items of a list type, or `undefined` for each other type. */
527
+ export function itemOf(type: unknown, domains: Readonly<Record<string, Domain>> | undefined): ValueType | undefined {
528
+ const form = formOf(type, domains);
529
+ return form.t === "list" ? form.item : undefined;
530
+ }
531
+
532
+ /** This function gives the type of a field of a record type, or `undefined` when the type is not a record with the field. */
533
+ export function fieldOf(type: unknown, key: string, domains: Readonly<Record<string, Domain>> | undefined): ValueType | undefined {
534
+ const form = formOf(type, domains);
535
+ return form.t === "record" && Object.hasOwn(form.fields, key) ? form.fields[key] : undefined;
536
+ }
537
+
538
+ /** This function gives the parameters and the result of a function type, or `undefined` for each other type. */
539
+ export function fnOf(type: unknown, domains: Readonly<Record<string, Domain>> | undefined): { readonly params: readonly ValueType[]; readonly to: ValueType } | undefined {
540
+ const form = formOf(type, domains);
541
+ return form.t === "fn" ? { params: form.params, to: form.to } : undefined;
542
+ }
543
+
544
+ /** This function tells if each value of a type can be a function. Such a type is the top, the bottom, a function type, or a union of them. */
545
+ export function callable(type: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth = 0): boolean {
546
+ if (depth > MAX_TYPE_DEPTH) return true;
547
+ const form = formOf(type, domains);
548
+ if (form.t === "top" || form.t === "bottom" || form.t === "fn") return true;
549
+ if (form.t === "union") return form.members.every((item) => callable(item, domains, depth + 1));
550
+ return form.t === "one" && form.values.length === 0;
551
+ }
552
+
553
+ /** This function tells if a type has no `unknown` at any depth. Then `fits` decides it without the gradual rule. */
554
+ export function isPrecise(type: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth = 0): boolean {
555
+ if (depth > MAX_TYPE_DEPTH) return false;
556
+ const form = formOf(type, domains);
557
+ const next = depth + 1;
558
+ switch (form.t) {
559
+ case "top":
560
+ return false;
561
+ case "list":
562
+ return isPrecise(form.item, domains, next);
563
+ case "record":
564
+ return Object.keys(form.fields).every((key) => isPrecise(form.fields[key], domains, next));
565
+ case "fn":
566
+ return form.params.every((item) => isPrecise(item, domains, next)) && isPrecise(form.to, domains, next);
567
+ case "union":
568
+ return form.members.every((item) => isPrecise(item, domains, next));
569
+ default:
570
+ return true;
571
+ }
572
+ }
573
+
574
+ /** The keys of the header of an envelope. A record with only these keys can have the form of an envelope. */
575
+ export const HEADER_KEYS: ReadonlySet<string> = /* @__PURE__ */ new Set(["type", "value", "name", "own"]);
576
+
577
+ /**
578
+ * This function tells if a member of a type can have the form of an envelope. A path asks the ladder for each step
579
+ * into an envelope, and it follows a reference document (DEREF.COLLAPSE). Thus the type of such a member does not
580
+ * tell what a path reaches.
581
+ *
582
+ * The test is conservative. A record that needs a key outside the header is not an
583
+ * envelope. A closed record without the fields `type` and `value` is not an envelope either. A domain refuses each
584
+ * reference document (`Domain`), and its steps tell what a step reaches.
585
+ */
586
+ export function holdsEnvelope(type: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth = 0): boolean {
587
+ if (depth > MAX_TYPE_DEPTH) return true;
588
+ const form = formOf(type, domains);
589
+ switch (form.t) {
590
+ case "top":
591
+ return true;
592
+ case "union":
593
+ return form.members.some((item) => holdsEnvelope(item, domains, depth + 1));
594
+ case "one":
595
+ return form.values.some((value) => isEnvelope(value));
596
+ case "record":
597
+ if (Object.keys(form.fields).some((key) => !form.optional.has(key) && !HEADER_KEYS.has(key))) return false;
598
+ return !form.closed || (Object.hasOwn(form.fields, "type") && Object.hasOwn(form.fields, "value"));
599
+ default:
600
+ return false;
601
+ }
602
+ }
603
+
604
+ /**
605
+ * What one step reaches from a member of a type: its type, or the fields of the type when no member has the step.
606
+ * The field list is empty when the type is not a record.
607
+ */
608
+ export type StepType = { readonly type: ValueType } | { readonly missing: readonly string[] };
609
+
610
+ /** This function gives the index of a key into a list, as a step into the value finds it, or -1. */
611
+ function indexOf(key: Key): number {
612
+ if (typeof key === "number") return Number.isInteger(key) && key >= 0 ? key : -1;
613
+ return /^\d+$/.test(key) ? Number(key) : -1;
614
+ }
615
+
616
+ /** This function adds the keys of a list to another list, without the keys that it has. */
617
+ function addKeys(into: string[], keys: readonly string[]): void {
618
+ for (const key of keys) if (!into.includes(key)) into.push(key);
619
+ }
620
+
621
+ /** This function gives the step of a key into one JSON value of `one`: the value that it reaches, or the fields of the value. */
622
+ function valueStep(value: unknown, key: Key): { readonly found: unknown } | { readonly fields: readonly string[] } {
623
+ if (Array.isArray(value)) {
624
+ if (key === "length") return { found: value.length };
625
+ const index = indexOf(key);
626
+ return index >= 0 && index < value.length ? { found: value[index] } : { fields: [] };
627
+ }
628
+ if (isRecord(value)) return Object.hasOwn(value, String(key)) ? { found: value[String(key)] } : { fields: Object.keys(value) };
629
+ return { fields: [] };
630
+ }
631
+
632
+ /**
633
+ * This function gives the type that a step with a key reaches from a member of a type (TYPE.PATH). The caller tests
634
+ * first that no member can be an envelope (`holdsEnvelope`).
635
+ *
636
+ * - A record gives the type of its field. A closed record without the field has no such step. An open record gives
637
+ * `unknown`.
638
+ * - A list gives the type of its items for an index, and `number` for `length`.
639
+ * - A base type reads the steps of its domain, then of each parent (`Domain.steps`).
640
+ * - A union joins the steps of its types. `one` gives the values that the step reaches.
641
+ * - Each other step gives `unknown`, and `never` gives `never`.
642
+ *
643
+ * @param examine - The function that examines a type of the steps of a domain, one time in a check.
644
+ */
645
+ export function stepType(
646
+ type: unknown,
647
+ key: Key,
648
+ domains: Readonly<Record<string, Domain>> | undefined,
649
+ examine?: (type: ValueType, what: string) => ValueType,
650
+ depth = 0,
651
+ ): StepType {
652
+ if (depth > MAX_TYPE_DEPTH) return { type: "unknown" };
653
+ const form = formOf(type, domains);
654
+ const name = String(key);
655
+ switch (form.t) {
656
+ case "bottom":
657
+ return { type: "never" };
658
+ case "base":
659
+ for (const each of lineOf(form.name, domains)) {
660
+ const steps = domainOf(each, domains)?.steps;
661
+ if (typeof steps !== "object" || steps === null || !Object.hasOwn(steps, name)) continue;
662
+ const found = steps[name] as ValueType;
663
+ return { type: examine === undefined ? found : examine(found, `type of the step ${name} of the domain ${each}`) };
664
+ }
665
+ return { type: "unknown" };
666
+ case "list":
667
+ if (key === "length") return { type: "number" };
668
+ return { type: indexOf(key) >= 0 ? form.item : "unknown" };
669
+ case "record":
670
+ if (Object.hasOwn(form.fields, name)) return { type: form.fields[name] as ValueType };
671
+ return form.closed ? { missing: Object.keys(form.fields) } : { type: "unknown" };
672
+ case "union": {
673
+ let joined: ValueType | undefined;
674
+ const fields: string[] = [];
675
+ for (const item of form.members) {
676
+ const step = stepType(item, key, domains, examine, depth + 1);
677
+ if ("missing" in step) addKeys(fields, step.missing);
678
+ else joined = joined === undefined ? step.type : join(joined, step.type, domains);
679
+ }
680
+ // A member without the step makes the path fail, thus only the other members give a type.
681
+ return joined === undefined ? { missing: fields } : { type: joined };
682
+ }
683
+ case "one": {
684
+ const found: Json[] = [];
685
+ const fields: string[] = [];
686
+ for (const value of form.values) {
687
+ const step = valueStep(value, key);
688
+ if ("fields" in step) addKeys(fields, step.fields);
689
+ else if (!found.some((item) => docEquals(item, step.found))) found.push(step.found as Json);
690
+ }
691
+ return found.length === 0 ? { missing: fields } : { type: { one: found } };
692
+ }
693
+ default:
694
+ return { type: "unknown" };
695
+ }
696
+ }
697
+
698
+ /** A term that `typeIssues` reads later: the term, its keys from the root and its depth. */
699
+ interface Pending {
700
+ readonly type: unknown;
701
+ readonly at: readonly Key[];
702
+ readonly depth: number;
703
+ }
704
+
705
+ /**
706
+ * This function lists the problems of a value type (TYPE.ISSUES). A problem is a term that is not a value type, a
707
+ * base type without a domain, or a term that is too deep. `fits` and `member` read each such term as `unknown`.
708
+ * Thus a check that reads a type from a contract can report it first.
709
+ *
710
+ * @param type - The value type, as JSON.
711
+ * @param domains - The domains of the machine. A domain of the kernel that the table does not name comes from `STD_DOMAINS`.
712
+ * @returns The problems. A term comes before its parts, and the parts keep their order.
713
+ */
714
+ export function typeIssues(type: unknown, domains?: Readonly<Record<string, Domain>>): readonly TypeIssue[] {
715
+ const issues: TypeIssue[] = [];
716
+ const stack: Pending[] = [{ type, at: [], depth: 0 }];
717
+ while (stack.length > 0) {
718
+ const { type: here, at, depth } = stack.pop() as Pending;
719
+ const issue = (reason: string, where: readonly Key[] = at): void => {
720
+ issues.push({ at: where, reason });
721
+ };
722
+ if (depth > MAX_TYPE_DEPTH) {
723
+ issue(`The value type is deeper than ${MAX_TYPE_DEPTH} terms.`);
724
+ continue;
725
+ }
726
+ if (typeof here === "string") {
727
+ if (here !== "unknown" && here !== "never" && domainOf(here, domains) === undefined) issue(`The machine has no domain ${here}.`);
728
+ continue;
729
+ }
730
+ const term = isRecord(here) ? termOf(here) : undefined;
731
+ if (!isRecord(here) || term === undefined) {
732
+ issue(`A value type is a text, or a record with one of the keys ${TERMS.join(", ")} and the keys that go with it.`);
733
+ continue;
734
+ }
735
+ // The parts go on the stack in the reverse order, thus the issues come in the order of the term.
736
+ const parts: Pending[] = [];
737
+ const part = (each: unknown, where: readonly Key[]): void => {
738
+ parts.push({ type: each, at: where, depth: depth + 1 });
739
+ };
740
+ const list = (key: string, what: string): readonly unknown[] => {
741
+ const items = here[key];
742
+ if (Array.isArray(items)) return items;
743
+ issue(`The value of ${key} is a list of ${what}.`, [...at, key]);
744
+ return [];
745
+ };
746
+ if (term === "list") part(here["list"], [...at, "list"]);
747
+ if (term === "union") list("union", "value types").forEach((each, index) => part(each, [...at, "union", index]));
748
+ if (term === "one") list("one", "JSON values");
749
+ if (term === "fn") {
750
+ list("fn", "value types").forEach((each, index) => part(each, [...at, "fn", index]));
751
+ if (Object.hasOwn(here, "to")) part(here["to"], [...at, "to"]);
752
+ else issue("A function type has the key to: the type of its result.");
753
+ }
754
+ if (term === "record") {
755
+ const fields = here["record"];
756
+ if (isRecord(fields)) for (const key of Object.keys(fields)) part(fields[key], [...at, "record", key]);
757
+ else issue("The value of record is a record of value types.", [...at, "record"]);
758
+ if (Object.hasOwn(here, "optional")) {
759
+ list("optional", "field names").forEach((name, index) => {
760
+ if (typeof name !== "string" || !isRecord(fields) || !Object.hasOwn(fields, name)) issue(`The optional field ${String(name)} is not a field of the record.`, [...at, "optional", index]);
761
+ });
762
+ }
763
+ if (Object.hasOwn(here, "closed") && typeof here["closed"] !== "boolean") issue("The value of closed is true or false.", [...at, "closed"]);
764
+ }
765
+ for (let index = parts.length - 1; index >= 0; index -= 1) stack.push(parts[index] as Pending);
766
+ }
767
+ return issues;
768
+ }