@mark1russell7/coeval 0.3.3 → 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.3.3",
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/deref.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type Envelope, type Json, type JsonRecord, type Key, type TypeRef, field, isEnvelope, isKey, isRecord, typePath } from "./doc.ts";
2
2
  import { type World, entriesAsJson, lookupKind } from "./world.ts";
3
+ import type { ErrorCode } from "./errors.ts";
3
4
 
4
5
  /**
5
6
  * The brand of a host thing: an object of the host that answers the steps of a path itself. The brand is a
@@ -83,8 +84,11 @@ export interface Context {
83
84
  /** The code of a `deref` evaluator: it answers one step of a path on a document of its kind. */
84
85
  export type DerefCode = (doc: Envelope, key: Key) => Thing | undefined;
85
86
 
86
- /** The result of a step document: what it reached, or the reason of a failure. */
87
- export type WalkResult = { readonly ok: true; readonly thing: Thing } | { readonly ok: false; readonly reason: string };
87
+ /**
88
+ * The result of a step document: what it reached, or the reason of a failure. A failure can give its code, for
89
+ * example `#REF!` for a position outside a space. The default code is `#N/A`.
90
+ */
91
+ export type WalkResult = { readonly ok: true; readonly thing: Thing } | { readonly ok: false; readonly reason: string; readonly code?: ErrorCode };
88
92
 
89
93
  /** The input of a walker: the code of a step document, for example `nearest`. */
90
94
  export interface WalkInput {
@@ -134,13 +138,13 @@ export interface Hop {
134
138
  readonly by: HopBy;
135
139
  }
136
140
 
137
- /** The codes of a failed resolution. */
141
+ /** The codes that a resolution gives itself. A walker can give another code for its failure. */
138
142
  export type ResolveCode = "#REF!" | "#NAME?" | "#N/A" | "#CYCLE!" | "#STEP?";
139
143
 
140
144
  /** The result of a resolution: what the path reached, with each hop, or an error with the hops before it. */
141
145
  export type Resolution =
142
146
  | { readonly ok: true; readonly thing: Thing; readonly hops: readonly Hop[] }
143
- | { readonly ok: false; readonly code: ResolveCode; readonly reason: string; readonly hops: readonly Hop[] };
147
+ | { readonly ok: false; readonly code: ResolveCode | ErrorCode; readonly reason: string; readonly hops: readonly Hop[] };
144
148
 
145
149
  /** The options of a resolution. */
146
150
  export interface ResolveOptions {
@@ -333,7 +337,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
333
337
  }
334
338
  const hops: Hop[] | undefined = options.hops === false ? undefined : [];
335
339
  const done = hops ?? NO_HOPS;
336
- const failed = (code: ResolveCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
340
+ const failed = (code: ResolveCode | ErrorCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
337
341
  let collapses = 0;
338
342
  const adopt = nav.adopt ?? ((value: unknown): Thing => value);
339
343
 
@@ -399,7 +403,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
399
403
  return reached === undefined ? undefined : adopt(reached.thing);
400
404
  },
401
405
  });
402
- if (!result.ok) return failed("#N/A", result.reason);
406
+ if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
403
407
  here = adopt(result.thing);
404
408
  hops?.push({ step: envelope as Json, thing: here, by: "step" });
405
409
  }
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
@@ -207,6 +259,8 @@ export interface CompileInput {
207
259
  * @returns The value with the items.
208
260
  */
209
261
  fill(items: readonly unknown[]): unknown;
262
+ /** `true` when the position of the document declares an address. A reference there gives the thing, not its value. */
263
+ readonly address?: boolean;
210
264
  }
211
265
 
212
266
  /**
@@ -246,6 +300,8 @@ export interface EvalInput {
246
300
  readonly context: Context;
247
301
  /** The keys from the root document to this document. */
248
302
  readonly place: readonly Key[];
303
+ /** `true` when the position of the document declares an address. */
304
+ readonly address?: boolean;
249
305
  /**
250
306
  * This function resolves a path from the context of the document.
251
307
  *
@@ -847,7 +903,7 @@ function evaluatorOf(world: World, envelope: Envelope | undefined, found: Lookup
847
903
  /** The meta of a node of a compilation without a trace. Only a trace reads the meta of a node. */
848
904
  const NO_META: Meta = Object.freeze({ path: [], doc: { type: "?", value: null }, declared: false, answer: NO_ANSWER, side: "derive" });
849
905
 
850
- function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number): { readonly code: Code; readonly run: RunCode } {
906
+ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number, address = false): { readonly code: Code; readonly run: RunCode } {
851
907
  const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
852
908
  const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
853
909
  const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" } : NO_META;
@@ -888,7 +944,10 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
888
944
  }
889
945
  const parts: CompiledPart[] = split.parts.map((slot) => {
890
946
  const item = slot.item;
891
- const compiled = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1);
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);
892
951
  // The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
893
952
  const header = typeof (item as { readonly name?: unknown } | null)?.name === "string" && isEnvelope(item) ? (item as { readonly name: string }).name : undefined;
894
953
  return header === undefined
@@ -906,6 +965,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
906
965
  place,
907
966
  machine: c.machine,
908
967
  fill: fillOf(split, doc),
968
+ ...(address ? { address: true } : {}),
909
969
  };
910
970
  if (runner !== undefined) {
911
971
  const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
@@ -937,6 +997,20 @@ function placedCode(inner: Code, place: readonly Key[], name: string): Code {
937
997
  };
938
998
  }
939
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
+
940
1014
  /** This function gives the steps of a runner, with the place of each error and of each exception. */
941
1015
  function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
942
1016
  return function* (ctx, rt) {
@@ -971,6 +1045,7 @@ function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P
971
1045
 
972
1046
  function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
973
1047
  const { kind, typePath, doc, data, place } = input;
1048
+ const address = input.address === true ? { address: true } : {};
974
1049
  const compiled = input.parts;
975
1050
  const hasParts = compiled.length > 0;
976
1051
  return (ctx, rt) => {
@@ -989,6 +1064,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
989
1064
  parts,
990
1065
  context: ctx,
991
1066
  place,
1067
+ ...address,
992
1068
  resolve: (path, options) => rt.resolve(ctx, path, options),
993
1069
  effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
994
1070
  fill: input.fill,
package/src/formula.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Json, isRecord } from "./doc.ts";
2
2
  import type { Kind, WorldEntry } from "./world.ts";
3
- import { valueOfThing } from "./deref.ts";
3
+ import { type Context, type Resolution, valueOfThing } from "./deref.ts";
4
4
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
5
5
  import type { Code, CompileInput, CompiledEvaluator, CompiledPart, Evaluator, Machine } from "./evaluate.ts";
6
6
 
@@ -13,8 +13,8 @@ export type OpFn = (...args: never[]) => unknown;
13
13
  */
14
14
  export type LazyOp = (args: readonly (() => Result)[]) => Result;
15
15
 
16
- /** The code of an extension: it gives the value of the data of an extension document. */
17
- export type ExtFn = (data: Json) => Result;
16
+ /** The code of an extension: it gives the value of the data of an extension document, in the context of the document. */
17
+ export type ExtFn = (data: Json, context: Context) => Result;
18
18
 
19
19
  /**
20
20
  * The rules of one formula language: its operations, and the rules that differ between languages.
@@ -30,6 +30,8 @@ export type ExtFn = (data: Json) => Result;
30
30
  * - `omit`: the errors that leave a field out of a `record`, as an absent value.
31
31
  * - `missing`: the values that a reference cannot give. Such a value gives `#N/A`.
32
32
  * - `check`: a check of the value of an operation, for example "a number must be finite".
33
+ * - `combine`: the error of two or more failed parts of an operation, a `record` or a `list`. With it, each part
34
+ * evaluates, and one failed part gives its own error. Without it, the first error stops the evaluation.
33
35
  */
34
36
  export interface FormulaProfile {
35
37
  readonly ops?: Readonly<Record<string, OpFn>> | ((name: string) => OpFn | undefined);
@@ -41,8 +43,12 @@ export interface FormulaProfile {
41
43
  readonly omit?: (error: EvalError) => boolean;
42
44
  readonly missing?: (value: unknown) => boolean;
43
45
  readonly check?: (value: unknown, op: string) => Result | undefined;
46
+ readonly combine?: (errors: readonly EvalError[]) => EvalError;
44
47
  }
45
48
 
49
+ /** The mark of a field that the profile leaves out of a record. */
50
+ const SKIPPED: unique symbol = Symbol("skipped");
51
+
46
52
  /** The value of a name whose binding failed. A reference to the name gives the error. */
47
53
  export class Held {
48
54
  constructor(readonly error: EvalError) {}
@@ -97,6 +103,17 @@ function compiled(compile: (input: CompileInput) => Code): CompiledEvaluator {
97
103
  return { compile };
98
104
  }
99
105
 
106
+ /** This function evaluates parts in order. It gives the value of each part, or the first error. */
107
+ function valuesOf(parts: readonly CompiledPart[], ctx: Context, rt: Parameters<Code>[1]): unknown[] | Failed {
108
+ const values = new Array<unknown>(parts.length);
109
+ for (let index = 0; index < parts.length; index += 1) {
110
+ const result = (parts[index] as CompiledPart).code(ctx, rt);
111
+ if (!result.ok) return result;
112
+ values[index] = result.value;
113
+ }
114
+ return values;
115
+ }
116
+
100
117
  function partsOf(value: unknown): Readonly<Record<string, CompiledPart>> {
101
118
  return isRecord(value) ? (value as unknown as Readonly<Record<string, CompiledPart>>) : {};
102
119
  }
@@ -168,6 +185,22 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
168
185
  return Object.hasOwn(ops, name) ? ops[name] : undefined;
169
186
  };
170
187
  const notTruth = (what: string): Failed => fail("#VALUE!", `The ${what} must give a truth value.`);
188
+ const combine = profile.combine;
189
+ /** This function evaluates each part. Without `combine`, the first error stops it. A field that `omit` skips is absent. */
190
+ const collect = (parts: readonly CompiledPart[], ctx: Context, rt: Parameters<Code>[1], skip?: (error: EvalError) => boolean): (readonly unknown[]) | Failed => {
191
+ const values = new Array<unknown>(parts.length);
192
+ let errors: EvalError[] | undefined;
193
+ for (let index = 0; index < parts.length; index += 1) {
194
+ const result = (parts[index] as CompiledPart).code(ctx, rt);
195
+ if (result.ok) values[index] = result.value;
196
+ else if (skip !== undefined && skip(result.error)) values[index] = SKIPPED;
197
+ else if (combine === undefined) return result;
198
+ else (errors ??= []).push(result.error);
199
+ }
200
+ // The errors exist only with combine, so the second test only narrows the type.
201
+ if (errors === undefined || combine === undefined) return values;
202
+ return { ok: false, error: errors.length === 1 ? (errors[0] as EvalError) : combine(errors) };
203
+ };
171
204
 
172
205
  return {
173
206
  "formula.lit": compiled(({ doc }) => {
@@ -175,7 +208,7 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
175
208
  return () => result;
176
209
  }),
177
210
 
178
- "formula.ref": compiled(({ doc }) => {
211
+ "formula.ref": compiled(({ doc, parts, fill, address }) => {
179
212
  const path = doc.value;
180
213
  if (!Array.isArray(path) || path.length === 0) {
181
214
  const bad = fail("#VALUE!", 'The value of ref is a path: a list that starts with a root, for example ["names", "a"].');
@@ -183,17 +216,37 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
183
216
  }
184
217
  // A path through a name whose binding failed gives the error of the binding.
185
218
  const head = path[0] === "names" && typeof path[1] === "string" ? path[1] : undefined;
219
+ const give = (found: Resolution): Result => {
220
+ if (!found.ok) return fail(found.code, found.reason);
221
+ // An address gives what the path reaches: a host thing stays a thing.
222
+ if (address === true) return ok(found.thing);
223
+ const reached = valueOfThing(found.thing);
224
+ if (reached instanceof Held) return { ok: false, error: reached.error };
225
+ if (missing !== undefined && missing(reached)) return fail("#N/A", "The path reaches no value.");
226
+ return ok(reached);
227
+ };
228
+ // The step documents of a path are its parts when the kind ref has a deep shape. They evaluate first, in order.
229
+ const computed = parts.length > 0;
186
230
  return (ctx, rt) => {
187
231
  if (head !== undefined && Object.hasOwn(ctx.names, head)) {
188
232
  const bound = ctx.names[head];
189
233
  if (bound instanceof Held) return { ok: false, error: bound.error };
190
234
  }
191
- const found = rt.resolve(ctx, path);
192
- if (!found.ok) return fail(found.code, found.reason);
193
- const value = valueOfThing(found.thing);
194
- if (value instanceof Held) return { ok: false, error: value.error };
195
- if (missing !== undefined && missing(value)) return fail("#N/A", "The path reaches no value.");
196
- return ok(value);
235
+ if (!computed) return give(rt.resolve(ctx, path));
236
+ const steps = valuesOf(parts, ctx, rt);
237
+ return Array.isArray(steps) ? give(rt.resolve(ctx, fill(steps) as readonly unknown[])) : steps;
238
+ };
239
+ }),
240
+
241
+ "formula.step": compiled(({ doc, parts, fill }) => {
242
+ // A step document without parts is its own value. A step document with parts gets the result of each part.
243
+ if (parts.length === 0) {
244
+ const same = ok(doc);
245
+ return () => same;
246
+ }
247
+ return (ctx, rt) => {
248
+ const values = valuesOf(parts, ctx, rt);
249
+ return Array.isArray(values) ? ok({ ...doc, value: fill(values) as Json }) : values;
197
250
  };
198
251
  }),
199
252
 
@@ -201,6 +254,19 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
201
254
  const fields = partsOf(value);
202
255
  const keys = Object.keys(fields);
203
256
  const codes = keys.map((key) => (fields[key] as CompiledPart).code);
257
+ if (combine !== undefined) {
258
+ // With combine, each field evaluates, and two failed fields give one error.
259
+ const parts = keys.map((key) => fields[key] as CompiledPart);
260
+ return (ctx, rt) => {
261
+ const values = collect(parts, ctx, rt, omit);
262
+ if (!Array.isArray(values)) return values as Failed;
263
+ const out: Record<string, unknown> = {};
264
+ keys.forEach((key, index) => {
265
+ if (values[index] !== SKIPPED) setOwn(out, key, values[index]);
266
+ });
267
+ return ok(out);
268
+ };
269
+ }
204
270
  return (ctx, rt) => {
205
271
  const out: Record<string, unknown> = {};
206
272
  for (let index = 0; index < keys.length; index += 1) {
@@ -216,13 +282,8 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
216
282
  }),
217
283
 
218
284
  "formula.list": compiled(({ parts }) => (ctx, rt) => {
219
- const out = new Array<unknown>(parts.length);
220
- for (let index = 0; index < parts.length; index += 1) {
221
- const result = (parts[index] as CompiledPart).code(ctx, rt);
222
- if (!result.ok) return result;
223
- out[index] = result.value;
224
- }
225
- return ok(out);
285
+ const values = collect(parts, ctx, rt);
286
+ return Array.isArray(values) ? ok(values) : (values as Failed);
226
287
  }),
227
288
 
228
289
  "formula.let": compiled(({ value }) => {
@@ -340,6 +401,17 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
340
401
  }
341
402
  const done = (out: unknown): Result => (check === undefined ? ok(out) : (check(out, name) ?? ok(out)));
342
403
  const failed = (error: unknown): Result => thrown(error, `the operation ${name}`);
404
+ if (combine !== undefined) {
405
+ return (ctx, rt) => {
406
+ const values = collect(parts, ctx, rt);
407
+ if (!Array.isArray(values)) return values as Failed;
408
+ try {
409
+ return done(fn(...values));
410
+ } catch (error) {
411
+ return failed(error);
412
+ }
413
+ };
414
+ }
343
415
  if (parts.length === 1) {
344
416
  const [a] = parts as readonly [CompiledPart];
345
417
  return (ctx, rt) => {
@@ -389,9 +461,9 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
389
461
  const unknown = fail("#NAME?", `The language has no extension ${name}.`);
390
462
  return () => unknown;
391
463
  }
392
- return () => {
464
+ return (ctx) => {
393
465
  try {
394
- return handler(doc.value);
466
+ return handler(doc.value, ctx);
395
467
  } catch (error) {
396
468
  return thrown(error, `the extension ${name}`);
397
469
  }
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/parts.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Json, type Key, isEnvelope, isRecord } from "./doc.ts";
1
+ import { type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
2
2
  import type { Shape } from "./world.ts";
3
3
 
4
4
  /** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
@@ -9,6 +9,8 @@ export interface PartSlot {
9
9
  readonly key: string;
10
10
  readonly item: Json;
11
11
  readonly of?: string;
12
+ /** `true` when the position of the part declares an address: a reference there gives what its path reaches. */
13
+ readonly address?: true;
12
14
  }
13
15
 
14
16
  type Template =
@@ -54,6 +56,27 @@ function hasSlots(template: Template): boolean {
54
56
  return template.t !== "data";
55
57
  }
56
58
 
59
+ /** This function gives the reason why a value is not data of a shape, or `undefined` when it is. */
60
+ function dataFault(spec: Extract<Shape, { readonly shape: "text" | "number" | "one" }>, item: Json): string | undefined {
61
+ if (spec.shape === "text") return typeof item === "string" ? undefined : "The value must be a text.";
62
+ if (spec.shape === "one") return spec.values.some((value) => docEquals(value, item)) ? undefined : `The value must be one of ${JSON.stringify(spec.values)}.`;
63
+ if (typeof item !== "number") return "The value must be a number.";
64
+ if (spec.whole === true && !Number.isInteger(item)) return "The value must be a whole number.";
65
+ if (spec.min !== undefined && !(item >= spec.min)) return `The value must be ${String(spec.min)} or more.`;
66
+ if (spec.max !== undefined && !(item <= spec.max)) return `The value must be ${String(spec.max)} or less.`;
67
+ return undefined;
68
+ }
69
+
70
+ /** This function stops a split when a part has a type that its position does not permit. */
71
+ function kindFault(kinds: readonly (readonly Key[])[], item: Json, of: string | undefined, path: readonly Key[]): void {
72
+ const type = isEnvelope(item) ? typePath(item.type) : of === undefined ? undefined : [of];
73
+ const fits = type !== undefined && kinds.some((kind) => kind.length <= type.length && kind.every((key, index) => String(key) === String(type[index])));
74
+ if (fits) return;
75
+ const names = kinds.map((kind) => kind.join(".")).join(", ");
76
+ const what = isEnvelope(item) ? `The kind ${typeName(item.type)} is not permitted here.` : "A value without an envelope is not permitted here.";
77
+ throw new Bad(`${what} The permitted kinds are ${names}.`, path);
78
+ }
79
+
57
80
  /**
58
81
  * This function splits a value into its parts, by the shape that its kind declares. The evaluator, the view and
59
82
  * the static kit use the same split, thus each one sees the same parts.
@@ -66,8 +89,8 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
66
89
  // A value without parts is data: the split needs no template.
67
90
  if (shape === undefined || shape.shape === "none") return { ok: true, parts: NO_SLOTS, data: value, fill: () => value };
68
91
  const parts: PartSlot[] = [];
69
- const slot = (item: Json, path: readonly Key[], of: string | undefined): Template => {
70
- parts.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }) });
92
+ const slot = (item: Json, path: readonly Key[], of: string | undefined, address = false): Template => {
93
+ parts.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }), ...(address ? { address: true as const } : {}) });
71
94
  return { t: "slot", index: parts.length - 1 };
72
95
  };
73
96
 
@@ -86,12 +109,25 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
86
109
 
87
110
  const walk = (spec: Shape | undefined, item: Json, path: readonly Key[]): Template => {
88
111
  if (spec === undefined || spec.shape === "none") return { t: "data", value: item };
89
- if (spec.shape === "term") return slot(item, path, spec.of);
112
+ if (spec.shape === "text" || spec.shape === "number" || spec.shape === "one") {
113
+ const reason = dataFault(spec, item);
114
+ if (reason !== undefined) throw new Bad(reason, path);
115
+ return { t: "data", value: item };
116
+ }
117
+ if (spec.shape === "term") {
118
+ if (spec.kinds !== undefined) kindFault(spec.kinds, item, spec.of, path);
119
+ return slot(item, path, spec.of, spec.address === true);
120
+ }
90
121
  if (spec.shape === "deep") return deep(item, path, spec.of);
91
122
  if (spec.shape === "list") {
92
123
  if (!Array.isArray(item)) throw new Bad("The value must be a list.", path);
124
+ const list = item as readonly Json[];
125
+ if (spec.distinct === true) {
126
+ const twice = list.findIndex((child, index) => list.slice(0, index).some((before) => docEquals(before, child)));
127
+ if (twice !== -1) throw new Bad("The items of the list must be different.", [...path, twice]);
128
+ }
93
129
  const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
94
- return { t: "list", items: (item as readonly Json[]).map((child, index) => walk(each, child, [...path, index])) };
130
+ return { t: "list", items: list.map((child, index) => walk(each, child, [...path, index])) };
95
131
  }
96
132
  if (!isRecord(item)) throw new Bad("The value must be a record.", path);
97
133
  if (spec.shape === "map") {
@@ -106,6 +142,7 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
106
142
  : (spec.fields as Readonly<Record<string, Shape>>);
107
143
  const fields: (readonly [string, Template])[] = [];
108
144
  for (const key of Object.keys(item)) {
145
+ if (spec.closed === true && !Object.hasOwn(named, key)) throw new Bad(`The field ${key} is not a field of this kind.`, [...path, key]);
109
146
  const fieldShape = Object.hasOwn(named, key) ? named[key] : spec.rest;
110
147
  fields.push([key, walk(fieldShape, item[key] as Json, [...path, key])]);
111
148
  }