@mark1russell7/coeval 0.7.1 → 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/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
@@ -36,6 +36,7 @@ export {
36
36
  } from "./errors.ts";
37
37
  export {
38
38
  type Alias,
39
+ type AttributeDeclaration,
39
40
  type CapsRule,
40
41
  type Kind,
41
42
  type KindFacts,
@@ -47,9 +48,11 @@ export {
47
48
  type ScopeRule,
48
49
  type Shape as PartShape,
49
50
  type Side,
51
+ type TreeContract,
50
52
  type World,
51
53
  type WorldEntry,
52
54
  entriesAsJson,
55
+ equationType,
53
56
  isAlias,
54
57
  isKind,
55
58
  isNamespace,
@@ -157,7 +160,20 @@ export {
157
160
  prepare,
158
161
  run,
159
162
  } from "./evaluate.ts";
160
- export { type NearestOptions, STD_CAPS, STD_EVALUATORS, STD_MACHINE, STD_RUNNERS, STD_WALKERS, STD_WORLD, nearestWalker } from "./stdlib.ts";
163
+ export {
164
+ type AttributeRung,
165
+ type AttributeTrace,
166
+ type NearestOptions,
167
+ ATTRIBUTE_RUNGS,
168
+ STD_CAPS,
169
+ STD_EVALUATORS,
170
+ STD_MACHINE,
171
+ STD_RUNNERS,
172
+ STD_WALKERS,
173
+ STD_WORLD,
174
+ attributeTrace,
175
+ nearestWalker,
176
+ } from "./stdlib.ts";
161
177
  export {
162
178
  type Edge,
163
179
  type OrderProblem,
@@ -186,7 +202,9 @@ export {
186
202
  type Typing,
187
203
  type WalkOptions,
188
204
  type WalkProblem,
205
+ type WorldIssue,
189
206
  check,
207
+ checkWorld,
190
208
  mapRefs,
191
209
  refsOf,
192
210
  typeCheck,
package/src/parts.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
2
- import { type Kind, type Shape, type World, lookupKind } from "./world.ts";
2
+ import { type Kind, type NamesSource, type ScopeRule, type Shape, type World, lookupKind } from "./world.ts";
3
3
  import type { ValueType } from "./types.ts";
4
4
 
5
5
  /** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
@@ -268,3 +268,72 @@ export function pathMatches(path: readonly Key[], pattern: readonly Key[]): bool
268
268
  }
269
269
  return true;
270
270
  }
271
+
272
+ function valueAt(value: unknown, path: readonly Key[]): unknown {
273
+ let here: unknown = value;
274
+ for (const key of path) {
275
+ if (Array.isArray(here)) here = here[Number(key)];
276
+ else if (isRecord(here) && Object.hasOwn(here, String(key))) here = here[String(key)];
277
+ else return undefined;
278
+ }
279
+ return here;
280
+ }
281
+
282
+ function headerName(item: Json): string | undefined {
283
+ return isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
284
+ }
285
+
286
+ function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot[]): readonly string[] {
287
+ if ("fixed" in source) return source.fixed;
288
+ if ("keysAt" in source) {
289
+ const at = valueAt(value, source.keysAt);
290
+ return isRecord(at) ? Object.keys(at) : [];
291
+ }
292
+ if ("listAt" in source) {
293
+ const at = valueAt(value, source.listAt);
294
+ return Array.isArray(at) ? (at as readonly unknown[]).filter((item): item is string => typeof item === "string") : [];
295
+ }
296
+ if ("textAt" in source) {
297
+ const at = valueAt(value, source.textAt);
298
+ if (typeof at === "string") return [at];
299
+ return source.otherwise === undefined ? [] : [source.otherwise];
300
+ }
301
+ return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
302
+ }
303
+
304
+ /**
305
+ * This function gives the names that the scope rules of a kind give each part, and the names that only later parts
306
+ * see. `check`, `conforms` and the plan read the scope rules with it.
307
+ *
308
+ * @param rules - The scope rules of the kind.
309
+ * @param value - The value of the document.
310
+ * @param slots - The parts of the value, in order.
311
+ * @returns The names that each part sees, and the names that only the later parts see, by the index of the part.
312
+ */
313
+ export function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
314
+ const sees: string[][] = slots.map(() => []);
315
+ const later: string[][] = slots.map(() => []);
316
+ for (const rule of rules ?? []) {
317
+ const covered = slots.map((slot, index) => ({ slot, index })).filter(({ slot }) => rule.in.some((pattern) => pathMatches(slot.path, pattern)));
318
+ if (rule.sequential !== true) {
319
+ const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
320
+ for (const { index } of covered) sees[index]?.push(...names);
321
+ continue;
322
+ }
323
+ if ("headers" in rule.names) {
324
+ covered.forEach(({ index }, position) => {
325
+ const before = covered.slice(0, position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
326
+ const after = covered.slice(position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
327
+ sees[index]?.push(...before);
328
+ later[index]?.push(...after);
329
+ });
330
+ continue;
331
+ }
332
+ const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
333
+ covered.forEach(({ index }, position) => {
334
+ if (position > 0) sees[index]?.push(...names);
335
+ else later[index]?.push(...names);
336
+ });
337
+ }
338
+ return { sees, later };
339
+ }
package/src/reference.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type Envelope, type Json, type Key, isEnvelope, isRecord, typeName } from "./doc.ts";
2
2
  import { ask } from "./ladder.ts";
3
- import type { CapsRule, Kind } from "./world.ts";
3
+ import { type AttributeDeclaration, type CapsRule, type Kind, type TreeContract, equationType, lookupKind } from "./world.ts";
4
4
  import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
5
5
  import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
6
- import { STD_EVALUATORS } from "./stdlib.ts";
6
+ import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
7
7
  import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
8
8
  import {
9
9
  type AllOptions,
@@ -17,7 +17,6 @@ import {
17
17
  type RunInput,
18
18
  type RunPart,
19
19
  type Steps,
20
- intersectCaps,
21
20
  makeContext,
22
21
  permits,
23
22
  placeText,
@@ -150,13 +149,23 @@ function examine(env: Env, at: At): Seen | Failed {
150
149
  return { doc, kindText, typePath: read.typePath ?? [], kind, name, chosen, split };
151
150
  }
152
151
 
152
+ /**
153
+ * This function gives the effects that two lists of capabilities both permit (CAP.ONLY). The reference reads the rule
154
+ * from SPEC directly, and not from the intersection of the plan. Thus `agree` finds a bug in either one (D15).
155
+ */
156
+ function bothPermit(a: readonly Cap[], b: readonly Cap[]): readonly Cap[] {
157
+ const out: Cap[] = [];
158
+ for (const cap of [...a, ...b]) if (permits(a, cap) && permits(b, cap) && !permits(out, cap)) out.push(cap);
159
+ return out;
160
+ }
161
+
153
162
  /** This function gives the capabilities that the rules of a kind give one part (D14, CAP.CONTRACT, CAP.ONLY). */
154
163
  function capsOfPart(rules: readonly CapsRule[] | undefined, path: readonly Key[], caps: readonly Cap[]): readonly Cap[] {
155
164
  let out = caps;
156
165
  for (const rule of rules ?? []) {
157
166
  if (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(path, pattern))) continue;
158
167
  if (rule.rule === "mask") return [];
159
- if (rule.rule === "only") out = intersectCaps(out, rule.caps);
168
+ if (rule.rule === "only") out = bothPermit(out, rule.caps);
160
169
  }
161
170
  return out;
162
171
  }
@@ -420,13 +429,20 @@ export function reference(machine: Machine, doc: unknown, context: Context = mak
420
429
  scope: { prefix: "", counts: new Map(), alls: 0 },
421
430
  };
422
431
  const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
432
+ // A node of a tree reads the definitions of this evaluation for its equations (TREE.LADDER).
433
+ activeEnvs.push(env);
423
434
  try {
424
435
  return evaluateAt(env, { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx });
425
436
  } catch (error) {
426
437
  return placed(thrown(error, "the reference"), []);
438
+ } finally {
439
+ activeEnvs.pop();
427
440
  }
428
441
  }
429
442
 
443
+ /** The evaluations of the reference that run at this time, the innermost last. */
444
+ const activeEnvs: Env[] = [];
445
+
430
446
  /*
431
447
  * The definition of the decorated tree (TREE.*), written from docs/SPEC.md. It shares no code with `tree.node` of
432
448
  * stdlib.ts: its nodes, its memo, its marks for attributes in evaluation and its JSON are its own.
@@ -444,6 +460,16 @@ interface RefNode {
444
460
  readonly place: readonly Key[];
445
461
  readonly parts: readonly Part[];
446
462
  readonly ctx: Context;
463
+ /** The tree contract of the kind of the node (D42). */
464
+ readonly contract: TreeContract | undefined;
465
+ /** The type of a bare equation: the type `of` of the parts of the kind. */
466
+ readonly of: string | undefined;
467
+ /** The parent node in the same tree, for the rungs `parent` and `copy`. */
468
+ readonly up: RefNode | undefined;
469
+ /** The evaluation of the reference that made the node. Its equations evaluate with its definitions. */
470
+ readonly env: Env | undefined;
471
+ /** The host thing of the node. */
472
+ thing: HostThing;
447
473
  /** The results of the attributes that ended. */
448
474
  readonly values: Map<string, Result>;
449
475
  /** The attributes in evaluation. */
@@ -463,8 +489,86 @@ function refNodeOf(thing: unknown): RefNode | undefined {
463
489
  return typeof thing === "object" && thing !== null ? refNodes.get(thing) : undefined;
464
490
  }
465
491
 
466
- /** TREE.ONCE, TREE.MEMO and TREE.CYCLE: the result of one attribute, or `undefined` when the node has no such part. */
467
- function attributeOf(node: RefNode, self: HostThing, name: string): Result | undefined {
492
+ /**
493
+ * TREE.LADDER: an equation of the kind of a node evaluates in the reference. It gets the node as `self`, the names of
494
+ * its rung and no capabilities. A bare equation gets the type `of` of the parts of the kind.
495
+ */
496
+ function equationOf(node: RefNode, term: Json, names: Readonly<Record<string, unknown>>): Result {
497
+ const env = node.env;
498
+ if (env === undefined) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
499
+ const ctx: Context = { ...node.ctx, names, caps: [], self: node.thing };
500
+ return reference(env.machine, term, ctx, { definitions: env.definitions, ...(node.of === undefined ? {} : { declaredType: node.of }) });
501
+ }
502
+
503
+ function termIn<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
504
+ return table === undefined || !Object.hasOwn(table, name) ? undefined : table[name];
505
+ }
506
+
507
+ /** TREE.DOWN: the names of a child for an equation of `down` of its parent: `child`, `index` and `prev`. */
508
+ function namesOfChild(up: RefNode, child: RefNode): Result {
509
+ const children = attributeOf(up, "children");
510
+ if (children !== undefined && !children.ok) return children;
511
+ const list: readonly unknown[] = children !== undefined && Array.isArray(children.value) ? children.value : [];
512
+ const index = list.indexOf(child.thing);
513
+ return ok({ child: child.thing, index: index < 0 ? null : index, prev: index > 0 ? list[index - 1] : null });
514
+ }
515
+
516
+ /** TREE.FOLD: the unit, then `combine` with the attribute of each node of this tree in the list `children`. */
517
+ function foldOf(node: RefNode, name: string, fold: NonNullable<AttributeDeclaration["fold"]>): Result {
518
+ let acc = equationOf(node, fold.unit, {});
519
+ if (!acc.ok) return acc;
520
+ const children = attributeOf(node, "children");
521
+ if (children === undefined) return acc;
522
+ if (!children.ok) return children;
523
+ if (!Array.isArray(children.value)) return fail("#VALUE!", `The fold of ${name} needs a list in children.`);
524
+ for (const item of children.value) {
525
+ const child = refNodeOf(item);
526
+ if (child === undefined || child.tree !== node.tree) continue;
527
+ const value = attributeOf(child, name);
528
+ if (value === undefined) return fail("#N/A", `No rung gives ${nameOfAttribute(child, name)}.`);
529
+ if (!value.ok) return value;
530
+ acc = equationOf(node, fold.combine, { acc: acc.value, item: value.value });
531
+ if (!acc.ok) return acc;
532
+ }
533
+ return acc;
534
+ }
535
+
536
+ /**
537
+ * TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
538
+ * answer, and the next rung then tries.
539
+ */
540
+ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string) => Result | undefined])[] = [
541
+ ["own", (node, name) => node.parts.find((item) => item.key === name)?.evaluate({ ...node.ctx, self: node.thing })],
542
+ [
543
+ "kind",
544
+ (node, name) => {
545
+ const term = termIn(node.contract?.equations, name);
546
+ return term === undefined ? undefined : equationOf(node, term, {});
547
+ },
548
+ ],
549
+ [
550
+ "parent",
551
+ (node, name) => {
552
+ const up = node.up;
553
+ const term = up === undefined ? undefined : termIn(up.contract?.down, name);
554
+ if (up === undefined || term === undefined) return undefined;
555
+ const names = namesOfChild(up, node);
556
+ return names.ok ? equationOf(up, term, names.value as Readonly<Record<string, unknown>>) : names;
557
+ },
558
+ ],
559
+ ["copy", (node, name) => (node.up !== undefined && termIn(node.contract?.attributes, name)?.copy === true ? attributeOf(node.up, name) : undefined)],
560
+ [
561
+ "default",
562
+ (node, name) => {
563
+ const declared = termIn(node.contract?.attributes, name);
564
+ if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
565
+ return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
566
+ },
567
+ ],
568
+ ];
569
+
570
+ /** TREE.ONCE, TREE.MEMO and TREE.CYCLE: the result of one attribute, or `undefined` when no rung of its ladder answers. */
571
+ function attributeOf(node: RefNode, name: string): Result | undefined {
468
572
  const done = node.values.get(name);
469
573
  if (done !== undefined) return done;
470
574
  if (node.pending.has(name)) {
@@ -472,14 +576,16 @@ function attributeOf(node: RefNode, self: HostThing, name: string): Result | und
472
576
  const steps = from < 0 ? [nameOfAttribute(node, name)] : inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
473
577
  return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
474
578
  }
475
- const part = node.parts.find((item) => item.key === name);
476
- if (part === undefined) return undefined;
477
579
  node.pending.add(name);
478
580
  inEvaluation.push({ node, name });
479
581
  try {
480
- const result = part.evaluate({ ...node.ctx, self });
481
- node.values.set(name, result);
482
- return result;
582
+ for (const [, rung] of REF_RUNGS) {
583
+ const result = rung(node, name);
584
+ if (result === undefined) continue;
585
+ node.values.set(name, result);
586
+ return result;
587
+ }
588
+ return undefined;
483
589
  } finally {
484
590
  node.pending.delete(name);
485
591
  inEvaluation.pop();
@@ -490,7 +596,7 @@ function attributeOf(node: RefNode, self: HostThing, name: string): Result | und
490
596
  function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
491
597
  const record: Record<string, unknown> = {};
492
598
  for (const part of node.parts) {
493
- const result = attributeOf(node, self, part.key) as Result;
599
+ const result = attributeOf(node, part.key) as Result;
494
600
  if (!result.ok) return result;
495
601
  const value = plainValue(result.value, [...open, self]);
496
602
  if (!value.ok) return value;
@@ -528,30 +634,50 @@ function plainValue(value: unknown, open: readonly object[]): Result {
528
634
 
529
635
  /**
530
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
531
- * 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).
532
639
  */
533
- const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
534
- 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);
535
644
  const nested = outer !== undefined && outer.tree.open;
536
- const node: RefNode = { tree: nested ? outer.tree : { open: true }, kind, place, parts, ctx: context, values: new Map(), pending: new Set() };
645
+ const found = machine === undefined || typePath.length === 0 ? undefined : lookupKind(machine.world, typePath);
646
+ const contract = found?.ok === true ? found.kind.tree : undefined;
647
+ const of = contract === undefined || found?.ok !== true ? undefined : equationType(found.kind);
648
+ const node: RefNode = {
649
+ tree: nested ? outer.tree : { open: true },
650
+ kind,
651
+ place,
652
+ parts,
653
+ ctx: context,
654
+ contract,
655
+ of,
656
+ up: nested ? outer : undefined,
657
+ env: activeEnvs[activeEnvs.length - 1],
658
+ thing: null as unknown as HostThing,
659
+ values: new Map(),
660
+ pending: new Set(),
661
+ };
537
662
  const self: HostThing & { readonly type: string } = {
538
663
  [THING]: true,
539
664
  type: kind,
540
665
  label: `node ${kind}`,
541
666
  deref: (key): Thing | undefined => {
542
- if (key === "parent") return context.self;
667
+ if (key === "parent") return parent;
543
668
  if (key === "type") return kind;
544
- const result = attributeOf(node, self, String(key));
669
+ const result = attributeOf(node, String(key));
545
670
  if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
546
671
  throw new Failure(result.error);
547
672
  },
548
- has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)),
673
+ has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)) || node.values.has(String(key)),
549
674
  toJson: () => {
550
675
  const result = jsonOfNode(node, self, []);
551
676
  if (result.ok) return result.value;
552
677
  throw new Failure(result.error);
553
678
  },
554
679
  };
680
+ node.thing = self;
555
681
  refNodes.set(self, node);
556
682
  if (nested) return ok(self);
557
683
  try {