@mark1russell7/coeval 0.3.2 → 0.4.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.3.2",
4
+ "version": "0.4.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/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,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
2
- import { splitParts } from "./parts.ts";
2
+ import { type Split, splitParts } from "./parts.ts";
3
3
  import { ask, type Answer } from "./ladder.ts";
4
4
  import { 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";
@@ -207,6 +207,8 @@ export interface CompileInput {
207
207
  * @returns The value with the items.
208
208
  */
209
209
  fill(items: readonly unknown[]): unknown;
210
+ /** `true` when the position of the document declares an address. A reference there gives the thing, not its value. */
211
+ readonly address?: boolean;
210
212
  }
211
213
 
212
214
  /**
@@ -246,6 +248,8 @@ export interface EvalInput {
246
248
  readonly context: Context;
247
249
  /** The keys from the root document to this document. */
248
250
  readonly place: readonly Key[];
251
+ /** `true` when the position of the document declares an address. */
252
+ readonly address?: boolean;
249
253
  /**
250
254
  * This function resolves a path from the context of the document.
251
255
  *
@@ -847,7 +851,7 @@ function evaluatorOf(world: World, envelope: Envelope | undefined, found: Lookup
847
851
  /** The meta of a node of a compilation without a trace. Only a trace reads the meta of a node. */
848
852
  const NO_META: Meta = Object.freeze({ path: [], doc: { type: "?", value: null }, declared: false, answer: NO_ANSWER, side: "derive" });
849
853
 
850
- function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number): { readonly code: Code; readonly run: RunCode } {
854
+ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number, address = false): { readonly code: Code; readonly run: RunCode } {
851
855
  const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
852
856
  const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
853
857
  const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" } : NO_META;
@@ -888,7 +892,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
888
892
  }
889
893
  const parts: CompiledPart[] = split.parts.map((slot) => {
890
894
  const item = slot.item;
891
- const compiled = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1);
895
+ const compiled = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
892
896
  // The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
893
897
  const header = typeof (item as { readonly name?: unknown } | null)?.name === "string" && isEnvelope(item) ? (item as { readonly name: string }).name : undefined;
894
898
  return header === undefined
@@ -905,41 +909,74 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
905
909
  parts,
906
910
  place,
907
911
  machine: c.machine,
908
- fill: (items) => (split.parts.length === 0 ? doc.value : split.fill(items)),
912
+ fill: fillOf(split, doc),
913
+ ...(address ? { address: true } : {}),
909
914
  };
910
915
  if (runner !== undefined) {
911
916
  const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
912
- const run: RunCode = function* (ctx, rt) {
913
- try {
914
- return placed(yield* inner(ctx, rt), place);
915
- } catch (error) {
916
- return placed(thrown(error, `the runner ${name}`), place);
917
- }
918
- };
919
- const code: Code = (ctx, rt) =>
920
- rt.running
921
- ? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
922
- : drive(run(ctx, rt));
923
- return finish(c, meta, code, run);
917
+ const run = placedRun(inner, place, name);
918
+ return finish(c, meta, runnerCode(run, place, kindText), run);
924
919
  }
925
920
 
926
921
  const inner: Code = isCompiledEvaluator(evaluator as Evaluator)
927
922
  ? (evaluator as CompiledEvaluator).compile(input)
928
923
  : interpret(evaluator as EvalFn, input, split.fill);
929
- const code: Code = (ctx, rt) => {
924
+ const code = placedCode(inner, place, name);
925
+ return finish(c, meta, code, runOf(code));
926
+ }
927
+
928
+ /*
929
+ * The helpers below make the closures that a plan keeps. Each one has its own small scope. A closure that a
930
+ * compilation made in its own scope keeps each variable of that scope that any closure there reads, for example
931
+ * the split of the value. Thus a plan with these helpers keeps only its code.
932
+ */
933
+
934
+ /** This function gives the code of a document: the code of its evaluator, with the place of each error and of each exception. */
935
+ function placedCode(inner: Code, place: readonly Key[], name: string): Code {
936
+ return (ctx, rt) => {
930
937
  try {
931
938
  return placed(inner(ctx, rt), place);
932
939
  } catch (error) {
933
940
  return placed(thrown(error, `the evaluator ${name}`), place);
934
941
  }
935
942
  };
936
- return finish(c, meta, code, function* (ctx, rt) {
943
+ }
944
+
945
+ /** This function gives the steps of a runner, with the place of each error and of each exception. */
946
+ function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
947
+ return function* (ctx, rt) {
948
+ try {
949
+ return placed(yield* inner(ctx, rt), place);
950
+ } catch (error) {
951
+ return placed(thrown(error, `the runner ${name}`), place);
952
+ }
953
+ };
954
+ }
955
+
956
+ /** This function gives the code of a runner in an evaluation: its steps, which cannot wait. */
957
+ function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
958
+ return (ctx, rt) =>
959
+ rt.running
960
+ ? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
961
+ : drive(run(ctx, rt));
962
+ }
963
+
964
+ /** This function gives the steps of a run of an evaluator. The code runs at the first step, and it does not wait. */
965
+ function runOf(code: Code): RunCode {
966
+ return function* (ctx, rt) {
937
967
  return code(ctx, rt);
938
- });
968
+ };
969
+ }
970
+
971
+ /** This function gives the fill of a compile input: the value with an item in the place of each part. */
972
+ function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P>(items: readonly P[]) => unknown {
973
+ return (items) => (split.parts.length === 0 ? doc.value : split.fill(items));
939
974
  }
940
975
 
976
+
941
977
  function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
942
978
  const { kind, typePath, doc, data, place } = input;
979
+ const address = input.address === true ? { address: true } : {};
943
980
  const compiled = input.parts;
944
981
  const hasParts = compiled.length > 0;
945
982
  return (ctx, rt) => {
@@ -958,6 +995,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
958
995
  parts,
959
996
  context: ctx,
960
997
  place,
998
+ ...address,
961
999
  resolve: (path, options) => rt.resolve(ctx, path, options),
962
1000
  effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
963
1001
  fill: input.fill,
@@ -1021,16 +1059,21 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
1021
1059
  };
1022
1060
  }
1023
1061
  const compiled: CompiledPart = { key: "", path: [], code: root.code, run: root.run };
1062
+ return planOf(machine, c.traced, compiled);
1063
+ }
1064
+
1065
+ /** This function makes the object of a plan. Its closures keep only the machine and the compiled root. */
1066
+ function planOf(machine: Machine, traced: boolean, root: CompiledPart): Plan {
1024
1067
  return {
1025
1068
  machine,
1026
- traced: c.traced,
1027
- root: compiled,
1069
+ traced,
1070
+ root,
1028
1071
  evaluate(context: Context = makeContext(), evalOptions?: EvalOptions): Result {
1029
- const s = new Session(machine, false, c.traced, undefined, undefined, evalOptions?.effects);
1072
+ const s = new Session(machine, false, traced, undefined, undefined, evalOptions?.effects);
1030
1073
  return root.code(rootContext(machine, context), s);
1031
1074
  },
1032
1075
  run(context: Context = makeContext(), runOptions: RunOptions = {}): Promise<Result> {
1033
- const s = new Session(machine, true, c.traced, runOptions.perform, runOptions.signal, undefined);
1076
+ const s = new Session(machine, true, traced, runOptions.perform, runOptions.signal, undefined);
1034
1077
  return driveWaiting(root.run(rootContext(machine, context), s), runOptions.signal);
1035
1078
  },
1036
1079
  };
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/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
  }