@mark1russell7/coeval 0.3.3 → 0.4.1
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 +5 -5
- package/build/index.d.ts +330 -224
- package/build/index.d.ts.map +1 -1
- package/build/index.js +361 -91
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/deref.ts +10 -6
- package/src/evaluate.ts +80 -4
- package/src/formula.ts +91 -19
- package/src/index.ts +3 -0
- package/src/parts.ts +42 -5
- package/src/static.ts +182 -48
- package/src/stdlib.ts +6 -2
- package/src/world.ts +39 -3
package/src/static.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ask } from "./ladder.ts";
|
|
|
4
4
|
import { type PartSlot, pathMatches, splitParts } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
|
|
6
6
|
import type { ErrorCode } from "./errors.ts";
|
|
7
|
-
import { type Machine, permits } from "./evaluate.ts";
|
|
7
|
+
import { type Machine, partCaps, permits } from "./evaluate.ts";
|
|
8
8
|
|
|
9
9
|
/** One document that a walk visits. */
|
|
10
10
|
export interface DocVisit {
|
|
@@ -12,25 +12,38 @@ 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;
|
|
35
|
+
/**
|
|
36
|
+
* The capabilities of the document: the capabilities of the walk, narrowed by the capability rules of the kinds
|
|
37
|
+
* around it (`Kind.caps`). A walk without capabilities gives none here.
|
|
38
|
+
*/
|
|
39
|
+
readonly caps?: readonly Cap[] | undefined;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
/** A position that a walk cannot read as a document. */
|
|
32
43
|
export interface WalkProblem {
|
|
44
|
+
/** The keys from the root document to the document with the problem. */
|
|
33
45
|
readonly place: readonly Key[];
|
|
46
|
+
/** The keys from the root JSON value to the value with the problem. */
|
|
34
47
|
readonly at: readonly Key[];
|
|
35
48
|
readonly code: "#PART?" | "#VALUE!";
|
|
36
49
|
readonly reason: string;
|
|
@@ -42,6 +55,8 @@ export interface WalkOptions {
|
|
|
42
55
|
readonly declaredType?: string;
|
|
43
56
|
/** The names that the host binds at the root. */
|
|
44
57
|
readonly names?: readonly string[];
|
|
58
|
+
/** The capabilities at the root. With them, each visit gets the capabilities of its document. */
|
|
59
|
+
readonly caps?: readonly Cap[];
|
|
45
60
|
/**
|
|
46
61
|
* This function gets each position that is not a document.
|
|
47
62
|
*
|
|
@@ -120,74 +135,155 @@ function withoutNames(options: WalkOptions): WalkOptions {
|
|
|
120
135
|
return rest;
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
/** A path that shares its start with other paths: the keys of one step, and the path before them. */
|
|
139
|
+
interface Trail {
|
|
140
|
+
readonly up: Trail | undefined;
|
|
141
|
+
readonly keys: readonly Key[];
|
|
142
|
+
full?: readonly Key[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** This function gives the keys of a trail, from its start. It keeps the result in the trail. */
|
|
146
|
+
function keysOf(trail: Trail | undefined): readonly Key[] {
|
|
147
|
+
if (trail === undefined) return [];
|
|
148
|
+
if (trail.full !== undefined) return trail.full;
|
|
149
|
+
const steps: Trail[] = [];
|
|
150
|
+
let here: Trail | undefined = trail;
|
|
151
|
+
while (here !== undefined && here.full === undefined) {
|
|
152
|
+
steps.push(here);
|
|
153
|
+
here = here.up;
|
|
154
|
+
}
|
|
155
|
+
const keys: Key[] = here?.full === undefined ? [] : [...here.full];
|
|
156
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) for (const key of (steps[index] as Trail).keys) keys.push(key);
|
|
157
|
+
trail.full = keys;
|
|
158
|
+
return keys;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A visit with its paths as trails. It makes the keys of a path only when a reader asks for them. */
|
|
162
|
+
class Visit implements DocVisit {
|
|
163
|
+
readonly #place: Trail | undefined;
|
|
164
|
+
readonly #at: Trail | undefined;
|
|
165
|
+
|
|
166
|
+
constructor(
|
|
167
|
+
place: Trail | undefined,
|
|
168
|
+
at: Trail | undefined,
|
|
169
|
+
readonly doc: Envelope,
|
|
170
|
+
readonly declared: boolean,
|
|
171
|
+
readonly depth: number,
|
|
172
|
+
readonly names: ReadonlySet<string>,
|
|
173
|
+
readonly later: ReadonlySet<string>,
|
|
174
|
+
readonly slot: readonly Key[],
|
|
175
|
+
readonly parent: DocVisit | undefined,
|
|
176
|
+
readonly kind: Kind | undefined,
|
|
177
|
+
readonly typePath: readonly Key[] | undefined,
|
|
178
|
+
readonly address: true | undefined,
|
|
179
|
+
readonly caps: readonly Cap[] | undefined,
|
|
180
|
+
) {
|
|
181
|
+
this.#place = place;
|
|
182
|
+
this.#at = at;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get place(): readonly Key[] {
|
|
186
|
+
return keysOf(this.#place);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get at(): readonly Key[] {
|
|
190
|
+
return keysOf(this.#at);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
123
194
|
/**
|
|
124
195
|
* This function visits each document of a document: the root and each part at each depth, by the contracts of the
|
|
125
|
-
* kinds. A part of a kind that the world does not have is not visited.
|
|
126
|
-
*
|
|
196
|
+
* kinds. A part of a kind that the world does not have is not visited. With capabilities in the options, the walk
|
|
197
|
+
* narrows them for each part by the rules of its kind, as a run does. The walk uses no stack of the host for the
|
|
198
|
+
* depth, thus a deep document cannot overflow it. The time and the memory of a walk are linear in the size of the
|
|
199
|
+
* document. A visit makes the keys of its place and of its path only when a reader asks for them.
|
|
127
200
|
*
|
|
128
201
|
* @param world - The world of the kinds.
|
|
129
202
|
* @param doc - The document, as JSON.
|
|
130
203
|
* @param visit - The function that gets each document. It gives `"skip"` to not visit the parts of a document.
|
|
131
|
-
* @param options - The type of a bare root value, the names
|
|
204
|
+
* @param options - The type of a bare root value, the names, the capabilities and a function for the problems.
|
|
132
205
|
*/
|
|
133
206
|
export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => void | "skip", options: WalkOptions = {}): void {
|
|
134
207
|
interface Item {
|
|
135
208
|
readonly value: unknown;
|
|
136
|
-
readonly place:
|
|
137
|
-
readonly at:
|
|
209
|
+
readonly place: Trail | undefined;
|
|
210
|
+
readonly at: Trail | undefined;
|
|
138
211
|
readonly declared: string | undefined;
|
|
139
212
|
readonly depth: number;
|
|
140
213
|
readonly names: ReadonlySet<string>;
|
|
141
214
|
readonly later: ReadonlySet<string>;
|
|
215
|
+
readonly address: boolean;
|
|
216
|
+
readonly slot: readonly Key[];
|
|
217
|
+
readonly parent: DocVisit | undefined;
|
|
218
|
+
readonly caps: readonly Cap[] | undefined;
|
|
142
219
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
220
|
+
const stack: Item[] = [
|
|
221
|
+
{
|
|
222
|
+
value: doc,
|
|
223
|
+
place: undefined,
|
|
224
|
+
at: undefined,
|
|
225
|
+
declared: options.declaredType,
|
|
226
|
+
depth: 0,
|
|
227
|
+
names: options.names === undefined || options.names.length === 0 ? NO_NAMES : new Set(options.names),
|
|
228
|
+
later: NO_NAMES,
|
|
229
|
+
address: false,
|
|
230
|
+
slot: [],
|
|
231
|
+
parent: undefined,
|
|
232
|
+
caps: options.caps,
|
|
233
|
+
},
|
|
234
|
+
];
|
|
145
235
|
while (stack.length > 0) {
|
|
146
236
|
const item = stack.pop() as Item;
|
|
147
237
|
const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
|
|
148
238
|
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." });
|
|
239
|
+
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
240
|
continue;
|
|
151
241
|
}
|
|
152
242
|
const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
|
|
153
243
|
const found = lookupKind(world, doc.type);
|
|
154
244
|
const kind = found.ok ? found.kind : undefined;
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
245
|
+
const here = new Visit(
|
|
246
|
+
item.place,
|
|
247
|
+
item.at,
|
|
248
|
+
doc,
|
|
249
|
+
envelope === undefined,
|
|
250
|
+
item.depth,
|
|
251
|
+
item.names,
|
|
252
|
+
item.later,
|
|
253
|
+
item.slot,
|
|
254
|
+
item.parent,
|
|
255
|
+
kind,
|
|
256
|
+
found.ok ? found.path : undefined,
|
|
257
|
+
item.address ? true : undefined,
|
|
258
|
+
item.caps,
|
|
259
|
+
);
|
|
260
|
+
const answer = visit(here);
|
|
170
261
|
if (answer === "skip" || kind === undefined) continue;
|
|
171
262
|
const split = splitParts(kind.parts, doc.value);
|
|
263
|
+
const base: Trail | undefined = envelope === undefined ? item.at : { up: item.at, keys: ["value"] };
|
|
172
264
|
if (!split.ok) {
|
|
173
265
|
const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
|
|
174
|
-
|
|
266
|
+
const where = keysOf({ up: base, keys: split.at });
|
|
267
|
+
options.problem?.({ place: keysOf(item.place), at: where, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
|
|
175
268
|
continue;
|
|
176
269
|
}
|
|
177
270
|
const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
|
|
178
|
-
const base = envelope === undefined ? item.at : [...item.at, "value"];
|
|
179
271
|
for (let index = split.parts.length - 1; index >= 0; index -= 1) {
|
|
180
272
|
const slot = split.parts[index] as PartSlot;
|
|
181
273
|
const extra = sees[index] ?? [];
|
|
182
274
|
const hidden = later[index] ?? [];
|
|
183
275
|
stack.push({
|
|
184
276
|
value: slot.item,
|
|
185
|
-
place:
|
|
186
|
-
at:
|
|
277
|
+
place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
|
|
278
|
+
at: slot.path.length === 0 ? base : { up: base, keys: slot.path },
|
|
187
279
|
declared: slot.of,
|
|
188
280
|
depth: item.depth + 1,
|
|
189
281
|
names: extra.length === 0 ? item.names : new Set([...item.names, ...extra]),
|
|
190
282
|
later: hidden.length === 0 ? item.later : new Set([...item.later, ...hidden]),
|
|
283
|
+
address: slot.address === true,
|
|
284
|
+
slot: slot.path,
|
|
285
|
+
parent: here,
|
|
286
|
+
caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
|
|
191
287
|
});
|
|
192
288
|
}
|
|
193
289
|
}
|
|
@@ -195,6 +291,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
195
291
|
|
|
196
292
|
/** One reference of a document. */
|
|
197
293
|
export interface RefUse {
|
|
294
|
+
/** The visit of the reference document: its parent, its depth and the names that it sees. */
|
|
295
|
+
readonly visit: DocVisit;
|
|
198
296
|
/** The place of the reference document, through the parts. */
|
|
199
297
|
readonly place: readonly Key[];
|
|
200
298
|
/** The keys from the root JSON value to the reference document. */
|
|
@@ -204,12 +302,14 @@ export interface RefUse {
|
|
|
204
302
|
readonly root: string;
|
|
205
303
|
/** `false` when a kind around the reference binds its first name. A reference to another root is free. */
|
|
206
304
|
readonly free: boolean;
|
|
305
|
+
/** `true` when the reference is an address: its position declares it. An address does not read a value. */
|
|
306
|
+
readonly address: boolean;
|
|
207
307
|
}
|
|
208
308
|
|
|
209
309
|
/**
|
|
210
310
|
* This function finds each reference of a document: each document of the type `ref` that a walk reaches. A
|
|
211
311
|
* 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.
|
|
312
|
+
* outside: its dependencies. When the kind `ref` has parts, the walk also finds the references in its step documents.
|
|
213
313
|
*
|
|
214
314
|
* @param world - The world of the kinds.
|
|
215
315
|
* @param doc - The document, as JSON.
|
|
@@ -229,8 +329,13 @@ export function refsOf(world: World, doc: unknown, options: WalkOptions = {}): r
|
|
|
229
329
|
const root = typeof path[0] === "string" ? path[0] : "";
|
|
230
330
|
const first = path[1];
|
|
231
331
|
const bound = root === "names" && isKey(first) && v.names.has(String(first));
|
|
232
|
-
|
|
233
|
-
|
|
332
|
+
// The place and the path of a reference are made only when a reader asks for them.
|
|
333
|
+
const use = { path, root, free: !bound, address: v.address === true } as RefUse;
|
|
334
|
+
Object.defineProperty(use, "place", { enumerable: true, get: () => v.place });
|
|
335
|
+
Object.defineProperty(use, "at", { enumerable: true, get: () => v.at });
|
|
336
|
+
Object.defineProperty(use, "visit", { enumerable: false, value: v });
|
|
337
|
+
out.push(use);
|
|
338
|
+
return undefined;
|
|
234
339
|
},
|
|
235
340
|
rest,
|
|
236
341
|
);
|
|
@@ -278,6 +383,8 @@ export interface Issue {
|
|
|
278
383
|
/** The keys from the root document to the document with the problem. */
|
|
279
384
|
readonly place: readonly Key[];
|
|
280
385
|
readonly reason: string;
|
|
386
|
+
/** The keys from the root JSON value to the value with the problem. */
|
|
387
|
+
readonly at?: readonly Key[];
|
|
281
388
|
}
|
|
282
389
|
|
|
283
390
|
/** The options of a check. */
|
|
@@ -286,10 +393,27 @@ export interface CheckOptions {
|
|
|
286
393
|
readonly declaredType?: string;
|
|
287
394
|
/** The names that the host binds at the root. `"any"` does not check the names. The default is no names. */
|
|
288
395
|
readonly names?: readonly string[] | "any";
|
|
289
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* The capabilities at the root. Without them, the check does not check the effects. The check narrows them for
|
|
398
|
+
* each part by the capability rules of the kinds, as a run does.
|
|
399
|
+
*/
|
|
290
400
|
readonly caps?: readonly Cap[];
|
|
291
401
|
}
|
|
292
402
|
|
|
403
|
+
/**
|
|
404
|
+
* This function gives the reason of a `#CAP!`. When a kind around the document narrowed the capabilities, the
|
|
405
|
+
* reason names the nearest such kind.
|
|
406
|
+
*/
|
|
407
|
+
function capReason(v: DocVisit, kind: readonly Key[]): string {
|
|
408
|
+
const text = kind.join(".");
|
|
409
|
+
for (let up = v.parent; up !== undefined; up = up.parent) {
|
|
410
|
+
if (up.caps !== undefined && permits(up.caps, kind)) {
|
|
411
|
+
return `The capabilities do not permit the effect ${text} here. The kind ${typeName(up.doc.type)} around it narrows them for its parts.`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return `The capabilities do not permit the effect ${text}.`;
|
|
415
|
+
}
|
|
416
|
+
|
|
293
417
|
/**
|
|
294
418
|
* This function checks a document before an evaluation, by the contracts of its kinds. It finds the problems
|
|
295
419
|
* that an evaluation finds when it reaches them:
|
|
@@ -301,7 +425,7 @@ export interface CheckOptions {
|
|
|
301
425
|
* - `#DEPTH!`: too much nesting.
|
|
302
426
|
* - `#REF!` and `#STEP?`: a bad path.
|
|
303
427
|
* - `#NAME?`: a name that nothing binds.
|
|
304
|
-
* - `#CAP!`: an effect that the capabilities do not permit.
|
|
428
|
+
* - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
|
|
305
429
|
*
|
|
306
430
|
* @param machine - The world and the code of the evaluators.
|
|
307
431
|
* @param doc - The document, as JSON.
|
|
@@ -319,34 +443,41 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
319
443
|
doc,
|
|
320
444
|
(v) => {
|
|
321
445
|
if (v.depth > maxDepth) {
|
|
322
|
-
issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents
|
|
446
|
+
issues.push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.`, at: v.at });
|
|
323
447
|
return "skip";
|
|
324
448
|
}
|
|
325
449
|
const header = v.declared ? {} : v.doc;
|
|
326
450
|
const answer = ask(machine.world, header, "eval", v.declared ? { declaredType: typeName(v.doc.type) } : {});
|
|
327
451
|
if (answer.error === "unknown-kind") {
|
|
328
|
-
issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}
|
|
452
|
+
issues.push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.`, at: v.at });
|
|
329
453
|
return "skip";
|
|
330
454
|
}
|
|
331
455
|
const name = answer.evaluator;
|
|
332
456
|
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)}
|
|
457
|
+
issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
|
|
334
458
|
}
|
|
335
|
-
|
|
459
|
+
const caps = v.caps;
|
|
460
|
+
if (caps !== undefined && v.kind?.effects !== undefined) {
|
|
336
461
|
const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
|
|
337
462
|
for (const kind of kinds) {
|
|
338
|
-
if (!permits(
|
|
463
|
+
if (!permits(caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: capReason(v, kind), at: v.at });
|
|
339
464
|
}
|
|
340
465
|
}
|
|
341
|
-
|
|
466
|
+
const isPath = isRef(v.doc);
|
|
467
|
+
if (!isPath && v.typePath?.length === 1 && v.typePath[0] === "ref") {
|
|
468
|
+
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"])] });
|
|
469
|
+
}
|
|
470
|
+
if (!isPath) return undefined;
|
|
342
471
|
const path = v.doc.value as readonly Json[];
|
|
343
472
|
const root = path[0];
|
|
473
|
+
// The JSON path of a problem of a path goes to its step.
|
|
474
|
+
const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
|
|
344
475
|
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(", ")}
|
|
476
|
+
issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
|
|
346
477
|
return "skip";
|
|
347
478
|
}
|
|
348
479
|
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(", ")}
|
|
480
|
+
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
481
|
}
|
|
351
482
|
const first = path[1];
|
|
352
483
|
if (root === ("names" as Root) && !anyName && isKey(first) && !v.names.has(String(first))) {
|
|
@@ -355,22 +486,25 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
355
486
|
code: "#NAME?",
|
|
356
487
|
place: v.place,
|
|
357
488
|
reason: later ? `The name ${String(first)} is bound by a later step.` : `Nothing binds the name ${String(first)}.`,
|
|
489
|
+
at: stepAt(1),
|
|
358
490
|
});
|
|
359
491
|
}
|
|
360
|
-
path.
|
|
361
|
-
if (isKey(step)) return;
|
|
492
|
+
path.forEach((step, index) => {
|
|
493
|
+
if (index === 0 || isKey(step)) return;
|
|
362
494
|
const found = isEnvelope(step) ? lookupKind(machine.world, step.type) : undefined;
|
|
363
495
|
const walker = found?.ok === true && Object.hasOwn(found.kind.verbs, "step") ? found.kind.verbs["step"] : undefined;
|
|
364
496
|
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." });
|
|
497
|
+
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
498
|
}
|
|
367
499
|
});
|
|
368
|
-
|
|
500
|
+
// The step documents of the path are parts when the kind ref has a deep shape. The walk checks them too.
|
|
501
|
+
return undefined;
|
|
369
502
|
},
|
|
370
503
|
{
|
|
371
504
|
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
372
505
|
...(Array.isArray(options.names) ? { names: options.names } : {}),
|
|
373
|
-
|
|
506
|
+
...(options.caps === undefined ? {} : { caps: options.caps }),
|
|
507
|
+
problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at }),
|
|
374
508
|
},
|
|
375
509
|
);
|
|
376
510
|
return issues;
|
package/src/stdlib.ts
CHANGED
|
@@ -69,18 +69,21 @@ export const STD_WORLD: World = {
|
|
|
69
69
|
pure: {
|
|
70
70
|
verbs: { eval: "control.pure", view: "view.op" },
|
|
71
71
|
parts: { shape: "list" },
|
|
72
|
+
caps: [{ rule: "mask" }],
|
|
72
73
|
summary: "It evaluates its part without capabilities, thus an effect in it fails.",
|
|
73
74
|
},
|
|
74
75
|
log: {
|
|
75
76
|
verbs: { eval: "effect.log", view: "view.op" },
|
|
76
77
|
parts: { shape: "list", of: "text" },
|
|
77
78
|
side: "run",
|
|
79
|
+
effects: ["log"],
|
|
78
80
|
summary: "It writes the result of its part to the log. This is an effect.",
|
|
79
81
|
},
|
|
80
82
|
write: {
|
|
81
83
|
verbs: { eval: "effect.write", view: "view.op" },
|
|
82
84
|
parts: { shape: "record", fields: ["value"] },
|
|
83
85
|
side: "run",
|
|
86
|
+
effects: ["write"],
|
|
84
87
|
summary: "It writes the result of its part to the store, at its key. This is an effect.",
|
|
85
88
|
},
|
|
86
89
|
},
|
|
@@ -269,10 +272,11 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
|
269
272
|
}
|
|
270
273
|
return ok(last);
|
|
271
274
|
},
|
|
272
|
-
|
|
275
|
+
// The contract of pure masks the capabilities of its part, and the kernel applies it. Thus the runner does not.
|
|
276
|
+
"control.pure": function* ({ parts }) {
|
|
273
277
|
const only = parts[0];
|
|
274
278
|
if (only === undefined) return fail("#VALUE!", "pure needs one part.");
|
|
275
|
-
return yield* only.evaluate(
|
|
279
|
+
return yield* only.evaluate();
|
|
276
280
|
},
|
|
277
281
|
"control.if": function* ({ parts }) {
|
|
278
282
|
const test = parts.find((part) => part.key === "test");
|
package/src/world.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, type TypeRef, field, isRecord, typePath } from "./doc.ts";
|
|
2
|
+
import type { Cap } from "./deref.ts";
|
|
2
3
|
|
|
3
4
|
/** The two sides of evaluation. `derive` is pure. `run` causes effects in order. */
|
|
4
5
|
export type Side = "derive" | "run";
|
|
@@ -14,11 +15,16 @@ export type Side = "derive" | "run";
|
|
|
14
15
|
* - `record`: the value is a record. Each named field has its shape. A list of names makes each named field a
|
|
15
16
|
* part. The other fields have the shape `rest` (the default is data). `required` names the fields that must be.
|
|
16
17
|
* - `deep`: each envelope at any depth of the value is a part, and the rest is data.
|
|
18
|
+
* - `text`, `number` and `one`: the value is data of that type. `one` names the values that it permits.
|
|
19
|
+
*
|
|
20
|
+
* A `term` with `address: true` is an address: a reference there gives what its path reaches, not the value. A
|
|
21
|
+
* `term` with `kinds` permits only a part whose type starts with one of the paths. A `record` with `closed: true`
|
|
22
|
+
* permits only its named fields. A `list` with `distinct: true` permits no two equal items.
|
|
17
23
|
*/
|
|
18
24
|
export type Shape =
|
|
19
25
|
| { readonly shape: "none" }
|
|
20
|
-
| { readonly shape: "term"; readonly of?: string }
|
|
21
|
-
| { readonly shape: "list"; readonly of?: string; readonly item?: Shape }
|
|
26
|
+
| { readonly shape: "term"; readonly of?: string; readonly address?: boolean; readonly kinds?: readonly (readonly Key[])[] }
|
|
27
|
+
| { readonly shape: "list"; readonly of?: string; readonly item?: Shape; readonly distinct?: boolean }
|
|
22
28
|
| { readonly shape: "map"; readonly of?: string; readonly item?: Shape }
|
|
23
29
|
| {
|
|
24
30
|
readonly shape: "record";
|
|
@@ -26,8 +32,12 @@ export type Shape =
|
|
|
26
32
|
readonly of?: string;
|
|
27
33
|
readonly required?: readonly string[];
|
|
28
34
|
readonly rest?: Shape;
|
|
35
|
+
readonly closed?: boolean;
|
|
29
36
|
}
|
|
30
|
-
| { readonly shape: "deep"; readonly of?: string }
|
|
37
|
+
| { readonly shape: "deep"; readonly of?: string }
|
|
38
|
+
| { readonly shape: "text" }
|
|
39
|
+
| { readonly shape: "number"; readonly whole?: boolean; readonly min?: number; readonly max?: number }
|
|
40
|
+
| { readonly shape: "one"; readonly values: readonly Json[] };
|
|
31
41
|
|
|
32
42
|
/** The former name of `Shape`. */
|
|
33
43
|
export type PartsSpec = Shape;
|
|
@@ -60,6 +70,22 @@ export interface ScopeRule {
|
|
|
60
70
|
readonly sequential?: boolean;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
/**
|
|
74
|
+
* A capability rule of a kind: what the kind gives some of its parts from the capabilities of its document. A rule
|
|
75
|
+
* only narrows. A part does not get a capability that its document does not have (CAP.NARROW).
|
|
76
|
+
*
|
|
77
|
+
* - `same`: the parts get the capabilities of the document. This is the default.
|
|
78
|
+
* - `mask`: the parts get no capabilities, as the part of `pure`.
|
|
79
|
+
* - `only`: the parts get only the effects that the capabilities of the document and the list `caps` both permit.
|
|
80
|
+
*
|
|
81
|
+
* `in` names the parts that the rule covers, as patterns of their paths in the value. The key `"*"` matches each
|
|
82
|
+
* key. A rule without `in` covers each part.
|
|
83
|
+
*/
|
|
84
|
+
export type CapsRule =
|
|
85
|
+
| { readonly rule: "same"; readonly in?: readonly (readonly Key[])[] }
|
|
86
|
+
| { readonly rule: "mask"; readonly in?: readonly (readonly Key[])[] }
|
|
87
|
+
| { readonly rule: "only"; readonly caps: readonly Cap[]; readonly in?: readonly (readonly Key[])[] };
|
|
88
|
+
|
|
63
89
|
/** A kind gives the name of an evaluator for each verb, and the contract of its value. */
|
|
64
90
|
export interface Kind {
|
|
65
91
|
readonly verbs: Readonly<Record<string, string>>;
|
|
@@ -67,6 +93,13 @@ export interface Kind {
|
|
|
67
93
|
readonly parts?: Shape;
|
|
68
94
|
/** The names that the kind binds for its parts. */
|
|
69
95
|
readonly scope?: readonly ScopeRule[];
|
|
96
|
+
/**
|
|
97
|
+
* The capabilities that the kind gives its parts, as rules. A part that no rule covers gets the capabilities of
|
|
98
|
+
* the document. A part that several rules cover gets only what each rule permits. The static kit reads the rules,
|
|
99
|
+
* and the kernel narrows the context of each part by them. Thus an evaluator does not narrow the capabilities
|
|
100
|
+
* itself.
|
|
101
|
+
*/
|
|
102
|
+
readonly caps?: readonly CapsRule[];
|
|
70
103
|
/** The side of the kind. A kind of the run side causes effects. The default is `derive`. */
|
|
71
104
|
readonly side?: Side;
|
|
72
105
|
/**
|
|
@@ -209,6 +242,9 @@ export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Js
|
|
|
209
242
|
|
|
210
243
|
function describeShape(shape: Shape | undefined): Json {
|
|
211
244
|
if (shape === undefined || shape.shape === "none") return "none";
|
|
245
|
+
if (shape.shape === "text") return "a text";
|
|
246
|
+
if (shape.shape === "number") return "a number";
|
|
247
|
+
if (shape.shape === "one") return `one of ${JSON.stringify(shape.values)}`;
|
|
212
248
|
if (shape.shape === "term") return `a part${shape.of === undefined ? "" : ` of ${shape.of}`}`;
|
|
213
249
|
if (shape.shape === "deep") return "each document in the value";
|
|
214
250
|
if (shape.shape === "list") return `a list${shape.of === undefined ? "" : ` of ${shape.of}`}`;
|