@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/README.md +4 -4
- package/build/index.d.ts +56 -9
- package/build/index.d.ts.map +1 -1
- package/build/index.js +306 -98
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/deref.ts +10 -6
- package/src/evaluate.ts +66 -23
- package/src/formula.ts +91 -19
- package/src/parts.ts +42 -5
- package/src/static.ts +144 -42
- package/src/world.ts +15 -3
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:
|
|
137
|
-
readonly at:
|
|
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
|
|
144
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const answer = visit(
|
|
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
|
-
|
|
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:
|
|
186
|
-
at:
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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}`}`;
|