@mark1russell7/coeval 0.4.0 → 0.4.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.4.0",
4
+ "version": "0.4.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",
package/src/evaluate.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
2
- import { type Split, splitParts } from "./parts.ts";
2
+ import { type Split, pathMatches, splitParts } from "./parts.ts";
3
3
  import { ask, type Answer } from "./ladder.ts";
4
- import { type Lookup, type Side, type World, lookupKind } from "./world.ts";
4
+ import { type CapsRule, type Lookup, type Side, type World, lookupKind } from "./world.ts";
5
5
  import { type ErrorCode, type EvalError, type Result, fail, ok, placed, thrown, describe } from "./errors.ts";
6
6
  import {
7
7
  type Cap,
@@ -74,6 +74,58 @@ export function narrow(parent: Context, next: Context): Context {
74
74
  return caps.length === next.caps.length ? next : { ...next, caps };
75
75
  }
76
76
 
77
+ /** No capabilities. One list serves each masked part. */
78
+ const NO_CAPS: readonly Cap[] = Object.freeze([]);
79
+
80
+ /** No capability rules. One list serves each part that no rule narrows. */
81
+ const NO_RULES: readonly CapsRule[] = Object.freeze([]);
82
+
83
+ /**
84
+ * This function gives the capabilities that two lists both permit. An effect is permitted by the result when each
85
+ * list permits it. When the list `b` permits each capability of `a`, the result is the list `a` itself.
86
+ *
87
+ * @param a - The first list, for example the capabilities of a document.
88
+ * @param b - The second list, for example the list of a rule `only`.
89
+ * @returns The capabilities that both lists permit.
90
+ */
91
+ export function intersectCaps(a: readonly Cap[], b: readonly Cap[]): readonly Cap[] {
92
+ if (a.every((cap) => permits(b, cap))) return a;
93
+ const out: Cap[] = [];
94
+ for (const cap of b) if (permits(a, cap)) out.push(cap);
95
+ for (const cap of a) if (permits(b, cap) && !permits(out, cap)) out.push(cap);
96
+ return out;
97
+ }
98
+
99
+ /** This function gives the capability rules of a kind that cover a part and change its capabilities. */
100
+ function rulesFor(rules: readonly CapsRule[] | undefined, path: readonly Key[]): readonly CapsRule[] {
101
+ if (rules === undefined || rules.length === 0) return NO_RULES;
102
+ const out = rules.filter((rule) => rule.rule !== "same" && (rule.in === undefined || rule.in.some((pattern) => pathMatches(path, pattern))));
103
+ return out.length === 0 ? NO_RULES : out;
104
+ }
105
+
106
+ /** This function applies the rules that cover a part to the capabilities of its document. */
107
+ function capsUnder(rules: readonly CapsRule[], caps: readonly Cap[]): readonly Cap[] {
108
+ let out = caps;
109
+ for (const rule of rules) {
110
+ if (rule.rule === "mask") return NO_CAPS;
111
+ if (rule.rule === "only") out = intersectCaps(out, rule.caps);
112
+ }
113
+ return out;
114
+ }
115
+
116
+ /**
117
+ * This function gives the capabilities that a kind gives one of its parts, by the capability rules of its contract.
118
+ * The kernel and the static kit both use it, thus a run and a check narrow the same way.
119
+ *
120
+ * @param rules - The capability rules of the kind (`Kind.caps`).
121
+ * @param path - The path of the part in the value of the document.
122
+ * @param caps - The capabilities of the document.
123
+ * @returns The capabilities of the part: the same list or a smaller one.
124
+ */
125
+ export function partCaps(rules: readonly CapsRule[] | undefined, path: readonly Key[], caps: readonly Cap[]): readonly Cap[] {
126
+ return capsUnder(rulesFor(rules, path), caps);
127
+ }
128
+
77
129
  /**
78
130
  * The steps of an evaluation that can wait. It yields a promise only when the host makes it wait. A runner takes the
79
131
  * result of a part with `yield*`, thus a run goes on at once until an effect waits. A runner does not yield a
@@ -892,7 +944,10 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
892
944
  }
893
945
  const parts: CompiledPart[] = split.parts.map((slot) => {
894
946
  const item = slot.item;
895
- const compiled = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
947
+ // The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
948
+ const own = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
949
+ const rules = rulesFor(kind?.caps, slot.path);
950
+ const compiled = rules.length === 0 ? own : gated(rules, own);
896
951
  // The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
897
952
  const header = typeof (item as { readonly name?: unknown } | null)?.name === "string" && isEnvelope(item) ? (item as { readonly name: string }).name : undefined;
898
953
  return header === undefined
@@ -942,6 +997,20 @@ function placedCode(inner: Code, place: readonly Key[], name: string): Code {
942
997
  };
943
998
  }
944
999
 
1000
+ /**
1001
+ * This function gives the code of a part that capability rules cover. The code narrows the context of the part by
1002
+ * the rules before the part starts.
1003
+ */
1004
+ function gated(rules: readonly CapsRule[], part: { readonly code: Code; readonly run: RunCode }): { readonly code: Code; readonly run: RunCode } {
1005
+ const inner = part.code;
1006
+ const steps = part.run;
1007
+ const under = (ctx: Context): Context => {
1008
+ const caps = capsUnder(rules, ctx.caps);
1009
+ return caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0) ? ctx : { ...ctx, caps };
1010
+ };
1011
+ return { code: (ctx, rt) => inner(under(ctx), rt), run: (ctx, rt) => steps(under(ctx), rt) };
1012
+ }
1013
+
945
1014
  /** This function gives the steps of a runner, with the place of each error and of each exception. */
946
1015
  function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
947
1016
  return function* (ctx, rt) {
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export {
36
36
  } from "./errors.ts";
37
37
  export {
38
38
  type Alias,
39
+ type CapsRule,
39
40
  type Kind,
40
41
  type Lookup,
41
42
  type Namespace,
@@ -127,9 +128,11 @@ export {
127
128
  evaluate,
128
129
  failures,
129
130
  flatten,
131
+ intersectCaps,
130
132
  makeContext,
131
133
  narrow,
132
134
  nodeAt,
135
+ partCaps,
133
136
  permits,
134
137
  planner,
135
138
  prepare,
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
  /**