@mark1russell7/coeval 0.22.0 → 0.24.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/index.ts CHANGED
@@ -60,9 +60,11 @@ export {
60
60
  type World,
61
61
  type WorldEntry,
62
62
  CIRCULAR_LIMIT,
63
+ GENERATION_LIMIT,
63
64
  argsKey,
64
65
  atArgsOf,
65
66
  atRuleOf,
67
+ documentKey,
66
68
  circularOf,
67
69
  collectOf,
68
70
  effectsOfVerb,
@@ -104,6 +106,7 @@ export {
104
106
  export { type StepKey, type StepLadder, type StepNeed, type StepReason, type StepRefusal, type StepWalk, checkStep, isStep, stepLadder, stepOf, stepRank } from "./steps.ts";
105
107
  export {
106
108
  type Cap,
109
+ type CompiledPath,
107
110
  type Context,
108
111
  type DerefCode,
109
112
  type Hop,
@@ -121,6 +124,7 @@ export {
121
124
  type Walker,
122
125
  ROOTS,
123
126
  THING,
127
+ compilePath,
124
128
  context,
125
129
  derefStep,
126
130
  isHostThing,
@@ -157,6 +161,7 @@ export {
157
161
  type RunFn,
158
162
  type RunInput,
159
163
  type RunOptions,
164
+ type Clause,
160
165
  type RunPart,
161
166
  type Runner,
162
167
  type Runtime,
@@ -173,9 +178,11 @@ export {
173
178
  narrow,
174
179
  nodeAt,
175
180
  partCaps,
181
+ partGrants,
176
182
  permits,
177
183
  planner,
178
184
  prepare,
185
+ resolveCompiled,
179
186
  run,
180
187
  } from "./evaluate.ts";
181
188
  export {
@@ -255,11 +262,15 @@ export {
255
262
  jsTruthy,
256
263
  } from "./formula.ts";
257
264
  export { decodeDomain, decodeValue, encodeValue } from "./codec.ts";
265
+ export { type CodecFinding, type CodecLaw, type CodecLawsOptions, type CodecProbe, codecLaws } from "./codec-laws.ts";
258
266
  export {
259
267
  type ParseResult,
260
268
  type SyntaxAssoc,
261
269
  type SyntaxCalls,
270
+ type SyntaxConstructs,
262
271
  type SyntaxEnvelopes,
272
+ type SyntaxFinding,
273
+ type SyntaxFindingCode,
263
274
  type SyntaxFixity,
264
275
  type SyntaxForm,
265
276
  type SyntaxFormArgs,
@@ -270,7 +281,9 @@ export {
270
281
  type SyntaxProfile,
271
282
  type SyntaxRefPattern,
272
283
  type SyntaxRefs,
284
+ MAX_SYNTAX_ISSUES,
273
285
  STD_SYNTAX,
286
+ checkSyntax,
274
287
  parse,
275
288
  print,
276
289
  syntaxFormat,
@@ -282,12 +295,14 @@ export { sameResult } from "./same.ts";
282
295
  export { type PrintOptions, type ReadPrints, type ReadsCheck, type VerifyOptions, printReads, verifyReads } from "./reads.ts";
283
296
  export { type Durable, type DurableCounts, type DurableOptions, type Epochs, durable } from "./durable.ts";
284
297
  export {
298
+ type CycleIssue,
285
299
  type NodeEdge,
286
300
  type NodeInfo,
287
301
  type NodeRunOptions,
288
302
  type Schedule,
289
303
  type ScheduleCounts,
290
304
  type ScheduleOptions,
305
+ checkCycles,
291
306
  edgesOf,
292
307
  schedule,
293
308
  } from "./schedule.ts";
package/src/instances.ts CHANGED
@@ -504,7 +504,22 @@ function rungReads(b: Builder, id: string): void {
504
504
  collectReads(b, node, key, collect, later);
505
505
  return;
506
506
  }
507
- if (declared?.default !== undefined) termReads(b, termRefs(b, node.kind, `tree.attributes.${key}.default`, declared.default), node, undefined, later);
507
+ if (declared?.default !== undefined) {
508
+ termReads(b, termRefs(b, node.kind, `tree.attributes.${key}.default`, declared.default), node, undefined, later);
509
+ return;
510
+ }
511
+ // The document of a higher-order attribute is known only in the run, thus its reads are not known (TREE.HIGHER-CHECK).
512
+ if (declared?.higher !== undefined) {
513
+ termReads(b, termRefs(b, node.kind, `tree.attributes.${key}.higher`, declared.higher), node, undefined, later);
514
+ edge(b, undefined, later);
515
+ return;
516
+ }
517
+ // The last rung reads the attribute `forwards` and an attribute of a node that only the run knows (TREE.FORWARD-CHECK).
518
+ const forwards = tree?.forwards;
519
+ if (typeof forwards === "string" && forwards !== key) {
520
+ edge(b, vertexOf(b, node, forwards), later);
521
+ edge(b, undefined, later);
522
+ }
508
523
  }
509
524
 
510
525
  /** This function gives the list children of a node before the run. A node without a rung for it has no children. */
package/src/meaning.ts CHANGED
@@ -58,12 +58,14 @@ export interface ScheduleEngine {
58
58
  read(id: string): Result;
59
59
  }
60
60
 
61
- /** What a new engine gets beside its machine: the context of the host, and the type of a bare root value. */
61
+ /** What a new engine gets beside its machine. These are the context of the host, the type of a bare root value and the `self` of each node. */
62
62
  export interface ScheduleEngineStart {
63
63
  /** The first context of the host, without the root `world`. */
64
64
  readonly context: Context;
65
65
  /** The type of a bare root value, when the options of the harness name one. */
66
66
  readonly declaredType?: string;
67
+ /** The `self` of each node, when the options of the harness have `selves`. It has the form of the option of `schedule`. */
68
+ readonly selfOf?: (id: string, host: Context) => unknown;
67
69
  }
68
70
 
69
71
  /**
@@ -110,6 +112,12 @@ export interface AgreeScheduleOptions {
110
112
  readonly values?: readonly unknown[];
111
113
  /** The contexts of the host at the start of the runs, one after the other. */
112
114
  readonly contexts?: readonly Context[];
115
+ /**
116
+ * The values of the `self` of a node (SCHED.NODE-AGREES). With them, the option `selfOf` of each engine gives each
117
+ * node its own `self`. Some nodes then share one document, and a change of data can give a node a new `self`. The
118
+ * value `undefined` keeps the `self` of the host. The default gives each node the context of the host.
119
+ */
120
+ readonly selves?: readonly unknown[];
113
121
  /** The number of runs. The default is 100. */
114
122
  readonly count?: number;
115
123
  /** The number of commands in one run. The default is 30. */
@@ -155,6 +163,10 @@ export interface ScheduleAgreementCounts {
155
163
  readonly same: number;
156
164
  /** The new machines. */
157
165
  readonly machines: number;
166
+ /** The changes of data with a new `self` of a node, only with the option `selves`. */
167
+ readonly selves: number;
168
+ /** The documents that a node shares with another node, only with the option `selves`. */
169
+ readonly shared: number;
158
170
  }
159
171
 
160
172
  /** The outcome of `agreeSchedule`: what the harness reached, or the first disagreement. */
@@ -266,7 +278,7 @@ function shuffle<T>(items: readonly T[], rng: Random): T[] {
266
278
  * This function evaluates each node of a graph from scratch, with the meaning of D31. Each node gets a new plan and
267
279
  * one value. The algorithm of Tarjan over the demands finds the cycles. The function keeps nothing between calls.
268
280
  */
269
- function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base: Context, declaredType: string | undefined): ReadonlyMap<string, Result> {
281
+ function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base: Context, declaredType: string | undefined, own: ReadonlyMap<string, unknown> | undefined): ReadonlyMap<string, Result> {
270
282
  const declared = declaredType === undefined ? {} : { declaredType };
271
283
  const values = new Map<string, Result>();
272
284
  const index = new Map<string, number>();
@@ -298,7 +310,8 @@ function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base:
298
310
  lowest.set(id, index.get(id) as number);
299
311
  component.push(id);
300
312
  evaluating.push(id);
301
- let result = prepare(machine, docs.get(id), declared).evaluate(ctx);
313
+ const self = own?.get(id);
314
+ let result = prepare(machine, docs.get(id), declared).evaluate(self === undefined ? ctx : { ...ctx, self });
302
315
  evaluating.pop();
303
316
  if ((lowest.get(id) as number) < (index.get(id) as number)) result = cycleOf(id);
304
317
  else if (component.splice(component.indexOf(id)).length > 1 || selfReads.has(id)) result = cycleOf(id);
@@ -340,7 +353,8 @@ function named(result: Result): string {
340
353
  /**
341
354
  * This function examines an engine of a host against an evaluation from scratch (SCHED.AGREES, R4 P4, D58). Each run
342
355
  * makes a new engine and a random graph of named nodes from the document generator. Then it gives random commands:
343
- * edits, new nodes, removed nodes, the same document again, new machines and changes of data.
356
+ * edits, new nodes, removed nodes, the same document again, new machines and changes of data. The option `selves`
357
+ * adds a `self` for each node and documents that nodes share (SCHED.NODE-AGREES).
344
358
  *
345
359
  * - Each read command evaluates the whole graph from scratch two times, with the meaning of D31.
346
360
  * - It then demands each node in a random order, or one node, and compares each answer with `sameResult`.
@@ -366,6 +380,7 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
366
380
  const hostNames = options.names ?? ["a", "b", "x", "n"];
367
381
  const hostValues = options.values ?? standardValues();
368
382
  const contexts = options.contexts ?? standardContexts();
383
+ const selves = options.selves;
369
384
  const total = options.count ?? 100;
370
385
  const steps = options.steps ?? 30;
371
386
  const first = options.seed ?? 9_000_011;
@@ -376,7 +391,7 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
376
391
  if (isClosure(a) || isClosure(b)) return isClosure(a) && isClosure(b) && docEquals(closureParams(a), closureParams(b));
377
392
  return undefined;
378
393
  };
379
- const counts = { reads: 0, compared: 0, unrepeatable: 0, values: 0, cycles: 0, runs: 0, data: 0, empty: 0, edits: 0, additions: 0, removes: 0, same: 0, machines: 0 };
394
+ const counts = { reads: 0, compared: 0, unrepeatable: 0, values: 0, cycles: 0, runs: 0, data: 0, empty: 0, edits: 0, additions: 0, removes: 0, same: 0, machines: 0, selves: 0, shared: 0 };
380
395
  const declared = declaredType === undefined ? {} : { declaredType };
381
396
  for (let run = 0; run < total; run += 1) {
382
397
  const seed = first + run;
@@ -385,7 +400,18 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
385
400
  let at = 0;
386
401
  let machine = machines[0] as Machine;
387
402
  let made = 0;
403
+ const own = selves === undefined ? undefined : new Map<string, unknown>();
404
+ const pool: unknown[] = [];
388
405
  const make = (): unknown => {
406
+ if (selves !== undefined && pool.length > 0 && rng.chance(0.4)) {
407
+ counts.shared += 1;
408
+ return rng.pick(pool);
409
+ }
410
+ const doc = generated();
411
+ pool.push(doc);
412
+ return doc;
413
+ };
414
+ const generated = (): unknown => {
389
415
  if (rng.chance(wired)) return wire(rng, node);
390
416
  const roll = rng.int(10);
391
417
  if (roll === 0) return value(rng);
@@ -395,15 +421,16 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
395
421
  return runs && roll === 9 ? { type: "log", value: [doc] } : doc;
396
422
  };
397
423
  const docs = new Map<string, unknown>(nodes.map((id) => [id, make()]));
424
+ if (own !== undefined && selves !== undefined) for (const id of nodes) own.set(id, rng.pick(selves));
398
425
  const start = contexts[run % contexts.length] as Context;
399
426
  let base: Context = { ...start, names: { ...start.names } };
400
- const host = engine(machine, { ...declared, context: base });
427
+ const host = engine(machine, { ...declared, context: base, ...(own === undefined ? {} : { selfOf: (id: string) => own.get(id) }) });
401
428
  for (const [id, doc] of docs) host.set(id, doc);
402
429
  for (let step = 0; step < steps; step += 1) {
403
430
  const what = rng.int(12);
404
431
  if (what < 4) {
405
- const fresh = fromScratch(machine, docs, base, declaredType);
406
- const again = fromScratch(machine, docs, base, declaredType);
432
+ const fresh = fromScratch(machine, docs, base, declaredType, own);
433
+ const again = fromScratch(machine, docs, base, declaredType, own);
407
434
  const ids = shuffle([...docs.keys()], rng);
408
435
  // Half of the reads demand each node in a random order, and the other half demand one node.
409
436
  for (const id of rng.chance(0.5) ? ids : ids.slice(0, 1)) {
@@ -428,9 +455,15 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
428
455
  }
429
456
  }
430
457
  } else if (what < 6) {
431
- base = { ...base, names: change(base, rng, hostNames, hostValues) };
458
+ if (own !== undefined && selves !== undefined && rng.chance(0.5)) {
459
+ // A new `self` of one node, with the same context object of the host.
460
+ own.set(rng.pick(nodes), rng.pick(selves));
461
+ counts.selves += 1;
462
+ } else {
463
+ base = { ...base, names: change(base, rng, hostNames, hostValues) };
464
+ counts.data += 1;
465
+ }
432
466
  host.changeData(base);
433
- counts.data += 1;
434
467
  } else if (what === 6) {
435
468
  // A change of data that changes no value: the same context object again.
436
469
  host.changeData(base);
package/src/parts.ts CHANGED
@@ -41,10 +41,13 @@ export type Split =
41
41
  | { readonly ok: false; readonly reason: string; readonly at: readonly Key[] };
42
42
 
43
43
  class Bad {
44
- constructor(
45
- readonly reason: string,
46
- readonly at: readonly Key[],
47
- ) {}
44
+ readonly reason: string;
45
+ readonly at: readonly Key[];
46
+
47
+ constructor(reason: string, at: readonly Key[]) {
48
+ this.reason = reason;
49
+ this.at = at;
50
+ }
48
51
  }
49
52
 
50
53
  /** This function sets an own data property. Only the key `__proto__` needs `defineProperty`. */
package/src/providers.ts CHANGED
@@ -150,6 +150,7 @@ function unionOf(types: readonly ValueType[]): ValueType {
150
150
 
151
151
  /** The state of one reading of a schema: the root, the issues and the definitions that it read. */
152
152
  class SchemaReader {
153
+ readonly root: unknown;
153
154
  readonly issues: TypeIssue[] = [];
154
155
  /** The type of each definition that a reference reached. Each reading of a definition is sound, thus the first stays. */
155
156
  readonly defs = new Map<string, ValueType>();
@@ -158,7 +159,9 @@ class SchemaReader {
158
159
  /** The number of schemas with an own `$id` around the current schema. A reference inside them has another base. */
159
160
  inner = 0;
160
161
 
161
- constructor(readonly root: unknown) {}
162
+ constructor(root: unknown) {
163
+ this.root = root;
164
+ }
162
165
 
163
166
  issue(at: readonly Key[], reason: string): void {
164
167
  this.issues.push({ at, reason });
package/src/reference.ts CHANGED
@@ -1,12 +1,27 @@
1
1
  import { type Envelope, type Json, type Key, docEquals, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
2
  import { ask } from "./ladder.ts";
3
- import { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, atArgsOf, atRuleOf, circularOf, collectOf, equationType, lookupKind } from "./world.ts";
3
+ import {
4
+ type AttributeDeclaration,
5
+ type CapsRule,
6
+ type CircularRule,
7
+ type Kind,
8
+ type TreeContract,
9
+ GENERATION_LIMIT,
10
+ atArgsOf,
11
+ atRuleOf,
12
+ circularOf,
13
+ collectOf,
14
+ documentKey,
15
+ equationType,
16
+ lookupKind,
17
+ } from "./world.ts";
4
18
  import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
5
19
  import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, ROOTS, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
6
20
  import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
7
21
  import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
8
22
  import {
9
23
  type AllOptions,
24
+ type Clause,
10
25
  type Effect,
11
26
  type EvalFn,
12
27
  type EvalInput,
@@ -281,6 +296,44 @@ function freeIn(root: Context, ctx: Context, path: readonly unknown[]): boolean
281
296
  return bound === Object.hasOwn(root.names, key) && (!bound || Object.is(ctx.names[key], root.names[key]));
282
297
  }
283
298
 
299
+ /** The clauses of one active `handle`, as SPEC tells: the frame around it, and if its body has ended. */
300
+ interface RefFrame {
301
+ readonly clauses: readonly Clause[];
302
+ readonly outer: RefFrame | undefined;
303
+ /** The list of the effects of the evaluation: a frame of another evaluation has ended. */
304
+ readonly owner: unknown;
305
+ ended: boolean;
306
+ }
307
+
308
+ /** The key of the frame of a context. */
309
+ const REF_FRAME = /* @__PURE__ */ Symbol("coeval.reference.frame");
310
+
311
+ /** This function gives the frame of a context. */
312
+ function refFrame(ctx: Context): RefFrame | undefined {
313
+ return (ctx as Context & { readonly [REF_FRAME]?: RefFrame })[REF_FRAME];
314
+ }
315
+
316
+ /**
317
+ * This function finds the clause for an effect (HANDLE.DO, HANDLE.FORWARD). An effect without a capability gets no clause
318
+ * (HANDLE.NARROW). A clause of a frame that has ended gives `#CAP!` (HANDLE.ENDED).
319
+ */
320
+ function clauseAt(env: Env, ctx: Context, kind: Cap, place: readonly Key[]): Clause | Failed | undefined {
321
+ let frame = refFrame(ctx);
322
+ if (frame === undefined) return undefined;
323
+ const keys = typeof kind === "string" ? kind.split(".") : kind;
324
+ const machine = env.machine;
325
+ const allowed = machine.permits === undefined ? permits(ctx.caps, keys) : machine.permits(ctx.caps, keys, ctx);
326
+ if (!allowed) return undefined;
327
+ for (; frame !== undefined; frame = frame.outer) {
328
+ for (const clause of frame.clauses) {
329
+ if (!permits([clause.op], keys)) continue;
330
+ if (frame.ended || frame.owner !== env.effects) return fail("#CAP!", `The handler of the effect ${keys.join(".")} has ended.`, { place });
331
+ return clause;
332
+ }
333
+ }
334
+ return undefined;
335
+ }
336
+
284
337
  /**
285
338
  * This function causes an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). It records the effect with its
286
339
  * address (RUN.ADDRESS) and its outcome (RUN.OUTCOME). The mock answers the effect at once (RUN.MOCK). Without a
@@ -300,6 +353,8 @@ function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: reado
300
353
  function causeFromEvaluator(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
301
354
  const text = typeof kind === "string" ? kind : kind.join(".");
302
355
  if (env.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
356
+ const clause = clauseAt(env, ctx, kind, place);
357
+ if (clause !== undefined) return "ok" in clause ? clause : drive(clause.answer(payload), place, text);
303
358
  return cause(env, ctx, kind, payload, place);
304
359
  }
305
360
 
@@ -309,6 +364,8 @@ function causeFromEvaluator(env: Env, ctx: Context, kind: Cap, payload: unknown,
309
364
  * (RUN.OUTCOME).
310
365
  */
311
366
  function* causeAndWait(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
367
+ const clause = clauseAt(env, ctx, kind, place);
368
+ if (clause !== undefined) return "ok" in clause ? clause : yield* clause.answer(payload);
312
369
  if (!env.running) return cause(env, ctx, kind, payload, place);
313
370
  const effect = recordEffect(env, ctx, kind, payload, place);
314
371
  if ("ok" in effect) return effect;
@@ -466,6 +523,17 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
466
523
  };
467
524
  }
468
525
 
526
+ /** This function evaluates a part under a frame: the part also gets the kinds of the clauses (CAP.GRANT). */
527
+ function* handledAt(env: Env, part: At, clauses: readonly Clause[]): Steps {
528
+ const frame: RefFrame = { clauses, outer: refFrame(part.ctx), owner: env.effects, ended: false };
529
+ const ctx = { ...part.ctx, caps: [...part.ctx.caps, ...clauses.map((clause) => clause.op)], [REF_FRAME]: frame };
530
+ try {
531
+ return yield* stepsAt(env, { ...part, ctx });
532
+ } finally {
533
+ frame.ended = true;
534
+ }
535
+ }
536
+
469
537
  function runInput(env: Env, seen: Seen, at: At): RunInput {
470
538
  const { split, doc } = seen;
471
539
  const parts: RunPart[] = split.parts.map((slot) => {
@@ -475,6 +543,7 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
475
543
  path: slot.path,
476
544
  ...(name === undefined ? {} : { name }),
477
545
  evaluate: (given?: Context) => stepsAt(env, partAt(seen, at, slot, given)),
546
+ handled: (clauses: readonly Clause[]) => handledAt(env, partAt(seen, at, slot, undefined), clauses),
478
547
  };
479
548
  });
480
549
  const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));
@@ -882,6 +951,55 @@ function collectIn(node: RefNode, name: string, declared: unknown): Result {
882
951
  return visit(node) ?? acc;
883
952
  }
884
953
 
954
+ /** TREE.HIGHER-DEPTH: the depth of each root of a generated document in the reference. */
955
+ const refGenerations = /* @__PURE__ */ new WeakMap<RefNode, number>();
956
+
957
+ /** TREE.HIGHER-DEPTH: the depth of the nearest root of a generated document at or above a node of the reference, or 0. */
958
+ function generationIn(node: RefNode): number {
959
+ for (let at: RefNode | undefined = node; at !== undefined; at = at.up) {
960
+ const depth = refGenerations.get(at);
961
+ if (depth !== undefined) return depth;
962
+ }
963
+ return 0;
964
+ }
965
+
966
+ /**
967
+ * TREE.HIGHER: a higher-order attribute. The equation gives a document as data, and the document evaluates with the
968
+ * node as `self`. The reference compiles each document again, thus a structural key is not a part of the meaning.
969
+ */
970
+ function higherOf(node: RefNode, name: string, term: Json): Result {
971
+ const value = equationOf(node, term, {});
972
+ if (!value.ok) return value;
973
+ const text = nameOfAttribute(node, name);
974
+ if (documentKey(value.value) === undefined) return fail("#VALUE!", `The equation of the higher-order attribute ${text} gives a value that is not JSON data.`);
975
+ const depth = generationIn(node) + 1;
976
+ if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The higher-order attribute ${text} is under more than ${GENERATION_LIMIT} generated documents.`);
977
+ const root = equationOf(node, value.value as Json, {});
978
+ if (!root.ok) return root;
979
+ const made = refNodeOf(root.value);
980
+ if (made === undefined) return fail("#VALUE!", `The document of the higher-order attribute ${text} gives no node of the tree.`);
981
+ refGenerations.set(made, depth);
982
+ return root;
983
+ }
984
+
985
+ /** TREE.FORWARD: the last rung. The attribute `forwards` gives a node, and that node answers through its own ladder. */
986
+ function forwardOf(node: RefNode, name: string): Result | undefined {
987
+ const target: unknown = node.contract?.forwards;
988
+ if (target === undefined || target === name) return undefined;
989
+ const text = nameOfAttribute(node, name);
990
+ if (typeof target !== "string") return fail("#VALUE!", `The attribute ${text} has a contract with a bad field forwards. It must be a text.`);
991
+ const value = attributeOf(node, target);
992
+ if (value === undefined || !value.ok) return value;
993
+ const to = refNodeOf(value.value);
994
+ if (to === undefined) return fail("#VALUE!", `The attribute ${target} that ${text} forwards to gives no node of a tree.`);
995
+ if (!refGenerations.has(to)) {
996
+ const depth = generationIn(node) + 1;
997
+ if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The attribute ${text} forwards through more than ${GENERATION_LIMIT} generated documents.`);
998
+ refGenerations.set(to, depth);
999
+ }
1000
+ return attributeOf(to, name);
1001
+ }
1002
+
885
1003
  /**
886
1004
  * TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
887
1005
  * answer, and the next rung then tries.
@@ -912,9 +1030,11 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
912
1030
  const declared = termIn(node.contract?.attributes, name);
913
1031
  if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
914
1032
  if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
915
- return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
1033
+ if (declared?.default !== undefined) return equationOf(node, declared.default, {});
1034
+ return declared?.higher === undefined ? undefined : higherOf(node, name, declared.higher);
916
1035
  },
917
1036
  ],
1037
+ ["forward", forwardOf],
918
1038
  ];
919
1039
 
920
1040
  /** TREE.LADDER: the answer of the first rung that answers, or `undefined`. */