@mark1russell7/coeval 0.7.1 → 0.8.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/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;
@@ -530,10 +636,26 @@ function plainValue(value: unknown, open: readonly object[]): Result {
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
637
  * tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error.
532
638
  */
533
- const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
639
+ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath }) => {
534
640
  const outer = refNodeOf(context.self);
535
641
  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() };
642
+ const found = machine === undefined || typePath.length === 0 ? undefined : lookupKind(machine.world, typePath);
643
+ const contract = found?.ok === true ? found.kind.tree : undefined;
644
+ const of = contract === undefined || found?.ok !== true ? undefined : equationType(found.kind);
645
+ const node: RefNode = {
646
+ tree: nested ? outer.tree : { open: true },
647
+ kind,
648
+ place,
649
+ parts,
650
+ ctx: context,
651
+ contract,
652
+ of,
653
+ up: nested ? outer : undefined,
654
+ env: activeEnvs[activeEnvs.length - 1],
655
+ thing: null as unknown as HostThing,
656
+ values: new Map(),
657
+ pending: new Set(),
658
+ };
537
659
  const self: HostThing & { readonly type: string } = {
538
660
  [THING]: true,
539
661
  type: kind,
@@ -541,17 +663,18 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
541
663
  deref: (key): Thing | undefined => {
542
664
  if (key === "parent") return context.self;
543
665
  if (key === "type") return kind;
544
- const result = attributeOf(node, self, String(key));
666
+ const result = attributeOf(node, String(key));
545
667
  if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
546
668
  throw new Failure(result.error);
547
669
  },
548
- has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)),
670
+ has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)) || node.values.has(String(key)),
549
671
  toJson: () => {
550
672
  const result = jsonOfNode(node, self, []);
551
673
  if (result.ok) return result.value;
552
674
  throw new Failure(result.error);
553
675
  },
554
676
  };
677
+ node.thing = self;
555
678
  refNodes.set(self, node);
556
679
  if (nested) return ok(self);
557
680
  try {
package/src/static.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
- import type { Kind, NamesSource, ScopeRule, World } from "./world.ts";
1
+ import { type Envelope, type Json, type Key, isKey, isRecord, typeName } from "./doc.ts";
2
+ import { type Kind, type World, equationType } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
- import { type PartSlot, partsOf, pathMatches } from "./parts.ts";
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";
6
6
  import type { ErrorCode } from "./errors.ts";
7
7
  import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
@@ -67,75 +67,6 @@ export interface WalkOptions {
67
67
  readonly problem?: (problem: WalkProblem) => void;
68
68
  }
69
69
 
70
- function valueAt(value: unknown, path: readonly Key[]): unknown {
71
- let here: unknown = value;
72
- for (const key of path) {
73
- if (Array.isArray(here)) here = here[Number(key)];
74
- else if (isRecord(here) && Object.hasOwn(here, String(key))) here = here[String(key)];
75
- else return undefined;
76
- }
77
- return here;
78
- }
79
-
80
- function headerName(item: Json): string | undefined {
81
- return isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
82
- }
83
-
84
- function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot[]): readonly string[] {
85
- if ("fixed" in source) return source.fixed;
86
- if ("keysAt" in source) {
87
- const at = valueAt(value, source.keysAt);
88
- return isRecord(at) ? Object.keys(at) : [];
89
- }
90
- if ("listAt" in source) {
91
- const at = valueAt(value, source.listAt);
92
- return Array.isArray(at) ? (at as readonly unknown[]).filter((item): item is string => typeof item === "string") : [];
93
- }
94
- if ("textAt" in source) {
95
- const at = valueAt(value, source.textAt);
96
- if (typeof at === "string") return [at];
97
- return source.otherwise === undefined ? [] : [source.otherwise];
98
- }
99
- return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
100
- }
101
-
102
- /**
103
- * This function gives the names that the scope rules of a kind give each part, and the names that only later parts
104
- * see. `check` and `conforms` read the scope rules with it.
105
- *
106
- * @param rules - The scope rules of the kind.
107
- * @param value - The value of the document.
108
- * @param slots - The parts of the value, in order.
109
- * @returns The names that each part sees, and the names that only the later parts see, by the index of the part.
110
- */
111
- export function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
112
- const sees: string[][] = slots.map(() => []);
113
- const later: string[][] = slots.map(() => []);
114
- for (const rule of rules ?? []) {
115
- const covered = slots.map((slot, index) => ({ slot, index })).filter(({ slot }) => rule.in.some((pattern) => pathMatches(slot.path, pattern)));
116
- if (rule.sequential !== true) {
117
- const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
118
- for (const { index } of covered) sees[index]?.push(...names);
119
- continue;
120
- }
121
- if ("headers" in rule.names) {
122
- covered.forEach(({ index }, position) => {
123
- const before = covered.slice(0, position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
124
- const after = covered.slice(position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
125
- sees[index]?.push(...before);
126
- later[index]?.push(...after);
127
- });
128
- continue;
129
- }
130
- const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
131
- covered.forEach(({ index }, position) => {
132
- if (position > 0) sees[index]?.push(...names);
133
- else later[index]?.push(...names);
134
- });
135
- }
136
- return { sees, later };
137
- }
138
-
139
70
  /** The names of a walk without names. A walk does not change a set at any time: it makes a new set to add names. */
140
71
  const NO_NAMES: ReadonlySet<string> = /* @__PURE__ */ new Set();
141
72
 
@@ -595,11 +526,55 @@ interface Drive {
595
526
  /** What the code of a document does with a wait: an evaluator stops it, and a runner passes it on. */
596
527
  type Waits = boolean | Drive;
597
528
 
529
+ /** The names that an equation of `down` sees (TREE.DOWN). */
530
+ const DOWN_NAMES: readonly string[] = /* @__PURE__ */ Object.freeze(["child", "index", "prev"]);
531
+
532
+ /** The names that the term `combine` of a fold sees (TREE.FOLD). */
533
+ const FOLD_NAMES: readonly string[] = /* @__PURE__ */ Object.freeze(["acc", "item"]);
534
+
535
+ /**
536
+ * This function gives each equation of the tree contract of a kind, with the names that it sees (D42). An equation of
537
+ * `equations`, a `unit` and a `default` see no names.
538
+ */
539
+ function equationsOf(kind: Kind): readonly { readonly label: string; readonly term: Json; readonly names: readonly string[] }[] {
540
+ const tree = kind.tree;
541
+ if (tree === undefined) return [];
542
+ const out: { readonly label: string; readonly term: Json; readonly names: readonly string[] }[] = [];
543
+ for (const [name, term] of Object.entries(tree.equations ?? {})) out.push({ label: `tree.equations.${name}`, term, names: [] });
544
+ for (const [name, term] of Object.entries(tree.down ?? {})) out.push({ label: `tree.down.${name}`, term, names: DOWN_NAMES });
545
+ for (const [name, declared] of Object.entries(tree.attributes ?? {})) {
546
+ if (declared.fold !== undefined) {
547
+ out.push({ label: `tree.attributes.${name}.fold.unit`, term: declared.fold.unit, names: [] });
548
+ out.push({ label: `tree.attributes.${name}.fold.combine`, term: declared.fold.combine, names: FOLD_NAMES });
549
+ }
550
+ if (declared.default !== undefined) out.push({ label: `tree.attributes.${name}.default`, term: declared.default, names: [] });
551
+ }
552
+ return out;
553
+ }
554
+
555
+ /**
556
+ * This function checks the equations of the tree contract of a kind, each with the names of its rung (TREE.DOWN-NAMES).
557
+ * An equation evaluates without capabilities, thus a check with capabilities gives it none. Each issue gets the place
558
+ * of the document of the kind, and its reason names the equation.
559
+ */
560
+ function equationIssues(machine: Machine, kind: Kind, kindText: string, options: CheckOptions, seen: Set<Kind>): readonly Issue[] {
561
+ const of = equationType(kind);
562
+ const out: Issue[] = [];
563
+ for (const { label, term, names } of equationsOf(kind)) {
564
+ const inner: CheckOptions = { names, ...(of === undefined ? {} : { declaredType: of }), ...(options.caps === undefined ? {} : { caps: [] }) };
565
+ for (const issue of checkAll(machine, term, inner, undefined, seen)) {
566
+ out.push({ code: issue.code, place: [], reason: `In the equation ${label} of the kind ${kindText}: ${issue.reason}` });
567
+ }
568
+ }
569
+ return out;
570
+ }
571
+
598
572
  /**
599
573
  * This function finds the issues of `check`. With a list of nodes, it also records each document that it visits for
600
- * the pass of types. It marks each document with an issue.
574
+ * the pass of types. It marks each document with an issue. The set `seen` holds the kinds whose equations the check
575
+ * walked, thus it walks the equations of each kind one time.
601
576
  */
602
- function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes: TypeNode[] | undefined): readonly Issue[] {
577
+ function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes: TypeNode[] | undefined, seen: Set<Kind> = new Set()): readonly Issue[] {
603
578
  const issues: Issue[] = [];
604
579
  const verb = options.verb ?? "eval";
605
580
  const indices = nodes === undefined ? undefined : new Map<DocVisit, number>();
@@ -643,6 +618,11 @@ function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes:
643
618
  push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.`, at: v.at });
644
619
  return "skip";
645
620
  }
621
+ // The equations of a kind are code of the kind. The check walks them at the first document of the kind (D42).
622
+ if (v.kind?.tree !== undefined && !seen.has(v.kind)) {
623
+ seen.add(v.kind);
624
+ for (const issue of equationIssues(machine, v.kind, typeName(v.doc.type), options, seen)) push({ ...issue, place: v.place, at: v.at });
625
+ }
646
626
  const name = answer.evaluator;
647
627
  if (current !== undefined) current.evaluator = name;
648
628
  // A runner hides the evaluator of its name (MACHINE.RUNNER), as in the compilation of a plan.