@mark1russell7/coeval 0.4.0 → 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 +3 -3
- package/build/index.d.ts +281 -222
- package/build/index.d.ts.map +1 -1
- package/build/index.js +100 -17
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/evaluate.ts +72 -3
- package/src/index.ts +3 -0
- package/src/static.ts +39 -7
- package/src/stdlib.ts +6 -2
- package/src/world.ts +24 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/coeval",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./build/index.js",
|
package/src/evaluate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
|
|
2
|
-
import { type Split, splitParts } from "./parts.ts";
|
|
2
|
+
import { type Split, pathMatches, splitParts } from "./parts.ts";
|
|
3
3
|
import { ask, type Answer } from "./ladder.ts";
|
|
4
|
-
import { type Lookup, type Side, type World, lookupKind } from "./world.ts";
|
|
4
|
+
import { type CapsRule, type Lookup, type Side, type World, lookupKind } from "./world.ts";
|
|
5
5
|
import { type ErrorCode, type EvalError, type Result, fail, ok, placed, thrown, describe } from "./errors.ts";
|
|
6
6
|
import {
|
|
7
7
|
type Cap,
|
|
@@ -74,6 +74,58 @@ export function narrow(parent: Context, next: Context): Context {
|
|
|
74
74
|
return caps.length === next.caps.length ? next : { ...next, caps };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** No capabilities. One list serves each masked part. */
|
|
78
|
+
const NO_CAPS: readonly Cap[] = Object.freeze([]);
|
|
79
|
+
|
|
80
|
+
/** No capability rules. One list serves each part that no rule narrows. */
|
|
81
|
+
const NO_RULES: readonly CapsRule[] = Object.freeze([]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* This function gives the capabilities that two lists both permit. An effect is permitted by the result when each
|
|
85
|
+
* list permits it. When the list `b` permits each capability of `a`, the result is the list `a` itself.
|
|
86
|
+
*
|
|
87
|
+
* @param a - The first list, for example the capabilities of a document.
|
|
88
|
+
* @param b - The second list, for example the list of a rule `only`.
|
|
89
|
+
* @returns The capabilities that both lists permit.
|
|
90
|
+
*/
|
|
91
|
+
export function intersectCaps(a: readonly Cap[], b: readonly Cap[]): readonly Cap[] {
|
|
92
|
+
if (a.every((cap) => permits(b, cap))) return a;
|
|
93
|
+
const out: Cap[] = [];
|
|
94
|
+
for (const cap of b) if (permits(a, cap)) out.push(cap);
|
|
95
|
+
for (const cap of a) if (permits(b, cap) && !permits(out, cap)) out.push(cap);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** This function gives the capability rules of a kind that cover a part and change its capabilities. */
|
|
100
|
+
function rulesFor(rules: readonly CapsRule[] | undefined, path: readonly Key[]): readonly CapsRule[] {
|
|
101
|
+
if (rules === undefined || rules.length === 0) return NO_RULES;
|
|
102
|
+
const out = rules.filter((rule) => rule.rule !== "same" && (rule.in === undefined || rule.in.some((pattern) => pathMatches(path, pattern))));
|
|
103
|
+
return out.length === 0 ? NO_RULES : out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** This function applies the rules that cover a part to the capabilities of its document. */
|
|
107
|
+
function capsUnder(rules: readonly CapsRule[], caps: readonly Cap[]): readonly Cap[] {
|
|
108
|
+
let out = caps;
|
|
109
|
+
for (const rule of rules) {
|
|
110
|
+
if (rule.rule === "mask") return NO_CAPS;
|
|
111
|
+
if (rule.rule === "only") out = intersectCaps(out, rule.caps);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* This function gives the capabilities that a kind gives one of its parts, by the capability rules of its contract.
|
|
118
|
+
* The kernel and the static kit both use it, thus a run and a check narrow the same way.
|
|
119
|
+
*
|
|
120
|
+
* @param rules - The capability rules of the kind (`Kind.caps`).
|
|
121
|
+
* @param path - The path of the part in the value of the document.
|
|
122
|
+
* @param caps - The capabilities of the document.
|
|
123
|
+
* @returns The capabilities of the part: the same list or a smaller one.
|
|
124
|
+
*/
|
|
125
|
+
export function partCaps(rules: readonly CapsRule[] | undefined, path: readonly Key[], caps: readonly Cap[]): readonly Cap[] {
|
|
126
|
+
return capsUnder(rulesFor(rules, path), caps);
|
|
127
|
+
}
|
|
128
|
+
|
|
77
129
|
/**
|
|
78
130
|
* The steps of an evaluation that can wait. It yields a promise only when the host makes it wait. A runner takes the
|
|
79
131
|
* result of a part with `yield*`, thus a run goes on at once until an effect waits. A runner does not yield a
|
|
@@ -892,7 +944,10 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
892
944
|
}
|
|
893
945
|
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
894
946
|
const item = slot.item;
|
|
895
|
-
|
|
947
|
+
// The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
|
|
948
|
+
const own = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
|
|
949
|
+
const rules = rulesFor(kind?.caps, slot.path);
|
|
950
|
+
const compiled = rules.length === 0 ? own : gated(rules, own);
|
|
896
951
|
// The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
|
|
897
952
|
const header = typeof (item as { readonly name?: unknown } | null)?.name === "string" && isEnvelope(item) ? (item as { readonly name: string }).name : undefined;
|
|
898
953
|
return header === undefined
|
|
@@ -942,6 +997,20 @@ function placedCode(inner: Code, place: readonly Key[], name: string): Code {
|
|
|
942
997
|
};
|
|
943
998
|
}
|
|
944
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* This function gives the code of a part that capability rules cover. The code narrows the context of the part by
|
|
1002
|
+
* the rules before the part starts.
|
|
1003
|
+
*/
|
|
1004
|
+
function gated(rules: readonly CapsRule[], part: { readonly code: Code; readonly run: RunCode }): { readonly code: Code; readonly run: RunCode } {
|
|
1005
|
+
const inner = part.code;
|
|
1006
|
+
const steps = part.run;
|
|
1007
|
+
const under = (ctx: Context): Context => {
|
|
1008
|
+
const caps = capsUnder(rules, ctx.caps);
|
|
1009
|
+
return caps === ctx.caps || (caps.length === 0 && ctx.caps.length === 0) ? ctx : { ...ctx, caps };
|
|
1010
|
+
};
|
|
1011
|
+
return { code: (ctx, rt) => inner(under(ctx), rt), run: (ctx, rt) => steps(under(ctx), rt) };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
945
1014
|
/** This function gives the steps of a runner, with the place of each error and of each exception. */
|
|
946
1015
|
function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
|
|
947
1016
|
return function* (ctx, rt) {
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export {
|
|
|
36
36
|
} from "./errors.ts";
|
|
37
37
|
export {
|
|
38
38
|
type Alias,
|
|
39
|
+
type CapsRule,
|
|
39
40
|
type Kind,
|
|
40
41
|
type Lookup,
|
|
41
42
|
type Namespace,
|
|
@@ -127,9 +128,11 @@ export {
|
|
|
127
128
|
evaluate,
|
|
128
129
|
failures,
|
|
129
130
|
flatten,
|
|
131
|
+
intersectCaps,
|
|
130
132
|
makeContext,
|
|
131
133
|
narrow,
|
|
132
134
|
nodeAt,
|
|
135
|
+
partCaps,
|
|
133
136
|
permits,
|
|
134
137
|
planner,
|
|
135
138
|
prepare,
|
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 {
|
|
@@ -32,6 +32,11 @@ export interface DocVisit {
|
|
|
32
32
|
readonly later: ReadonlySet<string>;
|
|
33
33
|
/** `true` when the position of the document declares an address: a reference there is not a read of a value. */
|
|
34
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;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/** A position that a walk cannot read as a document. */
|
|
@@ -50,6 +55,8 @@ export interface WalkOptions {
|
|
|
50
55
|
readonly declaredType?: string;
|
|
51
56
|
/** The names that the host binds at the root. */
|
|
52
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[];
|
|
53
60
|
/**
|
|
54
61
|
* This function gets each position that is not a document.
|
|
55
62
|
*
|
|
@@ -169,6 +176,7 @@ class Visit implements DocVisit {
|
|
|
169
176
|
readonly kind: Kind | undefined,
|
|
170
177
|
readonly typePath: readonly Key[] | undefined,
|
|
171
178
|
readonly address: true | undefined,
|
|
179
|
+
readonly caps: readonly Cap[] | undefined,
|
|
172
180
|
) {
|
|
173
181
|
this.#place = place;
|
|
174
182
|
this.#at = at;
|
|
@@ -185,14 +193,15 @@ class Visit implements DocVisit {
|
|
|
185
193
|
|
|
186
194
|
/**
|
|
187
195
|
* This function visits each document of a document: the root and each part at each depth, by the contracts of the
|
|
188
|
-
* kinds. A part of a kind that the world does not have is not visited.
|
|
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
|
|
189
198
|
* depth, thus a deep document cannot overflow it. The time and the memory of a walk are linear in the size of the
|
|
190
199
|
* document. A visit makes the keys of its place and of its path only when a reader asks for them.
|
|
191
200
|
*
|
|
192
201
|
* @param world - The world of the kinds.
|
|
193
202
|
* @param doc - The document, as JSON.
|
|
194
203
|
* @param visit - The function that gets each document. It gives `"skip"` to not visit the parts of a document.
|
|
195
|
-
* @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.
|
|
196
205
|
*/
|
|
197
206
|
export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => void | "skip", options: WalkOptions = {}): void {
|
|
198
207
|
interface Item {
|
|
@@ -206,6 +215,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
206
215
|
readonly address: boolean;
|
|
207
216
|
readonly slot: readonly Key[];
|
|
208
217
|
readonly parent: DocVisit | undefined;
|
|
218
|
+
readonly caps: readonly Cap[] | undefined;
|
|
209
219
|
}
|
|
210
220
|
const stack: Item[] = [
|
|
211
221
|
{
|
|
@@ -219,6 +229,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
219
229
|
address: false,
|
|
220
230
|
slot: [],
|
|
221
231
|
parent: undefined,
|
|
232
|
+
caps: options.caps,
|
|
222
233
|
},
|
|
223
234
|
];
|
|
224
235
|
while (stack.length > 0) {
|
|
@@ -244,6 +255,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
244
255
|
kind,
|
|
245
256
|
found.ok ? found.path : undefined,
|
|
246
257
|
item.address ? true : undefined,
|
|
258
|
+
item.caps,
|
|
247
259
|
);
|
|
248
260
|
const answer = visit(here);
|
|
249
261
|
if (answer === "skip" || kind === undefined) continue;
|
|
@@ -271,6 +283,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
271
283
|
address: slot.address === true,
|
|
272
284
|
slot: slot.path,
|
|
273
285
|
parent: here,
|
|
286
|
+
caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
|
|
274
287
|
});
|
|
275
288
|
}
|
|
276
289
|
}
|
|
@@ -380,10 +393,27 @@ export interface CheckOptions {
|
|
|
380
393
|
readonly declaredType?: string;
|
|
381
394
|
/** The names that the host binds at the root. `"any"` does not check the names. The default is no names. */
|
|
382
395
|
readonly names?: readonly string[] | "any";
|
|
383
|
-
/**
|
|
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
|
+
*/
|
|
384
400
|
readonly caps?: readonly Cap[];
|
|
385
401
|
}
|
|
386
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
|
+
|
|
387
417
|
/**
|
|
388
418
|
* This function checks a document before an evaluation, by the contracts of its kinds. It finds the problems
|
|
389
419
|
* that an evaluation finds when it reaches them:
|
|
@@ -395,7 +425,7 @@ export interface CheckOptions {
|
|
|
395
425
|
* - `#DEPTH!`: too much nesting.
|
|
396
426
|
* - `#REF!` and `#STEP?`: a bad path.
|
|
397
427
|
* - `#NAME?`: a name that nothing binds.
|
|
398
|
-
* - `#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.
|
|
399
429
|
*
|
|
400
430
|
* @param machine - The world and the code of the evaluators.
|
|
401
431
|
* @param doc - The document, as JSON.
|
|
@@ -426,10 +456,11 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
426
456
|
if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
|
|
427
457
|
issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
|
|
428
458
|
}
|
|
429
|
-
|
|
459
|
+
const caps = v.caps;
|
|
460
|
+
if (caps !== undefined && v.kind?.effects !== undefined) {
|
|
430
461
|
const kinds: readonly (readonly Key[])[] = v.kind.effects === "type" ? [v.typePath ?? []] : v.kind.effects.map((kind) => kind.split("."));
|
|
431
462
|
for (const kind of kinds) {
|
|
432
|
-
if (!permits(
|
|
463
|
+
if (!permits(caps, kind)) issues.push({ code: "#CAP!", place: v.place, reason: capReason(v, kind), at: v.at });
|
|
433
464
|
}
|
|
434
465
|
}
|
|
435
466
|
const isPath = isRef(v.doc);
|
|
@@ -472,6 +503,7 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
472
503
|
{
|
|
473
504
|
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
474
505
|
...(Array.isArray(options.names) ? { names: options.names } : {}),
|
|
506
|
+
...(options.caps === undefined ? {} : { caps: options.caps }),
|
|
475
507
|
problem: (problem) => issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at }),
|
|
476
508
|
},
|
|
477
509
|
);
|
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";
|
|
@@ -69,6 +70,22 @@ export interface ScopeRule {
|
|
|
69
70
|
readonly sequential?: boolean;
|
|
70
71
|
}
|
|
71
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
|
+
|
|
72
89
|
/** A kind gives the name of an evaluator for each verb, and the contract of its value. */
|
|
73
90
|
export interface Kind {
|
|
74
91
|
readonly verbs: Readonly<Record<string, string>>;
|
|
@@ -76,6 +93,13 @@ export interface Kind {
|
|
|
76
93
|
readonly parts?: Shape;
|
|
77
94
|
/** The names that the kind binds for its parts. */
|
|
78
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[];
|
|
79
103
|
/** The side of the kind. A kind of the run side causes effects. The default is `derive`. */
|
|
80
104
|
readonly side?: Side;
|
|
81
105
|
/**
|