@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/src/static.ts CHANGED
@@ -12,25 +12,33 @@ export interface DocVisit {
12
12
  readonly place: readonly Key[];
13
13
  /** The keys from the root JSON value to this document. A change at this path changes this document. */
14
14
  readonly at: readonly Key[];
15
+ /** The visit of the document that holds this document as a part. The root has none. */
16
+ readonly parent?: DocVisit | undefined;
17
+ /** The keys from the value of the parent to this document: the path of the part. The root has none. */
18
+ readonly slot: readonly Key[];
15
19
  /** The document. A bare value has the type that its position declares. */
16
20
  readonly doc: Envelope;
17
21
  /** `true` when the type came from the position and not from an envelope. */
18
22
  readonly declared: boolean;
19
23
  /** The kind of the document, when the world has it. */
20
- readonly kind?: Kind;
24
+ readonly kind?: Kind | undefined;
21
25
  /** The keys of the kind after each alias. */
22
- readonly typePath?: readonly Key[];
26
+ readonly typePath?: readonly Key[] | undefined;
23
27
  /** The number of documents around this document. The root has 0. */
24
28
  readonly depth: number;
25
29
  /** The names that the kinds around this document bind for it. */
26
30
  readonly names: ReadonlySet<string>;
27
31
  /** The names that a sequence around this document binds only for the parts after it. */
28
32
  readonly later: ReadonlySet<string>;
33
+ /** `true` when the position of the document declares an address: a reference there is not a read of a value. */
34
+ readonly address?: true | undefined;
29
35
  }
30
36
 
31
37
  /** A position that a walk cannot read as a document. */
32
38
  export interface WalkProblem {
39
+ /** The keys from the root document to the document with the problem. */
33
40
  readonly place: readonly Key[];
41
+ /** The keys from the root JSON value to the value with the problem. */
34
42
  readonly at: readonly Key[];
35
43
  readonly code: "#PART?" | "#VALUE!";
36
44
  readonly reason: string;
@@ -120,10 +128,66 @@ function withoutNames(options: WalkOptions): WalkOptions {
120
128
  return rest;
121
129
  }
122
130
 
131
+ /** A path that shares its start with other paths: the keys of one step, and the path before them. */
132
+ interface Trail {
133
+ readonly up: Trail | undefined;
134
+ readonly keys: readonly Key[];
135
+ full?: readonly Key[];
136
+ }
137
+
138
+ /** This function gives the keys of a trail, from its start. It keeps the result in the trail. */
139
+ function keysOf(trail: Trail | undefined): readonly Key[] {
140
+ if (trail === undefined) return [];
141
+ if (trail.full !== undefined) return trail.full;
142
+ const steps: Trail[] = [];
143
+ let here: Trail | undefined = trail;
144
+ while (here !== undefined && here.full === undefined) {
145
+ steps.push(here);
146
+ here = here.up;
147
+ }
148
+ const keys: Key[] = here?.full === undefined ? [] : [...here.full];
149
+ for (let index = steps.length - 1; index >= 0; index -= 1) for (const key of (steps[index] as Trail).keys) keys.push(key);
150
+ trail.full = keys;
151
+ return keys;
152
+ }
153
+
154
+ /** A visit with its paths as trails. It makes the keys of a path only when a reader asks for them. */
155
+ class Visit implements DocVisit {
156
+ readonly #place: Trail | undefined;
157
+ readonly #at: Trail | undefined;
158
+
159
+ constructor(
160
+ place: Trail | undefined,
161
+ at: Trail | undefined,
162
+ readonly doc: Envelope,
163
+ readonly declared: boolean,
164
+ readonly depth: number,
165
+ readonly names: ReadonlySet<string>,
166
+ readonly later: ReadonlySet<string>,
167
+ readonly slot: readonly Key[],
168
+ readonly parent: DocVisit | undefined,
169
+ readonly kind: Kind | undefined,
170
+ readonly typePath: readonly Key[] | undefined,
171
+ readonly address: true | undefined,
172
+ ) {
173
+ this.#place = place;
174
+ this.#at = at;
175
+ }
176
+
177
+ get place(): readonly Key[] {
178
+ return keysOf(this.#place);
179
+ }
180
+
181
+ get at(): readonly Key[] {
182
+ return keysOf(this.#at);
183
+ }
184
+ }
185
+
123
186
  /**
124
187
  * This function visits each document of a document: the root and each part at each depth, by the contracts of the
125
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
126
- * depth, thus a deep document cannot overflow it.
189
+ * depth, thus a deep document cannot overflow it. The time and the memory of a walk are linear in the size of the
190
+ * document. A visit makes the keys of its place and of its path only when a reader asks for them.
127
191
  *
128
192
  * @param world - The world of the kinds.
129
193
  * @param doc - The document, as JSON.
@@ -133,61 +197,80 @@ function withoutNames(options: WalkOptions): WalkOptions {
133
197
  export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => void | "skip", options: WalkOptions = {}): void {
134
198
  interface Item {
135
199
  readonly value: unknown;
136
- readonly place: readonly Key[];
137
- readonly at: readonly Key[];
200
+ readonly place: Trail | undefined;
201
+ readonly at: Trail | undefined;
138
202
  readonly declared: string | undefined;
139
203
  readonly depth: number;
140
204
  readonly names: ReadonlySet<string>;
141
205
  readonly later: ReadonlySet<string>;
206
+ readonly address: boolean;
207
+ readonly slot: readonly Key[];
208
+ readonly parent: DocVisit | undefined;
142
209
  }
143
- const names: ReadonlySet<string> = options.names === undefined || options.names.length === 0 ? NO_NAMES : new Set(options.names);
144
- const stack: Item[] = [{ value: doc, place: [], at: [], declared: options.declaredType, depth: 0, names, later: NO_NAMES }];
210
+ const stack: Item[] = [
211
+ {
212
+ value: doc,
213
+ place: undefined,
214
+ at: undefined,
215
+ declared: options.declaredType,
216
+ depth: 0,
217
+ names: options.names === undefined || options.names.length === 0 ? NO_NAMES : new Set(options.names),
218
+ later: NO_NAMES,
219
+ address: false,
220
+ slot: [],
221
+ parent: undefined,
222
+ },
223
+ ];
145
224
  while (stack.length > 0) {
146
225
  const item = stack.pop() as Item;
147
226
  const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
148
227
  if (envelope === undefined && item.declared === undefined) {
149
- options.problem?.({ place: item.place, at: item.at, code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
228
+ options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
150
229
  continue;
151
230
  }
152
231
  const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
153
232
  const found = lookupKind(world, doc.type);
154
233
  const kind = found.ok ? found.kind : undefined;
155
- const seen: DocVisit =
156
- kind === undefined
157
- ? { place: item.place, at: item.at, doc, declared: envelope === undefined, depth: item.depth, names: item.names, later: item.later }
158
- : {
159
- place: item.place,
160
- at: item.at,
161
- doc,
162
- declared: envelope === undefined,
163
- kind,
164
- typePath: found.ok ? found.path : [],
165
- depth: item.depth,
166
- names: item.names,
167
- later: item.later,
168
- };
169
- const answer = visit(seen);
234
+ const here = new Visit(
235
+ item.place,
236
+ item.at,
237
+ doc,
238
+ envelope === undefined,
239
+ item.depth,
240
+ item.names,
241
+ item.later,
242
+ item.slot,
243
+ item.parent,
244
+ kind,
245
+ found.ok ? found.path : undefined,
246
+ item.address ? true : undefined,
247
+ );
248
+ const answer = visit(here);
170
249
  if (answer === "skip" || kind === undefined) continue;
171
250
  const split = splitParts(kind.parts, doc.value);
251
+ const base: Trail | undefined = envelope === undefined ? item.at : { up: item.at, keys: ["value"] };
172
252
  if (!split.ok) {
173
253
  const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
174
- options.problem?.({ place: item.place, at: item.at, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
254
+ const where = keysOf({ up: base, keys: split.at });
255
+ options.problem?.({ place: keysOf(item.place), at: where, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
175
256
  continue;
176
257
  }
177
258
  const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
178
- const base = envelope === undefined ? item.at : [...item.at, "value"];
179
259
  for (let index = split.parts.length - 1; index >= 0; index -= 1) {
180
260
  const slot = split.parts[index] as PartSlot;
181
261
  const extra = sees[index] ?? [];
182
262
  const hidden = later[index] ?? [];
183
263
  stack.push({
184
264
  value: slot.item,
185
- place: [...item.place, ...slot.path],
186
- at: [...base, ...slot.path],
265
+ place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
266
+ at: slot.path.length === 0 ? base : { up: base, keys: slot.path },
187
267
  declared: slot.of,
188
268
  depth: item.depth + 1,
189
269
  names: extra.length === 0 ? item.names : new Set([...item.names, ...extra]),
190
270
  later: hidden.length === 0 ? item.later : new Set([...item.later, ...hidden]),
271
+ address: slot.address === true,
272
+ slot: slot.path,
273
+ parent: here,
191
274
  });
192
275
  }
193
276
  }
@@ -195,6 +278,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
195
278
 
196
279
  /** One reference of a document. */
197
280
  export interface RefUse {
281
+ /** The visit of the reference document: its parent, its depth and the names that it sees. */
282
+ readonly visit: DocVisit;
198
283
  /** The place of the reference document, through the parts. */
199
284
  readonly place: readonly Key[];
200
285
  /** The keys from the root JSON value to the reference document. */
@@ -204,12 +289,14 @@ export interface RefUse {
204
289
  readonly root: string;
205
290
  /** `false` when a kind around the reference binds its first name. A reference to another root is free. */
206
291
  readonly free: boolean;
292
+ /** `true` when the reference is an address: its position declares it. An address does not read a value. */
293
+ readonly address: boolean;
207
294
  }
208
295
 
209
296
  /**
210
297
  * This function finds each reference of a document: each document of the type `ref` that a walk reaches. A
211
298
  * reference to a name that a kind around it binds is not free. The others are what the document reads from
212
- * outside: its dependencies.
299
+ * outside: its dependencies. When the kind `ref` has parts, the walk also finds the references in its step documents.
213
300
  *
214
301
  * @param world - The world of the kinds.
215
302
  * @param doc - The document, as JSON.
@@ -229,8 +316,13 @@ export function refsOf(world: World, doc: unknown, options: WalkOptions = {}): r
229
316
  const root = typeof path[0] === "string" ? path[0] : "";
230
317
  const first = path[1];
231
318
  const bound = root === "names" && isKey(first) && v.names.has(String(first));
232
- out.push({ place: v.place, at: v.at, path, root, free: !bound });
233
- return "skip";
319
+ // The place and the path of a reference are made only when a reader asks for them.
320
+ const use = { path, root, free: !bound, address: v.address === true } as RefUse;
321
+ Object.defineProperty(use, "place", { enumerable: true, get: () => v.place });
322
+ Object.defineProperty(use, "at", { enumerable: true, get: () => v.at });
323
+ Object.defineProperty(use, "visit", { enumerable: false, value: v });
324
+ out.push(use);
325
+ return undefined;
234
326
  },
235
327
  rest,
236
328
  );
@@ -278,6 +370,8 @@ export interface Issue {
278
370
  /** The keys from the root document to the document with the problem. */
279
371
  readonly place: readonly Key[];
280
372
  readonly reason: string;
373
+ /** The keys from the root JSON value to the value with the problem. */
374
+ readonly at?: readonly Key[];
281
375
  }
282
376
 
283
377
  /** The options of a check. */
@@ -319,34 +413,40 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
319
413
  doc,
320
414
  (v) => {
321
415
  if (v.depth > maxDepth) {
322
- issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.` });
416
+ issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.`, at: v.at });
323
417
  return "skip";
324
418
  }
325
419
  const header = v.declared ? {} : v.doc;
326
420
  const answer = ask(machine.world, header, "eval", v.declared ? { declaredType: typeName(v.doc.type) } : {});
327
421
  if (answer.error === "unknown-kind") {
328
- issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.` });
422
+ issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.`, at: v.at });
329
423
  return "skip";
330
424
  }
331
425
  const name = answer.evaluator;
332
426
  if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
333
- issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.` });
427
+ issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
334
428
  }
335
429
  if (options.caps !== undefined && v.kind?.effects !== undefined) {
336
430
  const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
337
431
  for (const kind of kinds) {
338
- if (!permits(options.caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: `The capabilities do not permit the effect ${kind.join(".")}.` });
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 });
339
433
  }
340
434
  }
341
- if (!isRef(v.doc)) return undefined;
435
+ const isPath = isRef(v.doc);
436
+ if (!isPath && v.typePath?.length === 1 && v.typePath[0] === "ref") {
437
+ issues.push({ code: "#VALUE!", place: v.place, reason: "The value of ref is a path: a list that starts with a root.", at: [...v.at, ...(v.declared ? [] : ["value"])] });
438
+ }
439
+ if (!isPath) return undefined;
342
440
  const path = v.doc.value as readonly Json[];
343
441
  const root = path[0];
442
+ // The JSON path of a problem of a path goes to its step.
443
+ const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
344
444
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
345
- issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.` });
445
+ issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
346
446
  return "skip";
347
447
  }
348
448
  if (!roots.includes(root)) {
349
- issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.` });
449
+ issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });
350
450
  }
351
451
  const first = path[1];
352
452
  if (root === ("names" as Root) && !anyName && isKey(first) && !v.names.has(String(first))) {
@@ -355,22 +455,24 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
355
455
  code: "#NAME?",
356
456
  place: v.place,
357
457
  reason: later ? `The name ${String(first)} is bound by a later step.` : `Nothing binds the name ${String(first)}.`,
458
+ at: stepAt(1),
358
459
  });
359
460
  }
360
- path.slice(1).forEach((step) => {
361
- if (isKey(step)) return;
461
+ path.forEach((step, index) => {
462
+ if (index === 0 || isKey(step)) return;
362
463
  const found = isEnvelope(step) ? lookupKind(machine.world, step.type) : undefined;
363
464
  const walker = found?.ok === true && Object.hasOwn(found.kind.verbs, "step") ? found.kind.verbs["step"] : undefined;
364
465
  if (walker === undefined || !has(machine.walkers, walker)) {
365
- issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step." });
466
+ issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step.", at: stepAt(index) });
366
467
  }
367
468
  });
368
- return "skip";
469
+ // The step documents of the path are parts when the kind ref has a deep shape. The walk checks them too.
470
+ return undefined;
369
471
  },
370
472
  {
371
473
  ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
372
474
  ...(Array.isArray(options.names) ? { names: options.names } : {}),
373
- problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason }),
475
+ problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at }),
374
476
  },
375
477
  );
376
478
  return issues;
package/src/world.ts CHANGED
@@ -14,11 +14,16 @@ export type Side = "derive" | "run";
14
14
  * - `record`: the value is a record. Each named field has its shape. A list of names makes each named field a
15
15
  * part. The other fields have the shape `rest` (the default is data). `required` names the fields that must be.
16
16
  * - `deep`: each envelope at any depth of the value is a part, and the rest is data.
17
+ * - `text`, `number` and `one`: the value is data of that type. `one` names the values that it permits.
18
+ *
19
+ * A `term` with `address: true` is an address: a reference there gives what its path reaches, not the value. A
20
+ * `term` with `kinds` permits only a part whose type starts with one of the paths. A `record` with `closed: true`
21
+ * permits only its named fields. A `list` with `distinct: true` permits no two equal items.
17
22
  */
18
23
  export type Shape =
19
24
  | { readonly shape: "none" }
20
- | { readonly shape: "term"; readonly of?: string }
21
- | { readonly shape: "list"; readonly of?: string; readonly item?: Shape }
25
+ | { readonly shape: "term"; readonly of?: string; readonly address?: boolean; readonly kinds?: readonly (readonly Key[])[] }
26
+ | { readonly shape: "list"; readonly of?: string; readonly item?: Shape; readonly distinct?: boolean }
22
27
  | { readonly shape: "map"; readonly of?: string; readonly item?: Shape }
23
28
  | {
24
29
  readonly shape: "record";
@@ -26,8 +31,12 @@ export type Shape =
26
31
  readonly of?: string;
27
32
  readonly required?: readonly string[];
28
33
  readonly rest?: Shape;
34
+ readonly closed?: boolean;
29
35
  }
30
- | { readonly shape: "deep"; readonly of?: string };
36
+ | { readonly shape: "deep"; readonly of?: string }
37
+ | { readonly shape: "text" }
38
+ | { readonly shape: "number"; readonly whole?: boolean; readonly min?: number; readonly max?: number }
39
+ | { readonly shape: "one"; readonly values: readonly Json[] };
31
40
 
32
41
  /** The former name of `Shape`. */
33
42
  export type PartsSpec = Shape;
@@ -209,6 +218,9 @@ export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Js
209
218
 
210
219
  function describeShape(shape: Shape | undefined): Json {
211
220
  if (shape === undefined || shape.shape === "none") return "none";
221
+ if (shape.shape === "text") return "a text";
222
+ if (shape.shape === "number") return "a number";
223
+ if (shape.shape === "one") return `one of ${JSON.stringify(shape.values)}`;
212
224
  if (shape.shape === "term") return `a part${shape.of === undefined ? "" : ` of ${shape.of}`}`;
213
225
  if (shape.shape === "deep") return "each document in the value";
214
226
  if (shape.shape === "list") return `a list${shape.of === undefined ? "" : ` of ${shape.of}`}`;