@mark1russell7/coeval 0.24.1 → 0.25.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 CHANGED
@@ -722,6 +722,71 @@ function narrowAt(type: unknown, path: readonly string[], at: number, narrowed:
722
722
  return { record: fields, ...(optional.length === 0 ? {} : { optional }), ...(form.closed ? { closed: true } : {}) };
723
723
  }
724
724
 
725
+ /**
726
+ * What the result of a test proves of the value at one path (STATIC.OCCURRENCE, D147). The path starts at a root, or at
727
+ * `names` and a name, and each later key is a field of a record. The fact `is` tells that the value is a member of the
728
+ * type, and `not` tells that it is not. The facts `present` and `absent` tell if the path reaches a value.
729
+ */
730
+ export type Proof =
731
+ | { readonly path: readonly string[]; readonly fact: "is" | "not"; readonly type: ValueType }
732
+ | { readonly path: readonly string[]; readonly fact: "present" | "absent" };
733
+
734
+ /** This function gives the type of a value that is a member of both types. The result is the narrower known type. */
735
+ function meetOf(type: ValueType, proved: ValueType, domains: Readonly<Record<string, Domain>> | undefined): ValueType {
736
+ if (isUnknown(proved, domains)) return type;
737
+ const form = formOf(type, domains);
738
+ if (form.t === "one") return { one: form.values.filter((value) => member(value, proved, domains)) };
739
+ return !isUnknown(type, domains) && isPrecise(type, domains) && fits(type, proved, domains) ? type : proved;
740
+ }
741
+
742
+ /** This function gives the type of a value of the first type that is not a member of the second type. */
743
+ function withoutOf(type: ValueType, removed: ValueType, domains: Readonly<Record<string, Domain>> | undefined): ValueType {
744
+ if (isUnknown(removed, domains)) return type;
745
+ const form = formOf(type, domains);
746
+ if (form.t === "one") return { one: form.values.filter((value) => !member(value, removed, domains)) };
747
+ if (form.t === "union") {
748
+ const members = form.members.map((each) => withoutOf(each, removed, domains)).filter((each) => each !== "never");
749
+ return members.length === 0 ? "never" : members.length === 1 ? (members[0] as ValueType) : { union: members };
750
+ }
751
+ // Only a precise type can leave: a gradual type fits each type, but its values do not.
752
+ return isPrecise(type, domains) && fits(type, removed, domains) ? "never" : type;
753
+ }
754
+
755
+ /**
756
+ * This function gives a type with one proof at a path (STATIC.OCCURRENCE). A path that leaves the records of the type
757
+ * gives the type back. A fact about a field also tells if the field exists. A value of a type without `undefined`
758
+ * exists. An optional field without a remaining type does not.
759
+ *
760
+ * @param type - The type of the value at the first `at` keys of the path.
761
+ * @param proof - The proof.
762
+ * @param at - The number of keys of the path that the type already holds.
763
+ * @param domains - The domains of the machine.
764
+ * @returns The narrowed type.
765
+ */
766
+ export function narrowType(type: ValueType, proof: Proof, at: number, domains: Readonly<Record<string, Domain>> | undefined): ValueType {
767
+ if (at === proof.path.length) {
768
+ if (proof.fact === "is") return meetOf(type, proof.type, domains);
769
+ return proof.fact === "not" ? withoutOf(type, proof.type, domains) : type;
770
+ }
771
+ const form = formOf(type, domains);
772
+ const key = proof.path[at] as string;
773
+ if (form.t !== "record" || !Object.hasOwn(form.fields, key)) return type;
774
+ const before = form.fields[key] as ValueType;
775
+ const optional = new Set(form.optional);
776
+ let after: ValueType | undefined = narrowType(before, proof, at + 1, domains);
777
+ if (at + 1 === proof.path.length && optional.has(key)) {
778
+ if (proof.fact === "present" || (proof.fact === "is" && !member(undefined, proof.type, domains))) optional.delete(key);
779
+ else if (proof.fact === "absent" || after === "never") after = undefined;
780
+ }
781
+ const fields: Record<string, ValueType> = {};
782
+ for (const name of Object.keys(form.fields)) if (name !== key) setOwn(fields, name, form.fields[name] as ValueType);
783
+ // The absent case removes the field of a closed record. An open record keeps it as an optional field of the type `never`.
784
+ if (after !== undefined) setOwn(fields, key, after);
785
+ else if (form.closed) optional.delete(key);
786
+ else setOwn(fields, key, "never");
787
+ return { record: fields, ...(optional.size === 0 ? {} : { optional: [...optional] }), ...(form.closed ? { closed: true } : {}) };
788
+ }
789
+
725
790
  /**
726
791
  * This function is the split rule of `fits` (TYPE.FITS-FINITE, D84, TYPE.FITS-PATH, D89). A record type fits when each
727
792
  * record of its split fits. A split takes the first field in the order of `splitsOf`. Each member of the field gives a
package/src/typing.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Envelope, type Key, isRecord, typeName } from "./doc.ts";
2
2
  import type { Machine } from "./evaluate.ts";
3
3
  import type { DocVisit, Issue, WorldIssue } from "./static.ts";
4
- import { type Domain, type ValueType, domainOf, fits, isPrecise, isUnknown, typeIssues, typeText } from "./types.ts";
4
+ import { type Domain, type Proof, type ValueType, domainOf, fits, isPrecise, isUnknown, narrowType, typeIssues, typeText } from "./types.ts";
5
5
  import { type Kind, type Shape, eachKind, kindAt } from "./world.ts";
6
6
 
7
7
  /*
@@ -58,6 +58,27 @@ export interface TypeInput {
58
58
  * @returns The type.
59
59
  */
60
60
  contract(type: unknown, what: string): ValueType;
61
+ /**
62
+ * This function gives the path that a part reads, from the rule `reads` of the part (STATIC.OCCURRENCE).
63
+ *
64
+ * @param path - The path of the part in the value.
65
+ * @returns The path of the read, or `undefined` for each other part.
66
+ */
67
+ readOf(path: readonly Key[]): readonly string[] | undefined;
68
+ /**
69
+ * This function gives what the result of a part proves. Only a rule with `usesProofs` gets this, for a part that the
70
+ * check typed before.
71
+ *
72
+ * @param path - The path of the part in the value.
73
+ * @returns The proofs of the part.
74
+ */
75
+ provenBy(path: readonly Key[]): Proven;
76
+ }
77
+
78
+ /** What a test proves of its paths: `yes` when the test is true, and `no` when it is false (STATIC.OCCURRENCE). */
79
+ export interface Proven {
80
+ readonly yes: readonly Proof[];
81
+ readonly no: readonly Proof[];
61
82
  }
62
83
 
63
84
  /** What a type rule tells for one part: the type that the part must have, and the types of the names of the part. */
@@ -73,6 +94,10 @@ export interface PartType {
73
94
  * Each other root keeps its type. The parts of an evaluator without a rule get `unknown` for each of these roots.
74
95
  */
75
96
  readonly roots?: Omit<RootTypes, "names">;
97
+ /** The proofs that hold in the part (STATIC.OCCURRENCE). They narrow the names and the roots of the part. */
98
+ readonly narrow?: readonly Proof[];
99
+ /** `true` when the proofs of `narrow` also hold in each later part. */
100
+ readonly keep?: boolean;
76
101
  }
77
102
 
78
103
  /**
@@ -82,7 +107,7 @@ export interface PartType {
82
107
  *
83
108
  * The host keeps the types true: each root of the context holds a member of its type. A host thing, or a machine
84
109
  * with `adopt`, keeps each step true too. The context binds each name of `names`, thus the option `names` of `check`
85
- * lists it too.
110
+ * lists it too. A narrowing also needs one value for two reads of one path (STATIC.OCCURRENCE).
86
111
  */
87
112
  export interface RootTypes {
88
113
  /** The type of each name that the host binds. A name of the host without a type here has the type `unknown`. */
@@ -144,6 +169,24 @@ export interface TypeRule {
144
169
  * @returns The value types that the rule reads, and the problems that the rule finds in its declarations.
145
170
  */
146
171
  readonly declared?: () => readonly Declared[];
172
+ /** The field of the value that the check types before each other part. A branch then reads the proofs of its test. */
173
+ readonly first?: string;
174
+ /** `true` when the rule reads `provenBy`. The check asks the rule `proves` only of a part of such a rule. */
175
+ readonly usesProofs?: boolean;
176
+ /**
177
+ * This function gives the path that the document reads. A path with a computed step is not such a path.
178
+ *
179
+ * @param input - The document.
180
+ * @returns The path, or `undefined`.
181
+ */
182
+ readonly reads?: (input: TypeInput) => readonly string[] | undefined;
183
+ /**
184
+ * This function gives what the result of the document proves (STATIC.OCCURRENCE).
185
+ *
186
+ * @param input - The document and its parts.
187
+ * @returns The proofs of a true result and of a false result.
188
+ */
189
+ readonly proves?: (input: TypeInput) => Proven;
147
190
  }
148
191
 
149
192
  /**
@@ -220,6 +263,9 @@ export interface TypeNode {
220
263
  marked: boolean;
221
264
  }
222
265
 
266
+ /** The result of a test that proves nothing. */
267
+ const NO_PROOFS: Proven = { yes: [], no: [] };
268
+
223
269
  /** The result of the pass of types. */
224
270
  export interface TypePass {
225
271
  readonly type: ValueType;
@@ -255,8 +301,11 @@ interface Frame {
255
301
  next: number;
256
302
  readonly typed: Map<string, ValueType>;
257
303
  readonly needs: readonly Need[];
258
- readonly names: ReadonlyMap<string, ValueType>;
259
- readonly roots: Roots;
304
+ /** The types of the names for the parts. A part with `keep` changes them for the later parts. */
305
+ names: ReadonlyMap<string, ValueType>;
306
+ roots: Roots;
307
+ /** The proofs of the typed parts, for a rule with `usesProofs`. */
308
+ proven: Map<string, Proven> | undefined;
260
309
  }
261
310
 
262
311
  function pathKey(path: readonly Key[]): string {
@@ -271,8 +320,12 @@ export function ruleOf(machine: Machine, name: string | undefined): TypeRule | u
271
320
  return typeof rule === "object" && rule !== null && typeof (rule as TypeRule).synth === "function" ? (rule as TypeRule) : undefined;
272
321
  }
273
322
 
274
- /** This function gives the order of the parts of a document: first the parts that give the names of a rule `keysAt`. */
275
- function orderOf(node: TypeNode, nodes: readonly TypeNode[]): readonly number[] {
323
+ /** This function gives the order of the parts of a document. The part `first` of the rule comes first, or else the parts that give the names of a rule `keysAt`. */
324
+ function orderOf(node: TypeNode, nodes: readonly TypeNode[], first: string | undefined): readonly number[] {
325
+ if (first !== undefined) {
326
+ const isFirst = (index: number): boolean => String((nodes[index] as TypeNode).visit.slot[0]) === first;
327
+ return [...node.children.filter(isFirst), ...node.children.filter((index) => !isFirst(index))];
328
+ }
276
329
  const sources = (node.visit.kind?.scope ?? []).flatMap((rule) => ("keysAt" in rule.names ? [rule.names.keysAt] : []));
277
330
  if (sources.length === 0 || node.children.length < 2) return node.children;
278
331
  const gives = (index: number): boolean => {
@@ -298,7 +351,7 @@ function message(error: unknown): string {
298
351
  * @param rootTypes - The types of the roots and of the names of the host.
299
352
  * @returns The type of the root, the type issues in the order of the document, and the type of each document.
300
353
  */
301
- export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: string, expect: ValueType | undefined, rootTypes: RootTypes = {}): TypePass {
354
+ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: string, expect: ValueType | undefined, rootTypes: RootTypes = {}, hasEffect: (node: TypeNode) => boolean = () => false): TypePass {
302
355
  if (nodes.length === 0) return { type: "unknown", issues: [], places: [] };
303
356
  const domains = machine.domains;
304
357
  const contracts = verb === "eval";
@@ -306,6 +359,28 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
306
359
  const types = new Array<ValueType>(nodes.length);
307
360
  const needed = new Array<ValueType | undefined>(nodes.length);
308
361
  const seen = new Set<unknown>();
362
+ const inputs = new Array<TypeInput>(nodes.length);
363
+ let changes: boolean | undefined;
364
+
365
+ // A document with an effect can change a root between two reads, thus only its names narrow (STATIC.OCCURRENCE-EFFECT).
366
+ const rootsChange = (): boolean => (changes ??= nodes.some(hasEffect));
367
+
368
+ const narrowed = (names: ReadonlyMap<string, ValueType>, roots: Roots, proofs: readonly Proof[]): { readonly names: ReadonlyMap<string, ValueType>; readonly roots: Roots } => {
369
+ let nextNames = names;
370
+ let nextRoots = roots;
371
+ for (const proof of proofs) {
372
+ const head = proof.path[0];
373
+ if (head === "names") {
374
+ const name = proof.path[1];
375
+ if (name === undefined) continue;
376
+ if (nextNames === names) nextNames = new Map(names);
377
+ (nextNames as Map<string, ValueType>).set(name, narrowType(nextNames.get(name) ?? "unknown", proof, 2, domains));
378
+ } else if ((head === "self" || head === "world" || head === "event") && !rootsChange()) {
379
+ nextRoots = { ...nextRoots, [head]: narrowType(nextRoots[head], proof, 1, domains) };
380
+ }
381
+ }
382
+ return { names: nextNames, roots: nextRoots };
383
+ };
309
384
 
310
385
  const issueAt = (index: number, problem: TypeProblem): void => {
311
386
  const visit = (nodes[index] as TypeNode).visit;
@@ -345,8 +420,18 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
345
420
  nameType: (name) => names.get(name),
346
421
  rootType: (root) => (root === "caps" ? CAPS_TYPE : root === "self" || root === "world" || root === "event" ? roots[root] : "unknown"),
347
422
  contract: (type, what) => contract(index, type, what),
423
+ readOf: (path) => {
424
+ const key = pathKey(path);
425
+ const part = node.children.find((child) => pathKey((nodes[child] as TypeNode).visit.slot) === key);
426
+ const read = part === undefined ? undefined : ruleOf(machine, (nodes[part] as TypeNode).evaluator)?.reads;
427
+ return read === undefined || (nodes[part as number] as TypeNode).marked ? undefined : read(inputs[part as number] as TypeInput);
428
+ },
429
+ provenBy: (path) => frame.proven?.get(pathKey(path)) ?? NO_PROOFS,
348
430
  };
349
- return { index, rule: node.marked ? undefined : ruleOf(machine, node.evaluator), input, order: orderOf(node, nodes), next: 0, typed, needs, names, roots };
431
+ inputs[index] = input;
432
+ const rule = node.marked ? undefined : ruleOf(machine, node.evaluator);
433
+ const frame: Frame = { index, rule, input, order: orderOf(node, nodes, rule?.first), next: 0, typed, needs, names, roots, proven: undefined };
434
+ return frame;
350
435
  };
351
436
 
352
437
  const ruleFailed = (frame: Frame, error: unknown): void => {
@@ -391,6 +476,16 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
391
476
  if (contracts && node.expect !== undefined) need(contract(child, node.expect, `expected type of the ${what}`), what);
392
477
  if (part.expect !== undefined) need(part.expect, part.what ?? what);
393
478
  let names = frame.names;
479
+ let kept = frame.roots;
480
+ if (part.narrow !== undefined && part.narrow.length > 0) {
481
+ const after = narrowed(names, kept, part.narrow);
482
+ names = after.names;
483
+ kept = after.roots;
484
+ if (part.keep === true) {
485
+ frame.names = names;
486
+ frame.roots = kept;
487
+ }
488
+ }
394
489
  if (node.bound.length > 0) {
395
490
  // Each name that the scope rules bind hides the name around it. A name without a type from the rule gets `unknown`.
396
491
  const copy = new Map(names);
@@ -398,7 +493,7 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
398
493
  names = copy;
399
494
  }
400
495
  // No contract tells how an evaluator changes the roots, thus only a rule keeps their types for its parts (TYPE.ROOTS).
401
- let roots = frame.rule === undefined ? UNKNOWN_ROOTS : frame.roots;
496
+ let roots = frame.rule === undefined ? UNKNOWN_ROOTS : kept;
402
497
  const changes = part.roots;
403
498
  if (typeof changes === "object" && changes !== null) {
404
499
  const copy: Record<(typeof CHANGING)[number], ValueType> = { ...roots };
@@ -432,7 +527,17 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
432
527
  }
433
528
  types[frame.index] = type;
434
529
  needed[frame.index] = frame.needs.length === 0 ? undefined : (frame.needs[frame.needs.length - 1] as Need).type;
435
- stack[stack.length - 1]?.typed.set(pathKey(node.visit.slot), type);
530
+ const parent = stack[stack.length - 1];
531
+ if (parent === undefined) continue;
532
+ const key = pathKey(node.visit.slot);
533
+ parent.typed.set(key, type);
534
+ if (parent.rule?.usesProofs === true && frame.rule?.proves !== undefined) {
535
+ try {
536
+ (parent.proven ??= new Map()).set(key, frame.rule.proves(frame.input));
537
+ } catch (error) {
538
+ ruleFailed(frame, error);
539
+ }
540
+ }
436
541
  }
437
542
 
438
543
  const places = nodes.map((node, index) => {