@mark1russell7/coeval 0.8.0 → 0.10.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.8.0",
4
+ "version": "0.10.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/conform.ts CHANGED
@@ -32,7 +32,7 @@ import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
32
32
  import { STD_EVALUATORS } from "./stdlib.ts";
33
33
  import { sameValue } from "./agree.ts";
34
34
  import { type Random, random } from "./generate.ts";
35
- import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, isKind, isNamespace, lookupKind } from "./world.ts";
35
+ import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
36
36
 
37
37
  /*
38
38
  * The conformance tests of one evaluator (K92, R7 P3, D36). `conforms` calls the code of one evaluator or runner
@@ -85,8 +85,9 @@ export interface Finding {
85
85
  /** The options of `conforms`. */
86
86
  export interface ConformOptions {
87
87
  /**
88
- * The verb that the evaluator serves. The default is `eval`. The facts of a contract (`effects`, `pure` and `lazy`)
89
- * describe only `eval`, thus `conforms` checks C2, C3 and C4 only for `eval` (D23).
88
+ * The verb that the evaluator serves. The default is `eval`. The facts `pure` and `lazy` of a contract describe only
89
+ * `eval`, thus `conforms` checks C3 and C4 only for `eval` (D23). `conforms` checks C2 for each verb that the
90
+ * contract declares effects for: `eval`, and each verb with an entry in `verbEffects` (D49).
90
91
  */
91
92
  readonly verb?: string;
92
93
  /**
@@ -276,6 +277,8 @@ interface Contract {
276
277
  readonly lazy: readonly LazyRule[] | undefined;
277
278
  /** `true` when the facts describe the verb: they describe only `eval`. */
278
279
  readonly describes: boolean;
280
+ /** `true` when the contract declares the effects of the verb: for `eval`, and for a verb in `verbEffects`. */
281
+ readonly declares: boolean;
279
282
  readonly caps: readonly Cap[];
280
283
  }
281
284
 
@@ -487,13 +490,16 @@ function typesNaming(world: World, name: string, verb: string): readonly TypeRef
487
490
  /** This function gives the facts of the kind of a type. The hook `facts` of the machine wins over the contract. */
488
491
  function contractOf(env: Env, kind: Kind, typePath: readonly Key[], caps: readonly Cap[] | undefined): Contract {
489
492
  const facts = env.machine.facts?.(typePath, env.name);
490
- const effects = kind.effects;
493
+ // A verb without a declaration keeps the effects of eval for the capabilities of the probes, as before D49.
494
+ const own = effectsOfVerb(kind, env.verb);
495
+ const effects = own ?? kind.effects;
491
496
  const declared: readonly Cap[] = effects === undefined ? [] : effects === "type" ? [typePath] : effects;
492
497
  return {
493
498
  declared,
494
499
  pure: (facts?.pure ?? kind.pure) === true,
495
500
  lazy: facts?.lazy ?? kind.lazy,
496
501
  describes: env.verb === "eval",
502
+ declares: own !== undefined,
497
503
  caps: caps ?? [...declared, "probe"],
498
504
  };
499
505
  }
@@ -872,13 +878,15 @@ function checkShot(env: Env, sample: Sample, contract: Contract, scenario: Scena
872
878
  report(env, "C1", `The code gives its effect ${effect.kind} ${capsText(effect.context.caps)}, and its document has ${capsText(docCaps)}.`, probe);
873
879
  }
874
880
  }
875
- if (contract.describes) {
876
- // C2: only the declared effects.
881
+ if (contract.declares) {
882
+ // C2: only the declared effects of the verb (VERB.EFFECTS).
877
883
  for (const effect of shot.caused) {
878
884
  if (!permits(contract.declared, effect.kind)) {
879
- report(env, "C2", `The code causes the effect ${effect.kind}, and the contract of ${sample.type} declares ${contract.declared.length === 0 ? "no effects" : capsText(contract.declared)}.`, probe);
885
+ report(env, "C2", `The code causes the effect ${effect.kind}, and the contract of ${sample.type} declares ${contract.declared.length === 0 ? "no effects" : capsText(contract.declared)}${env.verb === "eval" ? "" : ` for the verb ${env.verb}`}.`, probe);
880
886
  }
881
887
  }
888
+ }
889
+ if (contract.describes) {
882
890
  // C4: a pure kind causes no effect.
883
891
  const first = shot.caused[0];
884
892
  if (contract.pure && first !== undefined) report(env, "C4", `The kind ${sample.type} is pure, and the code causes the effect ${first.kind}.`, probe);
package/src/evaluate.ts CHANGED
@@ -913,6 +913,8 @@ class Session implements Runtime {
913
913
  const free = isFree(reading.root, ctx, path);
914
914
  // A kind that sets another self can give the old self on, as the parent of a node of a tree does (READS.KEPT).
915
915
  if (!free && path[0] === "self" && !reading.self) this.keep(reading, ["self"]);
916
+ // A free read of the whole root self already holds what a kept read of self holds, as the root of a tree reads it.
917
+ else if (free && path.length === 1 && path[0] === "self") reading.self = true;
916
918
  let found: Resolution;
917
919
  try {
918
920
  found = this.find(ctx, path, compiled, hops);
package/src/facts.ts CHANGED
@@ -8,7 +8,8 @@ import { type DocVisit, declaredEffects, walk } from "./static.ts";
8
8
 
9
9
  /**
10
10
  * One read of a document: a free reference, with its marks (FACTS.READS). A plan resolves the path of the reference
11
- * when it evaluates the reference. The static read covers each path that the plan resolves there (FACTS.SOUND).
11
+ * when it evaluates the reference. The static read covers each path that the plan resolves there (FACTS.SOUND). A
12
+ * root that a kind declares in `reads` is a read too, with the path of the root alone.
12
13
  */
13
14
  export interface StaticRead {
14
15
  /** The root of the path. */
@@ -34,7 +35,10 @@ export interface StaticRead {
34
35
 
35
36
  /** The static facts of one document (D24). They depend only on the document and the machine, not on a context. */
36
37
  export interface DocFacts {
37
- /** The free references, in the order of the document (FACTS.READS). A reference to a name that a kind binds is not here. */
38
+ /**
39
+ * The free references, in the order of the document (FACTS.READS). A reference to a name that a kind binds is not
40
+ * here. The roots that a kind declares in `reads` are here too, each one at the place of its document.
41
+ */
38
42
  readonly reads: readonly StaticRead[];
39
43
  /** `"run"` when one kind of the document is on the run side (FACTS.SIDE). */
40
44
  readonly side: Side;
@@ -146,7 +150,8 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
146
150
  * by the contracts of its kinds:
147
151
  *
148
152
  * - `reads`: each free reference, with the root and the keys of its path. A read under a lazy part has the mark
149
- * `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`.
153
+ * `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`. A kind that
154
+ * declares `reads` adds a read of each of its roots at each document of the kind.
150
155
  * - `side` and `effects`: from the contract of each kind.
151
156
  * - `pure`: each kind declares `pure`, and the document has no effect and is not volatile.
152
157
  * - `volatile`: a kind, an operation or a step document declares `volatile`, or the host marks a read.
@@ -200,13 +205,22 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
200
205
  }
201
206
  const code = codeOf(world, v.declared ? undefined : v.doc.own, kind, verb, "eval");
202
207
  const answer = code.name === undefined ? undefined : hook?.(v.typePath ?? [], code.name);
208
+ let roots: readonly Root[] | undefined;
203
209
  if (code.described) {
204
210
  note(answer?.pure ?? kind.pure, answer?.volatile ?? kind.volatile);
205
211
  above.set(v, { lazy, rules: answer?.lazy ?? kind.lazy });
212
+ roots = answer?.reads ?? kind.reads;
206
213
  } else {
207
214
  // The contract of the kind does not describe this code. Only the hook can.
208
215
  note(answer?.pure, answer?.volatile);
209
216
  above.set(v, { lazy, rules: answer?.lazy ?? EACH });
217
+ roots = answer?.reads;
218
+ }
219
+ // The code of the kind resolves each of these roots as a whole, at the place of its document (FACTS.KIND-READS).
220
+ for (const root of roots ?? NONE) {
221
+ const read: StaticRead = { root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(v.place), at: tight(v.at) };
222
+ reads.push(read);
223
+ if (options.volatile?.(read) === true) volatile = true;
210
224
  }
211
225
  if (kind.side === "run") side = "run";
212
226
  // check and effectsOf read the declared effects with the same function (D27).
package/src/formula.ts CHANGED
@@ -4,7 +4,7 @@ import { type CompiledPath, type Context, type Resolution, compilePath, valueOfT
4
4
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
5
5
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled } from "./evaluate.ts";
6
6
  import { type MachinePart, mergeMachines } from "./merge.ts";
7
- import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, stepType, typeText } from "./types.ts";
7
+ import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeText } from "./types.ts";
8
8
  import type { TypeInput, TypeRule } from "./typing.ts";
9
9
  import { decodeDomain, domainTypeOf } from "./codec.ts";
10
10
 
@@ -42,6 +42,8 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
42
42
  * evaluates, and one failed part gives its own error. Without it, the first error stops the evaluation.
43
43
  * - `pure`: the pure operations, for `factsOf` (D24). `true` names each operation that is not volatile. The default
44
44
  * names no operation.
45
+ * - `pureExt`: the pure extensions of `ext`, by their name, for `factsOf` and the folds of a plan (PLAN.FOLD, D47).
46
+ * The default names no extension. A standard extension is pure without a name here.
45
47
  * - `volatile`: the operations whose results can change while their arguments stay the same, for example a clock.
46
48
  * - `sigs`: the signatures of the operations, by their name, for a check of types (TYPE.SIGNATURE). A list gives
47
49
  * overloads. An operation without a signature takes and gives `unknown` (TYPE.GRADUAL).
@@ -61,13 +63,14 @@ export interface FormulaProfile {
61
63
  readonly check?: (value: unknown, op: string) => Result | undefined;
62
64
  readonly combine?: (errors: readonly EvalError[]) => EvalError;
63
65
  readonly pure?: true | readonly string[];
66
+ readonly pureExt?: readonly string[];
64
67
  readonly volatile?: readonly string[];
65
68
  readonly sigs?: Readonly<Record<string, Signature | readonly Signature[]>> | ((name: string) => Signature | readonly Signature[] | undefined);
66
69
  }
67
70
 
68
71
  /**
69
- * The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter of
70
- * the type `unknown` at the end of `params` is optional. A later version can add a key, for example the type that a
72
+ * The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter at
73
+ * 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
71
74
  * true result proves.
72
75
  */
73
76
  export interface Signature {
@@ -209,9 +212,13 @@ export function formulaKinds(): Readonly<Record<string, WorldEntry>> {
209
212
  return kinds as Readonly<Record<string, WorldEntry>>;
210
213
  }
211
214
 
212
- /** This function gives the type of a value of a profile: the value itself as `one`. `undefined` and a function give `unknown`. */
215
+ /**
216
+ * This function gives the type of a value of a profile: the value itself as `one`. `undefined` gives the base type
217
+ * `undefined`, because JSON and thus `one` cannot hold it (D48). A function gives `unknown`.
218
+ */
213
219
  function valueType(value: unknown): ValueType {
214
- return value === undefined || typeof value === "function" ? "unknown" : { one: [value as Json] };
220
+ if (value === undefined) return "undefined";
221
+ return typeof value === "function" ? "unknown" : { one: [value as Json] };
215
222
  }
216
223
 
217
224
  /** This function tells if a value has the form of a signature: a record with `params` as a list, and `to`. */
@@ -236,14 +243,14 @@ function listText(names: readonly string[]): string {
236
243
  }
237
244
 
238
245
  /**
239
- * This function tells if a signature takes a count of arguments. A parameter of the type `unknown` at the end is
240
- * optional, because a missing argument is `undefined`.
246
+ * This function tells if a signature takes a count of arguments. A parameter at the end is optional when its type
247
+ * holds `undefined`, because a missing argument is `undefined`. `unknown` and the base type `undefined` hold it.
241
248
  *
242
249
  * @returns `undefined` when the number is correct, or else the text of the numbers that the signature takes.
243
250
  */
244
251
  function arityOf(sig: Signature, count: number, domains: Machine["domains"]): string | undefined {
245
252
  let least = sig.params.length;
246
- while (least > 0 && isUnknown(sig.params[least - 1], domains)) least -= 1;
253
+ while (least > 0 && member(undefined, sig.params[least - 1] as ValueType, domains)) least -= 1;
247
254
  const most = sig.rest === undefined ? sig.params.length : Number.POSITIVE_INFINITY;
248
255
  if (count >= least && count <= most) return undefined;
249
256
  if (most === Number.POSITIVE_INFINITY) return `at least ${argumentsText(least)}`;
@@ -487,6 +494,8 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
487
494
  const name = nameOf(input);
488
495
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
489
496
  if (own) return "unknown";
497
+ // The standard extensions give the values that JSON cannot write, thus each has a base type (D48).
498
+ if (name === "undefined") return "undefined";
490
499
  if (name === "number") return "number";
491
500
  return name === "domain" ? domainTypeOf(input.doc.value, input.machine.domains) : "unknown";
492
501
  },
@@ -498,19 +507,119 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
498
507
  * This function makes the evaluators of the formula kit for one language. Each evaluator compiles its document
499
508
  * one time into code. Each evaluator also holds its type rule, for a check of types (TYPE.SYNTH).
500
509
  *
510
+ * Equal profiles give the same evaluator objects (COMPOSE.UNIFY, D46). Thus two machines of one profile merge without
511
+ * a conflict. Each call gives a new record. Each evaluator object is frozen, because many machines can share it.
512
+ *
501
513
  * @param profile - The operations and the rules of the language.
502
514
  * @returns The evaluators, by their name.
503
515
  */
504
516
  export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record<string, Evaluator>> {
517
+ return { ...kitOf(profile).evaluators };
518
+ }
519
+
520
+ /** This function makes new evaluators of the formula kit for the fields of one profile. */
521
+ function makeEvaluators(profile: FormulaProfile): Readonly<Record<string, Evaluator>> {
505
522
  const evaluators = formulaCode(profile);
506
523
  const types = formulaTypes(profile);
507
524
  const out: Record<string, Evaluator> = {};
508
525
  for (const name of Object.keys(evaluators)) {
509
526
  const evaluator = evaluators[name] as CompiledEvaluator;
510
527
  const type = Object.hasOwn(types, name) ? types[name] : undefined;
511
- out[name] = type === undefined ? evaluator : { compile: evaluator.compile, type };
528
+ out[name] = Object.freeze(type === undefined ? evaluator : { compile: evaluator.compile, type });
529
+ }
530
+ return Object.freeze(out);
531
+ }
532
+
533
+ /** The keys of a profile that the kit reads. The kit ignores each other key. */
534
+ const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
535
+ "ops",
536
+ "lazy",
537
+ "ext",
538
+ "truthy",
539
+ "otherwise",
540
+ "logic",
541
+ "omit",
542
+ "missing",
543
+ "check",
544
+ "combine",
545
+ "pure",
546
+ "pureExt",
547
+ "volatile",
548
+ "sigs",
549
+ ] as const satisfies readonly (keyof FormulaProfile)[]);
550
+
551
+ /** The evaluators and the hooks of the formula kit for one profile, with the fields that made them. */
552
+ interface Kit {
553
+ readonly fields: FormulaProfile;
554
+ readonly evaluators: Readonly<Record<string, Evaluator>>;
555
+ readonly known: (path: readonly Key[], evaluator: string) => true | string;
556
+ readonly facts: (path: readonly Key[], code: string) => KindFacts | undefined;
557
+ }
558
+
559
+ /** The number of recent kits that a profile in a new object can find by its fields. */
560
+ const RECENT_KITS = 16;
561
+
562
+ /** The kit of each profile object. A kit goes when its profile object goes. */
563
+ const kitsByProfile = /* @__PURE__ */ new WeakMap<object, Kit>();
564
+
565
+ /** The recent kits, the most recent first. The list holds `RECENT_KITS` kits or fewer. */
566
+ const recentKits: Kit[] = [];
567
+
568
+ /**
569
+ * This function gives the canonical form of a profile: a frozen record of the fields that the kit reads. A field
570
+ * with the value `undefined` is absent, but `otherwise` is present when the profile has it as an own key. A list
571
+ * is a frozen copy. Each other value stays the same object.
572
+ */
573
+ function fieldsOf(profile: FormulaProfile): FormulaProfile {
574
+ const out: Record<string, unknown> = {};
575
+ for (const key of PROFILE_KEYS) {
576
+ if (key === "otherwise" ? !Object.hasOwn(profile, key) : profile[key] === undefined) continue;
577
+ const value: unknown = profile[key];
578
+ out[key] = Array.isArray(value) ? Object.freeze([...(value as unknown[])]) : value;
579
+ }
580
+ return Object.freeze(out);
581
+ }
582
+
583
+ /**
584
+ * This function tells if two canonical forms are equal. They must have the same keys. A function, a record and a host
585
+ * value are equal only to themselves. Two lists are equal when their items are the same, in order.
586
+ */
587
+ function sameFields(a: FormulaProfile, b: FormulaProfile): boolean {
588
+ const left = a as Readonly<Record<string, unknown>>;
589
+ const right = b as Readonly<Record<string, unknown>>;
590
+ const keys = Object.keys(left);
591
+ if (keys.length !== Object.keys(right).length) return false;
592
+ return keys.every((key) => {
593
+ if (!Object.hasOwn(right, key)) return false;
594
+ const x = left[key];
595
+ const y = right[key];
596
+ if (Object.is(x, y)) return true;
597
+ return Array.isArray(x) && Array.isArray(y) && x.length === y.length && x.every((item, index) => Object.is(item, y[index]));
598
+ });
599
+ }
600
+
601
+ /**
602
+ * This function gives the kit of a profile (D46). A profile object that the kit knows, with the same fields, gives its
603
+ * kit. Then a recent kit with equal fields serves a new profile object. Else the function makes a new kit from the
604
+ * canonical form. Thus a later change of a field of the profile object gives a new kit, and an old kit does not change.
605
+ */
606
+ function kitOf(profile: FormulaProfile): Kit {
607
+ const fields = fieldsOf(profile);
608
+ const known = kitsByProfile.get(profile);
609
+ if (known !== undefined && sameFields(known.fields, fields)) return known;
610
+ const index = recentKits.findIndex((kit) => sameFields(kit.fields, fields));
611
+ let kit: Kit;
612
+ if (index >= 0) {
613
+ kit = recentKits[index] as Kit;
614
+ recentKits.splice(index, 1);
615
+ } else {
616
+ const hook = makeKnown(fields);
617
+ kit = { fields, evaluators: makeEvaluators(fields), known: hook, facts: makeFacts(fields, hook) };
512
618
  }
513
- return out;
619
+ recentKits.unshift(kit);
620
+ if (recentKits.length > RECENT_KITS) recentKits.length = RECENT_KITS;
621
+ kitsByProfile.set(profile, kit);
622
+ return kit;
514
623
  }
515
624
 
516
625
  /** This function makes the code of the evaluators of the formula kit, without their type rules. */
@@ -848,10 +957,17 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
848
957
  * `formula.ext` knows an extension of `ext` and each standard extension. The hook knows each name of the other
849
958
  * evaluators.
850
959
  *
960
+ * Equal profiles give the same hook (D46).
961
+ *
851
962
  * @param profile - The operations and the extensions of the language.
852
963
  * @returns The hook: `true`, or the reason of the `#NAME?` that the evaluator gives in a run.
853
964
  */
854
965
  export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[], evaluator: string) => true | string {
966
+ return kitOf(profile).known;
967
+ }
968
+
969
+ /** This function makes a new hook `known` for the fields of one profile. */
970
+ function makeKnown(profile: FormulaProfile): (path: readonly Key[], evaluator: string) => true | string {
855
971
  const hasOp = (name: string): boolean => {
856
972
  if (profile.lazy !== undefined && Object.hasOwn(profile.lazy, name)) return true;
857
973
  const ops = profile.ops;
@@ -873,10 +989,13 @@ export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[
873
989
  };
874
990
  }
875
991
 
876
- /** The facts of an extension of the profile. It gets the context, thus it can read outside `resolve`. */
992
+ /**
993
+ * The facts of an extension of the profile that `pureExt` does not name. It gets the context, thus it can read outside
994
+ * `resolve`.
995
+ */
877
996
  const NOT_PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: false });
878
997
 
879
- /** The facts of a standard extension. */
998
+ /** The facts of a standard extension, and of an extension that `pureExt` names. */
880
999
  const PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: true });
881
1000
 
882
1001
  /** The lazy rule of a lazy operation: it covers each argument. */
@@ -888,19 +1007,30 @@ const EACH_PART: readonly LazyRule[] = /* @__PURE__ */ Object.freeze([/* @__PURE
888
1007
  *
889
1008
  * - An operation of `lazy` has a lazy rule for each argument.
890
1009
  * - An operation is pure when the profile names it in `pure`, or when `pure` is `true`. A volatile operation is not pure.
891
- * - A standard extension is pure. An extension of the profile gets the context, thus it is not pure.
1010
+ * - A standard extension is pure. An extension of the profile is pure only when the profile names it in `pureExt` (D47).
1011
+ * Such an extension keeps C4: it reads the context only through `resolve`.
1012
+ *
1013
+ * Equal profiles give the same hook (D46).
892
1014
  *
893
1015
  * @param profile - The operations, the extensions and their facts.
894
1016
  * @returns The hook: the facts of a name, or `undefined` for the facts of the contract.
895
1017
  */
896
1018
  export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[], code: string) => KindFacts | undefined {
1019
+ return kitOf(profile).facts;
1020
+ }
1021
+
1022
+ /** This function makes a new hook `facts` for the fields of one profile, with the hook `known` of the same fields. */
1023
+ function makeFacts(profile: FormulaProfile, known: (path: readonly Key[], evaluator: string) => true | string): (path: readonly Key[], code: string) => KindFacts | undefined {
897
1024
  const volatile = new Set(profile.volatile ?? []);
898
1025
  const pure = profile.pure === true ? undefined : new Set(profile.pure ?? []);
899
- const known = formulaKnown(profile);
1026
+ const pureExt = new Set(profile.pureExt ?? []);
900
1027
  return (path, code) => {
901
1028
  if (code !== "formula.op" && code !== "formula.ext") return undefined;
902
1029
  const name = path.slice(1).join(".");
903
- if (code === "formula.ext") return profile.ext !== undefined && Object.hasOwn(profile.ext, name) ? NOT_PURE : Object.hasOwn(STD_EXT, name) ? PURE : undefined;
1030
+ if (code === "formula.ext") {
1031
+ if (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) return pureExt.has(name) ? PURE : NOT_PURE;
1032
+ return Object.hasOwn(STD_EXT, name) ? PURE : undefined;
1033
+ }
904
1034
  // An operation that the language does not have gives #NAME?. The kit gives it no facts.
905
1035
  if (known(path, code) !== true) return undefined;
906
1036
  const isVolatile = volatile.has(name);
@@ -922,14 +1052,16 @@ export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[
922
1052
  * The kit gives the domains of the kernel (`STD_DOMAINS`). Thus a domain of the language with the name `number` is a
923
1053
  * conflict. The language names `["domains", "number"]` in `replaces` to replace it.
924
1054
  *
1055
+ * Equal profiles give the same evaluators and the same hooks (COMPOSE.UNIFY, D46). Thus two machines of one profile
1056
+ * merge, also when each adds its own language. Two profiles with different fields give different evaluators, and
1057
+ * their machines conflict at each evaluator name of the kit.
1058
+ *
925
1059
  * @param profile - The operations and the rules of the language.
926
1060
  * @param more - The parts of a machine that the language adds, with the paths of the entries that it replaces.
927
1061
  * @returns The machine.
928
1062
  * @throws {MergeError} The language and the kit have a conflict.
929
1063
  */
930
1064
  export function formulaMachine(profile: FormulaProfile = {}, more: MachinePart = {}): Machine {
931
- return mergeMachines(
932
- { world: { kinds: formulaKinds() }, evaluators: formulaEvaluators(profile), domains: STD_DOMAINS, known: formulaKnown(profile), facts: formulaFacts(profile) },
933
- more,
934
- );
1065
+ const kit = kitOf(profile);
1066
+ return mergeMachines({ world: { kinds: formulaKinds() }, evaluators: { ...kit.evaluators }, domains: STD_DOMAINS, known: kit.known, facts: kit.facts }, more);
935
1067
  }
package/src/index.ts CHANGED
@@ -49,8 +49,10 @@ export {
49
49
  type Shape as PartShape,
50
50
  type Side,
51
51
  type TreeContract,
52
+ type VerbEffects,
52
53
  type World,
53
54
  type WorldEntry,
55
+ effectsOfVerb,
54
56
  entriesAsJson,
55
57
  equationType,
56
58
  isAlias,
@@ -202,7 +204,9 @@ export {
202
204
  type Typing,
203
205
  type WalkOptions,
204
206
  type WalkProblem,
207
+ type WorldIssue,
205
208
  check,
209
+ checkWorld,
206
210
  mapRefs,
207
211
  refsOf,
208
212
  typeCheck,
package/src/providers.ts CHANGED
@@ -235,6 +235,7 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
235
235
  let lists = false;
236
236
  for (const value of values) {
237
237
  if (value === null) bases.add("null");
238
+ else if (value === undefined) bases.add("undefined");
238
239
  else if (typeof value === "boolean") bases.add("bool");
239
240
  else if (typeof value === "number") bases.add("number");
240
241
  else if (typeof value === "string") bases.add("text");
@@ -243,10 +244,10 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
243
244
  // A hole of a list holds undefined, thus a loop by index reads it too.
244
245
  for (let index = 0; index < value.length; index += 1) items.push(value[index]);
245
246
  } else if (isRecord(value)) records.push(value);
246
- // A value without a type of the kernel, for example a function or undefined, has each type.
247
+ // A value without a type of the kernel, for example a function, has each type.
247
248
  else return "unknown";
248
249
  }
249
- for (const base of ["null", "bool", "number", "text"]) if (bases.has(base)) types.push(base);
250
+ for (const base of ["null", "undefined", "bool", "number", "text"]) if (bases.has(base)) types.push(base);
250
251
  if (lists) types.push({ list: samplesAt(items, closed, depth + 1) });
251
252
  if (records.length > 0) {
252
253
  // All records join into one record. A field that a record does not have is optional.
@@ -268,7 +269,7 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
268
269
  * This function infers a value type from sample values (TYPE.SAMPLES), as a type provider of F# does. A host uses it
269
270
  * for the type of a root, for example from some events.
270
271
  *
271
- * - `null`, a truth value, a number and a text give their base types, not their values.
272
+ * - `null`, `undefined`, a truth value, a number and a text give their base types, not their values.
272
273
  * - The items of each list join into one type of items.
273
274
  * - All records join into one record with each key of the samples. A key that a record does not have is optional.
274
275
  * - Values of different forms give a union, as `join` gives. No samples give `never`.
package/src/reference.ts CHANGED
@@ -634,10 +634,13 @@ function plainValue(value: unknown, open: readonly object[]): Result {
634
634
 
635
635
  /**
636
636
  * TREE.LAZY, TREE.PARENT, TREE.SIBLING and TREE.ERROR: a node of an open tree gives its thing, and the root of a new
637
- * tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error.
637
+ * tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error. The node
638
+ * reads `self` only through `resolve` (C4, TREE.SELF).
638
639
  */
639
- const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath }) => {
640
- const outer = refNodeOf(context.self);
640
+ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk }) => {
641
+ const reached = walk(["self"]);
642
+ const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
643
+ const outer = refNodeOf(parent);
641
644
  const nested = outer !== undefined && outer.tree.open;
642
645
  const found = machine === undefined || typePath.length === 0 ? undefined : lookupKind(machine.world, typePath);
643
646
  const contract = found?.ok === true ? found.kind.tree : undefined;
@@ -661,7 +664,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
661
664
  type: kind,
662
665
  label: `node ${kind}`,
663
666
  deref: (key): Thing | undefined => {
664
- if (key === "parent") return context.self;
667
+ if (key === "parent") return parent;
665
668
  if (key === "type") return kind;
666
669
  const result = attributeOf(node, String(key));
667
670
  if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;