@mark1russell7/coeval 0.8.0 → 0.9.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.9.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/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
@@ -498,19 +498,118 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
498
498
  * This function makes the evaluators of the formula kit for one language. Each evaluator compiles its document
499
499
  * one time into code. Each evaluator also holds its type rule, for a check of types (TYPE.SYNTH).
500
500
  *
501
+ * Equal profiles give the same evaluator objects (COMPOSE.UNIFY, D46). Thus two machines of one profile merge without
502
+ * a conflict. Each call gives a new record. Each evaluator object is frozen, because many machines can share it.
503
+ *
501
504
  * @param profile - The operations and the rules of the language.
502
505
  * @returns The evaluators, by their name.
503
506
  */
504
507
  export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record<string, Evaluator>> {
508
+ return { ...kitOf(profile).evaluators };
509
+ }
510
+
511
+ /** This function makes new evaluators of the formula kit for the fields of one profile. */
512
+ function makeEvaluators(profile: FormulaProfile): Readonly<Record<string, Evaluator>> {
505
513
  const evaluators = formulaCode(profile);
506
514
  const types = formulaTypes(profile);
507
515
  const out: Record<string, Evaluator> = {};
508
516
  for (const name of Object.keys(evaluators)) {
509
517
  const evaluator = evaluators[name] as CompiledEvaluator;
510
518
  const type = Object.hasOwn(types, name) ? types[name] : undefined;
511
- out[name] = type === undefined ? evaluator : { compile: evaluator.compile, type };
519
+ out[name] = Object.freeze(type === undefined ? evaluator : { compile: evaluator.compile, type });
520
+ }
521
+ return Object.freeze(out);
522
+ }
523
+
524
+ /** The keys of a profile that the kit reads. The kit ignores each other key. */
525
+ const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
526
+ "ops",
527
+ "lazy",
528
+ "ext",
529
+ "truthy",
530
+ "otherwise",
531
+ "logic",
532
+ "omit",
533
+ "missing",
534
+ "check",
535
+ "combine",
536
+ "pure",
537
+ "volatile",
538
+ "sigs",
539
+ ] as const satisfies readonly (keyof FormulaProfile)[]);
540
+
541
+ /** The evaluators and the hooks of the formula kit for one profile, with the fields that made them. */
542
+ interface Kit {
543
+ readonly fields: FormulaProfile;
544
+ readonly evaluators: Readonly<Record<string, Evaluator>>;
545
+ readonly known: (path: readonly Key[], evaluator: string) => true | string;
546
+ readonly facts: (path: readonly Key[], code: string) => KindFacts | undefined;
547
+ }
548
+
549
+ /** The number of recent kits that a profile in a new object can find by its fields. */
550
+ const RECENT_KITS = 16;
551
+
552
+ /** The kit of each profile object. A kit goes when its profile object goes. */
553
+ const kitsByProfile = /* @__PURE__ */ new WeakMap<object, Kit>();
554
+
555
+ /** The recent kits, the most recent first. The list holds `RECENT_KITS` kits or fewer. */
556
+ const recentKits: Kit[] = [];
557
+
558
+ /**
559
+ * This function gives the canonical form of a profile: a frozen record of the fields that the kit reads. A field
560
+ * with the value `undefined` is absent, but `otherwise` is present when the profile has it as an own key. A list
561
+ * is a frozen copy. Each other value stays the same object.
562
+ */
563
+ function fieldsOf(profile: FormulaProfile): FormulaProfile {
564
+ const out: Record<string, unknown> = {};
565
+ for (const key of PROFILE_KEYS) {
566
+ if (key === "otherwise" ? !Object.hasOwn(profile, key) : profile[key] === undefined) continue;
567
+ const value: unknown = profile[key];
568
+ out[key] = Array.isArray(value) ? Object.freeze([...(value as unknown[])]) : value;
512
569
  }
513
- return out;
570
+ return Object.freeze(out);
571
+ }
572
+
573
+ /**
574
+ * This function tells if two canonical forms are equal. They must have the same keys. A function, a record and a host
575
+ * value are equal only to themselves. Two lists are equal when their items are the same, in order.
576
+ */
577
+ function sameFields(a: FormulaProfile, b: FormulaProfile): boolean {
578
+ const left = a as Readonly<Record<string, unknown>>;
579
+ const right = b as Readonly<Record<string, unknown>>;
580
+ const keys = Object.keys(left);
581
+ if (keys.length !== Object.keys(right).length) return false;
582
+ return keys.every((key) => {
583
+ if (!Object.hasOwn(right, key)) return false;
584
+ const x = left[key];
585
+ const y = right[key];
586
+ if (Object.is(x, y)) return true;
587
+ return Array.isArray(x) && Array.isArray(y) && x.length === y.length && x.every((item, index) => Object.is(item, y[index]));
588
+ });
589
+ }
590
+
591
+ /**
592
+ * This function gives the kit of a profile (D46). A profile object that the kit knows, with the same fields, gives its
593
+ * kit. Then a recent kit with equal fields serves a new profile object. Else the function makes a new kit from the
594
+ * canonical form. Thus a later change of a field of the profile object gives a new kit, and an old kit does not change.
595
+ */
596
+ function kitOf(profile: FormulaProfile): Kit {
597
+ const fields = fieldsOf(profile);
598
+ const known = kitsByProfile.get(profile);
599
+ if (known !== undefined && sameFields(known.fields, fields)) return known;
600
+ const index = recentKits.findIndex((kit) => sameFields(kit.fields, fields));
601
+ let kit: Kit;
602
+ if (index >= 0) {
603
+ kit = recentKits[index] as Kit;
604
+ recentKits.splice(index, 1);
605
+ } else {
606
+ const hook = makeKnown(fields);
607
+ kit = { fields, evaluators: makeEvaluators(fields), known: hook, facts: makeFacts(fields, hook) };
608
+ }
609
+ recentKits.unshift(kit);
610
+ if (recentKits.length > RECENT_KITS) recentKits.length = RECENT_KITS;
611
+ kitsByProfile.set(profile, kit);
612
+ return kit;
514
613
  }
515
614
 
516
615
  /** This function makes the code of the evaluators of the formula kit, without their type rules. */
@@ -848,10 +947,17 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
848
947
  * `formula.ext` knows an extension of `ext` and each standard extension. The hook knows each name of the other
849
948
  * evaluators.
850
949
  *
950
+ * Equal profiles give the same hook (D46).
951
+ *
851
952
  * @param profile - The operations and the extensions of the language.
852
953
  * @returns The hook: `true`, or the reason of the `#NAME?` that the evaluator gives in a run.
853
954
  */
854
955
  export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[], evaluator: string) => true | string {
956
+ return kitOf(profile).known;
957
+ }
958
+
959
+ /** This function makes a new hook `known` for the fields of one profile. */
960
+ function makeKnown(profile: FormulaProfile): (path: readonly Key[], evaluator: string) => true | string {
855
961
  const hasOp = (name: string): boolean => {
856
962
  if (profile.lazy !== undefined && Object.hasOwn(profile.lazy, name)) return true;
857
963
  const ops = profile.ops;
@@ -890,13 +996,19 @@ const EACH_PART: readonly LazyRule[] = /* @__PURE__ */ Object.freeze([/* @__PURE
890
996
  * - An operation is pure when the profile names it in `pure`, or when `pure` is `true`. A volatile operation is not pure.
891
997
  * - A standard extension is pure. An extension of the profile gets the context, thus it is not pure.
892
998
  *
999
+ * Equal profiles give the same hook (D46).
1000
+ *
893
1001
  * @param profile - The operations, the extensions and their facts.
894
1002
  * @returns The hook: the facts of a name, or `undefined` for the facts of the contract.
895
1003
  */
896
1004
  export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[], code: string) => KindFacts | undefined {
1005
+ return kitOf(profile).facts;
1006
+ }
1007
+
1008
+ /** This function makes a new hook `facts` for the fields of one profile, with the hook `known` of the same fields. */
1009
+ function makeFacts(profile: FormulaProfile, known: (path: readonly Key[], evaluator: string) => true | string): (path: readonly Key[], code: string) => KindFacts | undefined {
897
1010
  const volatile = new Set(profile.volatile ?? []);
898
1011
  const pure = profile.pure === true ? undefined : new Set(profile.pure ?? []);
899
- const known = formulaKnown(profile);
900
1012
  return (path, code) => {
901
1013
  if (code !== "formula.op" && code !== "formula.ext") return undefined;
902
1014
  const name = path.slice(1).join(".");
@@ -922,14 +1034,16 @@ export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[
922
1034
  * The kit gives the domains of the kernel (`STD_DOMAINS`). Thus a domain of the language with the name `number` is a
923
1035
  * conflict. The language names `["domains", "number"]` in `replaces` to replace it.
924
1036
  *
1037
+ * Equal profiles give the same evaluators and the same hooks (COMPOSE.UNIFY, D46). Thus two machines of one profile
1038
+ * merge, also when each adds its own language. Two profiles with different fields give different evaluators, and
1039
+ * their machines conflict at each evaluator name of the kit.
1040
+ *
925
1041
  * @param profile - The operations and the rules of the language.
926
1042
  * @param more - The parts of a machine that the language adds, with the paths of the entries that it replaces.
927
1043
  * @returns The machine.
928
1044
  * @throws {MergeError} The language and the kit have a conflict.
929
1045
  */
930
1046
  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
- );
1047
+ const kit = kitOf(profile);
1048
+ return mergeMachines({ world: { kinds: formulaKinds() }, evaluators: { ...kit.evaluators }, domains: STD_DOMAINS, known: kit.known, facts: kit.facts }, more);
935
1049
  }
package/src/index.ts CHANGED
@@ -202,7 +202,9 @@ export {
202
202
  type Typing,
203
203
  type WalkOptions,
204
204
  type WalkProblem,
205
+ type WorldIssue,
205
206
  check,
207
+ checkWorld,
206
208
  mapRefs,
207
209
  refsOf,
208
210
  typeCheck,
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;
package/src/static.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, isKey, isRecord, typeName } from "./doc.ts";
2
- import { type Kind, type World, equationType } from "./world.ts";
2
+ import { type Kind, type Namespace, type World, type WorldEntry, equationType, isKind, isNamespace } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
4
  import { type PartSlot, partsOf, scopesOf } from "./parts.ts";
5
5
  import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
@@ -508,6 +508,74 @@ export function typeCheck(machine: Machine, doc: unknown, options: CheckOptions
508
508
  return { type: pass.type, issues: pass.issues.length === 0 ? issues : [...issues, ...pass.issues], places: pass.places };
509
509
  }
510
510
 
511
+ /**
512
+ * A problem of a world that `checkWorld` finds before a document exists (STATIC.COVERAGE). Each document of the
513
+ * kind gives this problem for the verb, thus `check` with the verb finds it too.
514
+ */
515
+ export interface WorldIssue {
516
+ /** The code of the problem. `#NOEVAL`: no rung of the ladder gives an evaluator for the verb. */
517
+ readonly code: "#NOEVAL";
518
+ /** The keys of the kind from the root of the world. For a fallback kind, the keys of its namespace. */
519
+ readonly kind: readonly Key[];
520
+ /** `true` for the fallback kind of a namespace or of the world. */
521
+ readonly fallback?: true;
522
+ /** The verb without an evaluator. */
523
+ readonly verb: string;
524
+ readonly reason: string;
525
+ }
526
+
527
+ /** This function tells if a table of verbs has an entry for a verb. */
528
+ function hasVerb(table: Readonly<Record<string, string>> | undefined, verb: string): boolean {
529
+ return table !== undefined && Object.hasOwn(table, verb);
530
+ }
531
+
532
+ /**
533
+ * This function checks the world of a machine before a document exists. It finds each kind that has no entry for a
534
+ * verb of the list, when the world has no default for that verb (STATIC.COVERAGE). A document of such a kind gets no
535
+ * evaluator for the verb, thus the check gives the same proof of coverage as a type system.
536
+ *
537
+ * The check visits each kind of each namespace, at each depth, and the fallback kind of each namespace and of the
538
+ * world. It does not follow an alias, because the kind of an alias has its own place. The check reads only the world.
539
+ *
540
+ * @param machine - The machine, for its world.
541
+ * @param verbs - The verbs that each kind of the language must answer, for example `eval`, `view` and `type`.
542
+ * @returns One issue for each kind and verb without an evaluator. The order is the order of the world, then the
543
+ * order of the verbs. The fallback kind of a namespace comes after the entries of that namespace.
544
+ */
545
+ export function checkWorld(machine: Machine, verbs: readonly string[]): readonly WorldIssue[] {
546
+ const world = machine.world;
547
+ const open = verbs.filter((verb) => !hasVerb(world.defaults, verb));
548
+ const out: WorldIssue[] = [];
549
+ if (open.length === 0) return out;
550
+ const visit = (kind: Kind, path: readonly Key[], fallback: boolean): void => {
551
+ const text = path.join(".");
552
+ const name = fallback ? (path.length === 0 ? "the fallback kind of the world" : `the fallback kind of ${text}`) : `the kind ${text}`;
553
+ for (const verb of open) {
554
+ if (hasVerb(kind.verbs, verb)) continue;
555
+ const reason = `${name.charAt(0).toUpperCase()}${name.slice(1)} has no entry for the verb ${verb}, and the world has no default for it.`;
556
+ out.push(fallback ? { code: "#NOEVAL", kind: path, fallback: true, verb, reason } : { code: "#NOEVAL", kind: path, verb, reason });
557
+ }
558
+ };
559
+ // The set holds the namespaces on the way, thus a namespace that holds itself does not loop.
560
+ const way = new Set<Namespace>();
561
+ const entries = (kinds: Readonly<Record<string, WorldEntry>>, path: readonly Key[]): void => {
562
+ for (const key of Object.keys(kinds)) {
563
+ const entry = kinds[key];
564
+ const here = [...path, key];
565
+ if (isKind(entry)) visit(entry, here, false);
566
+ else if (isNamespace(entry) && !way.has(entry)) {
567
+ way.add(entry);
568
+ entries(entry.kinds, here);
569
+ if (entry.fallback !== undefined) visit(entry.fallback, here, true);
570
+ way.delete(entry);
571
+ }
572
+ }
573
+ };
574
+ entries(world.kinds, []);
575
+ if (world.fallback !== undefined) visit(world.fallback, [], true);
576
+ return out;
577
+ }
578
+
511
579
  /**
512
580
  * A runner under an evaluator in a check of a run. The run drives its steps without a wait, thus only a wait for an
513
581
  * effect of its runners fails (RUN.DRIVE-NESTED).
package/src/stdlib.ts CHANGED
@@ -71,6 +71,8 @@ export const STD_WORLD: World = {
71
71
  // A node that nothing reads keeps its attributes without an evaluation (TREE.LAZY).
72
72
  pure: true,
73
73
  lazy: [{}],
74
+ // The code resolves self to find an open tree and the parent (C4, D44).
75
+ reads: ["self"],
74
76
  summary: "A node of a tree. Each part is an attribute that evaluates on demand, one time, with the node as self.",
75
77
  },
76
78
  box: {
@@ -79,6 +81,7 @@ export const STD_WORLD: World = {
79
81
  caps: [{ rule: "mask" }],
80
82
  pure: true,
81
83
  lazy: [{}],
84
+ reads: ["self"],
82
85
  summary: "A node of a tree, as panel. A formula in a box can read its parent and its siblings.",
83
86
  },
84
87
  math: {
@@ -396,11 +399,12 @@ function plainOf(value: unknown, path: Set<object>): Result {
396
399
  * step `type` gives its type. Another step reads an attribute through its ladder (TREE.LADDER). The rungs are a part of
397
400
  * the node, an equation of its kind and an equation `down` of the kind of its parent. A copy from the parent, then a
398
401
  * fold or a default, come last. A failed attribute throws `Failure` with its own error, thus a reference to it gives that error.
402
+ * The argument `outer` is the self of the context, that `treeNode` resolved. The node reads no root of the context directly (C4).
399
403
  */
400
- function treeThing(input: EvalInput, build: TreeBuild): HostThing {
404
+ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined): HostThing {
401
405
  const { parts, context, kind: type, place, machine } = input;
402
406
  const { tree, of } = contractOf(machine, input.typePath);
403
- const above = stateOf(context.self);
407
+ const above = stateOf(outer);
404
408
  // Only a node of the same tree is a parent for the rungs `parent` and `copy`.
405
409
  const up = above !== undefined && above.build === build ? above : undefined;
406
410
  const cells = new Map<string, Cell>();
@@ -522,7 +526,7 @@ function treeThing(input: EvalInput, build: TreeBuild): HostThing {
522
526
  type,
523
527
  label: `node ${type}`,
524
528
  deref(key: Key): Thing | undefined {
525
- if (key === "parent") return context.self;
529
+ if (key === "parent") return outer;
526
530
  if (key === "type") return type;
527
531
  const result = read(String(key));
528
532
  if (result === undefined) return undefined;
@@ -556,15 +560,24 @@ export function attributeTrace(node: Thing, attribute: string): AttributeTrace |
556
560
  return stateOf(node)?.trace(attribute);
557
561
  }
558
562
 
563
+ /** The path of the root `self`. A node of the tree resolves it, thus the list of reads holds the read (C4). */
564
+ const SELF_PATH: readonly Json[] = /* @__PURE__ */ Object.freeze(["self"]);
565
+
566
+ /** The options of the resolution of `self`: the node needs no hops. */
567
+ const NO_HOPS = /* @__PURE__ */ Object.freeze({ hops: false });
568
+
559
569
  /**
560
570
  * This function evaluates a node of the decorated tree. Inside an attribute of an open tree, the node gives its host
561
- * thing, and no attribute evaluates. Else the node is the root of a new tree, and it gives the JSON of the tree.
571
+ * thing, and no attribute evaluates. Else the node is the root of a new tree, and it gives the JSON of the tree. The
572
+ * node reads `self` through `resolve` (C4, D44). A machine that closes the root `self` thus gives no nested trees.
562
573
  */
563
574
  function treeNode(input: EvalInput): Result {
564
- const above = stateOf(input.context.self);
565
- if (above !== undefined && above.build.open) return ok(treeThing(input, above.build));
575
+ const found = input.resolve(SELF_PATH, NO_HOPS);
576
+ const outer = found.ok ? found.thing : undefined;
577
+ const above = stateOf(outer);
578
+ if (above !== undefined && above.build.open) return ok(treeThing(input, above.build, outer));
566
579
  const build: TreeBuild = { open: true };
567
- const root = treeThing(input, build);
580
+ const root = treeThing(input, build, outer);
568
581
  try {
569
582
  return (stateOf(root) as NodeState).json(new Set());
570
583
  } finally {
package/src/world.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, field, isRecord, typePath } from "./doc.ts";
2
- import type { Cap } from "./deref.ts";
2
+ import type { Cap, Root } from "./deref.ts";
3
3
  import type { ValueType } from "./types.ts";
4
4
 
5
5
  /** The two sides of evaluation. `derive` is pure. `run` causes effects in order. */
@@ -114,11 +114,14 @@ export interface LazyRule {
114
114
  * because the static facts do not see that read.
115
115
  * - `volatile`: `true` when its result can change while each read gives the same thing, for example a clock.
116
116
  * - `lazy`: the parts that an evaluation can skip and still give a value.
117
+ * - `reads`: the roots that the code of the kind resolves as a whole, with the path of one root. The static facts
118
+ * give a read of each root at each document of the kind (D44). The tree node reads `self` this way.
117
119
  */
118
120
  export interface KindFacts {
119
121
  readonly pure?: boolean;
120
122
  readonly volatile?: boolean;
121
123
  readonly lazy?: readonly LazyRule[];
124
+ readonly reads?: readonly Root[];
122
125
  }
123
126
 
124
127
  /**