@mark1russell7/coeval 0.4.0 → 0.4.2

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
@@ -4,7 +4,7 @@ import { ask } from "./ladder.ts";
4
4
  import { type PartSlot, pathMatches, splitParts } from "./parts.ts";
5
5
  import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
6
6
  import type { ErrorCode } from "./errors.ts";
7
- import { type Machine, permits } from "./evaluate.ts";
7
+ import { type Machine, partCaps, permits } from "./evaluate.ts";
8
8
 
9
9
  /** One document that a walk visits. */
10
10
  export interface DocVisit {
@@ -32,6 +32,11 @@ export interface DocVisit {
32
32
  readonly later: ReadonlySet<string>;
33
33
  /** `true` when the position of the document declares an address: a reference there is not a read of a value. */
34
34
  readonly address?: true | undefined;
35
+ /**
36
+ * The capabilities of the document: the capabilities of the walk, narrowed by the capability rules of the kinds
37
+ * around it (`Kind.caps`). A walk without capabilities gives none here.
38
+ */
39
+ readonly caps?: readonly Cap[] | undefined;
35
40
  }
36
41
 
37
42
  /** A position that a walk cannot read as a document. */
@@ -50,6 +55,8 @@ export interface WalkOptions {
50
55
  readonly declaredType?: string;
51
56
  /** The names that the host binds at the root. */
52
57
  readonly names?: readonly string[];
58
+ /** The capabilities at the root. With them, each visit gets the capabilities of its document. */
59
+ readonly caps?: readonly Cap[];
53
60
  /**
54
61
  * This function gets each position that is not a document.
55
62
  *
@@ -169,6 +176,7 @@ class Visit implements DocVisit {
169
176
  readonly kind: Kind | undefined,
170
177
  readonly typePath: readonly Key[] | undefined,
171
178
  readonly address: true | undefined,
179
+ readonly caps: readonly Cap[] | undefined,
172
180
  ) {
173
181
  this.#place = place;
174
182
  this.#at = at;
@@ -185,14 +193,15 @@ class Visit implements DocVisit {
185
193
 
186
194
  /**
187
195
  * This function visits each document of a document: the root and each part at each depth, by the contracts of the
188
- * kinds. A part of a kind that the world does not have is not visited. The walk uses no stack of the host for the
196
+ * kinds. A part of a kind that the world does not have is not visited. With capabilities in the options, the walk
197
+ * narrows them for each part by the rules of its kind, as a run does. The walk uses no stack of the host for the
189
198
  * depth, thus a deep document cannot overflow it. The time and the memory of a walk are linear in the size of the
190
199
  * document. A visit makes the keys of its place and of its path only when a reader asks for them.
191
200
  *
192
201
  * @param world - The world of the kinds.
193
202
  * @param doc - The document, as JSON.
194
203
  * @param visit - The function that gets each document. It gives `"skip"` to not visit the parts of a document.
195
- * @param options - The type of a bare root value, the names of the host, and a function for the problems.
204
+ * @param options - The type of a bare root value, the names, the capabilities and a function for the problems.
196
205
  */
197
206
  export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => void | "skip", options: WalkOptions = {}): void {
198
207
  interface Item {
@@ -206,6 +215,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
206
215
  readonly address: boolean;
207
216
  readonly slot: readonly Key[];
208
217
  readonly parent: DocVisit | undefined;
218
+ readonly caps: readonly Cap[] | undefined;
209
219
  }
210
220
  const stack: Item[] = [
211
221
  {
@@ -219,6 +229,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
219
229
  address: false,
220
230
  slot: [],
221
231
  parent: undefined,
232
+ caps: options.caps,
222
233
  },
223
234
  ];
224
235
  while (stack.length > 0) {
@@ -244,6 +255,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
244
255
  kind,
245
256
  found.ok ? found.path : undefined,
246
257
  item.address ? true : undefined,
258
+ item.caps,
247
259
  );
248
260
  const answer = visit(here);
249
261
  if (answer === "skip" || kind === undefined) continue;
@@ -271,6 +283,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
271
283
  address: slot.address === true,
272
284
  slot: slot.path,
273
285
  parent: here,
286
+ caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
274
287
  });
275
288
  }
276
289
  }
@@ -380,10 +393,27 @@ export interface CheckOptions {
380
393
  readonly declaredType?: string;
381
394
  /** The names that the host binds at the root. `"any"` does not check the names. The default is no names. */
382
395
  readonly names?: readonly string[] | "any";
383
- /** The capabilities at the root. Without them, the check does not check the effects. */
396
+ /**
397
+ * The capabilities at the root. Without them, the check does not check the effects. The check narrows them for
398
+ * each part by the capability rules of the kinds, as a run does.
399
+ */
384
400
  readonly caps?: readonly Cap[];
385
401
  }
386
402
 
403
+ /**
404
+ * This function gives the reason of a `#CAP!`. When a kind around the document narrowed the capabilities, the
405
+ * reason names the nearest such kind.
406
+ */
407
+ function capReason(v: DocVisit, kind: readonly Key[]): string {
408
+ const text = kind.join(".");
409
+ for (let up = v.parent; up !== undefined; up = up.parent) {
410
+ if (up.caps !== undefined && permits(up.caps, kind)) {
411
+ return `The capabilities do not permit the effect ${text} here. The kind ${typeName(up.doc.type)} around it narrows them for its parts.`;
412
+ }
413
+ }
414
+ return `The capabilities do not permit the effect ${text}.`;
415
+ }
416
+
387
417
  /**
388
418
  * This function checks a document before an evaluation, by the contracts of its kinds. It finds the problems
389
419
  * that an evaluation finds when it reaches them:
@@ -395,7 +425,7 @@ export interface CheckOptions {
395
425
  * - `#DEPTH!`: too much nesting.
396
426
  * - `#REF!` and `#STEP?`: a bad path.
397
427
  * - `#NAME?`: a name that nothing binds.
398
- * - `#CAP!`: an effect that the capabilities do not permit.
428
+ * - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
399
429
  *
400
430
  * @param machine - The world and the code of the evaluators.
401
431
  * @param doc - The document, as JSON.
@@ -426,10 +456,11 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
426
456
  if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
427
457
  issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
428
458
  }
429
- if (options.caps !== undefined && v.kind?.effects !== undefined) {
459
+ const caps = v.caps;
460
+ if (caps !== undefined && v.kind?.effects !== undefined) {
430
461
  const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
431
462
  for (const kind of kinds) {
432
- if (!permits(options.caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: `The capabilities do not permit the effect ${kind.join(".")}.`, at: v.at });
463
+ if (!permits(caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: capReason(v, kind), at: v.at });
433
464
  }
434
465
  }
435
466
  const isPath = isRef(v.doc);
@@ -472,6 +503,7 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
472
503
  {
473
504
  ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
474
505
  ...(Array.isArray(options.names) ? { names: options.names } : {}),
506
+ ...(options.caps === undefined ? {} : { caps: options.caps }),
475
507
  problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at }),
476
508
  },
477
509
  );
package/src/stdlib.ts CHANGED
@@ -69,18 +69,21 @@ export const STD_WORLD: World = {
69
69
  pure: {
70
70
  verbs: { eval: "control.pure", view: "view.op" },
71
71
  parts: { shape: "list" },
72
+ caps: [{ rule: "mask" }],
72
73
  summary: "It evaluates its part without capabilities, thus an effect in it fails.",
73
74
  },
74
75
  log: {
75
76
  verbs: { eval: "effect.log", view: "view.op" },
76
77
  parts: { shape: "list", of: "text" },
77
78
  side: "run",
79
+ effects: ["log"],
78
80
  summary: "It writes the result of its part to the log. This is an effect.",
79
81
  },
80
82
  write: {
81
83
  verbs: { eval: "effect.write", view: "view.op" },
82
84
  parts: { shape: "record", fields: ["value"] },
83
85
  side: "run",
86
+ effects: ["write"],
84
87
  summary: "It writes the result of its part to the store, at its key. This is an effect.",
85
88
  },
86
89
  },
@@ -269,10 +272,11 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
269
272
  }
270
273
  return ok(last);
271
274
  },
272
- "control.pure": function* ({ parts, context }) {
275
+ // The contract of pure masks the capabilities of its part, and the kernel applies it. Thus the runner does not.
276
+ "control.pure": function* ({ parts }) {
273
277
  const only = parts[0];
274
278
  if (only === undefined) return fail("#VALUE!", "pure needs one part.");
275
- return yield* only.evaluate({ ...context, caps: [] });
279
+ return yield* only.evaluate();
276
280
  },
277
281
  "control.if": function* ({ parts }) {
278
282
  const test = parts.find((part) => part.key === "test");
package/src/world.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, field, isRecord, typePath } from "./doc.ts";
2
+ import type { Cap } from "./deref.ts";
2
3
 
3
4
  /** The two sides of evaluation. `derive` is pure. `run` causes effects in order. */
4
5
  export type Side = "derive" | "run";
@@ -69,6 +70,22 @@ export interface ScopeRule {
69
70
  readonly sequential?: boolean;
70
71
  }
71
72
 
73
+ /**
74
+ * A capability rule of a kind: what the kind gives some of its parts from the capabilities of its document. A rule
75
+ * only narrows. A part does not get a capability that its document does not have (CAP.NARROW).
76
+ *
77
+ * - `same`: the parts get the capabilities of the document. This is the default.
78
+ * - `mask`: the parts get no capabilities, as the part of `pure`.
79
+ * - `only`: the parts get only the effects that the capabilities of the document and the list `caps` both permit.
80
+ *
81
+ * `in` names the parts that the rule covers, as patterns of their paths in the value. The key `"*"` matches each
82
+ * key. A rule without `in` covers each part.
83
+ */
84
+ export type CapsRule =
85
+ | { readonly rule: "same"; readonly in?: readonly (readonly Key[])[] }
86
+ | { readonly rule: "mask"; readonly in?: readonly (readonly Key[])[] }
87
+ | { readonly rule: "only"; readonly caps: readonly Cap[]; readonly in?: readonly (readonly Key[])[] };
88
+
72
89
  /** A kind gives the name of an evaluator for each verb, and the contract of its value. */
73
90
  export interface Kind {
74
91
  readonly verbs: Readonly<Record<string, string>>;
@@ -76,6 +93,13 @@ export interface Kind {
76
93
  readonly parts?: Shape;
77
94
  /** The names that the kind binds for its parts. */
78
95
  readonly scope?: readonly ScopeRule[];
96
+ /**
97
+ * The capabilities that the kind gives its parts, as rules. A part that no rule covers gets the capabilities of
98
+ * the document. A part that several rules cover gets only what each rule permits. The static kit reads the rules,
99
+ * and the kernel narrows the context of each part by them. Thus an evaluator does not narrow the capabilities
100
+ * itself.
101
+ */
102
+ readonly caps?: readonly CapsRule[];
79
103
  /** The side of the kind. A kind of the run side causes effects. The default is `derive`. */
80
104
  readonly side?: Side;
81
105
  /**