@mark1russell7/coeval 0.16.0 → 0.17.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/src/types.ts CHANGED
@@ -59,6 +59,23 @@ export interface Domain {
59
59
  * standard extension `ext.domain`. A domain without a codec has no such document.
60
60
  */
61
61
  readonly codec?: DomainCodec;
62
+ /**
63
+ * The full list of the members of the domain, when the domain holds few values, for example `true` and `false` of
64
+ * `bool` (TYPE.FITS-FINITE). `fits` reads it: a base type with a list fits a type that holds each member of the list.
65
+ * The language keeps the list complete: each member of the domain is in the list. A value that fails a test of the
66
+ * domain does not count. The check reads a list of at most 64 values.
67
+ */
68
+ readonly values?: readonly unknown[];
69
+ /**
70
+ * The covers of the domain (TYPE.FITS-COVER, D85). A cover is a list of domains under this domain, and its domains
71
+ * together hold each member of this domain. An example is a domain of negative numbers and a domain of the other
72
+ * numbers as a cover of `number`. `fits` reads it: a base type fits a union when each domain of a cover fits it.
73
+ *
74
+ * A split of a record also narrows a field of the domain to each domain of a cover (D90).
75
+ * The language keeps each cover true, and `fits` trusts it, as it trusts `values`. A cover that is empty, or that
76
+ * names this domain or a name that is not under it, does not count.
77
+ */
78
+ readonly covers?: readonly (readonly string[])[];
62
79
  }
63
80
 
64
81
  /**
@@ -94,10 +111,10 @@ export interface TypeIssue {
94
111
  * A machine that does not name one of these domains gets it from this table.
95
112
  */
96
113
  export const STD_DOMAINS: Readonly<Record<string, Domain>> = {
97
- null: { is: (value) => value === null, summary: "The value null." },
114
+ null: { is: (value) => value === null, values: [null], summary: "The value null." },
98
115
  // JSON has no undefined, thus a document holds it as ext.undefined (D48). The domain has no codec: the codec of values writes undefined first.
99
- undefined: { is: (value) => value === undefined, summary: "The value undefined. A document holds it as the extension ext.undefined." },
100
- bool: { is: (value) => typeof value === "boolean", summary: "A truth value: true or false." },
116
+ undefined: { is: (value) => value === undefined, values: [undefined], summary: "The value undefined. A document holds it as the extension ext.undefined." },
117
+ bool: { is: (value) => typeof value === "boolean", values: [true, false], summary: "A truth value: true or false." },
101
118
  number: { is: (value) => typeof value === "number", summary: "A number of the host, also NaN, an infinity and -0." },
102
119
  text: { is: (value) => typeof value === "string", summary: "A text." },
103
120
  };
@@ -105,6 +122,15 @@ export const STD_DOMAINS: Readonly<Record<string, Domain>> = {
105
122
  /** The deepest term that a value type can have. A deeper term reads as `unknown`, thus a check stays total. */
106
123
  const MAX_TYPE_DEPTH = 256;
107
124
 
125
+ /** The most values that the finite rule of `fits` reads for one type (TYPE.FITS-FINITE). A type with more values is not finite for the rule. */
126
+ const MAX_FINITE_VALUES = 64;
127
+
128
+ /** The most domains of covers that the cover rule of `fits` tests for one type (TYPE.FITS-COVER). A larger test gives `false`. */
129
+ const MAX_COVER_STEPS = 64;
130
+
131
+ /** The most records that one split of `fits` builds, with each split inside its work (TYPE.FITS-PATH, D89). A larger split gives `false`. */
132
+ const MAX_SPLIT_RECORDS = 64;
133
+
108
134
  /** The keys that each term permits beside its own key. */
109
135
  const TERM_KEYS: Readonly<Record<string, readonly string[]>> = {
110
136
  list: ["list"],
@@ -269,6 +295,7 @@ function recordFits(
269
295
  domains: Readonly<Record<string, Domain>> | undefined,
270
296
  depth: number,
271
297
  gradual: boolean,
298
+ budget: SplitBudget | undefined,
272
299
  ): boolean {
273
300
  if (b.closed && (!a.closed || Object.keys(a.fields).some((key) => !Object.hasOwn(b.fields, key)))) return false;
274
301
  for (const key of Object.keys(b.fields)) {
@@ -276,7 +303,7 @@ function recordFits(
276
303
  const needed = !b.optional.has(key);
277
304
  if (Object.hasOwn(a.fields, key)) {
278
305
  if (needed && a.optional.has(key)) return false;
279
- if (!fitsAt(a.fields[key], want, domains, depth, gradual)) return false;
306
+ if (!fitsAt(a.fields[key], want, domains, depth, gradual, true, budget)) return false;
280
307
  } else if (needed) {
281
308
  return false;
282
309
  } else if (!a.closed && formOf(want, domains).t !== "top") {
@@ -287,27 +314,403 @@ function recordFits(
287
314
  return true;
288
315
  }
289
316
 
317
+ /** This function adds a value to a list of distinct values, by `docEquals`. */
318
+ function addValue(out: unknown[], value: unknown): void {
319
+ if (!out.some((item) => docEquals(item, value))) out.push(value);
320
+ }
321
+
322
+ /**
323
+ * This function gives the values of a base type with a finite list. It reads the list of the first domain of the line
324
+ * with at most 64 values, and it keeps only the members of the type. Thus the list of a parent serves each domain under it.
325
+ */
326
+ function baseValues(name: string, domains: Readonly<Record<string, Domain>> | undefined): readonly unknown[] | undefined {
327
+ const domain = domainOf(name, domains);
328
+ if (domain === undefined) return undefined;
329
+ const line = Array.isArray(domain.values) || !Array.isArray(domain.under) ? [name] : lineOf(name, domains);
330
+ for (const each of line) {
331
+ const values: unknown = domainOf(each, domains)?.values;
332
+ if (!Array.isArray(values) || values.length > MAX_FINITE_VALUES) continue;
333
+ const out: unknown[] = [];
334
+ // A hole of the list holds undefined, thus a loop by index reads it too.
335
+ for (let index = 0; index < values.length; index += 1) if (inDomain(values[index], name, domains)) addValue(out, values[index]);
336
+ return out;
337
+ }
338
+ return undefined;
339
+ }
340
+
341
+ /** This function gives a copy of a record with one more field. */
342
+ function withField(record: Readonly<Record<string, unknown>>, key: string, value: unknown): Record<string, unknown> {
343
+ const out: Record<string, unknown> = {};
344
+ for (const name of Object.keys(record)) setOwn(out, name, record[name]);
345
+ setOwn(out, key, value);
346
+ return out;
347
+ }
348
+
349
+ /**
350
+ * This function gives the members of a record type, when they are few. A record with a needed field without members
351
+ * has no members. Only a closed record with a finite list for each field has a finite list.
352
+ */
353
+ function recordValues(form: Extract<Form, { readonly t: "record" }>, domains: Readonly<Record<string, Domain>> | undefined, depth: number): readonly unknown[] | undefined {
354
+ let out: readonly Record<string, unknown>[] = [{}];
355
+ let finite = form.closed;
356
+ for (const key of Object.keys(form.fields)) {
357
+ const values = valuesOf(form.fields[key], domains, depth);
358
+ const optional = form.optional.has(key);
359
+ if (values === undefined) {
360
+ finite = false;
361
+ continue;
362
+ }
363
+ if (values.length === 0 && !optional) return [];
364
+ if (!finite) continue;
365
+ if (out.length * (values.length + (optional ? 1 : 0)) > MAX_FINITE_VALUES) {
366
+ // The list is too long, but a later needed field without members can still empty the record.
367
+ finite = false;
368
+ continue;
369
+ }
370
+ out = out.flatMap((record) => [...(optional ? [record] : []), ...values.map((value) => withField(record, key, value))]);
371
+ }
372
+ return finite ? out : undefined;
373
+ }
374
+
375
+ /**
376
+ * This function gives a list of the members of a type, when the type has at most 64 members (TYPE.FITS-FINITE). Each
377
+ * other type gives `undefined`. Such a type is `unknown`, a base type without a finite list, or a function type. A
378
+ * list that can hold an item and an open record with members are also such types.
379
+ */
380
+ function valuesOf(type: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number): readonly unknown[] | undefined {
381
+ if (depth > MAX_TYPE_DEPTH) return undefined;
382
+ const form = formOf(type, domains);
383
+ const next = depth + 1;
384
+ switch (form.t) {
385
+ case "bottom":
386
+ return [];
387
+ case "base":
388
+ return baseValues(form.name, domains);
389
+ case "list":
390
+ return valuesOf(form.item, domains, next)?.length === 0 ? [[]] : undefined;
391
+ case "record":
392
+ return recordValues(form, domains, next);
393
+ case "one":
394
+ case "union": {
395
+ const out: unknown[] = [];
396
+ const items: readonly unknown[] = form.t === "one" ? form.values : form.members;
397
+ for (const item of items) {
398
+ const values = form.t === "one" ? [item] : valuesOf(item, domains, next);
399
+ if (values === undefined) return undefined;
400
+ for (const value of values) addValue(out, value);
401
+ if (out.length > MAX_FINITE_VALUES) return undefined;
402
+ }
403
+ return out;
404
+ }
405
+ default:
406
+ return undefined;
407
+ }
408
+ }
409
+
410
+ /** This function gives the members of a type at one depth, for `membersOf`. */
411
+ function membersAt(type: unknown, domains: Readonly<Record<string, Domain>> | undefined, samples: readonly unknown[], depth: number): readonly unknown[] {
412
+ const finite = valuesOf(type, domains, depth);
413
+ if (finite !== undefined) return finite;
414
+ const form = formOf(type, domains);
415
+ if (form.t !== "union" || depth > MAX_TYPE_DEPTH) return samples.filter((sample) => member(sample, type as ValueType, domains));
416
+ const out: unknown[] = [];
417
+ for (const item of form.members) for (const value of membersAt(item, domains, samples, depth + 1)) addValue(out, value);
418
+ return out;
419
+ }
420
+
421
+ /**
422
+ * This function gives members of a type for a test of a declaration (D86). A type with at most 64 members gives each
423
+ * member, from the lists `values` of its domains (TYPE.FITS-FINITE). A union gives the members of each of its types.
424
+ * Each other type gives the samples that are its members. The kernel does not export the function.
425
+ *
426
+ * @param type - The type.
427
+ * @param domains - The domains of the machine.
428
+ * @param samples - The values to test when the type has no full list of members.
429
+ * @returns Distinct members of the type.
430
+ */
431
+ export function membersOf(type: ValueType, domains: Readonly<Record<string, Domain>> | undefined, samples: readonly unknown[]): readonly unknown[] {
432
+ return membersAt(type, domains, samples, 0);
433
+ }
434
+
435
+ /** This function holds the rules of the shapes of `fits`: base types, lists, records and functions. */
436
+ function shapeFits(a: Form, b: Form, domains: Readonly<Record<string, Domain>> | undefined, next: number, gradual: boolean, budget: SplitBudget | undefined): boolean {
437
+ if (a.t === "base") return b.t === "base" && lineOf(a.name, domains).has(b.name);
438
+ if (a.t === "list") return b.t === "list" && fitsAt(a.item, b.item, domains, next, gradual, true, budget);
439
+ if (a.t === "record") return b.t === "record" && recordFits(a, b, domains, next, gradual, budget);
440
+ if (a.t !== "fn" || b.t !== "fn" || a.params.length > b.params.length) return false;
441
+ // A function fits when it accepts each argument of the expected type, and when its result fits.
442
+ return a.params.every((param, index) => fitsAt(b.params[index], param, domains, next, gradual, true, budget)) && fitsAt(a.to, b.to, domains, next, gradual, true, budget);
443
+ }
444
+
290
445
  /**
291
446
  * This function is the relation `fits` at one depth of two types. The order of its tests is the order of the rules of
292
447
  * `fits`. Without `gradual`, `unknown` is only the top: it fits only `unknown`. The join uses this strict form.
448
+ *
449
+ * When the rules of the shapes refuse a type with few members, the finite rule tests each member (TYPE.FITS-FINITE).
450
+ * The cover rule and the split rule come after it. Without `finite`, the call skips these three rules. The members of
451
+ * a union in `expected` get such calls, because the call for the union tests `found` against the full union.
452
+ *
453
+ * The budget is the budget of the outer split, when the call is part of the work of a split (TYPE.FITS-PATH, D89).
293
454
  */
294
- function fitsAt(found: unknown, expected: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number, gradual = true): boolean {
455
+ function fitsAt(
456
+ found: unknown,
457
+ expected: unknown,
458
+ domains: Readonly<Record<string, Domain>> | undefined,
459
+ depth: number,
460
+ gradual = true,
461
+ finite = true,
462
+ budget?: SplitBudget,
463
+ ): boolean {
295
464
  if (depth > MAX_TYPE_DEPTH) return true;
296
465
  const a = formOf(found, domains);
297
466
  const b = formOf(expected, domains);
298
467
  const next = depth + 1;
299
468
  if (b.t === "top" || a.t === "bottom") return true;
300
469
  if (a.t === "top") return gradual;
301
- if (a.t === "union") return a.members.every((item) => fitsAt(item, expected, domains, next, gradual));
470
+ if (a.t === "union") return a.members.every((item) => fitsAt(item, expected, domains, next, gradual, true, budget));
302
471
  if (a.t === "one") return a.values.every((item) => memberAt(item, expected, domains, next));
303
- if (b.t === "bottom") return false;
304
- if (b.t === "union") return b.members.some((item) => fitsAt(found, item, domains, next, gradual));
305
- if (a.t === "base") return b.t === "base" && lineOf(a.name, domains).has(b.name);
306
- if (a.t === "list") return b.t === "list" && fitsAt(a.item, b.item, domains, next, gradual);
307
- if (a.t === "record") return b.t === "record" && recordFits(a, b, domains, next, gradual);
308
- if (b.t !== "fn" || a.params.length > b.params.length) return false;
309
- // A function fits when it accepts each argument of the expected type, and when its result fits.
310
- return a.params.every((param, index) => fitsAt(b.params[index], param, domains, next, gradual)) && fitsAt(a.to, b.to, domains, next, gradual);
472
+ if (b.t === "union" ? b.members.some((item) => fitsAt(found, item, domains, next, gradual, false, budget)) : b.t !== "bottom" && shapeFits(a, b, domains, next, gradual, budget)) return true;
473
+ if (!finite || a.t === "fn") return false;
474
+ // The finite rule: a type with few members fits when each member is a member of `expected`.
475
+ const values = valuesOf(found, domains, next);
476
+ if (values !== undefined) return values.every((value) => memberAt(value, expected, domains, next));
477
+ // The cover rule: a base type fits when each domain of a cover of its line fits (D85).
478
+ if (a.t === "base") return (b.t === "union" || b.t === "one") && lineCovered(a.name, expected, domains, depth, gradual, undefined);
479
+ // The split rule: an open record, or a closed record with too many members, splits over a union or a `one` (D84, D89).
480
+ return a.t === "record" && (b.t === "union" || (b.t === "one" && a.closed)) && splitFits(found, a, expected, domains, depth, gradual, budget);
481
+ }
482
+
483
+ /** The state of the cover rule for one type: the result for each domain whose covers it tested, and the steps that are left. */
484
+ interface CoverState {
485
+ readonly held: Map<string, boolean>;
486
+ left: number;
487
+ }
488
+
489
+ /**
490
+ * This function is the cover rule of `fits` (TYPE.FITS-COVER, D85). A base type fits when a domain of its line has a
491
+ * cover, and each domain of the cover fits `expected`. The function makes the state at the first domain with covers.
492
+ */
493
+ function lineCovered(name: string, expected: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number, gradual: boolean, state: CoverState | undefined): boolean {
494
+ let now = state;
495
+ for (const each of lineOf(name, domains)) {
496
+ if (!Array.isArray(domainOf(each, domains)?.covers)) continue;
497
+ now ??= { held: new Map(), left: MAX_COVER_STEPS };
498
+ if (covered(each, expected, domains, depth, gradual, now)) return true;
499
+ }
500
+ return false;
501
+ }
502
+
503
+ /**
504
+ * This function tells if a cover of one domain holds the domain in `expected`. A cover that is empty, or that names
505
+ * the domain or a name that is not under it, does not count. Each domain of a cover costs one step, and the rule
506
+ * stops after 64 steps.
507
+ */
508
+ function covered(name: string, expected: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number, gradual: boolean, state: CoverState): boolean {
509
+ const known = state.held.get(name);
510
+ if (known !== undefined) return known;
511
+ // A domain on the way gives false until its cover holds, thus a cycle of covers ends.
512
+ state.held.set(name, false);
513
+ const covers: unknown = domainOf(name, domains)?.covers;
514
+ if (!Array.isArray(covers)) return false;
515
+ for (const cover of covers as readonly unknown[]) {
516
+ if (!Array.isArray(cover) || cover.length === 0) continue;
517
+ let held = true;
518
+ for (const part of cover as readonly unknown[]) {
519
+ state.left -= 1;
520
+ if (state.left < 0) return false;
521
+ if (typeof part !== "string" || part === name || !lineOf(part, domains).has(name) || !partFits(part, expected, domains, depth, gradual, state)) {
522
+ held = false;
523
+ break;
524
+ }
525
+ }
526
+ if (held) {
527
+ state.held.set(name, true);
528
+ return true;
529
+ }
530
+ }
531
+ return false;
532
+ }
533
+
534
+ /** This function tells if a domain of a cover fits `expected`. It tries the rules of the shapes, the finite rule and its own covers. */
535
+ function partFits(part: string, expected: unknown, domains: Readonly<Record<string, Domain>> | undefined, depth: number, gradual: boolean, state: CoverState): boolean {
536
+ if (fitsAt(part, expected, domains, depth, gradual, false)) return true;
537
+ const values = valuesOf(part, domains, depth + 1);
538
+ if (values !== undefined) return values.every((value) => memberAt(value, expected, domains, depth + 1));
539
+ return lineCovered(part, expected, domains, depth, gradual, state);
540
+ }
541
+
542
+ /**
543
+ * A field that the split rule can split: the path of keys to it, its cases, and if the field is optional. A value
544
+ * split has the members of the field type. A type split has case types: the domains of a cover, or the members of a
545
+ * union (TYPE.FITS-SPLIT-TYPE, D90).
546
+ */
547
+ interface Split {
548
+ readonly path: readonly string[];
549
+ readonly values?: readonly unknown[];
550
+ readonly types?: readonly ValueType[];
551
+ readonly optional: boolean;
552
+ }
553
+
554
+ /** The records that a split can still build. The outer split makes it, and each split inside its work takes from it (D89). */
555
+ interface SplitBudget {
556
+ left: number;
557
+ }
558
+
559
+ /**
560
+ * This function gives the fields that the split rule can split, in the order of the split (TYPE.FITS-PATH, D89). The
561
+ * fields of the record come first, in the order of the keys. Then the paths of the next depth follow, and so on. A path
562
+ * goes into a field only when the field is a record without a finite list. A field with a finite list of two or more
563
+ * cases is a split. An optional field also has the absent case.
564
+ *
565
+ * The type splits follow each value split, in the same order (D90). A base type with a valid cover in its line splits
566
+ * into the domains of the cover. A union without a finite list splits into its members. The prefix is the path of `a`.
567
+ */
568
+ function splitsOf(
569
+ a: Extract<Form, { readonly t: "record" }>,
570
+ domains: Readonly<Record<string, Domain>> | undefined,
571
+ depth: number,
572
+ prefix: readonly string[] = [],
573
+ ): readonly Split[] {
574
+ const out: Split[] = [];
575
+ const later: Split[] = [];
576
+ let level: { readonly path: readonly string[]; readonly form: Extract<Form, { readonly t: "record" }> }[] = [{ path: prefix, form: a }];
577
+ for (let at = depth + 1; level.length > 0 && at <= MAX_TYPE_DEPTH; at += 1) {
578
+ const deeper: typeof level = [];
579
+ for (const { path, form } of level) {
580
+ for (const key of Object.keys(form.fields)) {
581
+ const values = valuesOf(form.fields[key], domains, at);
582
+ const optional = form.optional.has(key);
583
+ if (values !== undefined) {
584
+ if (values.length + (optional ? 1 : 0) > 1) out.push({ path: [...path, key], values, optional });
585
+ continue;
586
+ }
587
+ const inner = formOf(form.fields[key], domains);
588
+ if (inner.t === "record") deeper.push({ path: [...path, key], form: inner });
589
+ else if (inner.t === "union") later.push({ path: [...path, key], types: inner.members, optional });
590
+ else if (inner.t === "base") {
591
+ const cover = coverOf(inner.name, domains, false);
592
+ if (cover !== undefined) later.push({ path: [...path, key], types: cover, optional });
593
+ }
594
+ }
595
+ }
596
+ level = deeper;
597
+ }
598
+ return later.length === 0 ? out : [...out, ...later];
599
+ }
600
+
601
+ /** This function tells if a cover of a domain is valid by the line rule of D85. Such a cover is not empty, and each name in it is a domain under the domain. */
602
+ function validCover(name: string, cover: unknown, domains: Readonly<Record<string, Domain>> | undefined): cover is readonly string[] {
603
+ return Array.isArray(cover) && cover.length > 0 && (cover as readonly unknown[]).every((part) => typeof part === "string" && part !== name && lineOf(part, domains).has(name));
604
+ }
605
+
606
+ /**
607
+ * This function gives the cover of a base type for a type split (D90). The cover is the first valid cover of the first
608
+ * domain in the line with a valid cover. With `own`, only the domain of the type itself counts. Each other type gives
609
+ * `undefined`.
610
+ */
611
+ function coverOf(name: string, domains: Readonly<Record<string, Domain>> | undefined, own: boolean): readonly string[] | undefined {
612
+ for (const each of own ? [name] : lineOf(name, domains)) {
613
+ const covers: unknown = domainOf(each, domains)?.covers;
614
+ if (!Array.isArray(covers)) continue;
615
+ for (const cover of covers as readonly unknown[]) if (validCover(each, cover, domains)) return cover;
616
+ }
617
+ return undefined;
618
+ }
619
+
620
+ /**
621
+ * This function gives the splits of a case of a type split, at the path of the case (D90). A case with a finite list
622
+ * splits by its members. A base type splits by a cover of its own domain, because the covers of its parents hold it
623
+ * already. A union splits by its members, and a record splits by the paths into its fields.
624
+ */
625
+ function againOf(type: ValueType, path: readonly string[], domains: Readonly<Record<string, Domain>> | undefined, depth: number): readonly Split[] {
626
+ const at = depth + path.length;
627
+ const values = valuesOf(type, domains, at);
628
+ if (values !== undefined) return values.length > 1 ? [{ path, values, optional: false }] : [];
629
+ const form = formOf(type, domains);
630
+ if (form.t === "record") return splitsOf(form, domains, at, path);
631
+ if (form.t === "union") return [{ path, types: form.members, optional: false }];
632
+ const cover = form.t === "base" ? coverOf(form.name, domains, true) : undefined;
633
+ return cover === undefined ? [] : [{ path, types: cover, optional: false }];
634
+ }
635
+
636
+ /**
637
+ * This function gives a copy of a record type with one field narrowed at a path. A case type replaces the field. The
638
+ * absent case removes the field of a closed record, and it gives an optional field of type `never` in an open record.
639
+ */
640
+ function narrowAt(type: unknown, path: readonly string[], at: number, narrowed: ValueType | undefined, domains: Readonly<Record<string, Domain>> | undefined): ValueType {
641
+ const form = formOf(type, domains);
642
+ const key = path[at];
643
+ if (form.t !== "record" || key === undefined) return type as ValueType;
644
+ const last = at + 1 === path.length;
645
+ const fields: Record<string, ValueType> = {};
646
+ for (const name of Object.keys(form.fields)) {
647
+ if (name !== key) setOwn(fields, name, form.fields[name] as ValueType);
648
+ else if (!last) setOwn(fields, name, narrowAt(form.fields[name], path, at + 1, narrowed, domains));
649
+ else if (narrowed !== undefined) setOwn(fields, name, narrowed);
650
+ else if (!form.closed) setOwn(fields, name, "never");
651
+ }
652
+ // A case at the field makes it needed. The absent case of an open record keeps it optional.
653
+ const optional = [...form.optional].filter((name) => !last || name !== key || (narrowed === undefined && !form.closed));
654
+ return { record: fields, ...(optional.length === 0 ? {} : { optional }), ...(form.closed ? { closed: true } : {}) };
655
+ }
656
+
657
+ /**
658
+ * This function is the split rule of `fits` (TYPE.FITS-FINITE, D84, TYPE.FITS-PATH, D89). A record type fits when each
659
+ * record of its split fits. A split takes the first field in the order of `splitsOf`. Each member of the field gives a
660
+ * record with the field narrowed to that member. An optional field also gives a record without the field. A type split
661
+ * narrows the field to each case type instead (TYPE.FITS-SPLIT-TYPE, D90).
662
+ *
663
+ * A record of the split that does not fit splits the next such field. A record of a type split first splits its case
664
+ * again, at the same path. One budget of 64 records serves the outer split and each split inside its work, at each
665
+ * depth. Thus nested splits do not multiply the work.
666
+ */
667
+ function splitFits(
668
+ found: unknown,
669
+ a: Extract<Form, { readonly t: "record" }>,
670
+ expected: unknown,
671
+ domains: Readonly<Record<string, Domain>> | undefined,
672
+ depth: number,
673
+ gradual: boolean,
674
+ budget: SplitBudget | undefined,
675
+ ): boolean {
676
+ const splits = splitsOf(a, domains, depth);
677
+ if (splits.length === 0) return false;
678
+ return casesFit(found as ValueType, a.closed, splits, 0, expected, domains, depth, gradual, budget ?? { left: MAX_SPLIT_RECORDS });
679
+ }
680
+
681
+ /** This function tells if each record of the split of one field fits `expected`. It splits the next field for a record that does not fit. */
682
+ function casesFit(
683
+ type: ValueType,
684
+ closed: boolean,
685
+ splits: readonly Split[],
686
+ index: number,
687
+ expected: unknown,
688
+ domains: Readonly<Record<string, Domain>> | undefined,
689
+ depth: number,
690
+ gradual: boolean,
691
+ budget: SplitBudget,
692
+ ): boolean {
693
+ const split = splits[index];
694
+ if (split === undefined) return false;
695
+ const types: readonly ValueType[] = split.values?.map((value) => ({ one: [value as Json] })) ?? split.types ?? [];
696
+ const cases: ValueType[] = types.map((each) => narrowAt(type, split.path, 0, each, domains));
697
+ if (split.optional) cases.push(narrowAt(type, split.path, 0, undefined, domains));
698
+ for (const [at, record] of cases.entries()) {
699
+ budget.left -= 1;
700
+ if (budget.left < 0) return false;
701
+ // The rules of the shapes first, then the finite rule for a closed record, then the split of the next field.
702
+ if (fitsAt(record, expected, domains, depth, gradual, false, budget)) continue;
703
+ const values = closed ? valuesOf(record, domains, depth + 1) : undefined;
704
+ if (values !== undefined) {
705
+ if (values.every((value) => memberAt(value, expected, domains, depth + 1))) continue;
706
+ return false;
707
+ }
708
+ // A case of a type split splits again at its path, before the next split (D90).
709
+ const again = split.types !== undefined && at < split.types.length ? againOf(split.types[at] as ValueType, split.path, domains, depth) : [];
710
+ const rest = again.length === 0 ? splits : [...again, ...splits.slice(index + 1)];
711
+ if (!casesFit(record, closed, rest, again.length === 0 ? index + 1 : 0, expected, domains, depth, gradual, budget)) return false;
712
+ }
713
+ return true;
311
714
  }
312
715
 
313
716
  /**
@@ -325,9 +728,33 @@ function fitsAt(found: unknown, expected: unknown, domains: Readonly<Record<stri
325
728
  * field is `unknown`.
326
729
  * - A function fits when it has the same or fewer parameters, each expected parameter fits its parameter, and its
327
730
  * result fits.
731
+ * - The finite rule comes after these rules (TYPE.FITS-FINITE). A type with at most 64 members fits when each member
732
+ * is a member of `expected`. Thus `bool` fits `{ "one": [true, false] }`.
733
+ * - A base type has few members when its domain or a parent declares `values`, as `null`, `undefined` and `bool` do.
734
+ * A union, a `one`, a closed record of such types and a list of `never` also have few members.
735
+ * - The cover rule comes next (TYPE.FITS-COVER, D85). A base type fits a union or a `one` when each domain of a cover
736
+ * fits. The cover is in `covers` of a domain of its line. Each domain of a cover is under the covered
737
+ * domain. Thus a cover of `number` by `neg` and `nonneg` gives `number` a fit against `neg | nonneg`.
738
+ * - The split rule comes last (D84). An open record, or a closed record with more than 64 members, splits a field with
739
+ * few members over a union. A closed record also splits over a `one`. Each record of the split narrows the field to
740
+ * one member, and each record must fit.
741
+ * Thus `{ "record": { "a": "bool" } }` fits a union of a record for each value of `a`.
742
+ * - A split also narrows a field of a record in a field, by its path (TYPE.FITS-PATH, D89). The fields of the record
743
+ * split first, then the paths of each next depth, in the order of the keys. A path goes into a record without a
744
+ * finite list. Thus `{a: {x: bool}}` fits the union of `{a: {x: true}}` and `{a: {x: false}}`.
745
+ * - A type split comes after each value split (TYPE.FITS-SPLIT-TYPE, D90). A field of a base type with a valid cover in
746
+ * its line splits into the domains of the first such cover. A field of a union without a finite list splits into its
747
+ * members. A case that does not fit splits again at its path, by the same rules. A base type there uses only a
748
+ * cover of its own domain. Thus `{a: number}` fits `{a: neg} | {a: nonneg}` when `neg` and `nonneg` cover `number`.
749
+ *
750
+ * The relation is sound: a member of `found` is a member of `expected` when no `unknown` is in the types
751
+ * (TYPE.FITS-SOUND). The finite rule trusts `values`, and the cover rule trusts `covers`. Thus a list without a member
752
+ * or a false cover makes the relation unsound for that domain.
328
753
  *
329
- * The relation is sound: a member of `found` is a member of `expected` when no `unknown` is in the types. It is not
330
- * complete. For example, `bool` does not fit `{ "one": [true, false] }`. The function does not throw.
754
+ * The relation is not complete. A split builds at most 64 records, with each split inside its work at each depth. A
755
+ * type split uses one cover of a field, not each cover of its line. A domain without `values` gets no finite rule, and
756
+ * neither does a type with more than 64 members that is not a record. The cover rule tests at most 64 domains of
757
+ * covers. The function does not throw.
331
758
  *
332
759
  * @param found - The type that a part has.
333
760
  * @param expected - The type that the position needs.