@mark1russell7/coeval 0.1.0 → 0.2.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 +41 -0
- package/build/index.d.ts +832 -148
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1898 -376
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +25 -21
- package/src/deref.ts +165 -54
- package/src/doc.ts +86 -17
- package/src/errors.ts +141 -0
- package/src/evaluate.ts +797 -281
- package/src/formula.ts +409 -0
- package/src/index.ts +72 -7
- package/src/parts.ts +140 -27
- package/src/static.ts +364 -0
- package/src/stdlib.ts +60 -33
- package/src/world.ts +96 -22
package/src/parts.ts
CHANGED
|
@@ -1,43 +1,156 @@
|
|
|
1
|
-
import { type Json, type
|
|
2
|
-
import type {
|
|
1
|
+
import { type Json, type Key, isEnvelope, isRecord } from "./doc.ts";
|
|
2
|
+
import type { Shape } from "./world.ts";
|
|
3
3
|
|
|
4
|
-
/** One part of a value: its
|
|
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 {
|
|
6
|
+
/** The keys from the value of the document to the part. */
|
|
7
|
+
readonly path: readonly Key[];
|
|
8
|
+
/** The path as one text, with a dot between the keys, for example `bind.x`. A part of a list has its index. */
|
|
6
9
|
readonly key: string;
|
|
7
10
|
readonly item: Json;
|
|
8
11
|
readonly of?: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
type Template =
|
|
15
|
+
| { readonly t: "data"; readonly value: unknown }
|
|
16
|
+
| { readonly t: "slot"; readonly index: number }
|
|
17
|
+
| { readonly t: "list"; readonly items: readonly Template[] }
|
|
18
|
+
| { readonly t: "record"; readonly fields: readonly (readonly [string, Template])[] };
|
|
19
|
+
|
|
20
|
+
/** The value of a document, split into its parts, by the shape that its kind declares. */
|
|
12
21
|
export type Split =
|
|
13
|
-
| {
|
|
14
|
-
|
|
22
|
+
| {
|
|
23
|
+
readonly ok: true;
|
|
24
|
+
/** The parts, in the order of the value. */
|
|
25
|
+
readonly parts: readonly PartSlot[];
|
|
26
|
+
/** The value without the parts: a field that holds a part is not in it, and a list of parts gives `null`. */
|
|
27
|
+
readonly data: Json;
|
|
28
|
+
/**
|
|
29
|
+
* This function gives the value with a handle in the place of each part.
|
|
30
|
+
*
|
|
31
|
+
* @param handles - One handle for each part, in the order of `parts`.
|
|
32
|
+
* @returns The value with the handles.
|
|
33
|
+
*/
|
|
34
|
+
fill<P>(handles: readonly P[]): unknown;
|
|
35
|
+
}
|
|
36
|
+
| { readonly ok: false; readonly reason: string; readonly at: readonly Key[] };
|
|
37
|
+
|
|
38
|
+
class Bad {
|
|
39
|
+
constructor(
|
|
40
|
+
readonly reason: string,
|
|
41
|
+
readonly at: readonly Key[],
|
|
42
|
+
) {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setOwn(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
46
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasSlots(template: Template): boolean {
|
|
50
|
+
return template.t !== "data";
|
|
51
|
+
}
|
|
15
52
|
|
|
16
53
|
/**
|
|
17
|
-
* This function splits a value into its
|
|
18
|
-
*
|
|
54
|
+
* This function splits a value into its parts, by the shape that its kind declares. The evaluator, the view and
|
|
55
|
+
* the static kit use the same split, thus each one sees the same parts.
|
|
19
56
|
*
|
|
20
|
-
* @param
|
|
57
|
+
* @param shape - The shape of the value. The default is data.
|
|
21
58
|
* @param value - The value of the document.
|
|
22
|
-
* @returns The
|
|
59
|
+
* @returns The parts and a way to fill their places, or the reason why the value does not have the shape.
|
|
23
60
|
*/
|
|
24
|
-
export function splitParts(
|
|
25
|
-
const of = (slot: { key: string; item: Json }, declared: string | undefined): PartSlot =>
|
|
26
|
-
declared === undefined ? slot : { ...slot, of: declared };
|
|
27
|
-
if (spec === undefined || spec.shape === "none") return { ok: true, data: value, parts: [] };
|
|
28
|
-
if (spec.shape === "list") {
|
|
29
|
-
if (!Array.isArray(value)) return { ok: false, reason: "The value must be a list." };
|
|
30
|
-
const items: readonly Json[] = value;
|
|
31
|
-
return { ok: true, data: null, parts: items.map((item, index) => of({ key: String(index), item }, spec.of)) };
|
|
32
|
-
}
|
|
33
|
-
if (!isRecord(value)) return { ok: false, reason: "The value must be a record." };
|
|
34
|
-
const record: JsonRecord = value;
|
|
35
|
-
const rest: Record<string, Json> = {};
|
|
36
|
-
for (const [key, item] of Object.entries(record)) if (!spec.fields.includes(key)) rest[key] = item;
|
|
61
|
+
export function splitParts(shape: Shape | undefined, value: Json): Split {
|
|
37
62
|
const parts: PartSlot[] = [];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
const slot = (item: Json, path: readonly Key[], of: string | undefined): Template => {
|
|
64
|
+
parts.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }) });
|
|
65
|
+
return { t: "slot", index: parts.length - 1 };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const deep = (item: Json, path: readonly Key[], of: string | undefined): Template => {
|
|
69
|
+
if (isEnvelope(item)) return slot(item, path, of);
|
|
70
|
+
if (Array.isArray(item)) {
|
|
71
|
+
const items = (item as readonly Json[]).map((child, index) => deep(child, [...path, index], of));
|
|
72
|
+
return items.some(hasSlots) ? { t: "list", items } : { t: "data", value: item };
|
|
73
|
+
}
|
|
74
|
+
if (isRecord(item)) {
|
|
75
|
+
const fields = Object.keys(item).map((key) => [key, deep(item[key] as Json, [...path, key], of)] as const);
|
|
76
|
+
return fields.some(([, child]) => hasSlots(child)) ? { t: "record", fields } : { t: "data", value: item };
|
|
77
|
+
}
|
|
78
|
+
return { t: "data", value: item };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const walk = (spec: Shape | undefined, item: Json, path: readonly Key[]): Template => {
|
|
82
|
+
if (spec === undefined || spec.shape === "none") return { t: "data", value: item };
|
|
83
|
+
if (spec.shape === "term") return slot(item, path, spec.of);
|
|
84
|
+
if (spec.shape === "deep") return deep(item, path, spec.of);
|
|
85
|
+
if (spec.shape === "list") {
|
|
86
|
+
if (!Array.isArray(item)) throw new Bad("The value must be a list.", path);
|
|
87
|
+
const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
|
|
88
|
+
return { t: "list", items: (item as readonly Json[]).map((child, index) => walk(each, child, [...path, index])) };
|
|
89
|
+
}
|
|
90
|
+
if (!isRecord(item)) throw new Bad("The value must be a record.", path);
|
|
91
|
+
if (spec.shape === "map") {
|
|
92
|
+
const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
|
|
93
|
+
return { t: "record", fields: Object.keys(item).map((key) => [key, walk(each, item[key] as Json, [...path, key])] as const) };
|
|
94
|
+
}
|
|
95
|
+
for (const name of spec.required ?? []) {
|
|
96
|
+
if (!Object.hasOwn(item, name)) throw new Bad(`The field ${name} is missing.`, path);
|
|
97
|
+
}
|
|
98
|
+
const named: Readonly<Record<string, Shape>> = Array.isArray(spec.fields)
|
|
99
|
+
? Object.fromEntries((spec.fields as readonly string[]).map((name) => [name, { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) } as Shape]))
|
|
100
|
+
: (spec.fields as Readonly<Record<string, Shape>>);
|
|
101
|
+
const fields: (readonly [string, Template])[] = [];
|
|
102
|
+
for (const key of Object.keys(item)) {
|
|
103
|
+
const fieldShape = Object.hasOwn(named, key) ? named[key] : spec.rest;
|
|
104
|
+
fields.push([key, walk(fieldShape, item[key] as Json, [...path, key])]);
|
|
105
|
+
}
|
|
106
|
+
return { t: "record", fields };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let template: Template;
|
|
110
|
+
try {
|
|
111
|
+
template = walk(shape, value, []);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof Bad) return { ok: false, reason: error.reason, at: error.at };
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const strip = (node: Template): Json | undefined => {
|
|
118
|
+
if (node.t === "data") return node.value as Json;
|
|
119
|
+
if (node.t === "slot") return undefined;
|
|
120
|
+
if (node.t === "list") return node.items.every((child) => child.t === "slot") ? null : (node.items.map((child) => strip(child) ?? null) as Json);
|
|
121
|
+
const out: Record<string, Json> = {};
|
|
122
|
+
for (const [key, child] of node.fields) {
|
|
123
|
+
const kept = strip(child);
|
|
124
|
+
if (kept !== undefined) setOwn(out, key, kept);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const fillWith = <P>(node: Template, handles: readonly P[]): unknown => {
|
|
130
|
+
if (node.t === "data") return node.value;
|
|
131
|
+
if (node.t === "slot") return handles[node.index];
|
|
132
|
+
if (node.t === "list") return node.items.map((child) => fillWith(child, handles));
|
|
133
|
+
const out: Record<string, unknown> = {};
|
|
134
|
+
for (const [key, child] of node.fields) setOwn(out, key, fillWith(child, handles));
|
|
135
|
+
return out;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return { ok: true, parts, data: strip(template) ?? null, fill: (handles) => fillWith(template, handles) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* This function tells if the path of a part matches a pattern of a scope rule. The key `"*"` of a pattern matches
|
|
143
|
+
* each key.
|
|
144
|
+
*
|
|
145
|
+
* @param path - The path of the part.
|
|
146
|
+
* @param pattern - The pattern.
|
|
147
|
+
* @returns `true` when the path matches.
|
|
148
|
+
*/
|
|
149
|
+
export function pathMatches(path: readonly Key[], pattern: readonly Key[]): boolean {
|
|
150
|
+
if (path.length !== pattern.length) return false;
|
|
151
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
152
|
+
const want = pattern[index];
|
|
153
|
+
if (want !== "*" && String(want) !== String(path[index])) return false;
|
|
41
154
|
}
|
|
42
|
-
return
|
|
155
|
+
return true;
|
|
43
156
|
}
|
package/src/static.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
|
+
import { type Kind, type NamesSource, type ScopeRule, type World, lookupKind } from "./world.ts";
|
|
3
|
+
import { ask } from "./ladder.ts";
|
|
4
|
+
import { type PartSlot, pathMatches, splitParts } from "./parts.ts";
|
|
5
|
+
import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
|
|
6
|
+
import type { ErrorCode } from "./errors.ts";
|
|
7
|
+
import { type Machine, permits } from "./evaluate.ts";
|
|
8
|
+
|
|
9
|
+
/** One document that a walk visits. */
|
|
10
|
+
export interface DocVisit {
|
|
11
|
+
/** The keys from the root document to this document, through the parts. It is the place of an error. */
|
|
12
|
+
readonly place: readonly Key[];
|
|
13
|
+
/** The keys from the root JSON value to this document. A change at this path changes this document. */
|
|
14
|
+
readonly at: readonly Key[];
|
|
15
|
+
/** The document. A bare value has the type that its position declares. */
|
|
16
|
+
readonly doc: Envelope;
|
|
17
|
+
/** `true` when the type came from the position and not from an envelope. */
|
|
18
|
+
readonly declared: boolean;
|
|
19
|
+
/** The kind of the document, when the world has it. */
|
|
20
|
+
readonly kind?: Kind;
|
|
21
|
+
/** The keys of the kind after each alias. */
|
|
22
|
+
readonly typePath?: readonly Key[];
|
|
23
|
+
/** The number of documents around this document. The root has 0. */
|
|
24
|
+
readonly depth: number;
|
|
25
|
+
/** The names that the kinds around this document bind for it. */
|
|
26
|
+
readonly names: ReadonlySet<string>;
|
|
27
|
+
/** The names that a sequence around this document binds only for the parts after it. */
|
|
28
|
+
readonly later: ReadonlySet<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A position that a walk cannot read as a document. */
|
|
32
|
+
export interface WalkProblem {
|
|
33
|
+
readonly place: readonly Key[];
|
|
34
|
+
readonly at: readonly Key[];
|
|
35
|
+
readonly code: "#PART?" | "#VALUE!";
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The options of a walk. */
|
|
40
|
+
export interface WalkOptions {
|
|
41
|
+
/** The type of a bare root value. */
|
|
42
|
+
readonly declaredType?: string;
|
|
43
|
+
/** The names that the host binds at the root. */
|
|
44
|
+
readonly names?: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* This function gets each position that is not a document.
|
|
47
|
+
*
|
|
48
|
+
* @param problem - The position and the reason.
|
|
49
|
+
*/
|
|
50
|
+
readonly problem?: (problem: WalkProblem) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function valueAt(value: unknown, path: readonly Key[]): unknown {
|
|
54
|
+
let here: unknown = value;
|
|
55
|
+
for (const key of path) {
|
|
56
|
+
if (Array.isArray(here)) here = here[Number(key)];
|
|
57
|
+
else if (isRecord(here) && Object.hasOwn(here, String(key))) here = here[String(key)];
|
|
58
|
+
else return undefined;
|
|
59
|
+
}
|
|
60
|
+
return here;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function headerName(item: Json): string | undefined {
|
|
64
|
+
return isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot[]): readonly string[] {
|
|
68
|
+
if ("fixed" in source) return source.fixed;
|
|
69
|
+
if ("keysAt" in source) {
|
|
70
|
+
const at = valueAt(value, source.keysAt);
|
|
71
|
+
return isRecord(at) ? Object.keys(at) : [];
|
|
72
|
+
}
|
|
73
|
+
if ("listAt" in source) {
|
|
74
|
+
const at = valueAt(value, source.listAt);
|
|
75
|
+
return Array.isArray(at) ? (at as readonly unknown[]).filter((item): item is string => typeof item === "string") : [];
|
|
76
|
+
}
|
|
77
|
+
if ("textAt" in source) {
|
|
78
|
+
const at = valueAt(value, source.textAt);
|
|
79
|
+
if (typeof at === "string") return [at];
|
|
80
|
+
return source.otherwise === undefined ? [] : [source.otherwise];
|
|
81
|
+
}
|
|
82
|
+
return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The names that the scope rules of a kind give each part, and the names that only later parts see. */
|
|
86
|
+
function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
|
|
87
|
+
const sees: string[][] = slots.map(() => []);
|
|
88
|
+
const later: string[][] = slots.map(() => []);
|
|
89
|
+
for (const rule of rules ?? []) {
|
|
90
|
+
const covered = slots.map((slot, index) => ({ slot, index })).filter(({ slot }) => rule.in.some((pattern) => pathMatches(slot.path, pattern)));
|
|
91
|
+
if (rule.sequential !== true) {
|
|
92
|
+
const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
|
|
93
|
+
for (const { index } of covered) sees[index]?.push(...names);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if ("headers" in rule.names) {
|
|
97
|
+
covered.forEach(({ index }, position) => {
|
|
98
|
+
const before = covered.slice(0, position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
99
|
+
const after = covered.slice(position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
100
|
+
sees[index]?.push(...before);
|
|
101
|
+
later[index]?.push(...after);
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
|
|
106
|
+
covered.forEach(({ index }, position) => {
|
|
107
|
+
if (position > 0) sees[index]?.push(...names);
|
|
108
|
+
else later[index]?.push(...names);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { sees, later };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* This function visits each document of a document: the root and each part at each depth, by the contracts of the
|
|
116
|
+
* 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
|
|
117
|
+
* depth, thus a deep document cannot overflow it.
|
|
118
|
+
*
|
|
119
|
+
* @param world - The world of the kinds.
|
|
120
|
+
* @param doc - The document, as JSON.
|
|
121
|
+
* @param visit - The function that gets each document. It gives `"skip"` to not visit the parts of a document.
|
|
122
|
+
* @param options - The type of a bare root value, the names of the host, and a function for the problems.
|
|
123
|
+
*/
|
|
124
|
+
export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => void | "skip", options: WalkOptions = {}): void {
|
|
125
|
+
interface Item {
|
|
126
|
+
readonly value: unknown;
|
|
127
|
+
readonly place: readonly Key[];
|
|
128
|
+
readonly at: readonly Key[];
|
|
129
|
+
readonly declared: string | undefined;
|
|
130
|
+
readonly depth: number;
|
|
131
|
+
readonly names: ReadonlySet<string>;
|
|
132
|
+
readonly later: ReadonlySet<string>;
|
|
133
|
+
}
|
|
134
|
+
const stack: Item[] = [
|
|
135
|
+
{ value: doc, place: [], at: [], declared: options.declaredType, depth: 0, names: new Set(options.names ?? []), later: new Set() },
|
|
136
|
+
];
|
|
137
|
+
while (stack.length > 0) {
|
|
138
|
+
const item = stack.pop() as Item;
|
|
139
|
+
const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
|
|
140
|
+
if (envelope === undefined && item.declared === undefined) {
|
|
141
|
+
options.problem?.({ place: item.place, at: item.at, code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
|
|
145
|
+
const found = lookupKind(world, doc.type);
|
|
146
|
+
const kind = found.ok ? found.kind : undefined;
|
|
147
|
+
const answer = visit({
|
|
148
|
+
place: item.place,
|
|
149
|
+
at: item.at,
|
|
150
|
+
doc,
|
|
151
|
+
declared: envelope === undefined,
|
|
152
|
+
...(kind === undefined ? {} : { kind, typePath: found.ok ? found.path : [] }),
|
|
153
|
+
depth: item.depth,
|
|
154
|
+
names: item.names,
|
|
155
|
+
later: item.later,
|
|
156
|
+
});
|
|
157
|
+
if (answer === "skip" || kind === undefined) continue;
|
|
158
|
+
const split = splitParts(kind.parts, doc.value);
|
|
159
|
+
if (!split.ok) {
|
|
160
|
+
const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
|
|
161
|
+
options.problem?.({ place: item.place, at: item.at, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
|
|
165
|
+
const base = envelope === undefined ? item.at : [...item.at, "value"];
|
|
166
|
+
for (let index = split.parts.length - 1; index >= 0; index -= 1) {
|
|
167
|
+
const slot = split.parts[index] as PartSlot;
|
|
168
|
+
const extra = sees[index] ?? [];
|
|
169
|
+
const hidden = later[index] ?? [];
|
|
170
|
+
stack.push({
|
|
171
|
+
value: slot.item,
|
|
172
|
+
place: [...item.place, ...slot.path],
|
|
173
|
+
at: [...base, ...slot.path],
|
|
174
|
+
declared: slot.of,
|
|
175
|
+
depth: item.depth + 1,
|
|
176
|
+
names: extra.length === 0 ? item.names : new Set([...item.names, ...extra]),
|
|
177
|
+
later: hidden.length === 0 ? item.later : new Set([...item.later, ...hidden]),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** One reference of a document. */
|
|
184
|
+
export interface RefUse {
|
|
185
|
+
/** The place of the reference document, through the parts. */
|
|
186
|
+
readonly place: readonly Key[];
|
|
187
|
+
/** The keys from the root JSON value to the reference document. */
|
|
188
|
+
readonly at: readonly Key[];
|
|
189
|
+
/** The path of the reference: its root and its steps. */
|
|
190
|
+
readonly path: readonly Json[];
|
|
191
|
+
readonly root: string;
|
|
192
|
+
/** `false` when a kind around the reference binds its first name. A reference to another root is free. */
|
|
193
|
+
readonly free: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* This function finds each reference of a document: each document of the type `ref` that a walk reaches. A
|
|
198
|
+
* reference to a name that a kind around it binds is not free. The others are what the document reads from
|
|
199
|
+
* outside: its dependencies.
|
|
200
|
+
*
|
|
201
|
+
* @param world - The world of the kinds.
|
|
202
|
+
* @param doc - The document, as JSON.
|
|
203
|
+
* @param options - The type of a bare root value. The names of the host do not change the result.
|
|
204
|
+
* @returns The references, in the order of the document.
|
|
205
|
+
*/
|
|
206
|
+
export function refsOf(world: World, doc: unknown, options: WalkOptions = {}): readonly RefUse[] {
|
|
207
|
+
const out: RefUse[] = [];
|
|
208
|
+
// The walk gets no names of the host: a name that the host binds is a dependency, thus its reference is free.
|
|
209
|
+
const { names: _host, ...rest } = options;
|
|
210
|
+
walk(
|
|
211
|
+
world,
|
|
212
|
+
doc,
|
|
213
|
+
(v) => {
|
|
214
|
+
if (!isRef(v.doc)) return undefined;
|
|
215
|
+
const path = v.doc.value as readonly Json[];
|
|
216
|
+
const root = typeof path[0] === "string" ? path[0] : "";
|
|
217
|
+
const first = path[1];
|
|
218
|
+
const bound = root === "names" && isKey(first) && v.names.has(String(first));
|
|
219
|
+
out.push({ place: v.place, at: v.at, path, root, free: !bound });
|
|
220
|
+
return "skip";
|
|
221
|
+
},
|
|
222
|
+
rest,
|
|
223
|
+
);
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setAt(value: unknown, path: readonly Key[], next: unknown): unknown {
|
|
228
|
+
if (path.length === 0) return next;
|
|
229
|
+
const [key, ...rest] = path as [Key, ...Key[]];
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
const copy = [...value];
|
|
232
|
+
copy[Number(key)] = setAt(value[Number(key)], rest, next);
|
|
233
|
+
return copy;
|
|
234
|
+
}
|
|
235
|
+
const record = (isRecord(value) ? value : {}) as Readonly<Record<string, unknown>>;
|
|
236
|
+
const copy: Record<string, unknown> = {};
|
|
237
|
+
for (const name of Object.keys(record)) Object.defineProperty(copy, name, { value: record[name], enumerable: true, writable: true, configurable: true });
|
|
238
|
+
Object.defineProperty(copy, String(key), { value: setAt(record[String(key)], rest, next), enumerable: true, writable: true, configurable: true });
|
|
239
|
+
return copy;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* This function changes the free references of a document. The document is not changed: the function gives a new
|
|
244
|
+
* document that shares each part without a change.
|
|
245
|
+
*
|
|
246
|
+
* @param world - The world of the kinds.
|
|
247
|
+
* @param doc - The document, as JSON.
|
|
248
|
+
* @param change - The function that gives the new document of a reference, or `undefined` to keep it.
|
|
249
|
+
* @param options - The type of a bare root value, and the names that the host binds at the root.
|
|
250
|
+
* @returns The new document.
|
|
251
|
+
*/
|
|
252
|
+
export function mapRefs(world: World, doc: unknown, change: (use: RefUse) => unknown, options: WalkOptions = {}): unknown {
|
|
253
|
+
let out = doc;
|
|
254
|
+
for (const use of refsOf(world, doc, options)) {
|
|
255
|
+
if (!use.free) continue;
|
|
256
|
+
const next = change(use);
|
|
257
|
+
if (next !== undefined) out = setAt(out, use.at, next);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A problem that a check finds before a document runs. */
|
|
263
|
+
export interface Issue {
|
|
264
|
+
readonly code: ErrorCode;
|
|
265
|
+
/** The keys from the root document to the document with the problem. */
|
|
266
|
+
readonly place: readonly Key[];
|
|
267
|
+
readonly reason: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The options of a check. */
|
|
271
|
+
export interface CheckOptions {
|
|
272
|
+
/** The type of a bare root value. */
|
|
273
|
+
readonly declaredType?: string;
|
|
274
|
+
/** The names that the host binds at the root. `"any"` does not check the names. The default is no names. */
|
|
275
|
+
readonly names?: readonly string[] | "any";
|
|
276
|
+
/** The capabilities at the root. Without them, the check does not check the effects. */
|
|
277
|
+
readonly caps?: readonly Cap[];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* This function checks a document before an evaluation, by the contracts of its kinds. It finds the problems
|
|
282
|
+
* that an evaluation finds when it reaches them:
|
|
283
|
+
*
|
|
284
|
+
* - `#KIND?`: a kind that the world does not have.
|
|
285
|
+
* - `#NOEVAL`: no evaluator.
|
|
286
|
+
* - `#VALUE!`: a value without the shape of its kind.
|
|
287
|
+
* - `#PART?`: a value without a type.
|
|
288
|
+
* - `#DEPTH!`: too much nesting.
|
|
289
|
+
* - `#REF!` and `#STEP?`: a bad path.
|
|
290
|
+
* - `#NAME?`: a name that nothing binds.
|
|
291
|
+
* - `#CAP!`: an effect that the capabilities do not permit.
|
|
292
|
+
*
|
|
293
|
+
* @param machine - The world and the code of the evaluators.
|
|
294
|
+
* @param doc - The document, as JSON.
|
|
295
|
+
* @param options - The type of a bare root value, the names of the host and the capabilities.
|
|
296
|
+
* @returns The problems, in the order of the document.
|
|
297
|
+
*/
|
|
298
|
+
export function check(machine: Machine, doc: unknown, options: CheckOptions = {}): readonly Issue[] {
|
|
299
|
+
const issues: Issue[] = [];
|
|
300
|
+
const maxDepth = machine.maxDepth ?? 1000;
|
|
301
|
+
const anyName = options.names === "any";
|
|
302
|
+
const roots: readonly string[] = machine.roots ?? ROOTS;
|
|
303
|
+
const has = (table: Readonly<Record<string, unknown>> | undefined, name: string): boolean => table !== undefined && Object.hasOwn(table, name);
|
|
304
|
+
walk(
|
|
305
|
+
machine.world,
|
|
306
|
+
doc,
|
|
307
|
+
(v) => {
|
|
308
|
+
if (v.depth > maxDepth) {
|
|
309
|
+
issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.` });
|
|
310
|
+
return "skip";
|
|
311
|
+
}
|
|
312
|
+
const header = v.declared ? {} : v.doc;
|
|
313
|
+
const answer = ask(machine.world, header, "eval", v.declared ? { declaredType: typeName(v.doc.type) } : {});
|
|
314
|
+
if (answer.error === "unknown-kind") {
|
|
315
|
+
issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.` });
|
|
316
|
+
return "skip";
|
|
317
|
+
}
|
|
318
|
+
const name = answer.evaluator;
|
|
319
|
+
if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
|
|
320
|
+
issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.` });
|
|
321
|
+
}
|
|
322
|
+
if (options.caps !== undefined && v.kind?.effects !== undefined) {
|
|
323
|
+
const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
|
|
324
|
+
for (const kind of kinds) {
|
|
325
|
+
if (!permits(options.caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: `The capabilities do not permit the effect ${kind.join(".")}.` });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!isRef(v.doc)) return undefined;
|
|
329
|
+
const path = v.doc.value as readonly Json[];
|
|
330
|
+
const root = path[0];
|
|
331
|
+
if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
|
|
332
|
+
issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.` });
|
|
333
|
+
return "skip";
|
|
334
|
+
}
|
|
335
|
+
if (!roots.includes(root)) {
|
|
336
|
+
issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.` });
|
|
337
|
+
}
|
|
338
|
+
const first = path[1];
|
|
339
|
+
if (root === ("names" as Root) && !anyName && isKey(first) && !v.names.has(String(first))) {
|
|
340
|
+
const later = v.later.has(String(first));
|
|
341
|
+
issues.push({
|
|
342
|
+
code: "#NAME?",
|
|
343
|
+
place: v.place,
|
|
344
|
+
reason: later ? `The name ${String(first)} is bound by a later step.` : `Nothing binds the name ${String(first)}.`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
path.slice(1).forEach((step) => {
|
|
348
|
+
if (isKey(step)) return;
|
|
349
|
+
const found = isEnvelope(step) ? lookupKind(machine.world, step.type) : undefined;
|
|
350
|
+
const walker = found?.ok === true && Object.hasOwn(found.kind.verbs, "step") ? found.kind.verbs["step"] : undefined;
|
|
351
|
+
if (walker === undefined || !has(machine.walkers, walker)) {
|
|
352
|
+
issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step." });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return "skip";
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
359
|
+
...(Array.isArray(options.names) ? { names: options.names } : {}),
|
|
360
|
+
problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason }),
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
return issues;
|
|
364
|
+
}
|