@mark1russell7/coeval 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.0",
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",
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
@@ -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. */
@@ -1288,14 +1377,35 @@ class Session implements Runtime {
1288
1377
  return (effect.outcome = fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place }));
1289
1378
  }
1290
1379
 
1380
+ /**
1381
+ * This function finds the clause that answers an effect (D146). Without it, the effect goes on to the host. An effect
1382
+ * without a capability gets no clause, thus `record` gives its `#CAP!` (HANDLE.NARROW). A clause of a frame that has
1383
+ * ended gives `#CAP!`, because the capability came from that frame (HANDLE.ENDED).
1384
+ */
1385
+ private clauseOf(given: Context, start: Frame, kind: Cap, place: readonly Key[]): Clause | Result | undefined {
1386
+ const ctx = inEpoch(given, this.epoch) ? granted(given) : ungranted(given, this.epoch);
1387
+ const keys = typeof kind === "string" ? kind.split(".") : kind;
1388
+ if (!this.permitted(ctx, keys)) return undefined;
1389
+ const found = clauseFor(start, keys);
1390
+ if (found === undefined) return undefined;
1391
+ if (found.frame.ended || found.frame.owner !== this) return fail("#CAP!", `The handler of the effect ${keys.join(".")} has ended.`, { place });
1392
+ return found.clause;
1393
+ }
1394
+
1291
1395
  effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
1292
1396
  const text = typeof kind === "string" ? kind : kind.join(".");
1293
1397
  if (this.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
1398
+ const frame = frameOf(ctx);
1399
+ const clause = frame === undefined ? undefined : this.clauseOf(ctx, frame, kind, place);
1400
+ if (clause !== undefined) return "ok" in clause ? clause : drive(clause.answer(payload), text);
1294
1401
  const made = this.record(ctx, kind, payload, place);
1295
1402
  return "ok" in made ? made : this.answer(made, place);
1296
1403
  }
1297
1404
 
1298
1405
  *runEffect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
1406
+ const frame = frameOf(ctx);
1407
+ const clause = frame === undefined ? undefined : this.clauseOf(ctx, frame, kind, place);
1408
+ if (clause !== undefined) return "ok" in clause ? clause : yield* clause.answer(payload);
1299
1409
  const made = this.record(ctx, kind, payload, place);
1300
1410
  if ("ok" in made) return made;
1301
1411
  const perform = this.perform;
@@ -1771,7 +1881,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1771
1881
  if (runner !== undefined) {
1772
1882
  let inner: RunCode;
1773
1883
  try {
1774
- inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
1884
+ inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill, kind?.caps?.some((rule) => rule.rule === "grant") === true);
1775
1885
  } catch (error) {
1776
1886
  return failingWith(c, meta, placed(thrown(error, `the runner ${name}`), place));
1777
1887
  }
@@ -2316,7 +2426,25 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
2316
2426
  };
2317
2427
  }
2318
2428
 
2319
- function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): RunCode {
2429
+ /**
2430
+ * This function runs a part under a new frame (D146). The part gets the capabilities of the document and the kinds of
2431
+ * the clauses (CAP.GRANT), with the grant of the kernel. A rule around an effect narrows them as it narrows any other
2432
+ * capability (HANDLE.NARROW). The frame ends with the steps.
2433
+ */
2434
+ function* handling(given: Context, clauses: readonly Clause[], rt: Runtime, run: (ctx: Context) => Steps): Steps {
2435
+ const ctx = granted(given);
2436
+ const frame: Frame = { clauses, outer: frameOf(ctx), owner: rt, ended: false };
2437
+ const caps = [...ctx.caps];
2438
+ for (const clause of clauses) if (!permits(caps, clause.op)) caps.push(clause.op);
2439
+ const inner: Granted & Framed = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx), [FRAME]: frame };
2440
+ try {
2441
+ return yield* run(inner);
2442
+ } finally {
2443
+ frame.ended = true;
2444
+ }
2445
+ }
2446
+
2447
+ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown, grants: boolean): RunCode {
2320
2448
  const { kind, typePath, doc, data, place, machine } = input;
2321
2449
  const refill = input.fill;
2322
2450
  const compiled = input.parts;
@@ -2327,6 +2455,8 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
2327
2455
  path: part.path,
2328
2456
  ...(part.name === undefined ? {} : { name: part.name }),
2329
2457
  evaluate: (override?: Context) => part.run(override === undefined ? ctx : handed(ctx, override), rt),
2458
+ // Only a kind with a rule grant pays for this function: most runners stay as fast as before.
2459
+ ...(grants ? { handled: (clauses: readonly Clause[]) => handling(ctx, clauses, rt, (inner) => part.run(inner, rt)) } : {}),
2330
2460
  }));
2331
2461
  return fn({
2332
2462
  kind,
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,
@@ -267,6 +269,8 @@ export {
267
269
  type SyntaxCalls,
268
270
  type SyntaxConstructs,
269
271
  type SyntaxEnvelopes,
272
+ type SyntaxFinding,
273
+ type SyntaxFindingCode,
270
274
  type SyntaxFixity,
271
275
  type SyntaxForm,
272
276
  type SyntaxFormArgs,
@@ -279,6 +283,7 @@ export {
279
283
  type SyntaxRefs,
280
284
  MAX_SYNTAX_ISSUES,
281
285
  STD_SYNTAX,
286
+ checkSyntax,
282
287
  parse,
283
288
  print,
284
289
  syntaxFormat,
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));
package/src/static.ts CHANGED
@@ -4,7 +4,7 @@ import { answerOf, ask } from "./ladder.ts";
4
4
  import { type PartSlot, partsOf, rulesCover, 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
- import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
7
+ import { type Machine, partCaps, partGrants, permits, waitedReason } from "./evaluate.ts";
8
8
  import type { ValueType } from "./types.ts";
9
9
  import { type RootTypes, type TypeNode, type Typed, machineTypeIssues, typePass } from "./typing.ts";
10
10
  import { cycleIssues } from "./instances.ts";
@@ -41,6 +41,8 @@ export interface DocVisit {
41
41
  * around it (`Kind.caps`). A walk without capabilities gives none here.
42
42
  */
43
43
  readonly caps?: readonly Cap[] | undefined;
44
+ /** The kinds of effects that a handler around the document answers (CAP.GRANT, D146). Such an effect does not reach the host. */
45
+ readonly handled?: readonly Cap[] | undefined;
44
46
  }
45
47
 
46
48
  /** A position that a walk cannot read as a document. */
@@ -114,6 +116,7 @@ class Visit implements DocVisit {
114
116
  readonly typePath: readonly Key[] | undefined;
115
117
  readonly address: true | undefined;
116
118
  readonly caps: readonly Cap[] | undefined;
119
+ readonly handled: readonly Cap[] | undefined;
117
120
  readonly #place: Trail | undefined;
118
121
  readonly #at: Trail | undefined;
119
122
  readonly #bound: readonly string[];
@@ -135,6 +138,7 @@ class Visit implements DocVisit {
135
138
  caps: readonly Cap[] | undefined,
136
139
  bound: readonly string[],
137
140
  expect: ValueType | undefined,
141
+ handled: readonly Cap[] | undefined,
138
142
  ) {
139
143
  this.doc = doc;
140
144
  this.declared = declared;
@@ -147,6 +151,7 @@ class Visit implements DocVisit {
147
151
  this.typePath = typePath;
148
152
  this.address = address;
149
153
  this.caps = caps;
154
+ this.handled = handled;
150
155
  this.#place = place;
151
156
  this.#at = at;
152
157
  this.#bound = bound;
@@ -199,6 +204,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
199
204
  readonly caps: readonly Cap[] | undefined;
200
205
  readonly bound: readonly string[];
201
206
  readonly expect: ValueType | undefined;
207
+ readonly handled: readonly Cap[] | undefined;
202
208
  }
203
209
  const stack: Item[] = [
204
210
  {
@@ -213,6 +219,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
213
219
  slot: [],
214
220
  parent: undefined,
215
221
  caps: options.caps,
222
+ handled: undefined,
216
223
  bound: [],
217
224
  expect: undefined,
218
225
  },
@@ -241,6 +248,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
241
248
  item.caps,
242
249
  item.bound,
243
250
  item.expect,
251
+ item.handled,
244
252
  );
245
253
  const answer = visit(here);
246
254
  if (answer === "skip" || kind === undefined) continue;
@@ -254,6 +262,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
254
262
  const slot = split.parts[index] as PartSlot;
255
263
  const extra = sees[index] ?? [];
256
264
  const hidden = later[index] ?? [];
265
+ const grants = partGrants(kind.caps, slot.path, doc.value);
257
266
  stack.push({
258
267
  value: slot.item,
259
268
  place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
@@ -265,7 +274,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
265
274
  address: slot.address === true,
266
275
  slot: slot.path,
267
276
  parent: here,
268
- caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
277
+ caps: item.caps === undefined || kind.caps === undefined ? item.caps : [...partCaps(kind.caps, slot.path, item.caps), ...grants],
278
+ handled: grants.length === 0 ? item.handled : [...(item.handled ?? []), ...grants],
269
279
  bound: extra,
270
280
  expect: slot.expect,
271
281
  });
@@ -770,6 +780,7 @@ const KEEPS_SELF: ReadonlySet<string> = /* @__PURE__ */ new Set([
770
780
  "control.seq",
771
781
  "control.pure",
772
782
  "control.if",
783
+ "control.handle",
773
784
  "list.all",
774
785
  "effect.log",
775
786
  "effect.write",
package/src/stdlib.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { type Json, type Key, docEquals, field, isRecord } from "./doc.ts";
1
+ import { type Json, type Key, docEquals, field, isKey, isRecord } from "./doc.ts";
2
2
  import {
3
3
  type EvalInput,
4
4
  type Evaluator,
5
+ type Clause,
5
6
  type InputWithRuntime,
6
7
  type Machine,
7
8
  type Plan,
@@ -144,6 +145,27 @@ export const STD_WORLD: World = {
144
145
  lazy: [{ after: 1 }],
145
146
  summary: "It evaluates its part without capabilities, thus an effect in it fails.",
146
147
  },
148
+ handle: {
149
+ verbs: { eval: "control.handle", view: "view.op" },
150
+ parts: {
151
+ shape: "record",
152
+ fields: {
153
+ with: { shape: "list", item: { shape: "record", fields: { op: { shape: "none" }, do: { shape: "term" }, abort: { shape: "term" } }, required: ["op"], closed: true } },
154
+ return: { shape: "term" },
155
+ body: { shape: "term" },
156
+ },
157
+ required: ["with", "body"],
158
+ closed: true,
159
+ },
160
+ scope: [
161
+ { names: { fixed: ["payload"] }, in: [["with", "*", "do"], ["with", "*", "abort"]] },
162
+ { names: { fixed: ["result"] }, in: [["return"]] },
163
+ ],
164
+ caps: [{ rule: "grant", from: ["with", "*", "op"], in: [["body"]] }],
165
+ // A clause runs at an effect of the body, and the clause return runs only after a body without a failure.
166
+ lazy: [{ in: [["with", "*", "do"], ["with", "*", "abort"], ["return"]] }],
167
+ summary: "It answers the effects of its body with its clauses, in place, thus a mock, a retry or a log is a document.",
168
+ },
147
169
  log: {
148
170
  verbs: { eval: "effect.log", view: "view.op" },
149
171
  parts: { shape: "list", of: "text" },
@@ -1493,6 +1515,18 @@ function* inOrder(parts: readonly RunPart[]): Generator<Promise<unknown>, { read
1493
1515
  return { values };
1494
1516
  }
1495
1517
 
1518
+ /** The value of a `handle` document, with a part in the place of each part. */
1519
+ interface HandleValue {
1520
+ readonly with: readonly { readonly op: Cap; readonly do?: RunPart; readonly abort?: RunPart }[];
1521
+ readonly return?: RunPart;
1522
+ readonly body?: RunPart;
1523
+ }
1524
+
1525
+ /** This function tells if a value names a kind of effects: a text, or a list of keys. */
1526
+ function isOp(value: unknown): value is Cap {
1527
+ return typeof value === "string" || (Array.isArray(value) && value.every(isKey));
1528
+ }
1529
+
1496
1530
  /**
1497
1531
  * The runners of the standard world, by name: the kinds that can hold effects. Each one evaluates its parts in
1498
1532
  * order. In a run, an effect ends before the next step starts.
@@ -1516,6 +1550,32 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
1516
1550
  if (only === undefined || parts.length > 1) return fail("#VALUE!", "pure needs exactly one part.");
1517
1551
  return yield* only.evaluate();
1518
1552
  },
1553
+ // A clause gets the context of the handle document, thus it has no more authority than its parent (HANDLE.CLAUSE).
1554
+ "control.handle": function* ({ value, context }) {
1555
+ const { with: list, return: after, body } = value as HandleValue;
1556
+ if (body?.handled === undefined) return fail("#VALUE!", "handle needs a body.");
1557
+ let stopped: Result | undefined;
1558
+ const clauses: Clause[] = [];
1559
+ for (const item of list) {
1560
+ const part = item.do ?? item.abort;
1561
+ if (!isOp(item.op)) return fail("#VALUE!", "The op of a clause of handle must be a text or a list of keys.");
1562
+ if (part === undefined || (item.do !== undefined && item.abort !== undefined)) return fail("#VALUE!", "Each clause of handle needs exactly one of do and abort.");
1563
+ const stops = item.do === undefined;
1564
+ clauses.push({
1565
+ op: item.op,
1566
+ answer: function* (payload) {
1567
+ const result = yield* part.evaluate({ ...context, names: { ...context.names, payload } });
1568
+ if (!stops) return result;
1569
+ stopped ??= result;
1570
+ return fail("#CALC!", "A clause abort of handle stopped the body.");
1571
+ },
1572
+ });
1573
+ }
1574
+ const result = yield* body.handled(clauses);
1575
+ if (stopped !== undefined) return stopped;
1576
+ if (!result.ok || after === undefined) return result;
1577
+ return yield* after.evaluate({ ...context, names: { ...context.names, result: result.value } });
1578
+ },
1519
1579
  "control.if": function* ({ parts }) {
1520
1580
  const test = parts.find((part) => part.key === "test");
1521
1581
  if (test === undefined) return fail("#VALUE!", "if needs a test.");