@mark1russell7/coeval 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/build/index.d.ts +302 -98
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3555 -2068
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/codec.ts +30 -10
- package/src/conform.ts +35 -9
- package/src/durable.ts +17 -5
- package/src/evaluate.ts +38 -2
- package/src/facts.ts +55 -19
- package/src/formula.ts +59 -4
- package/src/index.ts +16 -2
- package/src/instances.ts +637 -0
- package/src/merge.ts +6 -3
- package/src/reads.ts +186 -2
- package/src/reference.ts +20 -1
- package/src/replay.ts +123 -35
- package/src/static.ts +357 -50
- package/src/stdlib.ts +127 -6
- package/src/types.ts +103 -23
- package/src/typing.ts +143 -3
- package/src/world.ts +49 -0
package/src/stdlib.ts
CHANGED
|
@@ -236,7 +236,7 @@ export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
|
|
|
236
236
|
const found = resolve(data as readonly Json[]);
|
|
237
237
|
return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
|
|
238
238
|
},
|
|
239
|
-
"tree.node": (input) => treeNode(input),
|
|
239
|
+
"tree.node": (input) => treeNode(input, undefined),
|
|
240
240
|
};
|
|
241
241
|
|
|
242
242
|
/*
|
|
@@ -316,6 +316,57 @@ interface NodeState {
|
|
|
316
316
|
json(path: Set<object>): Result;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
/** The answer of the ladder for one attribute of one node (TREE.CELL, D108). */
|
|
320
|
+
export interface AttributeAnswer {
|
|
321
|
+
/** The result of the attribute. It is `undefined` when no rung answered. */
|
|
322
|
+
readonly result: Result | undefined;
|
|
323
|
+
/** The rung that answered. It is `undefined` when no rung answered. */
|
|
324
|
+
readonly by: AttributeRung | undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* This function gives the memo cell of one attribute of one node, for a host that keeps its cells between evaluations
|
|
329
|
+
* (TREE.CELL, D108). The kit uses it at each demand of an attribute in an evaluation.
|
|
330
|
+
*
|
|
331
|
+
* The array `place` is one object for each document of a plan, and it stays the same across the evaluations of that
|
|
332
|
+
* plan. A new plan gives new arrays. Thus a host keeps its cells by the identity of `place` and by `attribute`.
|
|
333
|
+
* A host that keeps its cells by the keys of `place` can give a stale answer after a change of the document.
|
|
334
|
+
*
|
|
335
|
+
* The function gives the answer of `compute`, or a kept answer. A host with signals gives a computed signal: a change
|
|
336
|
+
* of its store then makes only the attributes that read the store compute again. Each demand gives a new `compute`, and
|
|
337
|
+
* a cell that computes again uses the last one.
|
|
338
|
+
*
|
|
339
|
+
* The kit marks an attribute in evaluation before it uses the cell. Thus a cycle gives `#CYCLE!` before it reaches the
|
|
340
|
+
* cell again. An exception of the function that `compute` did not throw gives `#CYCLE!` with the chain of the demands.
|
|
341
|
+
*
|
|
342
|
+
* @param place - The keys from the root document to the document of the node.
|
|
343
|
+
* @param attribute - The name of the attribute.
|
|
344
|
+
* @param compute - The function that visits the rungs of the ladder.
|
|
345
|
+
* @returns The answer.
|
|
346
|
+
*/
|
|
347
|
+
export type AttributeCell = (place: readonly Key[], attribute: string, compute: () => AttributeAnswer) => AttributeAnswer;
|
|
348
|
+
|
|
349
|
+
/** The options of the tree kit (D108). */
|
|
350
|
+
export interface TreeOptions {
|
|
351
|
+
/**
|
|
352
|
+
* The memo cells of the host (TREE.CELL). The default is a plain memo for each node and attribute, for one
|
|
353
|
+
* evaluation (TREE.MEMO).
|
|
354
|
+
*/
|
|
355
|
+
readonly cell?: AttributeCell;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* This function gives the code of the kind of a node of the decorated tree, with the options of the host. A host puts
|
|
360
|
+
* the code at the name `tree.node` of its machine. Without a cell, the code is the standard code.
|
|
361
|
+
*
|
|
362
|
+
* @param options - The options, for example the memo cells of the host.
|
|
363
|
+
* @returns The code.
|
|
364
|
+
*/
|
|
365
|
+
export function treeNodeEvaluator(options: TreeOptions = {}): Evaluator {
|
|
366
|
+
const host = options.cell;
|
|
367
|
+
return host === undefined ? (STD_EVALUATORS["tree.node"] as Evaluator) : (input: EvalInput) => treeNode(input, host);
|
|
368
|
+
}
|
|
369
|
+
|
|
319
370
|
/**
|
|
320
371
|
* The memo cell of one attribute: in evaluation, or done with its result and the rung that answered. A done cell
|
|
321
372
|
* without a result tells that no rung answered.
|
|
@@ -373,6 +424,31 @@ function contractOf(machine: Machine | undefined, typePath: readonly Key[]): Nod
|
|
|
373
424
|
|
|
374
425
|
const BUSY: Cell = /* @__PURE__ */ Object.freeze({ busy: true });
|
|
375
426
|
|
|
427
|
+
/** The answer of the ladder when no rung answers. */
|
|
428
|
+
const NO_ANSWER: AttributeAnswer = /* @__PURE__ */ Object.freeze({ result: undefined, by: undefined });
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The marks of the attributes in evaluation, for each place of a plan, with the cells of a host (TREE.CELL). Nodes of
|
|
432
|
+
* two evaluations share their place and their kept cells, thus they share their marks.
|
|
433
|
+
*/
|
|
434
|
+
const marksByPlace = /* @__PURE__ */ new WeakMap<readonly Key[], Map<string, Cell>>();
|
|
435
|
+
|
|
436
|
+
/** The trees of the cells of a host, for each place of a root in a plan. Nodes of two evaluations stay in one tree. */
|
|
437
|
+
const buildsByPlace = /* @__PURE__ */ new WeakMap<readonly Key[], TreeBuild>();
|
|
438
|
+
|
|
439
|
+
/** This function gives the value of a weak map, or puts a new value in it. */
|
|
440
|
+
function keptAt<T>(map: WeakMap<readonly Key[], T>, place: readonly Key[], make: () => T): T {
|
|
441
|
+
let value = map.get(place);
|
|
442
|
+
if (value === undefined) {
|
|
443
|
+
value = make();
|
|
444
|
+
map.set(place, value);
|
|
445
|
+
}
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** A value that no exception has. */
|
|
450
|
+
const NOT_THROWN: unique symbol = /* @__PURE__ */ Symbol("coeval.tree.not-thrown") as never;
|
|
451
|
+
|
|
376
452
|
/** One attribute in evaluation. The text names it in the reason of a cycle. */
|
|
377
453
|
interface Demand {
|
|
378
454
|
readonly cells: Map<string, Cell>;
|
|
@@ -451,8 +527,10 @@ function plainOf(value: unknown, path: Set<object>): Result {
|
|
|
451
527
|
* the node, an equation of its kind and an equation `down` of the kind of its parent. A copy from the parent, then a
|
|
452
528
|
* fold or a default, come last. A failed attribute throws `Failure` with its own error, thus a reference to it gives that error.
|
|
453
529
|
* The argument `outer` is the self of the context, that `treeNode` resolved. The node reads no root of the context directly (C4).
|
|
530
|
+
*
|
|
531
|
+
* The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of the node.
|
|
454
532
|
*/
|
|
455
|
-
function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined): HostThing {
|
|
533
|
+
function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined, host: AttributeCell | undefined): HostThing {
|
|
456
534
|
const { parts, context, kind: type, place, machine } = input;
|
|
457
535
|
// The equations evaluate in the evaluation that made the node, thus their reads go in its list (READS.EQUATION).
|
|
458
536
|
const runtime = (input as InputWithRuntime)[RUNTIME];
|
|
@@ -461,6 +539,8 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined)
|
|
|
461
539
|
// Only a node of the same tree is a parent for the rungs `parent` and `copy`.
|
|
462
540
|
const up = above !== undefined && above.build === build ? above : undefined;
|
|
463
541
|
const cells = new Map<string, Cell>();
|
|
542
|
+
// With the cells of a host, the marks of a place serve each node of the place (TREE.CELL).
|
|
543
|
+
const marks = host === undefined ? cells : keptAt(marksByPlace, place, () => new Map<string, Cell>());
|
|
464
544
|
/** The positions of the items of the list `children`, for the names of the children. */
|
|
465
545
|
let positions: { readonly list: readonly unknown[]; readonly at: Map<unknown, number> } | undefined;
|
|
466
546
|
|
|
@@ -505,7 +585,45 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined)
|
|
|
505
585
|
return undefined;
|
|
506
586
|
};
|
|
507
587
|
|
|
588
|
+
/**
|
|
589
|
+
* This function gives an attribute through the cell of the host (TREE.CELL). The cell is the memo. The node keeps
|
|
590
|
+
* the last answer only for `has` and `trace`.
|
|
591
|
+
*/
|
|
592
|
+
const through =
|
|
593
|
+
host === undefined
|
|
594
|
+
? undefined
|
|
595
|
+
: (key: string): Result | undefined => {
|
|
596
|
+
const text = attributeText(key, type, place);
|
|
597
|
+
if (marks.has(key)) return cycleAt(marks, key, text);
|
|
598
|
+
if (tree === undefined && up?.tree === undefined && !parts.some((item) => item.key === key)) return undefined;
|
|
599
|
+
marks.set(key, BUSY);
|
|
600
|
+
demands.push({ cells: marks, key, text });
|
|
601
|
+
let thrown: unknown = NOT_THROWN;
|
|
602
|
+
let answer: AttributeAnswer;
|
|
603
|
+
try {
|
|
604
|
+
answer = host(place, key, () => {
|
|
605
|
+
try {
|
|
606
|
+
return climb(key) ?? NO_ANSWER;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
thrown = error;
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
} catch (error) {
|
|
613
|
+
// An exception of the kit or of the stack goes on, as without a host. Another exception is a cycle of the host.
|
|
614
|
+
if (error === thrown) throw error;
|
|
615
|
+
const chain = demands.map((demand) => demand.text).join(" -> ");
|
|
616
|
+
answer = { result: fail("#CYCLE!", `The cells of the host found a cycle in the demands: ${chain}.`, { cause: error }), by: undefined };
|
|
617
|
+
} finally {
|
|
618
|
+
demands.pop();
|
|
619
|
+
marks.delete(key);
|
|
620
|
+
}
|
|
621
|
+
cells.set(key, { busy: false, result: answer.result, by: answer.by });
|
|
622
|
+
return answer.result;
|
|
623
|
+
};
|
|
624
|
+
|
|
508
625
|
const read = (key: string): Result | undefined => {
|
|
626
|
+
if (through !== undefined) return through(key);
|
|
509
627
|
const cell = cells.get(key);
|
|
510
628
|
if (cell !== undefined) return cell.busy ? cycleAt(cells, key, attributeText(key, type, place)) : cell.result;
|
|
511
629
|
// A node without a contract has only its parts, as before D42. Thus a missing part needs no cell.
|
|
@@ -635,14 +753,17 @@ const NO_HOPS = /* @__PURE__ */ Object.freeze({ hops: false });
|
|
|
635
753
|
* This function evaluates a node of the decorated tree. Inside an attribute of an open tree, the node gives its host
|
|
636
754
|
* thing, and no attribute evaluates. Else the node is the root of a new tree, and it gives the JSON of the tree. The
|
|
637
755
|
* node reads `self` through `resolve` (C4, D44). A machine that closes the root `self` thus gives no nested trees.
|
|
756
|
+
* The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of each node.
|
|
638
757
|
*/
|
|
639
|
-
function treeNode(input: EvalInput): Result {
|
|
758
|
+
function treeNode(input: EvalInput, host: AttributeCell | undefined): Result {
|
|
640
759
|
const found = input.resolve(SELF_PATH, NO_HOPS);
|
|
641
760
|
const outer = found.ok ? found.thing : undefined;
|
|
642
761
|
const above = stateOf(outer);
|
|
643
|
-
if (above !== undefined && above.build.open) return ok(treeThing(input, above.build, outer));
|
|
644
|
-
|
|
645
|
-
const
|
|
762
|
+
if (above !== undefined && above.build.open) return ok(treeThing(input, above.build, outer, host));
|
|
763
|
+
// With the cells of a host, a kept value can hold a node of an earlier evaluation. Thus that node stays in the tree.
|
|
764
|
+
const build: TreeBuild = host === undefined ? { open: true } : keptAt(buildsByPlace, input.place, () => ({ open: true }));
|
|
765
|
+
build.open = true;
|
|
766
|
+
const root = treeThing(input, build, outer, host);
|
|
646
767
|
try {
|
|
647
768
|
return (stateOf(root) as NodeState).json(new Set());
|
|
648
769
|
} finally {
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface Domain {
|
|
|
66
66
|
* domain does not count. The check reads a list of at most 64 values.
|
|
67
67
|
*/
|
|
68
68
|
readonly values?: readonly unknown[];
|
|
69
|
+
/**
|
|
70
|
+
* Some members of the domain for the tests of `conforms`, for example `0` and `-1` of `number` (D99). The checks
|
|
71
|
+
* `truth` and `cover` add the samples of each domain to the palette. A sample that fails a test of a type does not
|
|
72
|
+
* count. The list is not a full list of the members, thus `fits` does not read it.
|
|
73
|
+
*/
|
|
74
|
+
readonly samples?: readonly unknown[];
|
|
69
75
|
/**
|
|
70
76
|
* The covers of the domain (TYPE.FITS-COVER, D85). A cover is a list of domains under this domain, and its domains
|
|
71
77
|
* together hold each member of this domain. An example is a domain of negative numbers and a domain of the other
|
|
@@ -73,7 +79,7 @@ export interface Domain {
|
|
|
73
79
|
*
|
|
74
80
|
* A split of a record also narrows a field of the domain to each domain of a cover (D90).
|
|
75
81
|
* 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.
|
|
82
|
+
* names this domain or a name that is not under it, does not count. `conforms` tests each cover from samples (D94).
|
|
77
83
|
*/
|
|
78
84
|
readonly covers?: readonly (readonly string[])[];
|
|
79
85
|
}
|
|
@@ -115,7 +121,7 @@ export const STD_DOMAINS: Readonly<Record<string, Domain>> = {
|
|
|
115
121
|
// 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.
|
|
116
122
|
undefined: { is: (value) => value === undefined, values: [undefined], summary: "The value undefined. A document holds it as the extension ext.undefined." },
|
|
117
123
|
bool: { is: (value) => typeof value === "boolean", values: [true, false], summary: "A truth value: true or false." },
|
|
118
|
-
number: { is: (value) => typeof value === "number", summary: "A number of the host, also NaN, an infinity and -0." },
|
|
124
|
+
number: { is: (value) => typeof value === "number", samples: [0, -1], summary: "A number of the host, also NaN, an infinity and -0." },
|
|
119
125
|
text: { is: (value) => typeof value === "string", summary: "A text." },
|
|
120
126
|
};
|
|
121
127
|
|
|
@@ -199,8 +205,12 @@ function formOf(type: unknown, domains: Readonly<Record<string, Domain>> | undef
|
|
|
199
205
|
return { t: "record", fields: fields as Readonly<Record<string, ValueType>>, optional: new Set(names), closed };
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
/**
|
|
203
|
-
function
|
|
208
|
+
/**
|
|
209
|
+
* This function gives each base type that holds the members of a base type: the type itself and each parent, through
|
|
210
|
+
* each level. A cycle of parents ends at a name that the line has, and a parent that is not a domain ends its branch.
|
|
211
|
+
* The function is internal: `encodeValue` uses it too (D109).
|
|
212
|
+
*/
|
|
213
|
+
export function lineOf(name: string, domains: Readonly<Record<string, Domain>> | undefined): ReadonlySet<string> {
|
|
204
214
|
const seen = new Set<string>([name]);
|
|
205
215
|
const queue = [name];
|
|
206
216
|
while (queue.length > 0) {
|
|
@@ -421,7 +431,8 @@ function membersAt(type: unknown, domains: Readonly<Record<string, Domain>> | un
|
|
|
421
431
|
/**
|
|
422
432
|
* This function gives members of a type for a test of a declaration (D86). A type with at most 64 members gives each
|
|
423
433
|
* 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
|
|
434
|
+
* Each other type gives the samples that are its members. The samples of the domains come after the given samples (D99).
|
|
435
|
+
* The kernel does not export the function.
|
|
425
436
|
*
|
|
426
437
|
* @param type - The type.
|
|
427
438
|
* @param domains - The domains of the machine.
|
|
@@ -429,7 +440,61 @@ function membersAt(type: unknown, domains: Readonly<Record<string, Domain>> | un
|
|
|
429
440
|
* @returns Distinct members of the type.
|
|
430
441
|
*/
|
|
431
442
|
export function membersOf(type: ValueType, domains: Readonly<Record<string, Domain>> | undefined, samples: readonly unknown[]): readonly unknown[] {
|
|
432
|
-
return membersAt(type, domains, samples, 0);
|
|
443
|
+
return membersAt(type, domains, withDomainSamples(samples, domains), 0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* This function adds the samples of the domains to a palette (D99). It reads the list `samples` of each domain of the
|
|
448
|
+
* table and of each standard domain that the table does not replace. The result holds each value one time.
|
|
449
|
+
*/
|
|
450
|
+
function withDomainSamples(palette: readonly unknown[], domains: Readonly<Record<string, Domain>> | undefined): readonly unknown[] {
|
|
451
|
+
const out: unknown[] = [];
|
|
452
|
+
for (const value of palette) addValue(out, value);
|
|
453
|
+
const names = new Set([...Object.keys(STD_DOMAINS), ...(domains === undefined ? [] : Object.keys(domains))]);
|
|
454
|
+
for (const name of names) {
|
|
455
|
+
const samples: unknown = domainOf(name, domains)?.samples;
|
|
456
|
+
if (Array.isArray(samples)) for (const value of samples as readonly unknown[]) addValue(out, value);
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** A false cover that a sample shows (D94): a member of a covered domain that no domain of the cover holds. */
|
|
462
|
+
export interface FalseCover {
|
|
463
|
+
/** The name of the covered domain. */
|
|
464
|
+
readonly domain: string;
|
|
465
|
+
/** The cover: the names of its domains. */
|
|
466
|
+
readonly cover: readonly string[];
|
|
467
|
+
/** The sample. It is a member of the covered domain. */
|
|
468
|
+
readonly value: unknown;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* This function finds the false covers of a language from samples (D94). A cover is false when a sample of the covered
|
|
473
|
+
* domain is in no domain of the cover. The function tests each valid cover of each domain of the table. It ignores a
|
|
474
|
+
* cover that does not count by the line rule of D85, because `fits` ignores it too. It also ignores a sample outside
|
|
475
|
+
* the covered domain.
|
|
476
|
+
*
|
|
477
|
+
* The samples of the domains come after the given samples (D99). `conforms` and a test of `fits` use the function. The
|
|
478
|
+
* kernel does not export it.
|
|
479
|
+
*
|
|
480
|
+
* @param domains - The domains of the language, by their names.
|
|
481
|
+
* @param samples - The samples of each covered domain, by the name of the domain.
|
|
482
|
+
* @returns Each sample that breaks a cover, in the order of the domains, the covers and the samples.
|
|
483
|
+
*/
|
|
484
|
+
export function falseCovers(domains: Readonly<Record<string, Domain>>, samples: Readonly<Record<string, readonly unknown[]>>): readonly FalseCover[] {
|
|
485
|
+
const out: FalseCover[] = [];
|
|
486
|
+
for (const [name, domain] of Object.entries(domains)) {
|
|
487
|
+
const covers: unknown = domain.covers;
|
|
488
|
+
if (!Array.isArray(covers)) continue;
|
|
489
|
+
const values = withDomainSamples((Object.hasOwn(samples, name) ? samples[name] : undefined) ?? [], domains);
|
|
490
|
+
for (const cover of covers as readonly unknown[]) {
|
|
491
|
+
if (!validCover(name, cover, domains)) continue;
|
|
492
|
+
for (const value of values) {
|
|
493
|
+
if (member(value, name, domains) && !cover.some((part) => member(value, part, domains))) out.push({ domain: name, cover, value });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
433
498
|
}
|
|
434
499
|
|
|
435
500
|
/** This function holds the rules of the shapes of `fits`: base types, lists, records and functions. */
|
|
@@ -542,12 +607,13 @@ function partFits(part: string, expected: unknown, domains: Readonly<Record<stri
|
|
|
542
607
|
/**
|
|
543
608
|
* 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
609
|
* 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).
|
|
610
|
+
* union (TYPE.FITS-SPLIT-TYPE, D90). A cover split has each valid cover of the field, in the order of the line (D102).
|
|
546
611
|
*/
|
|
547
612
|
interface Split {
|
|
548
613
|
readonly path: readonly string[];
|
|
549
614
|
readonly values?: readonly unknown[];
|
|
550
615
|
readonly types?: readonly ValueType[];
|
|
616
|
+
readonly covers?: readonly (readonly string[])[];
|
|
551
617
|
readonly optional: boolean;
|
|
552
618
|
}
|
|
553
619
|
|
|
@@ -563,7 +629,8 @@ interface SplitBudget {
|
|
|
563
629
|
* cases is a split. An optional field also has the absent case.
|
|
564
630
|
*
|
|
565
631
|
* 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
|
|
632
|
+
* into the domains of a cover, and it tries each valid cover (D102). A union without a finite list splits into its
|
|
633
|
+
* members. The prefix is the path of `a`.
|
|
567
634
|
*/
|
|
568
635
|
function splitsOf(
|
|
569
636
|
a: Extract<Form, { readonly t: "record" }>,
|
|
@@ -588,8 +655,8 @@ function splitsOf(
|
|
|
588
655
|
if (inner.t === "record") deeper.push({ path: [...path, key], form: inner });
|
|
589
656
|
else if (inner.t === "union") later.push({ path: [...path, key], types: inner.members, optional });
|
|
590
657
|
else if (inner.t === "base") {
|
|
591
|
-
const
|
|
592
|
-
if (
|
|
658
|
+
const covers = coversOf(inner.name, domains, false);
|
|
659
|
+
if (covers.length > 0) later.push({ path: [...path, key], covers, optional });
|
|
593
660
|
}
|
|
594
661
|
}
|
|
595
662
|
}
|
|
@@ -604,17 +671,17 @@ function validCover(name: string, cover: unknown, domains: Readonly<Record<strin
|
|
|
604
671
|
}
|
|
605
672
|
|
|
606
673
|
/**
|
|
607
|
-
* This function gives the
|
|
608
|
-
*
|
|
609
|
-
* `undefined`.
|
|
674
|
+
* This function gives the valid covers of a base type for a type split (D90, D102). The order is the order of the line,
|
|
675
|
+
* then the order of the covers of each domain. With `own`, only the domain of the type itself counts.
|
|
610
676
|
*/
|
|
611
|
-
function
|
|
677
|
+
function coversOf(name: string, domains: Readonly<Record<string, Domain>> | undefined, own: boolean): readonly (readonly string[])[] {
|
|
678
|
+
const out: (readonly string[])[] = [];
|
|
612
679
|
for (const each of own ? [name] : lineOf(name, domains)) {
|
|
613
680
|
const covers: unknown = domainOf(each, domains)?.covers;
|
|
614
681
|
if (!Array.isArray(covers)) continue;
|
|
615
|
-
for (const cover of covers as readonly unknown[]) if (validCover(each, cover, domains))
|
|
682
|
+
for (const cover of covers as readonly unknown[]) if (validCover(each, cover, domains)) out.push(cover);
|
|
616
683
|
}
|
|
617
|
-
return
|
|
684
|
+
return out;
|
|
618
685
|
}
|
|
619
686
|
|
|
620
687
|
/**
|
|
@@ -629,8 +696,8 @@ function againOf(type: ValueType, path: readonly string[], domains: Readonly<Rec
|
|
|
629
696
|
const form = formOf(type, domains);
|
|
630
697
|
if (form.t === "record") return splitsOf(form, domains, at, path);
|
|
631
698
|
if (form.t === "union") return [{ path, types: form.members, optional: false }];
|
|
632
|
-
const
|
|
633
|
-
return
|
|
699
|
+
const covers = form.t === "base" ? coversOf(form.name, domains, true) : [];
|
|
700
|
+
return covers.length === 0 ? [] : [{ path, covers, optional: false }];
|
|
634
701
|
}
|
|
635
702
|
|
|
636
703
|
/**
|
|
@@ -663,6 +730,8 @@ function narrowAt(type: unknown, path: readonly string[], at: number, narrowed:
|
|
|
663
730
|
* 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
731
|
* again, at the same path. One budget of 64 records serves the outer split and each split inside its work, at each
|
|
665
732
|
* depth. Thus nested splits do not multiply the work.
|
|
733
|
+
*
|
|
734
|
+
* A cover split tries each valid cover of its field until one fits (D102). Each try takes from the same budget.
|
|
666
735
|
*/
|
|
667
736
|
function splitFits(
|
|
668
737
|
found: unknown,
|
|
@@ -692,6 +761,15 @@ function casesFit(
|
|
|
692
761
|
): boolean {
|
|
693
762
|
const split = splits[index];
|
|
694
763
|
if (split === undefined) return false;
|
|
764
|
+
// A cover split tries each valid cover in its order. A failed try keeps the records that it took from the budget (D102).
|
|
765
|
+
if (split.covers !== undefined) {
|
|
766
|
+
for (const cover of split.covers) {
|
|
767
|
+
const tried = [...splits];
|
|
768
|
+
tried[index] = { path: split.path, types: cover, optional: split.optional };
|
|
769
|
+
if (casesFit(type, closed, tried, index, expected, domains, depth, gradual, budget)) return true;
|
|
770
|
+
}
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
695
773
|
const types: readonly ValueType[] = split.values?.map((value) => ({ one: [value as Json] })) ?? split.types ?? [];
|
|
696
774
|
const cases: ValueType[] = types.map((each) => narrowAt(type, split.path, 0, each, domains));
|
|
697
775
|
if (split.optional) cases.push(narrowAt(type, split.path, 0, undefined, domains));
|
|
@@ -743,16 +821,18 @@ function casesFit(
|
|
|
743
821
|
* split first, then the paths of each next depth, in the order of the keys. A path goes into a record without a
|
|
744
822
|
* finite list. Thus `{a: {x: bool}}` fits the union of `{a: {x: true}}` and `{a: {x: false}}`.
|
|
745
823
|
* - 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
|
|
747
|
-
* members. A case that does not fit splits again at its path, by the same rules. A base type there uses only
|
|
748
|
-
*
|
|
824
|
+
* its line splits into the domains of a cover. A field of a union without a finite list splits into its
|
|
825
|
+
* members. A case that does not fit splits again at its path, by the same rules. A base type there uses only the
|
|
826
|
+
* covers of its own domain. Thus `{a: number}` fits `{a: neg} | {a: nonneg}` when `neg` and `nonneg` cover `number`.
|
|
827
|
+
* - A field with more than one valid cover tries each cover until one fits (TYPE.FITS-SPLIT-COVERS, D102). The order
|
|
828
|
+
* is the order of the line, then the order of the covers of each domain.
|
|
749
829
|
*
|
|
750
830
|
* The relation is sound: a member of `found` is a member of `expected` when no `unknown` is in the types
|
|
751
831
|
* (TYPE.FITS-SOUND). The finite rule trusts `values`, and the cover rule trusts `covers`. Thus a list without a member
|
|
752
832
|
* or a false cover makes the relation unsound for that domain.
|
|
753
833
|
*
|
|
754
|
-
* The relation is not complete. A split builds at most 64 records, with each split inside its work at each depth.
|
|
755
|
-
*
|
|
834
|
+
* The relation is not complete. A split builds at most 64 records, with each split inside its work at each depth. The
|
|
835
|
+
* failed tries of covers take from these 64 records too. A domain without `values` gets no finite rule, and
|
|
756
836
|
* neither does a type with more than 64 members that is not a record. The cover rule tests at most 64 domains of
|
|
757
837
|
* covers. The function does not throw.
|
|
758
838
|
*
|
package/src/typing.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Envelope, type Key, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import type { Machine } from "./evaluate.ts";
|
|
3
|
-
import type { DocVisit, Issue } from "./static.ts";
|
|
4
|
-
import { type ValueType, fits, isUnknown, typeIssues, typeText } from "./types.ts";
|
|
3
|
+
import type { DocVisit, Issue, WorldIssue } from "./static.ts";
|
|
4
|
+
import { type ValueType, domainOf, fits, isUnknown, typeIssues, typeText } from "./types.ts";
|
|
5
|
+
import { type Kind, type Shape, eachKind, kindAt } from "./world.ts";
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
* The check of value types (R5 P3, D25). The pass reads the documents that the walk of `check` visited. It goes down
|
|
@@ -133,8 +134,36 @@ export interface TypeRule {
|
|
|
133
134
|
* @returns The type of the document.
|
|
134
135
|
*/
|
|
135
136
|
readonly synth: (input: TypeInput, report: (problem: TypeProblem) => void) => ValueType;
|
|
137
|
+
/**
|
|
138
|
+
* This function lists the declarations that the rule reads, for example the signatures of a formula profile
|
|
139
|
+
* (TYPE.WORLD, D110). `checkWorld` with the option `types` examines each one before a document exists. A rule
|
|
140
|
+
* without this function declares nothing.
|
|
141
|
+
*
|
|
142
|
+
* @returns The value types that the rule reads, and the problems that the rule finds in its declarations.
|
|
143
|
+
*/
|
|
144
|
+
readonly declared?: () => readonly Declared[];
|
|
136
145
|
}
|
|
137
146
|
|
|
147
|
+
/**
|
|
148
|
+
* One declaration of a type rule (TYPE.WORLD, D110). It is a value type that the rule reads, with what it is. Or it is
|
|
149
|
+
* a problem that the rule finds in a declaration, for example an overload that the check does not select at any time.
|
|
150
|
+
*/
|
|
151
|
+
export type Declared =
|
|
152
|
+
| {
|
|
153
|
+
/** The keys of the declaration in the declarations of the rule, for example `["sigs", "add", "to"]`. */
|
|
154
|
+
readonly at: readonly Key[];
|
|
155
|
+
/** The value type. `checkWorld` examines it with `typeIssues`. */
|
|
156
|
+
readonly type: unknown;
|
|
157
|
+
/** What the type is, for the reason of an issue, for example "result of a signature of ops.add". */
|
|
158
|
+
readonly what: string;
|
|
159
|
+
}
|
|
160
|
+
| {
|
|
161
|
+
/** The keys of the declaration in the declarations of the rule. */
|
|
162
|
+
readonly at: readonly Key[];
|
|
163
|
+
/** The reason of the problem. */
|
|
164
|
+
readonly problem: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
138
167
|
/** The type of one document of a check of types. */
|
|
139
168
|
export interface Typed {
|
|
140
169
|
/** The keys from the root document to the document. */
|
|
@@ -206,7 +235,7 @@ function pathKey(path: readonly Key[]): string {
|
|
|
206
235
|
}
|
|
207
236
|
|
|
208
237
|
/** This function gives the type rule of an evaluator name. A runner hides the evaluator of its name, and a runner has no rule yet (K29). */
|
|
209
|
-
function ruleOf(machine: Machine, name: string | undefined): TypeRule | undefined {
|
|
238
|
+
export function ruleOf(machine: Machine, name: string | undefined): TypeRule | undefined {
|
|
210
239
|
if (name === undefined || (machine.runners !== undefined && Object.hasOwn(machine.runners, name))) return undefined;
|
|
211
240
|
const evaluator = Object.hasOwn(machine.evaluators, name) ? machine.evaluators[name] : undefined;
|
|
212
241
|
const rule = typeof evaluator === "object" && evaluator !== null ? (evaluator as { readonly type?: unknown }).type : undefined;
|
|
@@ -388,3 +417,114 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
|
|
|
388
417
|
const issues = found.sort((a, b) => a.index - b.index).map((item) => item.issue);
|
|
389
418
|
return { type: types[0] ?? "unknown", issues, places };
|
|
390
419
|
}
|
|
420
|
+
|
|
421
|
+
/** The owner of a value type of a machine: a kind, a domain or a type rule. */
|
|
422
|
+
type Owner = Pick<WorldIssue, "kind" | "fallback" | "domain" | "evaluator">;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* This function examines each value type of a machine before a document exists (TYPE.WORLD, D110). It reads the
|
|
426
|
+
* `result` and each `expect` of each kind, the parents and the steps of each domain, and the declarations of each type
|
|
427
|
+
* rule. Each problem of `typeIssues` gives one issue with the keys of the bad term.
|
|
428
|
+
*
|
|
429
|
+
* A term shape with `kinds` and `expect` also gets an issue for each kind of its list whose `result` does not fit
|
|
430
|
+
* `expect`. A check of types gives `#TYPE!` at each document of that kind there.
|
|
431
|
+
*
|
|
432
|
+
* @param machine - The machine.
|
|
433
|
+
* @returns The issues: the kinds in the order of the world, then the domains, then the type rules.
|
|
434
|
+
*/
|
|
435
|
+
export function machineTypeIssues(machine: Machine): readonly WorldIssue[] {
|
|
436
|
+
const domains = machine.domains;
|
|
437
|
+
const world = machine.world;
|
|
438
|
+
const out: WorldIssue[] = [];
|
|
439
|
+
const add = (owner: Owner, at: readonly Key[], reason: string): void => {
|
|
440
|
+
out.push({ code: "#TYPE!", ...owner, verb: "eval", at, reason });
|
|
441
|
+
};
|
|
442
|
+
/** This function examines one value type. It gives `true` when the type has no problem. */
|
|
443
|
+
const examine = (owner: Owner, at: readonly Key[], type: unknown, what: string): boolean => {
|
|
444
|
+
const problems = typeIssues(type, domains);
|
|
445
|
+
for (const problem of problems) add(owner, [...at, ...problem.at], `The ${what} is not a correct value type. ${problem.reason}`);
|
|
446
|
+
return problems.length === 0;
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* This function finds each kind of the list `kinds` of a term whose result does not fit the `expect` of the term.
|
|
450
|
+
* Only a kind at the path itself counts. A path through an alias or to a fallback kind gives no issue.
|
|
451
|
+
*/
|
|
452
|
+
const results = (owner: Owner, kinds: readonly (readonly Key[])[], at: readonly Key[], expect: ValueType): void => {
|
|
453
|
+
if (isUnknown(expect, domains)) return;
|
|
454
|
+
kinds.forEach((path, index) => {
|
|
455
|
+
const found: Kind | undefined = Array.isArray(path) ? kindAt(world, path) : undefined;
|
|
456
|
+
const result = found?.result;
|
|
457
|
+
if (result === undefined || typeIssues(result, domains).length > 0 || fits(result, expect, domains)) return;
|
|
458
|
+
const text = path.join(".");
|
|
459
|
+
add(owner, [...at, "kinds", index], `The kind ${text} has the result ${typeText(result)}, and the part needs the type ${typeText(expect)}. Each document of ${text} here gets #TYPE!.`);
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
eachKind(world, (kind, path, fallback) => {
|
|
464
|
+
const owner: Owner = fallback ? { kind: path, fallback: true } : { kind: path };
|
|
465
|
+
const name = fallback ? (path.length === 0 ? "fallback kind of the world" : `fallback kind of ${path.join(".")}`) : `kind ${path.join(".")}`;
|
|
466
|
+
if (kind.result !== undefined) examine(owner, ["result"], kind.result, `result of the ${name}`);
|
|
467
|
+
// The set holds the shapes on the way, thus a shape that holds itself does not loop.
|
|
468
|
+
const way = new Set<Shape>();
|
|
469
|
+
const shape = (spec: Shape | undefined, at: readonly Key[]): void => {
|
|
470
|
+
if (typeof spec !== "object" || spec === null || way.has(spec)) return;
|
|
471
|
+
way.add(spec);
|
|
472
|
+
const expect = (spec as { readonly expect?: unknown }).expect;
|
|
473
|
+
const where = [...at, "expect"];
|
|
474
|
+
if (expect !== undefined && examine(owner, where, expect, `expected type at ${where.join(".")} of the ${name}`) && spec.shape === "term" && Array.isArray(spec.kinds)) {
|
|
475
|
+
results(owner, spec.kinds, at, expect as ValueType);
|
|
476
|
+
}
|
|
477
|
+
if (spec.shape === "list" || spec.shape === "map") shape(spec.item, [...at, "item"]);
|
|
478
|
+
if (spec.shape === "record") {
|
|
479
|
+
const fields: unknown = spec.fields;
|
|
480
|
+
if (typeof fields === "object" && fields !== null && !Array.isArray(fields)) {
|
|
481
|
+
for (const key of Object.keys(fields)) shape((fields as Readonly<Record<string, Shape>>)[key], [...at, "fields", key]);
|
|
482
|
+
}
|
|
483
|
+
shape(spec.rest, [...at, "rest"]);
|
|
484
|
+
}
|
|
485
|
+
way.delete(spec);
|
|
486
|
+
};
|
|
487
|
+
shape(kind.parts, ["parts"]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const table = domains ?? {};
|
|
491
|
+
for (const name of Object.keys(table)) {
|
|
492
|
+
// A table entry with the name of the top or the bottom has no effect (TYPE.OPEN).
|
|
493
|
+
if (name === "unknown" || name === "never") continue;
|
|
494
|
+
const owner: Owner = { kind: [], domain: name };
|
|
495
|
+
const domain = domainOf(name, domains);
|
|
496
|
+
if (domain === undefined) {
|
|
497
|
+
add(owner, ["is"], `The domain ${name} has no test is. Each use of the name ${name} reads as unknown.`);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const under: unknown = domain.under;
|
|
501
|
+
if (Array.isArray(under)) {
|
|
502
|
+
under.forEach((parent: unknown, index) => {
|
|
503
|
+
if (parent === "unknown" || parent === "never") return;
|
|
504
|
+
if (typeof parent !== "string" || domainOf(parent, domains) === undefined) add(owner, ["under", index], `The parent ${String(parent)} of the domain ${name} is not a domain of the machine.`);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const steps: unknown = domain.steps;
|
|
508
|
+
if (typeof steps === "object" && steps !== null) {
|
|
509
|
+
for (const key of Object.keys(steps)) examine(owner, ["steps", key], (steps as Readonly<Record<string, unknown>>)[key], `type of the step ${key} of the domain ${name}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (const name of Object.keys(machine.evaluators)) {
|
|
514
|
+
const declared = ruleOf(machine, name)?.declared;
|
|
515
|
+
if (typeof declared !== "function") continue;
|
|
516
|
+
const owner: Owner = { kind: [], evaluator: name };
|
|
517
|
+
let list: readonly Declared[];
|
|
518
|
+
try {
|
|
519
|
+
list = declared();
|
|
520
|
+
} catch (error) {
|
|
521
|
+
add(owner, [], `The type rule of ${name} fails: ${message(error)}`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
for (const item of list) {
|
|
525
|
+
if ("problem" in item) add(owner, item.at, item.problem);
|
|
526
|
+
else examine(owner, item.at, item.type, item.what);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
package/src/world.ts
CHANGED
|
@@ -323,6 +323,55 @@ export function isNamespace(entry: WorldEntry | undefined): entry is Namespace {
|
|
|
323
323
|
return entry !== undefined && !isKind(entry) && !isAlias(entry) && Object.hasOwn(entry, "kinds");
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* This function visits each kind of a world. These are the kinds of each namespace, at each depth, and the fallback
|
|
328
|
+
* kind of each namespace and of the world. It does not follow an alias, because the kind of an alias has its own place. A
|
|
329
|
+
* fallback kind has the keys of its namespace. It comes after the entries of that namespace. `checkWorld` reads the
|
|
330
|
+
* kinds with this one function.
|
|
331
|
+
*
|
|
332
|
+
* @param world - The world.
|
|
333
|
+
* @param visit - The function that gets each kind, its keys from the root of the world, and `true` for a fallback kind.
|
|
334
|
+
*/
|
|
335
|
+
export function eachKind(world: World, visit: (kind: Kind, path: readonly Key[], fallback: boolean) => void): void {
|
|
336
|
+
// The set holds the namespaces on the way, thus a namespace that holds itself does not loop.
|
|
337
|
+
const way = new Set<Namespace>();
|
|
338
|
+
const entries = (kinds: Readonly<Record<string, WorldEntry>>, path: readonly Key[]): void => {
|
|
339
|
+
for (const key of Object.keys(kinds)) {
|
|
340
|
+
const entry = kinds[key];
|
|
341
|
+
const here = [...path, key];
|
|
342
|
+
if (isKind(entry)) visit(entry, here, false);
|
|
343
|
+
else if (isNamespace(entry) && !way.has(entry)) {
|
|
344
|
+
way.add(entry);
|
|
345
|
+
entries(entry.kinds, here);
|
|
346
|
+
if (entry.fallback !== undefined) visit(entry.fallback, here, true);
|
|
347
|
+
way.delete(entry);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
entries(world.kinds, []);
|
|
352
|
+
if (world.fallback !== undefined) visit(world.fallback, [], true);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* This function gives the kind at a path of a world, without an alias and without a fallback kind. A namespace on the
|
|
357
|
+
* way gives its entries.
|
|
358
|
+
*
|
|
359
|
+
* @param world - The world.
|
|
360
|
+
* @param path - The keys from the root of the world.
|
|
361
|
+
* @returns The kind, or `undefined` when no kind is at the path.
|
|
362
|
+
*/
|
|
363
|
+
export function kindAt(world: World, path: readonly Key[]): Kind | undefined {
|
|
364
|
+
let entries = world.kinds;
|
|
365
|
+
let entry: WorldEntry | undefined;
|
|
366
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
367
|
+
entry = entryAt(entries, path[index] as Key);
|
|
368
|
+
if (index === path.length - 1) break;
|
|
369
|
+
if (!isNamespace(entry)) return undefined;
|
|
370
|
+
entries = entry.kinds;
|
|
371
|
+
}
|
|
372
|
+
return isKind(entry) ? entry : undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
326
375
|
function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): WorldEntry | undefined {
|
|
327
376
|
const name = String(key);
|
|
328
377
|
return Object.hasOwn(entries, name) ? entries[name] : undefined;
|