@mark1russell7/coeval 0.24.1 → 0.25.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.24.1",
4
+ "version": "0.25.0",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
package/src/formula.ts CHANGED
@@ -5,8 +5,8 @@ import { type CompiledPath, type Context, type Resolution, compilePath, valueOfT
5
5
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
6
6
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled, totalCode } from "./evaluate.ts";
7
7
  import { type MachinePart, mergeMachines } from "./merge.ts";
8
- import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeIssues, typeText } from "./types.ts";
9
- import type { Declared, PartType, TypeInput, TypeRule } from "./typing.ts";
8
+ import { type Domain, type Proof, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeIssues, typeText } from "./types.ts";
9
+ import type { Declared, PartType, Proven, TypeInput, TypeRule } from "./typing.ts";
10
10
  import { decodeDomain, domainTypeOf } from "./codec.ts";
11
11
  import { type StaticRead, factsOf } from "./facts.ts";
12
12
 
@@ -80,8 +80,7 @@ export interface FormulaProfile {
80
80
 
81
81
  /**
82
82
  * The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter at
83
- * the end of `params` is optional when `undefined` is a member of its type, for example `unknown`. A later version can add a key, for example the type that a
84
- * true result proves.
83
+ * the end of `params` is optional when `undefined` is a member of its type, for example `unknown`. A later version can add a key.
85
84
  */
86
85
  export interface Signature {
87
86
  /** The types of the parameters, in order. */
@@ -90,8 +89,21 @@ export interface Signature {
90
89
  readonly rest?: ValueType;
91
90
  /** The type of the result. */
92
91
  readonly to: ValueType;
92
+ /**
93
+ * The type that a true result proves of the first argument, and a false result refutes (STATIC.OCCURRENCE). The
94
+ * operation gives a true result for each member of the type, and for no other value.
95
+ */
96
+ readonly proves?: ValueType;
97
+ /** `true` when a true result tells that the first argument has a value, and a false result that it has none. */
98
+ readonly present?: boolean;
93
99
  }
94
100
 
101
+ /** The most proofs that one test keeps. A proof only adds knowledge, thus the check can leave one out. */
102
+ const MAX_PROOFS = 32;
103
+
104
+ /** The result of a test that proves nothing. */
105
+ const NO_PROOFS: Proven = { yes: [], no: [] };
106
+
95
107
  /** The mark of a field that the profile leaves out of a record. */
96
108
  const SKIPPED: unique symbol = /* @__PURE__ */ Symbol("skipped");
97
109
 
@@ -400,6 +412,7 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
400
412
  sig.params.forEach((param, position) => out.push({ at: [...at, "params", position], type: param, what: `parameter ${position + 1} of a signature of ops.${name}` }));
401
413
  if (sig.rest !== undefined) out.push({ at: [...at, "rest"], type: sig.rest, what: `rest parameter of a signature of ops.${name}` });
402
414
  out.push({ at: [...at, "to"], type: sig.to, what: `result of a signature of ops.${name}` });
415
+ if (sig.proves !== undefined) out.push({ at: [...at, "proves"], type: sig.proves, what: `proved type of a signature of ops.${name}` });
403
416
  const earlier = list.slice(0, index).findIndex((other) => isSignature(other) && sameParams(other, sig));
404
417
  if (earlier >= 0) out.push({ at, problem: `The overload ${index + 1} of ops.${name} has the parameters of the overload ${earlier + 1}. Thus the check never selects it.` });
405
418
  });
@@ -407,7 +420,21 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
407
420
  return out;
408
421
  };
409
422
  const logic = (word: "and" | "or"): TypeRule => ({
410
- part: (input, path) => truthPart(input, `term ${Number(path[0]) + 1} of ${word}`),
423
+ usesProofs: true,
424
+ // A later term runs only after a term that did not decide, thus it reads the proof of that result (STATIC.OCCURRENCE-LOGIC).
425
+ part: (input, path) => {
426
+ const index = Number(path[0]);
427
+ const part = truthPart(input, `term ${index + 1} of ${word}`);
428
+ if (index === 0) return part;
429
+ const before = input.provenBy([index - 1]);
430
+ return { ...part, narrow: word === "and" ? before.yes : before.no, keep: true };
431
+ },
432
+ proves: (input) => {
433
+ const each = input.paths.map((path) => input.provenBy(path));
434
+ const all = (side: "yes" | "no"): readonly Proof[] => each.flatMap((proven) => proven[side]).slice(0, MAX_PROOFS);
435
+ const one = each.length === 1 ? (each[0] as Proven) : undefined;
436
+ return word === "and" ? { yes: all("yes"), no: one?.no ?? [] } : { yes: one?.yes ?? [], no: all("no") };
437
+ },
411
438
  synth: (input) => {
412
439
  if (!logicValue) return "bool";
413
440
  // With the logic "value", the result is the term that decided, or the last term.
@@ -421,6 +448,11 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
421
448
 
422
449
  "formula.ref": {
423
450
  read: true,
451
+ reads: (input) => {
452
+ const path = input.doc.value;
453
+ if (input.address || !Array.isArray(path) || path.length === 0) return undefined;
454
+ return path.every((step): step is string => typeof step === "string") ? path : undefined;
455
+ },
424
456
  synth: (input, report) => {
425
457
  const path = input.doc.value;
426
458
  // An address gives a thing, not a value. The check gives #VALUE! for a path that is not a list.
@@ -542,9 +574,13 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
542
574
  "formula.if": {
543
575
  // The rule of if declares the truth type of the profile, thus one bad truth type gives one issue in checkWorld.
544
576
  declared: () => (declared ? [{ at: ["truthType"], type: truth, what: "truth type of the profile" }] : []),
577
+ first: "test",
578
+ usesProofs: true,
545
579
  part: (input, path) => {
546
580
  if (path[0] === "test") return truthPart(input, "test of if");
547
- return isUnknown(input.expected, input.machine.domains) ? {} : { expect: input.expected, what: `${String(path[0])} branch of if` };
581
+ const proven = input.provenBy(["test"]);
582
+ const narrow = path[0] === "then" ? proven.yes : proven.no;
583
+ return isUnknown(input.expected, input.machine.domains) ? { narrow } : { expect: input.expected, what: `${String(path[0])} branch of if`, narrow };
548
584
  },
549
585
  synth: (input) => {
550
586
  const value = input.doc.value;
@@ -558,6 +594,25 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
558
594
 
559
595
  "formula.op": {
560
596
  declared: sigsDeclared,
597
+ proves: (input) => {
598
+ // Overloads select after the check of the arguments, thus only one signature proves.
599
+ const sigs = signatures(input)?.sigs;
600
+ const sig = sigs?.length === 1 ? (sigs[0] as Signature) : undefined;
601
+ if (sig === undefined || (sig.proves === undefined && sig.present !== true) || input.paths.length === 0) return NO_PROOFS;
602
+ const path = input.readOf(input.paths[0] as readonly Key[]);
603
+ if (path === undefined) return NO_PROOFS;
604
+ const yes: Proof[] = [];
605
+ const no: Proof[] = [];
606
+ if (sig.proves !== undefined && typeIssues(input.contract(sig.proves, `proved type of ops.${nameOf(input)}`), input.machine.domains).length === 0) {
607
+ yes.push({ path, fact: "is", type: sig.proves });
608
+ no.push({ path, fact: "not", type: sig.proves });
609
+ }
610
+ if (sig.present === true) {
611
+ yes.push({ path, fact: "present" });
612
+ no.push({ path, fact: "absent" });
613
+ }
614
+ return { yes, no };
615
+ },
561
616
  part: (input, path) => {
562
617
  const read = signatures(input);
563
618
  // Only one signature gives the types of the arguments before their check. Overloads select after it.
package/src/index.ts CHANGED
@@ -79,6 +79,7 @@ export {
79
79
  } from "./world.ts";
80
80
  export {
81
81
  type DomainExtension,
82
+ type VerbEntries,
82
83
  type MachinePart,
83
84
  type MergeConflict,
84
85
  type MergePath,
@@ -348,6 +349,6 @@ export {
348
349
  replayer,
349
350
  } from "./replay.ts";
350
351
  export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
351
- export { type Domain, type DomainCodec, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
352
- export { type Boundary, type Declared, type PartType, type RootTypes, type TypeInput, type TypeProblem, type TypeRule, type Typed, CAPS_TYPE, boundariesOf } from "./typing.ts";
352
+ export { type Domain, type DomainCodec, type Proof, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
353
+ export { type Boundary, type Declared, type PartType, type Proven, type RootTypes, type TypeInput, type TypeProblem, type TypeRule, type Typed, CAPS_TYPE, boundariesOf } from "./typing.ts";
353
354
  export { type SampleOptions, type TypeReading, typeFromSamples, typeFromSchema } from "./providers.ts";
package/src/merge.ts CHANGED
@@ -7,14 +7,30 @@ import { type Kind, type World, type WorldEntry, isAlias, isKind, isNamespace }
7
7
  /** The keys from a part of a merge to one of its entries, for example `["kinds", "ref"]`. */
8
8
  export type MergePath = readonly Key[];
9
9
 
10
+ /** The entries of one kind of another part (COMPOSE.EXTEND-VERB). */
11
+ export interface VerbEntries {
12
+ /**
13
+ * The path of the kind, as `replaces` names it, for example `["kinds", "ref"]`, `["kinds", "ops", "kinds", "mul"]`,
14
+ * `["kinds", "ops", "fallback"]` or `["fallback"]`.
15
+ */
16
+ readonly kind: MergePath;
17
+ /** The evaluator names, by verb, as in `verbs` of a kind. */
18
+ readonly verbs: Readonly<Record<string, string>>;
19
+ }
20
+
10
21
  /**
11
22
  * A world as a part of a merge. Each field is optional. `replaces` names the paths of the entries that this part
12
- * replaces, for example `["kinds", "ref"]` or `["defaults", "view"]`. The result of a merge has no `replaces`.
23
+ * replaces, for example `["kinds", "ref"]` or `["defaults", "view"]`. The result of a merge has no `replaces`, no
24
+ * `verbs` and no `extends`.
13
25
  */
14
26
  export interface WorldPart {
15
27
  readonly kinds?: Readonly<Record<string, WorldEntry>>;
16
28
  readonly defaults?: Readonly<Record<string, string>>;
17
29
  readonly fallback?: Kind;
30
+ /** The verbs that this part owns. A part can add an entry of such a verb to a kind of another part. */
31
+ readonly verbs?: readonly string[];
32
+ /** The entries that this part adds to kinds. The merge adds them after each part (COMPOSE.EXTEND-VERB). */
33
+ readonly extends?: readonly VerbEntries[];
18
34
  readonly replaces?: readonly MergePath[];
19
35
  }
20
36
 
@@ -58,6 +74,8 @@ type Known = NonNullable<Machine["known"]>;
58
74
  type Facts = NonNullable<Machine["facts"]>;
59
75
 
60
76
  const NONE: ReadonlySet<string> = /* @__PURE__ */ new Set();
77
+ /** The verbs that the kernel reads. Thus no language owns them (COMPOSE.PIRACY). */
78
+ const KERNEL_VERBS: ReadonlySet<string> = /* @__PURE__ */ new Set(["eval", "deref", "step"]);
61
79
 
62
80
  function keyOf(path: MergePath): string {
63
81
  return JSON.stringify(path.map(String));
@@ -283,15 +301,112 @@ function mergeEntry(m: Merger, earlier: WorldEntry, later: WorldEntry, path: Mer
283
301
  return earlier;
284
302
  }
285
303
 
304
+ /** One entry of `extends`, with what the merge knows from its part. */
305
+ interface VerbEntry {
306
+ readonly kind: MergePath;
307
+ readonly verb: string;
308
+ readonly code: string;
309
+ readonly path: MergePath;
310
+ readonly part: number;
311
+ /** The part owns the verb, or it gives the kind. Thus the entry is not piracy. */
312
+ readonly allowed: boolean;
313
+ readonly replaced: boolean;
314
+ }
315
+
286
316
  interface WorldDraft {
287
317
  readonly kinds: Record<string, WorldEntry>;
288
318
  readonly defaults: Record<string, string>;
289
319
  fallback?: Kind;
320
+ /** The part that owns each verb. */
321
+ readonly owners: Map<string, WorldPart>;
322
+ readonly entries: VerbEntry[];
323
+ }
324
+
325
+ function worldDraft(): WorldDraft {
326
+ return { kinds: {}, defaults: {}, owners: new Map(), entries: [] };
327
+ }
328
+
329
+ interface KindTable {
330
+ readonly kinds: Readonly<Record<string, WorldEntry>>;
331
+ readonly fallback?: Kind;
332
+ }
333
+
334
+ /**
335
+ * This function gives a copy of a table with a change to the kind at a path, as `replaces` names the path. It gives
336
+ * `undefined` when no kind is at the path. An alias is not a kind.
337
+ */
338
+ function changeKind<T extends KindTable>(table: T, path: MergePath, change: (kind: Kind) => Kind): T | undefined {
339
+ const [first, key, ...rest] = path;
340
+ if (String(first) === "fallback" && key === undefined) return table.fallback === undefined ? undefined : { ...table, fallback: change(table.fallback) };
341
+ if (String(first) !== "kinds" || key === undefined) return undefined;
342
+ const entry = own(table.kinds, String(key));
343
+ const next = rest.length === 0 ? (isKind(entry) ? change(entry) : undefined) : isNamespace(entry) ? changeKind(entry, rest, change) : undefined;
344
+ if (next === undefined) return undefined;
345
+ const kinds = { ...table.kinds };
346
+ setOwn(kinds, String(key), next);
347
+ return { ...table, kinds };
290
348
  }
291
349
 
292
- function worldOf(draft: WorldDraft): World {
293
- const world = { kinds: { ...draft.kinds }, defaults: { ...draft.defaults } };
294
- return draft.fallback === undefined ? world : { ...world, fallback: draft.fallback };
350
+ /**
351
+ * This function adds the entries of `extends` to the kinds of a draft. A later part can give the kind, thus this step
352
+ * reads the whole draft. Without `m`, it reports no conflict: a function part sees the entries of the parts before it.
353
+ */
354
+ function addVerbs(draft: WorldDraft, m?: Merger): KindTable {
355
+ let table: KindTable = draft.fallback === undefined ? { kinds: draft.kinds } : { kinds: draft.kinds, fallback: draft.fallback };
356
+ for (const entry of draft.entries) {
357
+ if (!entry.allowed && !entry.replaced) {
358
+ m?.clash(entry.path, entry.part, "A part adds a verb that it does not own to a kind that it does not give.");
359
+ continue;
360
+ }
361
+ const next = changeKind(table, entry.kind, (kind) => {
362
+ const verbs: Record<string, string> = { ...kind.verbs };
363
+ const earlier = own(verbs, entry.verb);
364
+ if (earlier === undefined) m?.give(entry.path, entry.part);
365
+ else if (entry.replaced) m?.replace(entry.path, entry.part);
366
+ else if (earlier !== entry.code) m?.clash(entry.path, entry.part, "Two parts give two different evaluators for this verb of this kind.");
367
+ if (earlier === undefined || entry.replaced) setOwn(verbs, entry.verb, entry.code);
368
+ return { ...kind, verbs };
369
+ });
370
+ if (next !== undefined) table = next;
371
+ else if (!entry.replaced) m?.clash(entry.path, entry.part, "A part adds a verb to a path without a kind.");
372
+ }
373
+ return table;
374
+ }
375
+
376
+ function worldOf(draft: WorldDraft, m?: Merger): World {
377
+ const table = addVerbs(draft, m);
378
+ const world = { kinds: { ...table.kinds }, defaults: { ...draft.defaults } };
379
+ return table.fallback === undefined ? world : { ...world, fallback: table.fallback };
380
+ }
381
+
382
+ /** This function records the verbs that a part owns, and the entries that it adds to kinds. */
383
+ function mergeVerbs(m: Merger, draft: WorldDraft, part: WorldPart, index: number, replaces: ReadonlySet<string>, base: MergePath): void {
384
+ const owned = part.verbs ?? [];
385
+ for (const verb of owned) {
386
+ const path = [...base, "verbs", verb];
387
+ const owner = draft.owners.get(verb);
388
+ if (replaces.has(keyOf(path))) {
389
+ draft.owners.set(verb, part);
390
+ m.replace(path, index);
391
+ } else if (KERNEL_VERBS.has(verb)) {
392
+ m.clash(path, index, "The kernel owns this verb.");
393
+ } else if (owner === undefined) {
394
+ draft.owners.set(verb, part);
395
+ m.give(path, index);
396
+ } else if (!same(owner, part)) {
397
+ m.clash(path, index, "Two parts own this verb.");
398
+ }
399
+ }
400
+ const table: KindTable = part.fallback === undefined ? { kinds: part.kinds ?? {} } : { kinds: part.kinds ?? {}, fallback: part.fallback };
401
+ for (const { kind, verbs } of part.extends ?? []) {
402
+ const gives = changeKind(table, kind, (found) => found) !== undefined;
403
+ for (const verb of Object.keys(verbs)) {
404
+ const code = verbs[verb];
405
+ if (code === undefined) continue;
406
+ const path = [...base, ...kind, "verbs", verb];
407
+ draft.entries.push({ kind, verb, code, path, part: index, allowed: gives || owned.includes(verb), replaced: replaces.has(keyOf(path)) });
408
+ }
409
+ }
295
410
  }
296
411
 
297
412
  function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefined, index: number, replaces: ReadonlySet<string>, base: MergePath): void {
@@ -303,6 +418,7 @@ function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefine
303
418
  }
304
419
  const fallback = mergeFallback(m, draft.fallback, part.fallback, [...base, "fallback"], index, replaces);
305
420
  if (fallback !== undefined) draft.fallback = fallback;
421
+ mergeVerbs(m, draft, part, index, replaces, base);
306
422
  }
307
423
 
308
424
  /**
@@ -313,6 +429,8 @@ function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefine
313
429
  * - The defaults merge verb by verb, and the world keeps one fallback.
314
430
  * - Each other pair of entries at one path is a conflict, unless the later part names the path in `replaces`.
315
431
  * Then the later entry wins, with no merge below it.
432
+ * - The entries of `extends` join their kinds after each part, thus the order of the parts does not change them. A
433
+ * verb that a part does not own on a kind that it does not give is a conflict (COMPOSE.PIRACY).
316
434
  *
317
435
  * A part can be a function of the merge of the parts before it (`prev`, as an overlay of Nix). Thus a part can read
318
436
  * the entry that it replaces.
@@ -323,13 +441,14 @@ function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefine
323
441
  */
324
442
  export function mergeWorlds(...parts: readonly (WorldPart | ((prev: World) => WorldPart))[]): World {
325
443
  const m = new Merger();
326
- const draft: WorldDraft = { kinds: {}, defaults: {} };
444
+ const draft = worldDraft();
327
445
  parts.forEach((input, index) => {
328
446
  const part = typeof input === "function" ? input(worldOf(draft)) : input;
329
447
  mergeWorldInto(m, draft, part, index, new Set((part.replaces ?? []).map(keyOf)), []);
330
448
  });
449
+ const world = worldOf(draft, m);
331
450
  if (m.conflicts.length > 0) throw new MergeError(m.conflicts);
332
- return worldOf(draft);
451
+ return world;
333
452
  }
334
453
 
335
454
  /** The fields of a machine that merge by their own rules. Each other field is one value, or a conflict. */
@@ -392,12 +511,12 @@ function composeFacts(hooks: readonly { readonly part: number; readonly facts: F
392
511
  };
393
512
  }
394
513
 
395
- function machineOf(draft: MachineDraft): Machine {
514
+ function machineOf(draft: MachineDraft, m?: Merger): Machine {
396
515
  const known = composeKnown(draft.hooks, draft.replaced);
397
516
  const facts = composeFacts(draft.factHooks, draft.replaced);
398
517
  return {
399
518
  ...draft.rest,
400
- world: worldOf(draft.world),
519
+ world: worldOf(draft.world, m),
401
520
  evaluators: { ...draft.evaluators },
402
521
  ...(draft.runners === undefined ? {} : { runners: { ...draft.runners } }),
403
522
  ...(draft.derefs === undefined ? {} : { derefs: { ...draft.derefs } }),
@@ -474,7 +593,7 @@ function checkHidden(m: Merger, draft: MachineDraft): void {
474
593
  */
475
594
  export function mergeMachines(...parts: readonly (MachinePart | ((prev: Machine) => MachinePart))[]): Machine {
476
595
  const m = new Merger();
477
- const draft: MachineDraft = { world: { kinds: {}, defaults: {} }, evaluators: {}, hooks: [], factHooks: [], replaced: new Map(), rest: {}, parts: [] };
596
+ const draft: MachineDraft = { world: worldDraft(), evaluators: {}, hooks: [], factHooks: [], replaced: new Map(), rest: {}, parts: [] };
478
597
  parts.forEach((input, index) => {
479
598
  const part = typeof input === "function" ? input(machineOf(draft)) : input;
480
599
  draft.parts.push(part);
@@ -502,8 +621,9 @@ export function mergeMachines(...parts: readonly (MachinePart | ((prev: Machine)
502
621
  }
503
622
  });
504
623
  checkHidden(m, draft);
624
+ const machine = machineOf(draft, m);
505
625
  if (m.conflicts.length > 0) throw new MergeError(m.conflicts);
506
- return machineOf(draft);
626
+ return machine;
507
627
  }
508
628
 
509
629
  /** This function removes the entry at a path from a table of entries. It gives the same table when no entry is there. */
package/src/reference.ts CHANGED
@@ -1336,11 +1336,22 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1336
1336
  }
1337
1337
  };
1338
1338
 
1339
+ /**
1340
+ * The definition of the standard evaluator `ref.resolve` (REF.RESOLVE, D133). It resolves the steps of the path at each
1341
+ * evaluation. The plan compiles the same path one time.
1342
+ */
1343
+ const refDefinition: EvalFn = ({ data, resolve: walk }) => {
1344
+ if (!Array.isArray(data) || data.length === 0) return fail("#VALUE!", 'The value of ref is a path: a list that starts with a root, for example ["names", "a"].');
1345
+ const found = walk(data as readonly Json[]);
1346
+ return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
1347
+ };
1348
+
1339
1349
  /**
1340
1350
  * The definitions of the reference for the evaluators of the standard machine that need their own reading. The
1341
- * reference uses them for the standard evaluators, also without the option `definitions`. It holds `tree.node`.
1351
+ * reference uses them for the standard evaluators, also without the option `definitions`. It holds `tree.node` and
1352
+ * `ref.resolve`. A host that wraps `ref.resolve` wraps this function, because the standard evaluator is compiled.
1342
1353
  */
1343
- export const STD_DEFINITIONS: Definitions = { evaluators: { "tree.node": treeDefinition } };
1354
+ export const STD_DEFINITIONS: Definitions = { evaluators: { "tree.node": treeDefinition, "ref.resolve": refDefinition } };
1344
1355
 
1345
1356
  /* The definitions of the formula kit: one function evaluator for each construct, written from docs/SPEC.md. */
1346
1357
 
package/src/static.ts CHANGED
@@ -641,7 +641,8 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
641
641
  export function typeCheck(machine: Machine, doc: unknown, options: CheckOptions = {}): Typing {
642
642
  const nodes: TypeNode[] = [];
643
643
  const issues = checkAll(machine, doc, options, nodes);
644
- const pass = typePass(machine, nodes, options.verb ?? "eval", options.expect, options.rootTypes);
644
+ const verb = options.verb ?? "eval";
645
+ const pass = typePass(machine, nodes, verb, options.expect, options.rootTypes, (node) => declaredEffects(node.visit, verb, machine, node.evaluator).length > 0);
645
646
  return { type: pass.type, issues: pass.issues.length === 0 ? issues : [...issues, ...pass.issues], places: pass.places };
646
647
  }
647
648
 
package/src/stdlib.ts CHANGED
@@ -15,9 +15,10 @@ import {
15
15
  fail,
16
16
  ok,
17
17
  planner,
18
+ resolveCompiled,
18
19
  } from "./evaluate.ts";
19
20
  import { Failure } from "./errors.ts";
20
- import { type Cap, type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
21
+ import { type Cap, type HostThing, type Thing, type Walker, THING, compilePath, isHostThing, valueOfThing } from "./deref.ts";
21
22
  import type { World } from "./ladder.ts";
22
23
  import {
23
24
  type AttributeDeclaration,
@@ -289,12 +290,19 @@ export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
289
290
  const [a, b] = values as readonly number[];
290
291
  return ok((a ?? 0) > (b ?? 0));
291
292
  },
292
- "ref.resolve": ({ data, resolve }) => {
293
- if (!Array.isArray(data) || data.length === 0) {
294
- return fail("#VALUE!", "The value of ref is a path: a list that starts with a root, for example [\"names\", \"a\"].");
295
- }
296
- const found = resolve(data as readonly Json[]);
297
- return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
293
+ // The path is data of the document, thus it compiles one time for the machine of the plan (PLAN.PATH-STD, D133).
294
+ "ref.resolve": {
295
+ compile: ({ data, machine }) => {
296
+ if (!Array.isArray(data) || data.length === 0) {
297
+ const bad = fail("#VALUE!", "The value of ref is a path: a list that starts with a root, for example [\"names\", \"a\"].");
298
+ return () => bad;
299
+ }
300
+ const path = compilePath(machine, data);
301
+ return (ctx, rt) => {
302
+ const found = resolveCompiled(rt, ctx, path);
303
+ return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
304
+ };
305
+ },
298
306
  },
299
307
  "tree.node": (input) => treeNode(input, undefined),
300
308
  };
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