@mark1russell7/coeval 0.23.0 → 0.24.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.23.0",
4
+ "version": "0.24.1",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
@@ -36,8 +36,8 @@
36
36
  ],
37
37
  "devDependencies": {
38
38
  "@mark1russell7/cue": "^2.1.1",
39
- "@mark1russell7/rigor-bench": "^0.2.15",
40
- "@mark1russell7/rigor-statistics": "^0.1.12",
39
+ "@mark1russell7/rigor-bench": "^0.2.29",
40
+ "@mark1russell7/rigor-statistics": "^0.1.16",
41
41
  "@types/node": "^26.6.4",
42
42
  "esbuild": "^0.28.2",
43
43
  "typescript": "^7.0.2",
package/src/agree.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { type Key, docEquals, equalWith } from "./doc.ts";
2
2
  import { type Context, isHostThing } from "./deref.ts";
3
- import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
4
- import { type Effect, type Machine, type Perform, type Read, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
3
+ import { type EvalError, type Result, describe, isStackOverflow, thrown } from "./errors.ts";
4
+ import { type CheckOptions, typeCheck } from "./static.ts";
5
+ import { isPrecise, member, typeText } from "./types.ts";
6
+ import { boundariesOf } from "./typing.ts";
7
+ import { type Effect, type Machine, type Perform, type Read, type TraceNode, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
5
8
  import { Held, isClosure } from "./formula.ts";
6
9
  import { type Definitions, reference, referenceRun } from "./reference.ts";
7
10
  import { type ReplaySettle, recorder, relay, replayer } from "./replay.ts";
@@ -392,6 +395,15 @@ function resolutions(reads: readonly Read[]): readonly Read[] {
392
395
  return reads.filter((read) => read.free && read.kept !== true);
393
396
  }
394
397
 
398
+ /** This function gives the text of a value of a trace. It does not throw for a bigint or a record with a cycle. */
399
+ function valueText(value: unknown): string {
400
+ try {
401
+ return JSON.stringify(value) ?? describe(value);
402
+ } catch {
403
+ return describe(value);
404
+ }
405
+ }
406
+
395
407
  function pathText(path: readonly unknown[] | undefined): string {
396
408
  if (path === undefined) return "no read";
397
409
  try {
@@ -537,3 +549,94 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
537
549
  }
538
550
  return compared === undefined ? { ok: true, result, effects: first.effects } : { ok: true, result, effects: first.effects, reads: compared };
539
551
  }
552
+
553
+ /** The result of `agreeTypes`: the agreement of a check of types and a traced evaluation (STATIC.TYPES-AGREE). */
554
+ export type TypeAgreement =
555
+ | {
556
+ readonly ok: true;
557
+ /** `false` when the document has an issue, or a value breaks a position of an imprecise type. The rule then tells nothing. */
558
+ readonly checked: boolean;
559
+ /** The number of the values of the trace that the rule examined. */
560
+ readonly values: number;
561
+ }
562
+ | {
563
+ readonly ok: false;
564
+ /** `value`: a value is not in the type of its place. `argument`: it is not in the type that its position needs. `error`: a type error of the kernel. */
565
+ readonly clause: "value" | "argument" | "error";
566
+ /** The keys of the place, as texts. */
567
+ readonly place: readonly string[];
568
+ readonly reason: string;
569
+ };
570
+
571
+ /** The options of `agreeTypes`: the options of `check`, and `guards`. */
572
+ export interface AgreeTypesOptions extends CheckOptions {
573
+ /** `true`: the evaluation tests each boundary (PLAN.GUARD). The rule then holds for each context. The default is `false`. */
574
+ readonly guards?: boolean;
575
+ }
576
+
577
+ /** The type errors of the kernel, by the evaluator that gives them (STATIC.TYPES-AGREE). */
578
+ const TYPE_ERRORS: Readonly<Record<string, string>> = {
579
+ "formula.op": "#NAME?",
580
+ "formula.if": "#VALUE!",
581
+ "formula.and": "#VALUE!",
582
+ "formula.or": "#VALUE!",
583
+ };
584
+
585
+ /**
586
+ * This function examines the agreement rule for types (STATIC.TYPES-AGREE, D148). A document with no issue evaluates
587
+ * with a trace. Each value of the trace is a member of the type of its place. A value of a precise type is also a member
588
+ * of the type that its position needs. No document with precise parts gives a type error of the kernel.
589
+ *
590
+ * A language imports this function for its own property tests, with `agree`. A failure tells that a signature, a type
591
+ * rule, a cover or a root type does not hold. A value that breaks a position of an imprecise type ends the promise, and
592
+ * the result then has `checked: false` (TYPE.GRADUAL).
593
+ *
594
+ * @param machine - The world and the code of the evaluators, with their type rules.
595
+ * @param doc - The document.
596
+ * @param context - The context. Its roots are members of the root types.
597
+ * @param options - The options of `check`, with the root types, and `guards`.
598
+ * @returns The agreement, or the first place that breaks it.
599
+ */
600
+ export function agreeTypes(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeTypesOptions = {}): TypeAgreement {
601
+ const typing = typeCheck(machine, doc, options);
602
+ if (typing.issues.length > 0) return { ok: true, checked: false, values: 0 };
603
+ const keyOf = (path: readonly unknown[]): string => JSON.stringify(path.map(String));
604
+ const places = new Map(typing.places.map((item) => [keyOf(item.place), item]));
605
+ const domains = machine.domains;
606
+ const guards = options.guards === true ? boundariesOf(typing, domains) : undefined;
607
+ const given = { ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }), ...(guards === undefined ? {} : { guards }) };
608
+ const stack: TraceNode[] = [evaluate(machine, doc, context, given).trace];
609
+ const nodes: TraceNode[] = [];
610
+ while (stack.length > 0) {
611
+ const node = stack.pop() as TraceNode;
612
+ nodes.push(node);
613
+ stack.push(...node.children);
614
+ }
615
+ // A value that breaks a position of an imprecise type leaves the promise of each later type (K28).
616
+ for (const node of nodes) {
617
+ const place = places.get(keyOf(node.path));
618
+ if (place?.expected === undefined || !node.result.ok || isPrecise(place.type, domains)) continue;
619
+ if (!member(node.result.value, place.expected, domains)) return { ok: true, checked: false, values: 0 };
620
+ }
621
+ // The walk goes from the end, thus a part comes before the kind that reads it, and a finding names the origin.
622
+ let values = 0;
623
+ for (const node of nodes.reverse()) {
624
+ const place = places.get(keyOf(node.path));
625
+ if (place === undefined) continue;
626
+ const at = node.path.map(String);
627
+ if (!node.result.ok) {
628
+ const parts = node.children.map((child) => places.get(keyOf(child.path)));
629
+ const decided = node.children.every((child) => child.result.ok) && parts.every((part) => part?.expected !== undefined && isPrecise(part.type, domains));
630
+ const code = TYPE_ERRORS[node.answer.evaluator ?? ""];
631
+ if (decided && node.result.error.code === code) return { ok: false, clause: "error", place: at, reason: `The document gives ${code}, but it checks with no issue.` };
632
+ continue;
633
+ }
634
+ const value = node.result.value;
635
+ values += 1;
636
+ if (!member(value, place.type, domains)) return { ok: false, clause: "value", place: at, reason: `The value ${valueText(value)} is not a member of ${typeText(place.type)}.` };
637
+ if (place.expected !== undefined && !member(value, place.expected, domains)) {
638
+ return { ok: false, clause: "argument", place: at, reason: `The value ${valueText(value)} is not a member of the needed type ${typeText(place.expected)}.` };
639
+ }
640
+ }
641
+ return { ok: true, checked: true, values };
642
+ }
package/src/conform.ts CHANGED
@@ -867,7 +867,7 @@ function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sampl
867
867
  const fn = under.fn;
868
868
  return (shot) => {
869
869
  const ctx = shot.context;
870
- const parts: RunPart[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.steps(index, "evaluate", given ?? ctx) }));
870
+ const parts: RunPart[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.steps(index, "evaluate", given ?? ctx), handled: () => shot.steps(index, "evaluate", ctx) }));
871
871
  const input: RunInput = {
872
872
  ...base,
873
873
  value: fill(parts),
package/src/effects.ts CHANGED
@@ -46,7 +46,7 @@ function reaching(machine: Machine, doc: unknown, options: EffectsOptions): read
46
46
  doc,
47
47
  (v) => {
48
48
  const caps = v.caps ?? EACH;
49
- for (const kind of declaredEffects(v, "eval", machine)) if (rule(caps, kind)) out.push({ kind, visit: v });
49
+ for (const kind of declaredEffects(v, "eval", machine)) if (rule(caps, kind) && (v.handled === undefined || !permits(v.handled, kind))) out.push({ kind, visit: v });
50
50
  return undefined;
51
51
  },
52
52
  { caps: options.caps ?? EACH, ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }) },
package/src/evaluate.ts CHANGED
@@ -2,8 +2,8 @@ import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, isKey, ty
2
2
  import { type PartSlot, type Split, partsOf, pathMatches, scopesOf } from "./parts.ts";
3
3
  import { answerOf, ask, type Answer } from "./ladder.ts";
4
4
  import type { CapsRule, Kind, KindFacts, Side } from "./world.ts";
5
- import type { Domain } from "./types.ts";
6
- import type { TypeRule } from "./typing.ts";
5
+ import { type Domain, type ValueType, member, typeText } from "./types.ts";
6
+ import type { Boundary, TypeRule } from "./typing.ts";
7
7
  import type { StaticRead } from "./facts.ts";
8
8
  import { type ErrorCode, type EvalError, type Result, Failure, fail, ok, placed, thrown, describe } from "./errors.ts";
9
9
  import {
@@ -109,7 +109,7 @@ export function intersectCaps(a: readonly Cap[], b: readonly Cap[]): readonly Ca
109
109
  /** This function gives the capability rules of a kind that cover a part and change its capabilities. */
110
110
  function rulesFor(rules: readonly CapsRule[] | undefined, path: readonly Key[]): readonly CapsRule[] {
111
111
  if (rules === undefined || rules.length === 0) return NO_RULES;
112
- const out = rules.filter((rule) => rule.rule !== "same" && (rule.in === undefined || rule.in.some((pattern) => pathMatches(path, pattern))));
112
+ const out = rules.filter((rule) => rule.rule !== "same" && rule.rule !== "grant" && (rule.in === undefined || rule.in.some((pattern) => pathMatches(path, pattern))));
113
113
  return out.length === 0 ? NO_RULES : out;
114
114
  }
115
115
 
@@ -136,6 +136,87 @@ export function partCaps(rules: readonly CapsRule[] | undefined, path: readonly
136
136
  return capsUnder(rulesFor(rules, path), caps);
137
137
  }
138
138
 
139
+ /** This function collects the values at a path in a value. The key `"*"` steps into each item of a list. */
140
+ function collectAt(value: unknown, path: readonly Key[], index: number, out: Cap[]): void {
141
+ if (index === path.length) {
142
+ if (typeof value === "string" || (Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number"))) out.push(value as Cap);
143
+ return;
144
+ }
145
+ const key = path[index] as Key;
146
+ if (key === "*") {
147
+ if (Array.isArray(value)) for (const item of value) collectAt(item, path, index + 1, out);
148
+ return;
149
+ }
150
+ if (typeof value === "object" && value !== null) collectAt((value as Record<Key, unknown>)[key], path, index + 1, out);
151
+ }
152
+
153
+ /**
154
+ * This function gives the capabilities that the rules `grant` of a kind give one part (CAP.GRANT). The check and the
155
+ * walk read them from the document. The kernel gets the same list from the clauses of the runner.
156
+ *
157
+ * @param rules - The capability rules of the kind (`Kind.caps`).
158
+ * @param path - The path of the part in the value of the document.
159
+ * @param value - The value of the document.
160
+ * @returns The capabilities that the rules add, or an empty list.
161
+ */
162
+ export function partGrants(rules: readonly CapsRule[] | undefined, path: readonly Key[], value: unknown): readonly Cap[] {
163
+ if (rules === undefined) return NO_CAPS;
164
+ let out: Cap[] | undefined;
165
+ for (const rule of rules) {
166
+ if (rule.rule !== "grant" || (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(path, pattern)))) continue;
167
+ collectAt(value, rule.from, 0, (out ??= []));
168
+ }
169
+ return out ?? NO_CAPS;
170
+ }
171
+
172
+ /**
173
+ * One clause of a handler (D146): the effects under `op` go to `answer` and not to the host. The clause runs in place,
174
+ * as a part of the effect that it answers. Its result is the result of that effect.
175
+ */
176
+ export interface Clause {
177
+ /** The kind of the effects: the clause answers this kind and each kind under it (CAP.KEYS). */
178
+ readonly op: Cap;
179
+ /**
180
+ * This function answers one effect. It sees only the payload (HANDLE.SEALED).
181
+ *
182
+ * @param payload - The data of the effect.
183
+ * @returns The steps that give the result of the effect.
184
+ */
185
+ answer(payload: unknown): Steps;
186
+ }
187
+
188
+ /** The clauses of one active `handle`, and the frame around it. */
189
+ interface Frame {
190
+ readonly clauses: readonly Clause[];
191
+ readonly outer: Frame | undefined;
192
+ /** The runtime of the evaluation that started the frame. A frame of another evaluation has ended. */
193
+ readonly owner: Runtime;
194
+ /** `true` after the body ends. A closure can leave the body with the context of the frame (HANDLE.ENDED). */
195
+ ended: boolean;
196
+ }
197
+
198
+ /**
199
+ * The key of the frame of a context. Like the grant, it stays through a spread of a context, and only the kernel writes
200
+ * it. It is not a root (D10): a document cannot read it (HANDLE.SEALED).
201
+ */
202
+ const FRAME = /* @__PURE__ */ Symbol("coeval.frame");
203
+
204
+ /** A context that can have a frame. */
205
+ type Framed = Context & { readonly [FRAME]?: Frame };
206
+
207
+ /** This function gives the frame of a context, when it has one. */
208
+ function frameOf(ctx: Context): Frame | undefined {
209
+ return (ctx as Framed)[FRAME];
210
+ }
211
+
212
+ /** This function finds the frame and the clause that answer an effect. The innermost frame wins, then the first clause. */
213
+ function clauseFor(start: Frame, keys: readonly Key[]): { readonly frame: Frame; readonly clause: Clause } | undefined {
214
+ for (let frame: Frame | undefined = start; frame !== undefined; frame = frame.outer) {
215
+ for (const clause of frame.clauses) if (permits([clause.op], keys)) return { frame, clause };
216
+ }
217
+ return undefined;
218
+ }
219
+
139
220
  /**
140
221
  * The key of the grant of a context (CAP.NARROW-CODE): the capabilities that the kernel gave the document of the
141
222
  * context. A spread of a context keeps its grant. Thus compiled code passes the grant on in each context that it makes
@@ -669,6 +750,14 @@ export interface RunPart {
669
750
  * @returns The steps of the evaluation of the part.
670
751
  */
671
752
  evaluate(context?: Context): Steps;
753
+ /**
754
+ * This function evaluates the part under clauses that answer its effects (D146). The kernel sets it on each part of
755
+ * a kind with a rule `grant`. A part that a runner makes itself has none, and a compiled runner gets no such part.
756
+ *
757
+ * @param clauses - The clauses, in order. An effect goes to the first clause that names its kind.
758
+ * @returns The steps of the evaluation of the part.
759
+ */
760
+ handled?(clauses: readonly Clause[]): Steps;
672
761
  }
673
762
 
674
763
  /** The input of a runner: the input of an evaluator, with parts and effects that can wait. */
@@ -904,6 +993,8 @@ export interface PlanRunOptions {
904
993
  export interface RunOptions extends PlanRunOptions {
905
994
  /** The type of a bare root value. */
906
995
  readonly declaredType?: string;
996
+ /** The boundaries that test their values (PLAN.GUARD). The default is none. */
997
+ readonly guards?: readonly Boundary[];
907
998
  /** The verb of `run`, for example `format`. The default is `eval` (VERB.PREPARE). */
908
999
  readonly verb?: string;
909
1000
  }
@@ -934,6 +1025,8 @@ export interface EvaluateOptions {
934
1025
  readonly perform?: Perform;
935
1026
  /** A list that gets each read, in the order of the resolutions (READS.RECORD). */
936
1027
  readonly reads?: Read[];
1028
+ /** The boundaries that test their values (PLAN.GUARD). The default is none. */
1029
+ readonly guards?: readonly Boundary[];
937
1030
  }
938
1031
 
939
1032
  /** The options of a resolution without a record of hops, and with one. One object of each serves each call. */
@@ -1288,14 +1381,35 @@ class Session implements Runtime {
1288
1381
  return (effect.outcome = fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place }));
1289
1382
  }
1290
1383
 
1384
+ /**
1385
+ * This function finds the clause that answers an effect (D146). Without it, the effect goes on to the host. An effect
1386
+ * without a capability gets no clause, thus `record` gives its `#CAP!` (HANDLE.NARROW). A clause of a frame that has
1387
+ * ended gives `#CAP!`, because the capability came from that frame (HANDLE.ENDED).
1388
+ */
1389
+ private clauseOf(given: Context, start: Frame, kind: Cap, place: readonly Key[]): Clause | Result | undefined {
1390
+ const ctx = inEpoch(given, this.epoch) ? granted(given) : ungranted(given, this.epoch);
1391
+ const keys = typeof kind === "string" ? kind.split(".") : kind;
1392
+ if (!this.permitted(ctx, keys)) return undefined;
1393
+ const found = clauseFor(start, keys);
1394
+ if (found === undefined) return undefined;
1395
+ if (found.frame.ended || found.frame.owner !== this) return fail("#CAP!", `The handler of the effect ${keys.join(".")} has ended.`, { place });
1396
+ return found.clause;
1397
+ }
1398
+
1291
1399
  effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
1292
1400
  const text = typeof kind === "string" ? kind : kind.join(".");
1293
1401
  if (this.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
1402
+ const frame = frameOf(ctx);
1403
+ const clause = frame === undefined ? undefined : this.clauseOf(ctx, frame, kind, place);
1404
+ if (clause !== undefined) return "ok" in clause ? clause : drive(clause.answer(payload), text);
1294
1405
  const made = this.record(ctx, kind, payload, place);
1295
1406
  return "ok" in made ? made : this.answer(made, place);
1296
1407
  }
1297
1408
 
1298
1409
  *runEffect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
1410
+ const frame = frameOf(ctx);
1411
+ const clause = frame === undefined ? undefined : this.clauseOf(ctx, frame, kind, place);
1412
+ if (clause !== undefined) return "ok" in clause ? clause : yield* clause.answer(payload);
1299
1413
  const made = this.record(ctx, kind, payload, place);
1300
1414
  if ("ok" in made) return made;
1301
1415
  const perform = this.perform;
@@ -1590,6 +1704,11 @@ export interface PrepareOptions {
1590
1704
  * the code without the fold. A plan without folds ignores this option.
1591
1705
  */
1592
1706
  readonly fixed?: { readonly names?: Readonly<Record<string, unknown>> };
1707
+ /**
1708
+ * The boundaries of the document, from `boundariesOf` (PLAN.GUARD, D149). A value that is not a member of the type of
1709
+ * its boundary gives `#VALUE!` at that place. The default is none, and a plan then has no test.
1710
+ */
1711
+ readonly guards?: readonly Boundary[];
1593
1712
  }
1594
1713
 
1595
1714
  /** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
@@ -1630,6 +1749,8 @@ interface Compiler {
1630
1749
  readonly fold: boolean;
1631
1750
  /** The fixed names and their values, or `undefined` when the plan has none (PLAN.FIXED). */
1632
1751
  readonly fixed: Readonly<Record<string, unknown>> | undefined;
1752
+ /** The types of the boundaries by their place, or `undefined` when the plan has no guard (PLAN.GUARD). */
1753
+ readonly guards: ReadonlyMap<string, ValueType> | undefined;
1633
1754
  /** The runtime of the folds of this compilation. The first fold makes it. */
1634
1755
  probe?: Probe;
1635
1756
  }
@@ -1771,11 +1892,12 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1771
1892
  if (runner !== undefined) {
1772
1893
  let inner: RunCode;
1773
1894
  try {
1774
- inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
1895
+ inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill, kind?.caps?.some((rule) => rule.rule === "grant") === true);
1775
1896
  } catch (error) {
1776
1897
  return failingWith(c, meta, placed(thrown(error, `the runner ${name}`), place));
1777
1898
  }
1778
- const run = placedRun(inner, place, name);
1899
+ const guard = guardOf(c, place);
1900
+ const run = placedRun(guard === undefined ? inner : guardRun(inner, guard, c.machine.domains), place, name);
1779
1901
  return finish(c, meta, runnerCode(run, place, kindText), run);
1780
1902
  }
1781
1903
 
@@ -1785,8 +1907,9 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1785
1907
  } catch (error) {
1786
1908
  return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
1787
1909
  }
1910
+ const guard = guardOf(c, place);
1788
1911
  // Code that cannot fail needs no place and no catch, and it does not read the context of its grant (PLAN.TOTAL).
1789
- const done = finish(c, meta, TOTAL.has(inner) ? inner : placedCode(inner, place, name), undefined);
1912
+ const done = finish(c, meta, guard !== undefined ? placedCode(guardCode(inner, guard, c.machine.domains), place, name) : TOTAL.has(inner) ? inner : placedCode(inner, place, name), undefined);
1790
1913
  if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
1791
1914
  const free = freeOf(doc, kind, split.parts, frees);
1792
1915
  if (free === undefined) return done;
@@ -2198,6 +2321,34 @@ function gated(rules: readonly CapsRule[], part: Compiled): Compiled {
2198
2321
  * This function gives the steps of a runner, with the place of each error and of each exception. The steps get the
2199
2322
  * context that its grant permits (CAP.NARROW-CODE).
2200
2323
  */
2324
+ /** This function gives the key of a place in the guards of a plan. */
2325
+ function guardKey(place: readonly Key[]): string {
2326
+ return JSON.stringify(place.map(String));
2327
+ }
2328
+
2329
+ /** This function gives the type of the boundary at a place, when the plan has one (PLAN.GUARD). */
2330
+ function guardOf(c: Compiler, place: readonly Key[]): ValueType | undefined {
2331
+ return c.guards?.get(guardKey(place));
2332
+ }
2333
+
2334
+ /** This function gives the result of a part of a boundary: a value that is not a member gives `#VALUE!` (PLAN.GUARD). */
2335
+ function guarded(result: Result, type: ValueType, domains: Readonly<Record<string, Domain>> | undefined): Result {
2336
+ if (!result.ok || member(result.value, type, domains)) return result;
2337
+ return fail("#VALUE!", `The value is not a member of ${typeText(type)}, the type of its boundary.`);
2338
+ }
2339
+
2340
+ /** This function adds the test of a boundary to the code of a part. */
2341
+ function guardCode(inner: Code, type: ValueType, domains: Readonly<Record<string, Domain>> | undefined): Code {
2342
+ return (ctx, rt) => guarded(inner(ctx, rt), type, domains);
2343
+ }
2344
+
2345
+ /** This function adds the test of a boundary to the steps of a part. */
2346
+ function guardRun(inner: RunCode, type: ValueType, domains: Readonly<Record<string, Domain>> | undefined): RunCode {
2347
+ return function* (ctx, rt) {
2348
+ return guarded(yield* inner(ctx, rt), type, domains);
2349
+ };
2350
+ }
2351
+
2201
2352
  function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
2202
2353
  return function* (ctx, rt) {
2203
2354
  try {
@@ -2316,7 +2467,25 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
2316
2467
  };
2317
2468
  }
2318
2469
 
2319
- function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): RunCode {
2470
+ /**
2471
+ * This function runs a part under a new frame (D146). The part gets the capabilities of the document and the kinds of
2472
+ * the clauses (CAP.GRANT), with the grant of the kernel. A rule around an effect narrows them as it narrows any other
2473
+ * capability (HANDLE.NARROW). The frame ends with the steps.
2474
+ */
2475
+ function* handling(given: Context, clauses: readonly Clause[], rt: Runtime, run: (ctx: Context) => Steps): Steps {
2476
+ const ctx = granted(given);
2477
+ const frame: Frame = { clauses, outer: frameOf(ctx), owner: rt, ended: false };
2478
+ const caps = [...ctx.caps];
2479
+ for (const clause of clauses) if (!permits(caps, clause.op)) caps.push(clause.op);
2480
+ const inner: Granted & Framed = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx), [FRAME]: frame };
2481
+ try {
2482
+ return yield* run(inner);
2483
+ } finally {
2484
+ frame.ended = true;
2485
+ }
2486
+ }
2487
+
2488
+ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown, grants: boolean): RunCode {
2320
2489
  const { kind, typePath, doc, data, place, machine } = input;
2321
2490
  const refill = input.fill;
2322
2491
  const compiled = input.parts;
@@ -2327,6 +2496,8 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
2327
2496
  path: part.path,
2328
2497
  ...(part.name === undefined ? {} : { name: part.name }),
2329
2498
  evaluate: (override?: Context) => part.run(override === undefined ? ctx : handed(ctx, override), rt),
2499
+ // Only a kind with a rule grant pays for this function: most runners stay as fast as before.
2500
+ ...(grants ? { handled: (clauses: readonly Clause[]) => handling(ctx, clauses, rt, (inner) => part.run(inner, rt)) } : {}),
2330
2501
  }));
2331
2502
  return fn({
2332
2503
  kind,
@@ -2390,7 +2561,8 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
2390
2561
  const traced = options.trace === true;
2391
2562
  const fold = !traced && options.fold !== false;
2392
2563
  const fixed = fold ? fixedNamesOf(options.fixed?.names) : undefined;
2393
- const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold, fixed };
2564
+ const guards = options.guards === undefined || options.guards.length === 0 ? undefined : new Map(options.guards.map((each) => [guardKey(each.place), each.type]));
2565
+ const c: Compiler = { machine, traced, maxDepth: machine.maxDepth ?? 1000, verb, noAnswer, fold, fixed, guards };
2394
2566
  let root: Compiled;
2395
2567
  try {
2396
2568
  root = compileNode(c, doc, [], options.declaredType, 0);
@@ -2513,6 +2685,7 @@ export function evaluate(machine: Machine, doc: unknown, context: Context = make
2513
2685
  trace: true,
2514
2686
  ...(declaredType === undefined ? {} : { declaredType }),
2515
2687
  ...(given.verb === undefined ? {} : { verb: given.verb }),
2688
+ ...(given.guards === undefined ? {} : { guards: given.guards }),
2516
2689
  });
2517
2690
  const ctx = rootContext(machine, context);
2518
2691
  const s = new Session(machine, ctx.caps, false, true, given.perform, undefined, undefined, readingOf(given.reads, ctx));
@@ -2535,6 +2708,7 @@ export async function run(machine: Machine, doc: unknown, context: Context = mak
2535
2708
  const plan = prepare(machine, doc, {
2536
2709
  trace: true,
2537
2710
  ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
2711
+ ...(options.guards === undefined ? {} : { guards: options.guards }),
2538
2712
  ...(options.verb === undefined ? {} : { verb: options.verb }),
2539
2713
  });
2540
2714
  const ctx = rootContext(machine, context);
package/src/formula.ts CHANGED
@@ -420,6 +420,7 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
420
420
  "formula.lit": { synth: (input) => valueType(input.doc.value) },
421
421
 
422
422
  "formula.ref": {
423
+ read: true,
423
424
  synth: (input, report) => {
424
425
  const path = input.doc.value;
425
426
  // An address gives a thing, not a value. The check gives #VALUE! for a path that is not a list.
package/src/index.ts CHANGED
@@ -161,6 +161,7 @@ export {
161
161
  type RunFn,
162
162
  type RunInput,
163
163
  type RunOptions,
164
+ type Clause,
164
165
  type RunPart,
165
166
  type Runner,
166
167
  type Runtime,
@@ -177,6 +178,7 @@ export {
177
178
  narrow,
178
179
  nodeAt,
179
180
  partCaps,
181
+ partGrants,
180
182
  permits,
181
183
  planner,
182
184
  prepare,
@@ -195,6 +197,7 @@ export {
195
197
  STD_EVALUATORS,
196
198
  STD_MACHINE,
197
199
  STD_RUNNERS,
200
+ ifRunner,
198
201
  STD_WALKERS,
199
202
  STD_WORLD,
200
203
  WORLD_SYNTAX,
@@ -225,6 +228,7 @@ export {
225
228
  } from "./context.ts";
226
229
  export {
227
230
  type CheckOptions,
231
+ type DocChange,
228
232
  type DocVisit,
229
233
  type Issue,
230
234
  type RefUse,
@@ -235,6 +239,7 @@ export {
235
239
  type WorldOptions,
236
240
  check,
237
241
  checkWorld,
242
+ mapDocs,
238
243
  mapRefs,
239
244
  refsOf,
240
245
  typeCheck,
@@ -267,6 +272,8 @@ export {
267
272
  type SyntaxCalls,
268
273
  type SyntaxConstructs,
269
274
  type SyntaxEnvelopes,
275
+ type SyntaxFinding,
276
+ type SyntaxFindingCode,
270
277
  type SyntaxFixity,
271
278
  type SyntaxForm,
272
279
  type SyntaxFormArgs,
@@ -279,12 +286,13 @@ export {
279
286
  type SyntaxRefs,
280
287
  MAX_SYNTAX_ISSUES,
281
288
  STD_SYNTAX,
289
+ checkSyntax,
282
290
  parse,
283
291
  print,
284
292
  syntaxFormat,
285
293
  } from "./syntax.ts";
286
294
  export { type Definitions, type ReferenceOptions, type ReferenceRunOptions, STD_DEFINITIONS, formulaDefinitions, reference, referenceRun } from "./reference.ts";
287
- export { type AgreeOptions, type AgreeRunOptions, type Agreement, type Tier, type TierInput, type TierResult, agree, agreeRun, sameValue } from "./agree.ts";
295
+ export { type AgreeOptions, type AgreeRunOptions, type Agreement, type AgreeTypesOptions, type TypeAgreement, agreeTypes, type Tier, type TierInput, type TierResult, agree, agreeRun, sameValue } from "./agree.ts";
288
296
  export { type ConformOptions, type ConformProbe, type Finding, type Obligation, conforms } from "./conform.ts";
289
297
  export { sameResult } from "./same.ts";
290
298
  export { type PrintOptions, type ReadPrints, type ReadsCheck, type VerifyOptions, printReads, verifyReads } from "./reads.ts";
@@ -341,5 +349,5 @@ export {
341
349
  } from "./replay.ts";
342
350
  export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
343
351
  export { type Domain, type DomainCodec, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
344
- export { type Declared, type PartType, type RootTypes, type TypeInput, type TypeProblem, type TypeRule, type Typed, CAPS_TYPE } from "./typing.ts";
352
+ export { type Boundary, type Declared, type PartType, type RootTypes, type TypeInput, type TypeProblem, type TypeRule, type Typed, CAPS_TYPE, boundariesOf } from "./typing.ts";
345
353
  export { type SampleOptions, type TypeReading, typeFromSamples, typeFromSchema } from "./providers.ts";
package/src/reference.ts CHANGED
@@ -21,6 +21,7 @@ import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
21
21
  import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
22
22
  import {
23
23
  type AllOptions,
24
+ type Clause,
24
25
  type Effect,
25
26
  type EvalFn,
26
27
  type EvalInput,
@@ -295,6 +296,44 @@ function freeIn(root: Context, ctx: Context, path: readonly unknown[]): boolean
295
296
  return bound === Object.hasOwn(root.names, key) && (!bound || Object.is(ctx.names[key], root.names[key]));
296
297
  }
297
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
+
298
337
  /**
299
338
  * This function causes an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). It records the effect with its
300
339
  * address (RUN.ADDRESS) and its outcome (RUN.OUTCOME). The mock answers the effect at once (RUN.MOCK). Without a
@@ -314,6 +353,8 @@ function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: reado
314
353
  function causeFromEvaluator(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
315
354
  const text = typeof kind === "string" ? kind : kind.join(".");
316
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);
317
358
  return cause(env, ctx, kind, payload, place);
318
359
  }
319
360
 
@@ -323,6 +364,8 @@ function causeFromEvaluator(env: Env, ctx: Context, kind: Cap, payload: unknown,
323
364
  * (RUN.OUTCOME).
324
365
  */
325
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);
326
369
  if (!env.running) return cause(env, ctx, kind, payload, place);
327
370
  const effect = recordEffect(env, ctx, kind, payload, place);
328
371
  if ("ok" in effect) return effect;
@@ -480,6 +523,17 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
480
523
  };
481
524
  }
482
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
+
483
537
  function runInput(env: Env, seen: Seen, at: At): RunInput {
484
538
  const { split, doc } = seen;
485
539
  const parts: RunPart[] = split.parts.map((slot) => {
@@ -489,6 +543,7 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
489
543
  path: slot.path,
490
544
  ...(name === undefined ? {} : { name }),
491
545
  evaluate: (given?: Context) => stepsAt(env, partAt(seen, at, slot, given)),
546
+ handled: (clauses: readonly Clause[]) => handledAt(env, partAt(seen, at, slot, undefined), clauses),
492
547
  };
493
548
  });
494
549
  const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));