@mark1russell7/coeval 0.4.1 → 0.4.3

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.
@@ -0,0 +1,388 @@
1
+ import { type Json, type Key, docEquals, isEnvelope, typePath } from "./doc.ts";
2
+ import { type Kind, type ScopeRule, type Shape, type World, type WorldEntry, isAlias, isKind, isNamespace, lookupKind } from "./world.ts";
3
+ import { pathMatches } from "./parts.ts";
4
+ import { ROOTS } from "./deref.ts";
5
+
6
+ /** A source of random numbers with a seed. The same seed gives the same numbers. */
7
+ export interface Random {
8
+ /** This function gives a number from 0 up to 1, without 1. */
9
+ next(): number;
10
+ /** This function gives a whole number from 0 up to `below`, without `below`. */
11
+ int(below: number): number;
12
+ /** This function gives `true` with the chance `p`. */
13
+ chance(p: number): boolean;
14
+ /** This function gives one item of a list that is not empty. */
15
+ pick<T>(items: readonly T[]): T;
16
+ }
17
+
18
+ /**
19
+ * This function makes a source of random numbers with a seed (the algorithm mulberry32). It needs no package.
20
+ *
21
+ * @param seed - The seed: a whole number.
22
+ * @returns The source.
23
+ */
24
+ export function random(seed: number): Random {
25
+ let state = seed >>> 0;
26
+ const next = (): number => {
27
+ state = (state + 0x6d2b79f5) >>> 0;
28
+ let t = state;
29
+ t = Math.imul(t ^ (t >>> 15), t | 1);
30
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
31
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
32
+ };
33
+ const int = (below: number): number => Math.floor(next() * below);
34
+ return { next, int, chance: (p) => next() < p, pick: (items) => items[int(items.length)] as never };
35
+ }
36
+
37
+ /** The options of `generate`. */
38
+ export interface GenerateOptions {
39
+ /** The seed. The same seed and the same options give the same document. */
40
+ readonly seed: number;
41
+ /** The types that a document can have. The default is `typesOf(world)`. A namespace with a fallback needs its keys here. */
42
+ readonly types?: readonly (readonly Key[])[];
43
+ /** The type of a bare root value. */
44
+ readonly declaredType?: string;
45
+ /** The names that the host binds at the root. */
46
+ readonly names?: readonly string[];
47
+ /** The deepest nesting of documents. The default is 4. */
48
+ readonly maxDepth?: number;
49
+ /** The most documents in one document. The default is 24. */
50
+ readonly maxSize?: number;
51
+ /** The chance of a malformed choice at each document. The default is 0.05. */
52
+ readonly malformed?: number;
53
+ /**
54
+ * This function gives the data of a document of a type without parts, for example a number for the kind `number`.
55
+ *
56
+ * @param type - The keys of the type.
57
+ * @param random - The source of random numbers.
58
+ * @returns The data, or `undefined` for a random JSON value.
59
+ */
60
+ readonly data?: (type: readonly Key[], random: Random) => Json | undefined;
61
+ /**
62
+ * This function gives the types to prefer for a part, for example a function for the field `fn` of `call`.
63
+ *
64
+ * @param type - The keys of the type of the parent.
65
+ * @param path - The path of the part in the value of the parent.
66
+ * @returns The types, or `undefined` for no preference.
67
+ */
68
+ readonly hints?: (type: readonly Key[], path: readonly Key[]) => readonly (readonly Key[])[] | undefined;
69
+ /** The evaluator names for own entries. The default is none. */
70
+ readonly own?: readonly string[];
71
+ /** The keys for the steps of a path after its root and its name. */
72
+ readonly keys?: readonly Key[];
73
+ }
74
+
75
+ /**
76
+ * This function lists the types of a world: each kind and each alias, also in a namespace. A namespace with a
77
+ * fallback gives no types, because each key names a kind.
78
+ *
79
+ * @param world - The world.
80
+ * @returns The keys of each type.
81
+ */
82
+ export function typesOf(world: World): readonly (readonly Key[])[] {
83
+ const out: (readonly Key[])[] = [];
84
+ const visit = (entries: Readonly<Record<string, WorldEntry>>, prefix: readonly Key[]): void => {
85
+ for (const key of Object.keys(entries)) {
86
+ const entry = entries[key];
87
+ if (isKind(entry) || isAlias(entry)) out.push([...prefix, key]);
88
+ else if (isNamespace(entry)) visit(entry.kinds, [...prefix, key]);
89
+ }
90
+ };
91
+ visit(world.kinds, []);
92
+ return out;
93
+ }
94
+
95
+ const NAMES = ["a", "b", "x", "n", "last", "__proto__"] as const;
96
+ const FIELDS = ["a", "b", "c", "1", "__proto__"] as const;
97
+ const KEYS: readonly Key[] = ["a", "b", "x", 0, 1, "0", "length", "type", "value", "parent"];
98
+
99
+ /** This function tells if a kind can be a leaf: it needs no part. */
100
+ function leafKind(kind: Kind | undefined): boolean {
101
+ const shape = kind?.parts;
102
+ return shape === undefined || shape.shape === "none" || shape.shape === "list" || shape.shape === "map" || shape.shape === "text" || shape.shape === "number" || shape.shape === "one";
103
+ }
104
+
105
+ /** A position of a document: the type that it declares, the kinds that it permits, the names that it sees and its depth. */
106
+ interface Position {
107
+ readonly declared: string | undefined;
108
+ readonly kinds: readonly (readonly Key[])[] | undefined;
109
+ readonly names: readonly string[];
110
+ readonly depth: number;
111
+ readonly hint: readonly (readonly Key[])[] | undefined;
112
+ }
113
+
114
+ /** The names that the scope rules of one document bind, and the parts that see them. */
115
+ interface Scope {
116
+ readonly rules: readonly ScopeRule[];
117
+ /** The names of each rule, or the names of the headers so far for a rule `headers`. */
118
+ readonly names: string[][];
119
+ /** The number of parts that each rule covered so far. */
120
+ readonly covered: number[];
121
+ /** The data of a source of names, by its path as one text. */
122
+ readonly sources: Map<string, { readonly keys: readonly string[] } | { readonly list: readonly string[] } | { readonly text: string }>;
123
+ }
124
+
125
+ /**
126
+ * This function builds a random document for a world from the contracts of its kinds. It reads the shapes of the
127
+ * parts, the declared types, the permitted kinds and the names that the scope rules bind. Some choices are malformed
128
+ * on purpose, for example an unknown kind or an unknown name under a namespace with a fallback. Other examples are a
129
+ * value without its shape, a bare value without a type and a name that nothing binds. The depth and the size have
130
+ * limits.
131
+ *
132
+ * @param world - The world.
133
+ * @param options - The seed, the types, the limits and the data of the leaves.
134
+ * @returns The document, as JSON.
135
+ */
136
+ export function generate(world: World, options: GenerateOptions): unknown {
137
+ const rng = random(options.seed);
138
+ const types = options.types ?? typesOf(world);
139
+ const maxDepth = options.maxDepth ?? 4;
140
+ const maxSize = options.maxSize ?? 24;
141
+ const malformed = options.malformed ?? 0.05;
142
+ const keys = options.keys ?? KEYS;
143
+ const stepTypes = types.filter((type) => {
144
+ const found = lookupKind(world, type);
145
+ return found.ok && Object.hasOwn(found.kind.verbs, "step");
146
+ });
147
+ // A name under each namespace with a fallback that no evaluator knows, for example ["ops", "nope"].
148
+ const unknownNames: readonly (readonly Key[])[] = Object.keys(world.kinds)
149
+ .filter((key) => {
150
+ const entry = world.kinds[key];
151
+ return isNamespace(entry) && entry.fallback !== undefined;
152
+ })
153
+ .map((key) => [key, "nope"]);
154
+ let size = 0;
155
+
156
+ const kindOf = (type: readonly Key[]): Kind | undefined => {
157
+ const found = lookupKind(world, type);
158
+ return found.ok ? found.kind : undefined;
159
+ };
160
+
161
+ const anyJson = (depth = 0): Json => {
162
+ const roll = rng.int(depth > 1 ? 6 : 8);
163
+ if (roll === 0) return null;
164
+ if (roll === 1) return rng.chance(0.5);
165
+ if (roll === 2) return rng.pick([0, 1, 2, -1, 0.5, 1e21]);
166
+ if (roll === 3) return rng.pick(["", "a", "text", "0"]);
167
+ if (roll === 4) return rng.int(10);
168
+ if (roll === 5) return rng.pick(NAMES);
169
+ if (roll === 6) return Array.from({ length: rng.int(3) }, () => anyJson(depth + 1));
170
+ const out: Record<string, Json> = {};
171
+ for (let index = rng.int(3); index > 0; index -= 1) Object.defineProperty(out, rng.pick(FIELDS), { value: anyJson(depth + 1), enumerable: true, writable: true, configurable: true });
172
+ return out;
173
+ };
174
+
175
+ const dataOf = (type: readonly Key[]): Json => options.data?.(type, rng) ?? anyJson();
176
+
177
+ const renderType = (type: readonly Key[]): Json => {
178
+ const roll = rng.next();
179
+ if (roll < 0.06) return { type: "ref", value: ["world", ...type] };
180
+ if (type.length === 1 && roll < 0.8) return type[0] as Json;
181
+ return [...type];
182
+ };
183
+
184
+ const permitted = (type: readonly Key[], kinds: readonly (readonly Key[])[] | undefined): boolean =>
185
+ kinds === undefined || kinds.some((kind) => kind.length <= type.length && kind.every((key, index) => String(key) === String(type[index])));
186
+
187
+ const pickName = (names: readonly string[]): string => (names.length === 0 || rng.chance(malformed) ? rng.pick(["unbound", "zz"]) : rng.pick(names));
188
+
189
+ const someNames = (): string[] => {
190
+ const out = new Set<string>();
191
+ for (let index = 1 + rng.int(2); index > 0; index -= 1) out.add(rng.pick(NAMES));
192
+ return [...out];
193
+ };
194
+
195
+ const envelope = (type: readonly Key[], value: Json, header?: string): Json => {
196
+ const out: Record<string, Json> = { type: renderType(type), value };
197
+ if (header !== undefined) out["name"] = header;
198
+ if (options.own !== undefined && options.own.length > 0 && rng.chance(0.03)) out["own"] = { eval: rng.pick(options.own) };
199
+ return out;
200
+ };
201
+
202
+ /** A path from a root (D10). It uses the names in scope, and sometimes a step document. */
203
+ const pathFor = (pos: Position, refKind: Kind | undefined): Json => {
204
+ if (rng.chance(malformed)) return rng.pick<Json>(["names", [], { names: "a" }, ["nowhere", "a"]]);
205
+ const root = pos.names.length > 0 && rng.chance(0.75) ? "names" : rng.pick(ROOTS.filter((name) => name !== "names"));
206
+ const path: Json[] = [root];
207
+ if (root === "names") path.push(pickName(pos.names));
208
+ if (root === "world") path.push(String(rng.pick(types)[0]));
209
+ for (let index = rng.int(3); index > 0; index -= 1) {
210
+ if (stepTypes.length > 0 && rng.chance(0.2)) {
211
+ const type = rng.pick(stepTypes);
212
+ const kind = kindOf(type);
213
+ // With a deep shape of the kind ref, the value of a step document can hold parts.
214
+ const value = refKind?.parts?.shape === "deep" && kind?.parts !== undefined ? valueOf(kind.parts, [], { ...pos, depth: pos.depth + 1 }, emptyScope()) : dataOf(type);
215
+ path.push({ type: renderType(type), value });
216
+ } else {
217
+ path.push(rng.pick(keys) as Json);
218
+ }
219
+ }
220
+ return path;
221
+ };
222
+
223
+ const emptyScope = (): Scope => ({ rules: [], names: [], covered: [], sources: new Map() });
224
+
225
+ /** This function decides the names of the scope rules before the parts, so that each part knows what it sees. */
226
+ const scopeOf = (kind: Kind): Scope => {
227
+ const rules = kind.scope ?? [];
228
+ const scope: Scope = { rules, names: [], covered: rules.map(() => 0), sources: new Map() };
229
+ for (const rule of rules) {
230
+ const source = rule.names;
231
+ let names: string[] = [];
232
+ if ("fixed" in source) names = [...source.fixed];
233
+ else if ("keysAt" in source) {
234
+ names = someNames();
235
+ scope.sources.set(source.keysAt.join("."), { keys: names });
236
+ } else if ("listAt" in source) {
237
+ names = someNames();
238
+ scope.sources.set(source.listAt.join("."), { list: rng.chance(malformed) ? [...names, ...names] : names });
239
+ } else if ("textAt" in source) {
240
+ names = [rng.pick(NAMES)];
241
+ scope.sources.set(source.textAt.join("."), { text: names[0] as string });
242
+ }
243
+ scope.names.push(names);
244
+ }
245
+ return scope;
246
+ };
247
+
248
+ /** The names that a part sees from the scope rules of its parent. A rule `sequential` shows only earlier names. */
249
+ const seenBy = (scope: Scope, path: readonly Key[]): { readonly names: readonly string[]; readonly headers: boolean } => {
250
+ const names: string[] = [];
251
+ let headers = false;
252
+ scope.rules.forEach((rule, index) => {
253
+ if (!rule.in.some((pattern) => pathMatches(path, pattern))) return;
254
+ const ruleNames = scope.names[index] ?? [];
255
+ if ("headers" in rule.names) headers = true;
256
+ if (rule.sequential !== true || "headers" in rule.names || (scope.covered[index] ?? 0) > 0) names.push(...ruleNames);
257
+ });
258
+ return { names, headers };
259
+ };
260
+
261
+ /** This function records the part at a path, and the header name that it got, for the rules that cover it. */
262
+ const saw = (scope: Scope, path: readonly Key[], part: Json): void => {
263
+ const header = isEnvelope(part) && typeof (part as { readonly name?: unknown }).name === "string" ? (part as { readonly name: string }).name : undefined;
264
+ scope.rules.forEach((rule, index) => {
265
+ if (!rule.in.some((pattern) => pathMatches(path, pattern))) return;
266
+ scope.covered[index] = (scope.covered[index] ?? 0) + 1;
267
+ if ("headers" in rule.names && header !== undefined) scope.names[index]?.push(header);
268
+ });
269
+ };
270
+
271
+ const partAt = (pos: Position, scope: Scope, path: readonly Key[], spec: Extract<Shape, { readonly shape: "term" }> | { readonly shape: "term"; readonly of?: string }, parentType: readonly Key[] | undefined): Json => {
272
+ const seen = seenBy(scope, path);
273
+ const header = seen.headers && rng.chance(0.5) ? rng.pick(NAMES) : undefined;
274
+ const part = docAt(
275
+ {
276
+ declared: spec.of,
277
+ kinds: "kinds" in spec ? spec.kinds : undefined,
278
+ names: seen.names.length === 0 ? pos.names : [...pos.names, ...seen.names],
279
+ depth: pos.depth + 1,
280
+ hint: parentType === undefined ? undefined : options.hints?.(parentType, path),
281
+ },
282
+ header,
283
+ );
284
+ saw(scope, path, part);
285
+ return part;
286
+ };
287
+
288
+ /** This function builds a value of a shape (SHAPE.*). A part is a document at the next depth. */
289
+ const valueOf = (shape: Shape | undefined, path: readonly Key[], pos: Position, scope: Scope, type?: readonly Key[]): Json => {
290
+ const source = scope.sources.get(path.join("."));
291
+ if (source !== undefined && "list" in source) return [...source.list];
292
+ if (source !== undefined && "text" in source) return source.text;
293
+ const term = (of: string | undefined): { readonly shape: "term"; readonly of?: string } => (of === undefined ? { shape: "term" } : { shape: "term", of });
294
+ const small = (): number => (pos.depth >= maxDepth ? 0 : size >= maxSize ? rng.int(2) : rng.int(4));
295
+ if (shape === undefined || shape.shape === "none") return path.length === 0 && type !== undefined ? dataOf(type) : anyJson();
296
+ switch (shape.shape) {
297
+ case "term":
298
+ return partAt(pos, scope, path, shape, type);
299
+ case "list": {
300
+ const items = Array.from({ length: small() }, (_, index) => valueOf(shape.item ?? term(shape.of), [...path, index], pos, scope, type));
301
+ if (shape.distinct !== true || rng.chance(malformed)) return items;
302
+ return items.filter((item, index) => !items.slice(0, index).some((before) => docEquals(before, item)));
303
+ }
304
+ case "map": {
305
+ const out: Record<string, Json> = {};
306
+ const names = source !== undefined && "keys" in source ? source.keys : Array.from({ length: small() }, () => rng.pick(FIELDS));
307
+ for (const key of names) Object.defineProperty(out, key, { value: valueOf(shape.item ?? term(shape.of), [...path, key], pos, scope, type), enumerable: true, writable: true, configurable: true });
308
+ return out;
309
+ }
310
+ case "record": {
311
+ const named: Readonly<Record<string, Shape>> = Array.isArray(shape.fields)
312
+ ? Object.fromEntries((shape.fields as readonly string[]).map((name) => [name, term(shape.of)]))
313
+ : (shape.fields as Readonly<Record<string, Shape>>);
314
+ const fieldKeys = new Set<string>([...(shape.required ?? []), ...Object.keys(named).filter(() => rng.chance(0.75))]);
315
+ if (shape.closed !== true && shape.rest === undefined && rng.chance(0.15)) fieldKeys.add("extra");
316
+ const ordered = rng.chance(0.2) ? [...fieldKeys].reverse() : [...fieldKeys];
317
+ const out: Record<string, Json> = {};
318
+ for (const key of ordered) {
319
+ if (rng.chance(malformed / 2) && (shape.required ?? []).includes(key)) continue;
320
+ const fieldShape = Object.hasOwn(named, key) ? named[key] : shape.rest;
321
+ Object.defineProperty(out, key, { value: valueOf(fieldShape, [...path, key], pos, scope, type), enumerable: true, writable: true, configurable: true });
322
+ }
323
+ if (shape.closed === true && rng.chance(malformed)) out["stray"] = 1;
324
+ return out;
325
+ }
326
+ case "deep":
327
+ return rng.chance(0.5)
328
+ ? [partAt(pos, scope, [...path, 0], term(shape.of), type), anyJson()]
329
+ : { at: partAt(pos, scope, [...path, "at"], term(shape.of), type), data: anyJson() };
330
+ case "text":
331
+ return rng.chance(malformed) ? 1 : rng.pick(["", "a", "label"]);
332
+ case "number": {
333
+ if (rng.chance(malformed)) return "1";
334
+ const low = shape.min ?? -2;
335
+ const value = low + rng.int(5) + (shape.whole === true ? 0 : rng.pick([0, 0.5]));
336
+ return shape.max !== undefined && value > shape.max ? shape.max : value;
337
+ }
338
+ case "one":
339
+ return rng.chance(malformed) ? "other" : (rng.pick(shape.values) as Json);
340
+ }
341
+ };
342
+
343
+ /**
344
+ * A malformed document: an unknown kind, or an unknown name under a namespace with a fallback. It can also be a
345
+ * bad shape, a bare value without a type, or a bad reference.
346
+ */
347
+ const brokenAt = (pos: Position): Json => {
348
+ const roll = rng.int(5);
349
+ if (roll === 0 && unknownNames.length > 0 && rng.chance(0.5)) {
350
+ // The fallback kind answers the name, and its evaluator does not know it, for example an unknown operation.
351
+ const type = rng.pick(unknownNames);
352
+ const kind = kindOf(type);
353
+ return envelope(type, kind?.parts === undefined ? dataOf(type) : valueOf(kind.parts, [], pos, scopeOf(kind), type));
354
+ }
355
+ if (roll === 0) return { type: rng.pick<Json>(["nothing", ["no", "such"]]), value: 1 };
356
+ if (roll === 1) return pos.declared === undefined ? rng.pick<Json>([7, "bare", [1]]) : { type: "ref", value: ["names", "unbound"] };
357
+ if (roll === 2) {
358
+ const shaped = types.filter((type) => kindOf(type)?.parts !== undefined);
359
+ if (shaped.length > 0) return envelope(rng.pick(shaped), rng.pick<Json>([7, "x", null]));
360
+ }
361
+ if (roll === 3) return { type: "ref", value: rng.pick<Json>([["names", "unbound"], ["nowhere"], "names"]) };
362
+ return { type: rng.pick(types) as Json, value: anyJson() };
363
+ };
364
+
365
+ const docAt = (pos: Position, header?: string): Json => {
366
+ size += 1;
367
+ const leaf = pos.depth >= maxDepth || size >= maxSize;
368
+ if (rng.chance(malformed)) return brokenAt(pos);
369
+ if (pos.declared !== undefined && rng.chance(leaf ? 0.7 : 0.3)) {
370
+ const bare = dataOf([pos.declared]);
371
+ // A bare value with the form of an envelope is a document of its own type, and the position must permit that type.
372
+ if (!isEnvelope(bare) || permitted(typePath(bare.type) ?? [], pos.kinds)) return bare;
373
+ }
374
+ let candidates = types.filter((type) => permitted(type, pos.kinds) && (!leaf || leafKind(kindOf(type))));
375
+ if (pos.hint !== undefined && rng.chance(0.6)) {
376
+ const hinted = candidates.filter((type) => pos.hint?.some((hint) => hint.length === type.length && hint.every((key, index) => key === type[index])));
377
+ if (hinted.length > 0) candidates = hinted;
378
+ }
379
+ if (candidates.length === 0) candidates = [...types];
380
+ const type = rng.pick(candidates);
381
+ const kind = kindOf(type);
382
+ if (type.length === 1 && type[0] === "ref") return envelope(type, pathFor(pos, kind), header);
383
+ const value = kind === undefined ? dataOf(type) : valueOf(kind.parts, [], pos, scopeOf(kind), type);
384
+ return envelope(type, value, header);
385
+ };
386
+
387
+ return docAt({ declared: options.declaredType, kinds: undefined, names: options.names ?? [], depth: 0, hint: undefined });
388
+ }
package/src/index.ts CHANGED
@@ -95,7 +95,7 @@ export {
95
95
  worldThing,
96
96
  } from "./deref.ts";
97
97
  export { type Classification, type Shape, classify, tag } from "./classify.ts";
98
- export { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
98
+ export { type DocParts, type PartSlot, type Split, partsOf, pathMatches, splitParts } from "./parts.ts";
99
99
  export {
100
100
  type AllOptions,
101
101
  type Code,
@@ -181,7 +181,11 @@ export {
181
181
  STD_EXT,
182
182
  formulaEvaluators,
183
183
  formulaKinds,
184
+ formulaKnown,
184
185
  formulaMachine,
185
186
  isClosure,
186
187
  jsTruthy,
187
188
  } from "./formula.ts";
189
+ export { type Definitions, type ReferenceOptions, formulaDefinitions, reference } from "./reference.ts";
190
+ export { type AgreeOptions, type Agreement, type Tier, type TierResult, agree, sameValue } from "./agree.ts";
191
+ export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
package/src/parts.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
2
- import type { Shape } from "./world.ts";
1
+ import { type Envelope, type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
2
+ import { type Kind, type Shape, type World, lookupKind } 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. */
5
5
  export interface PartSlot {
@@ -56,6 +56,11 @@ function hasSlots(template: Template): boolean {
56
56
  return template.t !== "data";
57
57
  }
58
58
 
59
+ /** This function tells if a shape holds only data: no value of the shape can hold a part. */
60
+ function isData(shape: Shape): boolean {
61
+ return shape.shape === "none" || shape.shape === "text" || shape.shape === "number" || shape.shape === "one";
62
+ }
63
+
59
64
  /** This function gives the reason why a value is not data of a shape, or `undefined` when it is. */
60
65
  function dataFault(spec: Extract<Shape, { readonly shape: "text" | "number" | "one" }>, item: Json): string | undefined {
61
66
  if (spec.shape === "text") return typeof item === "string" ? undefined : "The value must be a text.";
@@ -127,7 +132,9 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
127
132
  if (twice !== -1) throw new Bad("The items of the list must be different.", [...path, twice]);
128
133
  }
129
134
  const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
130
- return { t: "list", items: list.map((child, index) => walk(each, child, [...path, index])) };
135
+ const items = list.map((child, index) => walk(each, child, [...path, index]));
136
+ // A list of data is data, also when it is empty. Only a list that can hold parts gives null in the data.
137
+ return isData(each) ? { t: "data", value: item } : { t: "list", items };
131
138
  }
132
139
  if (!isRecord(item)) throw new Bad("The value must be a record.", path);
133
140
  if (spec.shape === "map") {
@@ -181,6 +188,55 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
181
188
  return { ok: true, parts, data: strip(template) ?? null, fill: (handles) => fillWith(template, handles) };
182
189
  }
183
190
 
191
+ /**
192
+ * The kind and the parts of one document: what `partsOf` gives. `ok` is `false` only when the value is not a
193
+ * document: it has no envelope, and its position declares no type. A value without the shape of its kind gives a
194
+ * failed `split`.
195
+ */
196
+ export type DocParts =
197
+ | {
198
+ readonly ok: true;
199
+ /** The document. A bare value gets the type that its position declares. */
200
+ readonly doc: Envelope;
201
+ /** `true` when the type came from the position and not from an envelope. */
202
+ readonly declared: boolean;
203
+ /** The kind, or `undefined` when the world has no kind for the type. Then the value is data. */
204
+ readonly kind: Kind | undefined;
205
+ /** The keys of the kind after each alias, or `undefined` without a kind. */
206
+ readonly typePath: readonly Key[] | undefined;
207
+ /**
208
+ * The split of the value by the shape of the kind. The reason of a failed split is the reason of the `#VALUE!`
209
+ * of an evaluation: it names the kind. Its `at` gives the keys from the value to the problem.
210
+ */
211
+ readonly split: Split;
212
+ }
213
+ | { readonly ok: false; readonly code: "#PART?"; readonly reason: string };
214
+
215
+ /**
216
+ * This function reads one document by the contracts of the world. It finds the kind of the type. A bare value gets
217
+ * the type that its position declares. Then the function splits the value by the shape of the kind. The plan, the
218
+ * reference interpreter and the static kit read each document with this function. Thus they see the same parts.
219
+ *
220
+ * @param world - The world of the kinds.
221
+ * @param value - The document, or a bare value.
222
+ * @param declaredType - The type that the position of a bare value declares.
223
+ * @returns The document, its kind and its parts, or the reason why the value is not a document.
224
+ */
225
+ export function partsOf(world: World, value: unknown, declaredType?: string): DocParts {
226
+ const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
227
+ if (envelope === undefined && declaredType === undefined) return { ok: false, code: "#PART?", reason: "The value has no envelope, and its position declares no type." };
228
+ const doc: Envelope = envelope ?? { type: declaredType as string, value: value as Json };
229
+ const found = lookupKind(world, doc.type);
230
+ const kind = found.ok ? found.kind : undefined;
231
+ const split = splitParts(kind?.parts, doc.value);
232
+ const typePath = found.ok ? found.path : undefined;
233
+ if (split.ok) return { ok: true, doc, declared: envelope === undefined, kind, typePath, split };
234
+ // The reason names the place in the value and the kind, as each reader of the document gives it.
235
+ const where = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
236
+ const reason = `${split.reason}${where} The kind is ${typeName(doc.type)}.`;
237
+ return { ok: true, doc, declared: envelope === undefined, kind, typePath, split: { ok: false, reason, at: split.at } };
238
+ }
239
+
184
240
  /**
185
241
  * This function tells if the path of a part matches a pattern of a scope rule. The key `"*"` of a pattern matches
186
242
  * each key.