@mark1russell7/coeval 0.11.0 → 0.13.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.11.0",
4
+ "version": "0.13.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
@@ -23,6 +23,7 @@ import {
23
23
  type Runtime,
24
24
  type Steps,
25
25
  capKeys,
26
+ intersectCaps,
26
27
  makeContext,
27
28
  permits,
28
29
  rootContext,
@@ -371,8 +372,8 @@ class Shot implements Runtime {
371
372
  /** This function answers an effect as the kernel does: it checks the capability, and it asks the mock. */
372
373
  private answer(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
373
374
  const keys = capKeys(kind);
374
- // The kernel keeps only the capabilities that the document permits (H-GRANT).
375
- const caps = ctx.caps.filter((cap) => permits(this.context.caps, cap));
375
+ // The kernel keeps the capabilities that both the context and the document permit (H-GRANT, CAP.MEET).
376
+ const caps = intersectCaps(ctx.caps, this.context.caps);
376
377
  const allowed = this.machine.permits === undefined ? permits(caps, keys) : this.machine.permits(caps, keys, { ...ctx, caps });
377
378
  if (!allowed) return fail("#CAP!", `The context has no capability for the effect ${keys.join(".")}.`, { place });
378
379
  if (this.perform === undefined) return ok(payload);
package/src/deref.ts CHANGED
@@ -363,6 +363,8 @@ export function context(parts: Partial<Context> = {}): Context {
363
363
  }
364
364
 
365
365
  const MAX_COLLAPSE = 64;
366
+ /** The reason of a resolution that reaches the bound of collapses without a cycle that it found (DEREF.CHAIN-REASON). It is a plain string, thus a bundle drops it (PKG.PURE). */
367
+ const CHAIN_REASON = "The path follows more than 64 references. One resolution follows at most 64.";
366
368
  const NO_HOPS: readonly Hop[] = /* @__PURE__ */ Object.freeze([]);
367
369
 
368
370
  /** A value that can be a reference document: a record with the type `ref`. The full check comes after. */
@@ -560,7 +562,8 @@ function walkersOf(nav: Navigator, path: readonly unknown[]): KnownWalkers | und
560
562
  * - `#NAME?`: the root `names` has no such name.
561
563
  * - `#N/A`: a step reaches nothing.
562
564
  * - `#STEP?`: a step is not a key and not a step document.
563
- * - `#CYCLE!`: the references form a cycle.
565
+ * - `#CYCLE!`: the references form a cycle, or the resolution follows more than 64 references. The reason tells
566
+ * which (DEREF.CHAIN-REASON).
564
567
  *
565
568
  * @param nav - The world and the code of the evaluators.
566
569
  * @param ctx - The context.
@@ -590,15 +593,21 @@ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], ho
590
593
  const done = hops ?? NO_HOPS;
591
594
  const failed = (code: ResolveCode | ErrorCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
592
595
  let collapses = 0;
596
+ // The references whose paths are in evaluation. A reference that is in this list again closes a cycle.
597
+ let open: object[] | undefined;
593
598
  const adopt = nav.adopt ?? ((value: unknown): Thing => value);
594
599
 
595
600
  const collapse = (thing: Thing): Resolution => {
596
601
  let here = thing;
597
602
  if (nav.plain === true) return { ok: true, thing: here, hops: done };
598
603
  while (isRef(here)) {
604
+ if (open?.includes(here) === true) return failed("#CYCLE!", "The references form a cycle.");
599
605
  collapses += 1;
600
- if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", "The references form a cycle.");
606
+ if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", CHAIN_REASON);
607
+ open ??= [];
608
+ open.push(here);
601
609
  const inner = walk(here.value, undefined);
610
+ open.pop();
602
611
  if (!inner.ok) return inner;
603
612
  here = adopt(inner.thing);
604
613
  hops?.push({ step: "collapse", thing: here, by: "collapse" });
package/src/evaluate.ts CHANGED
@@ -68,8 +68,9 @@ export function permits(caps: readonly Cap[], kind: Cap): boolean {
68
68
  }
69
69
 
70
70
  /**
71
- * This function narrows a context for a part: the part keeps only the capabilities that the context of its parent
72
- * permits. Thus an evaluator cannot give a part more effects than it has.
71
+ * This function narrows a context for a part. The part keeps the meet `intersectCaps` of its list and the list of its
72
+ * parent (CAP.MEET). Thus an evaluator cannot give a part more effects than it has. A context whose list the parent
73
+ * permits stays the same object.
73
74
  *
74
75
  * @param parent - The context of the parent.
75
76
  * @param next - The context that the evaluator gives the part.
@@ -77,8 +78,8 @@ export function permits(caps: readonly Cap[], kind: Cap): boolean {
77
78
  */
78
79
  export function narrow(parent: Context, next: Context): Context {
79
80
  if (next === parent || next.caps === parent.caps) return next;
80
- const caps = next.caps.filter((cap) => permits(parent.caps, cap));
81
- return caps.length === next.caps.length ? next : { ...next, caps };
81
+ const caps = intersectCaps(next.caps, parent.caps);
82
+ return caps === next.caps ? next : { ...next, caps };
82
83
  }
83
84
 
84
85
  /** No capabilities. One list serves each masked part. */
@@ -144,14 +145,36 @@ const GRANT = /* @__PURE__ */ Symbol("coeval.grant");
144
145
  type Granted = Context & { readonly [GRANT]?: readonly Cap[] };
145
146
 
146
147
  /**
147
- * This function gives a context whose capabilities its grant permits (CAP.NARROW-CODE). A context whose grant is its
148
- * own list of capabilities stays the same object. Each other context keeps only the capabilities that its grant
149
- * permits, and a context without a grant keeps none. The result has the grant of its capabilities.
148
+ * This function gives the capabilities that a context and its grant both permit, the meet `intersectCaps` (CAP.MEET).
149
+ * A context whose grant is its own list gives that list after one comparison. A context without a grant gives none.
150
150
  */
151
- function granted(ctx: Context): Context {
151
+ function grantedCaps(ctx: Context): readonly Cap[] {
152
152
  const grant = (ctx as Granted)[GRANT];
153
- if (grant === ctx.caps) return ctx;
154
- const caps = grant === undefined ? NO_CAPS : ctx.caps.every((cap) => permits(grant, cap)) ? ctx.caps : ctx.caps.filter((cap) => permits(grant, cap));
153
+ if (grant === ctx.caps) return grant;
154
+ return grant === undefined ? NO_CAPS : intersectCaps(ctx.caps, grant);
155
+ }
156
+
157
+ /**
158
+ * This function gives a context whose capabilities its grant permits (CAP.NARROW-CODE, CAP.MEET). A context whose grant
159
+ * is its own list of capabilities stays the same object. Each other context keeps the meet of its list and its grant,
160
+ * and a context without a grant keeps none. The result has the grant of its capabilities.
161
+ */
162
+ function granted(ctx: Context): Context {
163
+ if ((ctx as Granted)[GRANT] === ctx.caps) return ctx;
164
+ const caps = grantedCaps(ctx);
165
+ const out: Granted = { ...ctx, caps, [GRANT]: caps };
166
+ return out;
167
+ }
168
+
169
+ /**
170
+ * This function gives the context in which the runtime resolves a path of compiled code (CAP.CAPS-GRANT, CAP.MEET).
171
+ * The context has the meet of its list and its grant, and a context without a grant has none. Thus a read of the root
172
+ * `caps` and a walker of a step document get the capabilities of the document from a widened context.
173
+ * A context that the code narrowed keeps its own list. A context whose list does not change stays the same object.
174
+ */
175
+ function readable(ctx: Context): Context {
176
+ const caps = grantedCaps(ctx);
177
+ if (caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0)) return ctx;
155
178
  const out: Granted = { ...ctx, caps, [GRANT]: caps };
156
179
  return out;
157
180
  }
@@ -896,15 +919,21 @@ class Session implements Runtime {
896
919
  return this.reading !== undefined;
897
920
  }
898
921
 
922
+ /**
923
+ * This function resolves a path in the context that the grant permits (CAP.CAPS-GRANT, CAP.MEET). Thus the root
924
+ * `caps` and a walker of a step document get the capabilities of the document.
925
+ */
899
926
  resolve(ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution {
900
- return this.read(ctx, path, undefined, options);
927
+ return this.read(readable(ctx), path, undefined, options);
901
928
  }
902
929
 
903
930
  /**
904
931
  * This function resolves a compiled path (PLAN.PATH). Without a list of reads and without a trace, the compiled
905
- * path answers at once. Each other resolution records its read, as `resolve` does.
932
+ * path answers at once. Each other resolution records its read, as `resolve` does. The path resolves in the context
933
+ * that the grant permits (CAP.CAPS-GRANT, CAP.MEET).
906
934
  */
907
- resolvePath(ctx: Context, compiled: CompiledPath): Resolution {
935
+ resolvePath(given: Context, compiled: CompiledPath): Resolution {
936
+ const ctx = readable(given);
908
937
  if (this.reading === undefined && !this.traced && compiled.nav === this.machine) return compiled.resolve(ctx);
909
938
  return this.read(ctx, compiled.path, compiled, undefined);
910
939
  }
package/src/reference.ts CHANGED
@@ -171,11 +171,13 @@ function capsOfPart(rules: readonly CapsRule[] | undefined, path: readonly Key[]
171
171
  }
172
172
 
173
173
  /**
174
- * This function gives the context of a part. A context that the evaluator gives keeps only the capabilities of the
175
- * parent (CAP.NARROW). Then the rules of the kind narrow it (CAP.CONTRACT).
174
+ * This function gives the context of a part. A context that the evaluator gives keeps the effects that both it and
175
+ * the parent permit (CAP.NARROW, CAP.MEET). A list that the parent permits stays as it is. Then the rules of the kind
176
+ * narrow it (CAP.CONTRACT).
176
177
  */
177
178
  function contextOfPart(seen: Seen, slot: PartSlot, parent: Context, given: Context | undefined): Context {
178
- const base = given === undefined || given === parent ? parent : { ...given, caps: given.caps.filter((cap) => permits(parent.caps, cap)) };
179
+ const base =
180
+ given === undefined || given === parent ? parent : given.caps.every((cap) => permits(parent.caps, cap)) ? given : { ...given, caps: bothPermit(parent.caps, given.caps) };
179
181
  const caps = capsOfPart(seen.kind?.caps, slot.path, base.caps);
180
182
  return caps === base.caps ? base : { ...base, caps };
181
183
  }
package/src/world.ts CHANGED
@@ -270,10 +270,28 @@ export interface World {
270
270
  readonly fallback?: Kind;
271
271
  }
272
272
 
273
- /** The result of a lookup of a type in the world. */
273
+ /**
274
+ * The result of a lookup of a type in the world. The reasons of a failure:
275
+ *
276
+ * - `unknown`: the world has no entry for a key, and no fallback kind.
277
+ * - `cycle`: the aliases form a cycle. The lookup reached a path a second time.
278
+ * - `too-long`: the lookup followed more than 32 aliases without a path a second time (DEREF.CHAIN-REASON).
279
+ * - `not-a-kind`: the path reaches an entry of the wrong sort. An example is an alias whose target is not in the world.
280
+ * - `computed`: the type is a computed type, thus it has no keys.
281
+ */
274
282
  export type Lookup =
275
283
  | { readonly ok: true; readonly kind: Kind; readonly path: readonly Key[]; readonly aliases: number }
276
- | { readonly ok: false; readonly reason: "unknown" | "cycle" | "not-a-kind" | "computed"; readonly path: readonly Key[] };
284
+ | { readonly ok: false; readonly reason: "unknown" | "cycle" | "too-long" | "not-a-kind" | "computed"; readonly path: readonly Key[] };
285
+
286
+ /** The most aliases that one lookup follows (WORLD.ALIAS-LIMIT). */
287
+ const MAX_ALIASES = 32;
288
+
289
+ /** This function tells if two paths of a lookup have the same keys. */
290
+ function samePath(a: readonly Key[], b: readonly Key[]): boolean {
291
+ if (a.length !== b.length) return false;
292
+ for (let index = 0; index < a.length; index += 1) if (a[index] !== b[index]) return false;
293
+ return true;
294
+ }
277
295
 
278
296
  /**
279
297
  * This function tells if an entry of the world is a kind.
@@ -312,7 +330,8 @@ function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): World
312
330
 
313
331
  /**
314
332
  * This function finds the kind that a type names. It walks the path from the root of the world, one key at a
315
- * time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup. In a
333
+ * time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup, and so
334
+ * does a chain of more than 32 aliases. In a
316
335
  * namespace with a fallback, a key that the namespace does not have gets the fallback kind. In an open world, a first
317
336
  * key that the world does not have gets the fallback kind of the world.
318
337
  *
@@ -325,6 +344,8 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
325
344
  if (start === undefined) return { ok: false, reason: "computed", path: [] };
326
345
  let path: readonly Key[] = start;
327
346
  let aliases = 0;
347
+ // The paths before each alias. The same path again closes a cycle, because the lookup of a path is always the same.
348
+ let seen: (readonly Key[])[] | undefined;
328
349
  for (;;) {
329
350
  let entries = world.kinds;
330
351
  let fallback: Kind | undefined = world.fallback;
@@ -339,12 +360,16 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
339
360
  }
340
361
  if (isAlias(entry)) {
341
362
  aliases += 1;
342
- if (aliases > 32) return { ok: false, reason: "cycle", path };
363
+ if (aliases > MAX_ALIASES) return { ok: false, reason: "too-long", path };
343
364
  const target = entry.value.slice(1);
344
365
  if (entry.value[0] !== "world" || !target.every((item) => typeof item === "string" || typeof item === "number")) {
345
366
  return { ok: false, reason: "not-a-kind", path };
346
367
  }
368
+ seen ??= [];
369
+ seen.push(path);
347
370
  path = [...(target as readonly Key[]), ...path.slice(index + 1)];
371
+ const next = path;
372
+ if (seen.some((before) => samePath(before, next))) return { ok: false, reason: "cycle", path };
348
373
  followed = true;
349
374
  break;
350
375
  }