@mark1russell7/coeval 0.6.3 → 0.7.1

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/static.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
- import { type Kind, type NamesSource, type ScopeRule, type World, lookupKind } from "./world.ts";
2
+ import type { Kind, NamesSource, ScopeRule, World } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
4
  import { type PartSlot, partsOf, pathMatches } from "./parts.ts";
5
- import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
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
- import { type Machine, partCaps, permits } from "./evaluate.ts";
7
+ import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
8
+ import type { ValueType } from "./types.ts";
9
+ import { type RootTypes, type TypeNode, type Typed, typePass } from "./typing.ts";
8
10
 
9
11
  /** One document that a walk visits. */
10
12
  export interface DocVisit {
@@ -97,8 +99,16 @@ function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot
97
99
  return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
98
100
  }
99
101
 
100
- /** The names that the scope rules of a kind give each part, and the names that only later parts see. */
101
- function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
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[])[] } {
102
112
  const sees: string[][] = slots.map(() => []);
103
113
  const later: string[][] = slots.map(() => []);
104
114
  for (const rule of rules ?? []) {
@@ -162,6 +172,8 @@ function keysOf(trail: Trail | undefined): readonly Key[] {
162
172
  class Visit implements DocVisit {
163
173
  readonly #place: Trail | undefined;
164
174
  readonly #at: Trail | undefined;
175
+ readonly #bound: readonly string[];
176
+ readonly #expect: ValueType | undefined;
165
177
 
166
178
  constructor(
167
179
  place: Trail | undefined,
@@ -177,9 +189,23 @@ class Visit implements DocVisit {
177
189
  readonly typePath: readonly Key[] | undefined,
178
190
  readonly address: true | undefined,
179
191
  readonly caps: readonly Cap[] | undefined,
192
+ bound: readonly string[],
193
+ expect: ValueType | undefined,
180
194
  ) {
181
195
  this.#place = place;
182
196
  this.#at = at;
197
+ this.#bound = bound;
198
+ this.#expect = expect;
199
+ }
200
+
201
+ /** The names that the scope rules of the parent bind for this document. The check of types reads them. */
202
+ get bound(): readonly string[] {
203
+ return this.#bound;
204
+ }
205
+
206
+ /** The type that the shape of the parent declares for this part. The check of types reads it. */
207
+ get expect(): ValueType | undefined {
208
+ return this.#expect;
183
209
  }
184
210
 
185
211
  get place(): readonly Key[] {
@@ -216,6 +242,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
216
242
  readonly slot: readonly Key[];
217
243
  readonly parent: DocVisit | undefined;
218
244
  readonly caps: readonly Cap[] | undefined;
245
+ readonly bound: readonly string[];
246
+ readonly expect: ValueType | undefined;
219
247
  }
220
248
  const stack: Item[] = [
221
249
  {
@@ -230,6 +258,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
230
258
  slot: [],
231
259
  parent: undefined,
232
260
  caps: options.caps,
261
+ bound: [],
262
+ expect: undefined,
233
263
  },
234
264
  ];
235
265
  while (stack.length > 0) {
@@ -254,6 +284,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
254
284
  read.typePath,
255
285
  item.address ? true : undefined,
256
286
  item.caps,
287
+ item.bound,
288
+ item.expect,
257
289
  );
258
290
  const answer = visit(here);
259
291
  if (answer === "skip" || kind === undefined) continue;
@@ -279,6 +311,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
279
311
  slot: slot.path,
280
312
  parent: here,
281
313
  caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
314
+ bound: extra,
315
+ expect: slot.expect,
282
316
  });
283
317
  }
284
318
  }
@@ -380,6 +414,10 @@ export interface Issue {
380
414
  readonly reason: string;
381
415
  /** The keys from the root JSON value to the value with the problem. */
382
416
  readonly at?: readonly Key[];
417
+ /** The type that the position needs, for a type issue (`#TYPE!`). */
418
+ readonly expected?: ValueType;
419
+ /** The type that the check found, for a type issue (`#TYPE!`). */
420
+ readonly found?: ValueType;
383
421
  }
384
422
 
385
423
  /** The options of a check. */
@@ -393,16 +431,89 @@ export interface CheckOptions {
393
431
  * each part by the capability rules of the kinds, as a run does.
394
432
  */
395
433
  readonly caps?: readonly Cap[];
434
+ /**
435
+ * The verb that goes up the ladder at each document, as in `prepare`. The default is `eval`. `#NOEVAL` and the hook
436
+ * `known` follow this verb. The contract declares the effects and the value types of `eval` only, thus only `eval`
437
+ * checks them (VERB.CHECK). The type rule of an evaluator serves each verb.
438
+ */
439
+ readonly verb?: string;
440
+ /**
441
+ * With `true`, the check also checks the value types (TYPE.CHECK). It reads the type rules of the evaluators, the
442
+ * signatures of the operations and the value types of the contracts. A type issue has the code `#TYPE!`. The
443
+ * default is `false`, thus a host opts in.
444
+ */
445
+ readonly types?: boolean;
446
+ /** The type that the root document must have, for the check of the value types. */
447
+ readonly expect?: ValueType;
448
+ /**
449
+ * With `true`, the check also finds the problems of a run (RUN.CAN-WAIT). A runner under a parent without a runner
450
+ * gives `#CALC!` when an effect of its runners can reach the host. The parent cannot pass that wait through.
451
+ * A runner without such an effect does not wait, and the run drives it (RUN.DRIVE-NESTED). A kind that declares
452
+ * effects and has no runner gives `#CALC!` too, because in a run only a runner can wait for the host. The default is
453
+ * `false`.
454
+ */
455
+ readonly run?: boolean;
456
+ /**
457
+ * The value types of the roots and of the names of the host, for the check of the value types (TYPE.ROOTS). A
458
+ * reference then gets the type that its path reaches. Only a check with `types: true` reads them.
459
+ */
460
+ readonly rootTypes?: RootTypes;
461
+ }
462
+
463
+ /** The result of `typeCheck`. */
464
+ export interface Typing {
465
+ /** The type of the root document. It is `unknown` when the root has an issue. */
466
+ readonly type: ValueType;
467
+ /** Each issue of the check. The type issues come after the other issues. */
468
+ readonly issues: readonly Issue[];
469
+ /** The type of each document, in the order of the document. */
470
+ readonly places: readonly Typed[];
471
+ }
472
+
473
+ /** The least authority: the capability `[]` permits each effect (CAP.KEYS). */
474
+ const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
475
+
476
+ /** No declared effect. One list serves each visit without effects. */
477
+ const NO_EFFECTS: readonly (readonly Key[])[] = /* @__PURE__ */ Object.freeze([]);
478
+
479
+ /**
480
+ * This function gives the effects that the contract of the kind of a visit declares, as keys (FACTS.EFFECTS). The
481
+ * text `"type"` gives the type of the document after each alias. A visit without a kind declares no effect. `check`,
482
+ * `factsOf` and `effectsOf` read the declarations with this one function (D27).
483
+ *
484
+ * @param v - The visit.
485
+ * @returns The keys of each declared effect, in the order of the contract.
486
+ */
487
+ export function declaredEffects(v: DocVisit): readonly (readonly Key[])[] {
488
+ const effects = v.kind?.effects;
489
+ if (effects === undefined || effects.length === 0) return NO_EFFECTS;
490
+ return effects === "type" ? [v.typePath ?? []] : effects.map((kind) => kind.split("."));
491
+ }
492
+
493
+ /**
494
+ * This function gives the rule of a machine for an effect, for the static kit (CAP.PERMITS). The rule is the hook
495
+ * `permits` of the machine, as in a run, or else the function `permits`. A check has no values, thus the hook gets a
496
+ * context with the capabilities of the document and the world of the machine. Its names, its self and its event are
497
+ * empty.
498
+ *
499
+ * @param machine - The machine.
500
+ * @returns The rule. It gets the capabilities of a document and the keys of an effect.
501
+ */
502
+ export function permitsOf(machine: Machine): (caps: readonly Cap[], kind: readonly Key[]) => boolean {
503
+ const hook = machine.permits;
504
+ if (hook === undefined) return permits;
505
+ let base: Context | undefined;
506
+ return (caps, kind) => hook(caps, kind, { ...(base ??= context({ world: worldThing(machine.world) })), caps });
396
507
  }
397
508
 
398
509
  /**
399
510
  * This function gives the reason of a `#CAP!`. When a kind around the document narrowed the capabilities, the
400
511
  * reason names the nearest such kind.
401
512
  */
402
- function capReason(v: DocVisit, kind: readonly Key[]): string {
513
+ function capReason(v: DocVisit, kind: readonly Key[], allowed: (caps: readonly Cap[], kind: readonly Key[]) => boolean): string {
403
514
  const text = kind.join(".");
404
515
  for (let up = v.parent; up !== undefined; up = up.parent) {
405
- if (up.caps !== undefined && permits(up.caps, kind)) {
516
+ if (up.caps !== undefined && allowed(up.caps, kind)) {
406
517
  return `The capabilities do not permit the effect ${text} here. The kind ${typeName(up.doc.type)} around it narrows them for its parts.`;
407
518
  }
408
519
  }
@@ -422,14 +533,91 @@ function capReason(v: DocVisit, kind: readonly Key[]): string {
422
533
  * - `#NAME?`: a name that nothing binds. With the hook `known` of the machine, also a name that the language does not
423
534
  * have, for example an operation.
424
535
  * - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
536
+ * The hook `permits` of the machine decides, as in a run (CAP.PERMITS).
537
+ * - `#CALC!`: with the option `run`, a runner under a parent without a runner, when an effect of its runners can reach
538
+ * the host. Also a kind that declares effects and has no runner (RUN.CAN-WAIT). The issue has the place and the
539
+ * reason of the run.
540
+ * - `#TYPE!`: with the option `types`, a part whose type does not fit the type that its position needs (TYPE.CHECK).
541
+ * A run does not always fail there, thus a type issue is a static finding, not an error of a run.
542
+ *
543
+ * The check asks the ladder for the verb of the options, `eval` by default (VERB.CHECK). The shapes, the paths and the
544
+ * names follow the contracts, thus they are the same for each verb. Only `eval` checks the declared effects.
425
545
  *
426
546
  * @param machine - The world and the code of the evaluators.
427
547
  * @param doc - The document, as JSON.
428
- * @param options - The type of a bare root value, the names of the host and the capabilities.
429
- * @returns The problems, in the order of the document.
548
+ * @param options - The type of a bare root value, the names of the host, the capabilities, the verb, the types and the run.
549
+ * @returns The problems, in the order of the document. The type issues come after the other issues.
430
550
  */
431
551
  export function check(machine: Machine, doc: unknown, options: CheckOptions = {}): readonly Issue[] {
552
+ return options.types === true ? typeCheck(machine, doc, options).issues : checkAll(machine, doc, options, undefined);
553
+ }
554
+
555
+ /**
556
+ * This function checks a document, and it also checks the value types (TYPE.CHECK, D25). It finds each issue of
557
+ * `check`. Then it synthesizes the type of each document from the types of its parts. It also checks each part
558
+ * against the type that its position needs. The type rule of the parent, the signature of an operation or the shape
559
+ * of the contract gives that type. A part with an issue gets the type `unknown`, thus one
560
+ * problem does not give a chain of issues.
561
+ *
562
+ * A type issue has the code `#TYPE!`, the type that the position needs (`expected`) and the type that the check found
563
+ * (`found`). It is a static finding: the evaluation of the document does not change.
564
+ *
565
+ * The option `rootTypes` gives the types of the roots and of the names of the host (TYPE.ROOTS). A reference then gets
566
+ * the type that its path reaches, and a field that a closed record does not have is a type issue (TYPE.CLOSED).
567
+ *
568
+ * @param machine - The world and the code of the evaluators, with their type rules.
569
+ * @param doc - The document, as JSON.
570
+ * @param options - The options of `check`, the type that the root must have and the types of the roots.
571
+ * @returns The type of the root, each issue, and the type of each document.
572
+ */
573
+ export function typeCheck(machine: Machine, doc: unknown, options: CheckOptions = {}): Typing {
574
+ const nodes: TypeNode[] = [];
575
+ const issues = checkAll(machine, doc, options, nodes);
576
+ const pass = typePass(machine, nodes, options.verb ?? "eval", options.expect, options.rootTypes);
577
+ return { type: pass.type, issues: pass.issues.length === 0 ? issues : [...issues, ...pass.issues], places: pass.places };
578
+ }
579
+
580
+ /**
581
+ * 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
582
+ * effect of its runners fails (RUN.DRIVE-NESTED).
583
+ */
584
+ interface Drive {
585
+ /** The issue of the run when a step waits. */
586
+ readonly issue: Issue;
587
+ /** The index of the issue in the order of the document. */
588
+ readonly index: number;
589
+ /** The node of the runner for the pass of types. */
590
+ readonly node: TypeNode | undefined;
591
+ /** `true` when an effect of its runners can reach the host, and thus wait. */
592
+ wait: boolean;
593
+ }
594
+
595
+ /** What the code of a document does with a wait: an evaluator stops it, and a runner passes it on. */
596
+ type Waits = boolean | Drive;
597
+
598
+ /**
599
+ * 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.
601
+ */
602
+ function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes: TypeNode[] | undefined): readonly Issue[] {
432
603
  const issues: Issue[] = [];
604
+ const verb = options.verb ?? "eval";
605
+ const indices = nodes === undefined ? undefined : new Map<DocVisit, number>();
606
+ let current: TypeNode | undefined;
607
+ const push = (issue: Issue): void => {
608
+ issues.push(issue);
609
+ if (current !== undefined) current.marked = true;
610
+ };
611
+ // The effects of a contract are the effects of eval (VERB.CONTRACT). An effect of another verb meets its test when
612
+ // it happens (RUN.CAP).
613
+ const effects = verb === "eval";
614
+ const allowed = options.caps === undefined ? undefined : permitsOf(machine);
615
+ // In a run, a part that can wait needs a parent that can wait (RUN.CAN-WAIT). The map tells for each document with
616
+ // code if its code passes a wait on: false for an evaluator, true for a runner whose waits reach the driver of the
617
+ // run, and a drive for a runner under an evaluator. A document without code fails before its parts, thus it is not
618
+ // in the map.
619
+ const waits = options.run === true ? new Map<DocVisit, Waits>() : undefined;
620
+ const drives: Drive[] = [];
433
621
  const maxDepth = machine.maxDepth ?? 1000;
434
622
  const anyName = options.names === "any";
435
623
  const roots: readonly string[] = machine.roots ?? ROOTS;
@@ -438,34 +626,70 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
438
626
  machine.world,
439
627
  doc,
440
628
  (v) => {
629
+ if (nodes !== undefined && indices !== undefined) {
630
+ current = { visit: v, bound: v instanceof Visit ? v.bound : [], expect: v instanceof Visit ? v.expect : undefined, children: [], evaluator: undefined, marked: false };
631
+ const parent = v.parent === undefined ? undefined : indices.get(v.parent);
632
+ if (parent !== undefined) nodes[parent]?.children.push(nodes.length);
633
+ indices.set(v, nodes.length);
634
+ nodes.push(current);
635
+ }
441
636
  if (v.depth > maxDepth) {
442
- issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.`, at: v.at });
637
+ push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.`, at: v.at });
443
638
  return "skip";
444
639
  }
445
640
  const header = v.declared ? {} : v.doc;
446
- const answer = ask(machine.world, header, "eval", v.declared ? { declaredType: typeName(v.doc.type) } : {});
641
+ const answer = ask(machine.world, header, verb, v.declared ? { declaredType: typeName(v.doc.type) } : {});
447
642
  if (answer.error === "unknown-kind") {
448
- issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.`, at: v.at });
643
+ push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.`, at: v.at });
449
644
  return "skip";
450
645
  }
451
646
  const name = answer.evaluator;
452
- if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
453
- issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
647
+ if (current !== undefined) current.evaluator = name;
648
+ // A runner hides the evaluator of its name (MACHINE.RUNNER), as in the compilation of a plan.
649
+ const runner = name !== undefined && has(machine.runners, name);
650
+ const coded = name !== undefined && (runner || has(machine.evaluators, name));
651
+ if (name === undefined || !coded) {
652
+ push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
454
653
  } else if (machine.known !== undefined) {
455
654
  // The evaluator gives #NAME? in a run for a name that the language does not have, for example an operation.
456
655
  const known = machine.known(v.typePath ?? [], name);
457
- if (known !== true) issues.push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
656
+ if (known !== true) push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
458
657
  }
459
658
  const caps = v.caps;
460
- if (caps !== undefined && v.kind?.effects !== undefined) {
461
- const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
462
- for (const kind of kinds) {
463
- if (!permits(caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: capReason(v, kind), at: v.at });
659
+ const declared = effects ? declaredEffects(v) : NO_EFFECTS;
660
+ if (allowed !== undefined && caps !== undefined) {
661
+ for (const kind of declared) {
662
+ if (!allowed(caps, kind)) push({ code: "#CAP!", place: v.place, reason: capReason(v, kind, allowed), at: v.at });
663
+ }
664
+ }
665
+ if (waits !== undefined && coded) {
666
+ // The parent of the root is the driver of the run, and it can wait. An evaluator gives its parts no wait.
667
+ const up = v.parent === undefined ? true : waits.get(v.parent);
668
+ let chain: Waits = false;
669
+ if (runner && up === false) {
670
+ // The run drives a runner under an evaluator, and only a real wait fails (RUN.DRIVE-NESTED). Its issue goes in
671
+ // here, in the order of the document, when an effect of its runners can wait.
672
+ const issue: Issue = { code: "#CALC!", place: v.place, reason: waitedReason(typeName(v.doc.type)), at: v.at };
673
+ chain = { issue, index: issues.length, node: current, wait: !effects };
674
+ drives.push(chain);
675
+ } else if (runner) {
676
+ chain = up ?? true;
677
+ }
678
+ waits.set(v, chain);
679
+ // A runner waits only for an effect that reaches the host (C6). A refused effect gives #CAP! without a wait.
680
+ if (typeof chain === "object" && !chain.wait) {
681
+ const rule = allowed ?? permits;
682
+ chain.wait = declared.some((kind) => caps === undefined || rule(caps, kind));
683
+ }
684
+ // In a run, the host does each effect, thus an evaluator cannot cause one.
685
+ const first = declared[0];
686
+ if (!runner && first !== undefined) {
687
+ push({ code: "#CALC!", place: v.place, reason: `The effect ${first.join(".")} needs a runner: in a run, the host does each effect.`, at: v.at });
464
688
  }
465
689
  }
466
690
  const isPath = isRef(v.doc);
467
691
  if (!isPath && v.typePath?.length === 1 && v.typePath[0] === "ref") {
468
- issues.push({ code: "#VALUE!", place: v.place, reason: "The value of ref is a path: a list that starts with a root.", at: [...v.at, ...(v.declared ? [] : ["value"])] });
692
+ push({ code: "#VALUE!", place: v.place, reason: "The value of ref is a path: a list that starts with a root.", at: [...v.at, ...(v.declared ? [] : ["value"])] });
469
693
  }
470
694
  if (!isPath) return undefined;
471
695
  const path = v.doc.value as readonly Json[];
@@ -473,17 +697,17 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
473
697
  // The JSON path of a problem of a path goes to its step.
474
698
  const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
475
699
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
476
- issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
700
+ push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
477
701
  // The step documents of the path are parts, and they evaluate before the path, thus the walk visits them.
478
702
  return undefined;
479
703
  }
480
704
  if (!roots.includes(root)) {
481
- issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });
705
+ push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });
482
706
  }
483
707
  const first = path[1];
484
708
  if (root === ("names" as Root) && !anyName && isKey(first) && !v.names.has(String(first))) {
485
709
  const later = v.later.has(String(first));
486
- issues.push({
710
+ push({
487
711
  code: "#NAME?",
488
712
  place: v.place,
489
713
  reason: later ? `The name ${String(first)} is bound by a later step.` : `Nothing binds the name ${String(first)}.`,
@@ -492,10 +716,9 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
492
716
  }
493
717
  path.forEach((step, index) => {
494
718
  if (index === 0 || isKey(step)) return;
495
- const found = isEnvelope(step) ? lookupKind(machine.world, step.type) : undefined;
496
- const walker = found?.ok === true && Object.hasOwn(found.kind.verbs, "step") ? found.kind.verbs["step"] : undefined;
497
- if (walker === undefined || !has(machine.walkers, walker)) {
498
- issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step.", at: stepAt(index) });
719
+ // The resolution finds the walker with the same function, thus the check and a run agree (VERB.LADDER).
720
+ if (walkerOf(machine, step) === undefined) {
721
+ push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step.", at: stepAt(index) });
499
722
  }
500
723
  });
501
724
  // The step documents of the path are parts when the kind ref has a deep shape. The walk checks them too.
@@ -504,9 +727,23 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
504
727
  {
505
728
  ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
506
729
  ...(Array.isArray(options.names) ? { names: options.names } : {}),
507
- ...(options.caps === undefined ? {} : { caps: options.caps }),
508
- problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at }),
730
+ // Without capabilities, a check of the run narrows the least authority, as effectsOf does. Thus a mask of a kind
731
+ // removes the effects that cannot wait. The check of the capabilities stays off.
732
+ ...(options.caps === undefined ? (waits === undefined ? {} : { caps: EACH }) : { caps: options.caps }),
733
+ problem: (problem) => {
734
+ issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at });
735
+ // A value without the shape of its kind is a problem of the document that the walk visited last. A part that is
736
+ // not a document has no visit, thus it marks nothing.
737
+ if (problem.code === "#VALUE!" && current !== undefined) current.marked = true;
738
+ },
509
739
  },
510
740
  );
741
+ // The issue of each drive that can wait goes in at its place. The last drive goes in first, thus each index stays.
742
+ for (let index = drives.length - 1; index >= 0; index -= 1) {
743
+ const drive = drives[index] as Drive;
744
+ if (!drive.wait) continue;
745
+ issues.splice(drive.index, 0, drive.issue);
746
+ if (drive.node !== undefined) drive.node.marked = true;
747
+ }
511
748
  return issues;
512
749
  }
package/src/stdlib.ts CHANGED
@@ -3,6 +3,7 @@ import { type Context, type EvalInput, type Evaluator, type Machine, type Part,
3
3
  import { Failure } from "./errors.ts";
4
4
  import { type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
5
5
  import type { World } from "./ladder.ts";
6
+ import { STD_DOMAINS } from "./types.ts";
6
7
 
7
8
  /**
8
9
  * The world of the examples: small kinds for numbers, lists, conditions, names, sequences and effects.
@@ -10,36 +11,43 @@ import type { World } from "./ladder.ts";
10
11
  */
11
12
  export const STD_WORLD: World = {
12
13
  kinds: {
13
- number: { verbs: { eval: "data.number", view: "view.value" }, summary: "A number. Its value is the result." },
14
- text: { verbs: { eval: "data.text", view: "view.value" }, summary: "A text. Its value is the result." },
15
- bool: { verbs: { eval: "data.bool", view: "view.value" }, summary: "A truth value. Its value is the result." },
14
+ number: { verbs: { eval: "data.number", view: "view.value" }, pure: true, summary: "A number. Its value is the result." },
15
+ text: { verbs: { eval: "data.text", view: "view.value" }, pure: true, summary: "A text. Its value is the result." },
16
+ bool: { verbs: { eval: "data.bool", view: "view.value" }, pure: true, summary: "A truth value. Its value is the result." },
16
17
  add: {
17
18
  verbs: { eval: "math.add", view: "view.op" },
18
19
  parts: { shape: "list", of: "number" },
20
+ pure: true,
19
21
  summary: "The sum of its parts. A bare number in its list gets the type number.",
20
22
  },
21
23
  multiply: {
22
24
  verbs: { eval: "math.multiply", view: "view.op" },
23
25
  parts: { shape: "list", of: "number" },
26
+ pure: true,
24
27
  summary: "The product of its parts.",
25
28
  },
26
29
  greater: {
27
30
  verbs: { eval: "math.greater", view: "view.op" },
28
31
  parts: { shape: "list", of: "number" },
32
+ pure: true,
29
33
  summary: "True when the first part is greater than the second part.",
30
34
  },
31
- list: { verbs: { eval: "list.all", view: "view.op" }, parts: { shape: "list" }, summary: "The results of its parts, as a list." },
35
+ list: { verbs: { eval: "list.all", view: "view.op" }, parts: { shape: "list" }, pure: true, summary: "The results of its parts, as a list." },
32
36
  if: {
33
37
  verbs: { eval: "control.if", view: "view.op" },
34
38
  parts: { shape: "record", fields: ["test", "then", "else"] },
39
+ pure: true,
40
+ lazy: [{ in: [["then"], ["else"]] }],
35
41
  summary: "It evaluates the test, then only the branch that the test selects.",
36
42
  },
37
43
  ref: {
38
44
  verbs: { eval: "ref.resolve", view: "view.ref" },
45
+ pure: true,
39
46
  summary: "A path from a root of the context. Each step goes up the ladder of the verb deref.",
40
47
  },
41
48
  nearest: {
42
49
  verbs: { step: "walk.nearest", view: "view.card" },
50
+ pure: true,
43
51
  summary: "A step of a path: it follows a key again and again, until it reaches a node of a type.",
44
52
  },
45
53
  panel: {
@@ -47,12 +55,17 @@ export const STD_WORLD: World = {
47
55
  parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
48
56
  // An attribute evaluates on demand, thus in an order that is not the order of the document. It causes no effect.
49
57
  caps: [{ rule: "mask" }],
58
+ // A node that nothing reads keeps its attributes without an evaluation (TREE.LAZY).
59
+ pure: true,
60
+ lazy: [{}],
50
61
  summary: "A node of a tree. Each part is an attribute that evaluates on demand, one time, with the node as self.",
51
62
  },
52
63
  box: {
53
64
  verbs: { eval: "tree.node", view: "view.op" },
54
65
  parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
55
66
  caps: [{ rule: "mask" }],
67
+ pure: true,
68
+ lazy: [{}],
56
69
  summary: "A node of a tree, as panel. A formula in a box can read its parent and its siblings.",
57
70
  },
58
71
  math: {
@@ -68,12 +81,16 @@ export const STD_WORLD: World = {
68
81
  { names: { headers: true }, in: [["*"]], sequential: true },
69
82
  { names: { fixed: ["last"] }, in: [["*"]], sequential: true },
70
83
  ],
84
+ pure: true,
71
85
  summary: "Its steps, in order. The result of a named step is a name for the later steps.",
72
86
  },
73
87
  pure: {
74
88
  verbs: { eval: "control.pure", view: "view.op" },
75
89
  parts: { shape: "list" },
76
90
  caps: [{ rule: "mask" }],
91
+ // The runner evaluates only the first part.
92
+ pure: true,
93
+ lazy: [{ after: 1 }],
77
94
  summary: "It evaluates its part without capabilities, thus an effect in it fails.",
78
95
  },
79
96
  log: {
@@ -432,7 +449,7 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
432
449
  // The contract of pure masks the capabilities of its part, and the kernel applies it. Thus the runner does not.
433
450
  "control.pure": function* ({ parts }) {
434
451
  const only = parts[0];
435
- if (only === undefined) return fail("#VALUE!", "pure needs one part.");
452
+ if (only === undefined || parts.length > 1) return fail("#VALUE!", "pure needs exactly one part.");
436
453
  return yield* only.evaluate();
437
454
  },
438
455
  "control.if": function* ({ parts }) {
@@ -464,8 +481,8 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
464
481
  },
465
482
  };
466
483
 
467
- /** The machine of the examples: the standard world, its evaluators, its runners and its walkers. */
468
- export const STD_MACHINE: Machine = { world: STD_WORLD, evaluators: STD_EVALUATORS, runners: STD_RUNNERS, walkers: STD_WALKERS };
484
+ /** The machine of the examples: the standard world, its evaluators, its runners, its walkers and the domains of the kernel. */
485
+ export const STD_MACHINE: Machine = { world: STD_WORLD, evaluators: STD_EVALUATORS, runners: STD_RUNNERS, walkers: STD_WALKERS, domains: STD_DOMAINS };
469
486
 
470
487
  /** The capabilities of the examples at the root: the two effects of the standard world. */
471
488
  export const STD_CAPS: readonly string[] = ["log", "write"];