@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/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.1",
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,
@@ -90,6 +91,7 @@ export {
90
91
  restrictMachine,
91
92
  restrictWorld,
92
93
  } from "./merge.ts";
94
+ export { type Language, type LanguageCode, type LanguageDefinition, compose, defineLanguage } from "./language.ts";
93
95
  export {
94
96
  type Answer,
95
97
  type AskOptions,
@@ -348,6 +350,6 @@ export {
348
350
  replayer,
349
351
  } from "./replay.ts";
350
352
  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";
353
+ export { type Domain, type DomainCodec, type Proof, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
354
+ 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
355
  export { type SampleOptions, type TypeReading, typeFromSamples, typeFromSchema } from "./providers.ts";
@@ -0,0 +1,147 @@
1
+ import { type Key, isRecord } from "./doc.ts";
2
+ import type { Machine } from "./evaluate.ts";
3
+ import { type MachinePart, type MergeConflict, type WorldPart, MergeError, mergeMachines } from "./merge.ts";
4
+
5
+ /** The code half of a language: each field of a machine part, without the world. */
6
+ export type LanguageCode = Omit<MachinePart, "world">;
7
+
8
+ /** The input of `defineLanguage`. */
9
+ export interface LanguageDefinition {
10
+ /** The name of the language. A composition holds one language of a name. */
11
+ readonly name: string;
12
+ /** The version, as `MAJOR.MINOR.PATCH`. */
13
+ readonly version: string;
14
+ /** The world of the language, with the verbs that it owns. It is JSON. */
15
+ readonly world?: WorldPart;
16
+ /** The lowest compatible version of each language that this language needs, by name (LANG.REQUIRES). */
17
+ readonly requires?: Readonly<Record<string, string>>;
18
+ /** The evaluators, the hooks and the other code. A function reads the merge of the languages before it. */
19
+ readonly code?: LanguageCode | ((prev: Machine) => LanguageCode);
20
+ }
21
+
22
+ /** One language as one frozen value, for `compose`. */
23
+ export interface Language {
24
+ readonly name: string;
25
+ readonly version: string;
26
+ readonly world: WorldPart;
27
+ readonly requires: Readonly<Record<string, string>>;
28
+ readonly code?: LanguageCode | ((prev: Machine) => LanguageCode);
29
+ /**
30
+ * The canonical JSON of the data half: `requires` and `world`, with the keys in order. Two equal data halves give
31
+ * one text, in each order of their keys (LANG.IDENTITY). The name, the version and this text identify a language.
32
+ */
33
+ readonly data: string;
34
+ }
35
+
36
+ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
37
+
38
+ function numbers(version: string): readonly [number, number, number] {
39
+ const found = VERSION.exec(version);
40
+ if (found === null) throw new TypeError(`A language version has the form MAJOR.MINOR.PATCH. The text ${JSON.stringify(version)} does not.`);
41
+ return [Number(found[1]), Number(found[2]), Number(found[3])];
42
+ }
43
+
44
+ /**
45
+ * This function tells if a version satisfies a lowest version, by the rules of a caret range of SemVer. The first
46
+ * number that is not zero stays the same, and the version is not lower.
47
+ */
48
+ function compatible(version: string, lowest: string): boolean {
49
+ const have = numbers(version);
50
+ const want = numbers(lowest);
51
+ const stable = want.findIndex((part) => part !== 0);
52
+ const fixed = stable < 0 ? 3 : stable + 1;
53
+ for (let index = 0; index < 3; index += 1) {
54
+ const [a, b] = [have[index] as number, want[index] as number];
55
+ if (a !== b) return index >= fixed && a > b;
56
+ }
57
+ return true;
58
+ }
59
+
60
+ /** This function gives the JSON text of a value with its keys in order. A value that is not JSON is an error. */
61
+ function canonical(value: unknown, path: readonly Key[]): string {
62
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
63
+ if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
64
+ if (Array.isArray(value)) return `[${value.map((item, index) => canonical(item, [...path, index])).join(",")}]`;
65
+ if (!isRecord(value)) throw new TypeError(`The data half of a language is JSON. The value at ${JSON.stringify(path)} is not.`);
66
+ const keys = Object.keys(value)
67
+ .filter((key) => value[key] !== undefined)
68
+ .sort();
69
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonical(value[key], [...path, key])}`).join(",")}}`;
70
+ }
71
+
72
+ function frozen<T>(value: T): T {
73
+ if (typeof value !== "object" || value === null) return value;
74
+ for (const item of Object.values(value)) frozen(item);
75
+ return Object.freeze(value);
76
+ }
77
+
78
+ /**
79
+ * This function makes one value of a language. The data half is JSON, thus a host can store it and compare it. The
80
+ * code half binds the evaluator names of the world.
81
+ *
82
+ * The language keeps a frozen copy of its data half, thus a later change to the input changes nothing.
83
+ *
84
+ * @param definition - The name, the version, the world, the needed languages and the code.
85
+ * @returns The language.
86
+ * @throws {TypeError} A version in the definition is not `MAJOR.MINOR.PATCH`, the name is empty, or the
87
+ * data half is not JSON.
88
+ */
89
+ export function defineLanguage(definition: LanguageDefinition): Language {
90
+ if (definition.name === "") throw new TypeError("A language has a name.");
91
+ numbers(definition.version);
92
+ const requires = definition.requires ?? {};
93
+ for (const name of Object.keys(requires)) numbers(requires[name] as string);
94
+ const data = canonical({ requires, world: definition.world ?? {} }, []);
95
+ const copy = frozen(JSON.parse(data) as { readonly requires: Readonly<Record<string, string>>; readonly world: WorldPart });
96
+ const language = { name: definition.name, version: definition.version, world: copy.world, requires: copy.requires, data };
97
+ return Object.freeze(definition.code === undefined ? language : { ...language, code: definition.code });
98
+ }
99
+
100
+ function partOf(language: Language): MachinePart | ((prev: Machine) => MachinePart) {
101
+ const { code, world } = language;
102
+ return typeof code === "function" ? (prev) => ({ ...code(prev), world }) : { ...code, world };
103
+ }
104
+
105
+ /**
106
+ * This function merges languages into one machine, in the order of the arguments, with `mergeMachines`.
107
+ *
108
+ * - A language comes after each language that it names in `requires`, at a compatible version (LANG.REQUIRES).
109
+ * - One language that comes twice counts once. Thus two languages can share a base.
110
+ * - Two versions of one name, or two data halves under one name and one version, are conflicts (LANG.COMPOSE).
111
+ *
112
+ * @param languages - The languages.
113
+ * @returns The machine.
114
+ * @throws {MergeError} The languages do not fit, or their merge has a conflict. The error lists each conflict.
115
+ */
116
+ export function compose(...languages: readonly Language[]): Machine {
117
+ const conflicts: MergeConflict[] = [];
118
+ const parts: (MachinePart | ((prev: Machine) => MachinePart))[] = [];
119
+ const seen = new Map<string, { readonly language: Language; readonly index: number }>();
120
+ languages.forEach((language, index) => {
121
+ const earlier = seen.get(language.name);
122
+ const conflict = (name: string, other: number, reason: string): void => {
123
+ conflicts.push({ path: ["languages", name], parts: [Math.min(other, index), Math.max(other, index)], reason });
124
+ };
125
+ if (earlier !== undefined) {
126
+ if (earlier.language.version !== language.version) conflict(language.name, earlier.index, "Two versions of one language.");
127
+ else if (earlier.language.data !== language.data) conflict(language.name, earlier.index, "Two languages with one name and one version differ.");
128
+ else if (earlier.language.code !== language.code) parts.push(partOf(language));
129
+ return;
130
+ }
131
+ for (const name of Object.keys(language.requires)) {
132
+ const needed = language.requires[name] as string;
133
+ const found = seen.get(name);
134
+ if (found === undefined) {
135
+ const later = languages.findIndex((other) => other.name === name);
136
+ if (later < 0) conflict(name, index, `A language requires ${name}, and the composition does not have it.`);
137
+ else conflict(name, later, `A language requires ${name}, and ${name} comes after it.`);
138
+ } else if (!compatible(found.language.version, needed)) {
139
+ conflict(name, found.index, `A language requires ${name} ${needed}, and the composition has ${found.language.version}.`);
140
+ }
141
+ }
142
+ seen.set(language.name, { language, index });
143
+ parts.push(partOf(language));
144
+ });
145
+ if (conflicts.length > 0) throw new MergeError(conflicts);
146
+ return mergeMachines(...parts);
147
+ }
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
  };