@mark1russell7/coeval 0.23.0 → 0.24.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 +2 -1
- package/build/index.d.ts +84 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +551 -11
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/conform.ts +1 -1
- package/src/effects.ts +1 -1
- package/src/evaluate.ts +133 -3
- package/src/index.ts +5 -0
- package/src/reference.ts +55 -0
- package/src/static.ts +13 -2
- package/src/stdlib.ts +61 -1
- package/src/syntax.ts +208 -1
- package/src/world.ts +7 -1
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
|
+
"version": "0.24.0",
|
|
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/conform.ts
CHANGED
|
@@ -867,7 +867,7 @@ function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sampl
|
|
|
867
867
|
const fn = under.fn;
|
|
868
868
|
return (shot) => {
|
|
869
869
|
const ctx = shot.context;
|
|
870
|
-
const parts: RunPart[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.steps(index, "evaluate", given ?? ctx) }));
|
|
870
|
+
const parts: RunPart[] = split.parts.map((slot, index) => ({ ...handleOf(slot), evaluate: (given?: Context) => shot.steps(index, "evaluate", given ?? ctx), handled: () => shot.steps(index, "evaluate", ctx) }));
|
|
871
871
|
const input: RunInput = {
|
|
872
872
|
...base,
|
|
873
873
|
value: fill(parts),
|
package/src/effects.ts
CHANGED
|
@@ -46,7 +46,7 @@ function reaching(machine: Machine, doc: unknown, options: EffectsOptions): read
|
|
|
46
46
|
doc,
|
|
47
47
|
(v) => {
|
|
48
48
|
const caps = v.caps ?? EACH;
|
|
49
|
-
for (const kind of declaredEffects(v, "eval", machine)) if (rule(caps, kind)) out.push({ kind, visit: v });
|
|
49
|
+
for (const kind of declaredEffects(v, "eval", machine)) if (rule(caps, kind) && (v.handled === undefined || !permits(v.handled, kind))) out.push({ kind, visit: v });
|
|
50
50
|
return undefined;
|
|
51
51
|
},
|
|
52
52
|
{ caps: options.caps ?? EACH, ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }) },
|
package/src/evaluate.ts
CHANGED
|
@@ -109,7 +109,7 @@ export function intersectCaps(a: readonly Cap[], b: readonly Cap[]): readonly Ca
|
|
|
109
109
|
/** This function gives the capability rules of a kind that cover a part and change its capabilities. */
|
|
110
110
|
function rulesFor(rules: readonly CapsRule[] | undefined, path: readonly Key[]): readonly CapsRule[] {
|
|
111
111
|
if (rules === undefined || rules.length === 0) return NO_RULES;
|
|
112
|
-
const out = rules.filter((rule) => rule.rule !== "same" && (rule.in === undefined || rule.in.some((pattern) => pathMatches(path, pattern))));
|
|
112
|
+
const out = rules.filter((rule) => rule.rule !== "same" && rule.rule !== "grant" && (rule.in === undefined || rule.in.some((pattern) => pathMatches(path, pattern))));
|
|
113
113
|
return out.length === 0 ? NO_RULES : out;
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -136,6 +136,87 @@ export function partCaps(rules: readonly CapsRule[] | undefined, path: readonly
|
|
|
136
136
|
return capsUnder(rulesFor(rules, path), caps);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/** This function collects the values at a path in a value. The key `"*"` steps into each item of a list. */
|
|
140
|
+
function collectAt(value: unknown, path: readonly Key[], index: number, out: Cap[]): void {
|
|
141
|
+
if (index === path.length) {
|
|
142
|
+
if (typeof value === "string" || (Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number"))) out.push(value as Cap);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const key = path[index] as Key;
|
|
146
|
+
if (key === "*") {
|
|
147
|
+
if (Array.isArray(value)) for (const item of value) collectAt(item, path, index + 1, out);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (typeof value === "object" && value !== null) collectAt((value as Record<Key, unknown>)[key], path, index + 1, out);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* This function gives the capabilities that the rules `grant` of a kind give one part (CAP.GRANT). The check and the
|
|
155
|
+
* walk read them from the document. The kernel gets the same list from the clauses of the runner.
|
|
156
|
+
*
|
|
157
|
+
* @param rules - The capability rules of the kind (`Kind.caps`).
|
|
158
|
+
* @param path - The path of the part in the value of the document.
|
|
159
|
+
* @param value - The value of the document.
|
|
160
|
+
* @returns The capabilities that the rules add, or an empty list.
|
|
161
|
+
*/
|
|
162
|
+
export function partGrants(rules: readonly CapsRule[] | undefined, path: readonly Key[], value: unknown): readonly Cap[] {
|
|
163
|
+
if (rules === undefined) return NO_CAPS;
|
|
164
|
+
let out: Cap[] | undefined;
|
|
165
|
+
for (const rule of rules) {
|
|
166
|
+
if (rule.rule !== "grant" || (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(path, pattern)))) continue;
|
|
167
|
+
collectAt(value, rule.from, 0, (out ??= []));
|
|
168
|
+
}
|
|
169
|
+
return out ?? NO_CAPS;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One clause of a handler (D146): the effects under `op` go to `answer` and not to the host. The clause runs in place,
|
|
174
|
+
* as a part of the effect that it answers. Its result is the result of that effect.
|
|
175
|
+
*/
|
|
176
|
+
export interface Clause {
|
|
177
|
+
/** The kind of the effects: the clause answers this kind and each kind under it (CAP.KEYS). */
|
|
178
|
+
readonly op: Cap;
|
|
179
|
+
/**
|
|
180
|
+
* This function answers one effect. It sees only the payload (HANDLE.SEALED).
|
|
181
|
+
*
|
|
182
|
+
* @param payload - The data of the effect.
|
|
183
|
+
* @returns The steps that give the result of the effect.
|
|
184
|
+
*/
|
|
185
|
+
answer(payload: unknown): Steps;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The clauses of one active `handle`, and the frame around it. */
|
|
189
|
+
interface Frame {
|
|
190
|
+
readonly clauses: readonly Clause[];
|
|
191
|
+
readonly outer: Frame | undefined;
|
|
192
|
+
/** The runtime of the evaluation that started the frame. A frame of another evaluation has ended. */
|
|
193
|
+
readonly owner: Runtime;
|
|
194
|
+
/** `true` after the body ends. A closure can leave the body with the context of the frame (HANDLE.ENDED). */
|
|
195
|
+
ended: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The key of the frame of a context. Like the grant, it stays through a spread of a context, and only the kernel writes
|
|
200
|
+
* it. It is not a root (D10): a document cannot read it (HANDLE.SEALED).
|
|
201
|
+
*/
|
|
202
|
+
const FRAME = /* @__PURE__ */ Symbol("coeval.frame");
|
|
203
|
+
|
|
204
|
+
/** A context that can have a frame. */
|
|
205
|
+
type Framed = Context & { readonly [FRAME]?: Frame };
|
|
206
|
+
|
|
207
|
+
/** This function gives the frame of a context, when it has one. */
|
|
208
|
+
function frameOf(ctx: Context): Frame | undefined {
|
|
209
|
+
return (ctx as Framed)[FRAME];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** This function finds the frame and the clause that answer an effect. The innermost frame wins, then the first clause. */
|
|
213
|
+
function clauseFor(start: Frame, keys: readonly Key[]): { readonly frame: Frame; readonly clause: Clause } | undefined {
|
|
214
|
+
for (let frame: Frame | undefined = start; frame !== undefined; frame = frame.outer) {
|
|
215
|
+
for (const clause of frame.clauses) if (permits([clause.op], keys)) return { frame, clause };
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
139
220
|
/**
|
|
140
221
|
* The key of the grant of a context (CAP.NARROW-CODE): the capabilities that the kernel gave the document of the
|
|
141
222
|
* context. A spread of a context keeps its grant. Thus compiled code passes the grant on in each context that it makes
|
|
@@ -669,6 +750,14 @@ export interface RunPart {
|
|
|
669
750
|
* @returns The steps of the evaluation of the part.
|
|
670
751
|
*/
|
|
671
752
|
evaluate(context?: Context): Steps;
|
|
753
|
+
/**
|
|
754
|
+
* This function evaluates the part under clauses that answer its effects (D146). The kernel sets it on each part of
|
|
755
|
+
* a kind with a rule `grant`. A part that a runner makes itself has none, and a compiled runner gets no such part.
|
|
756
|
+
*
|
|
757
|
+
* @param clauses - The clauses, in order. An effect goes to the first clause that names its kind.
|
|
758
|
+
* @returns The steps of the evaluation of the part.
|
|
759
|
+
*/
|
|
760
|
+
handled?(clauses: readonly Clause[]): Steps;
|
|
672
761
|
}
|
|
673
762
|
|
|
674
763
|
/** The input of a runner: the input of an evaluator, with parts and effects that can wait. */
|
|
@@ -1288,14 +1377,35 @@ class Session implements Runtime {
|
|
|
1288
1377
|
return (effect.outcome = fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place }));
|
|
1289
1378
|
}
|
|
1290
1379
|
|
|
1380
|
+
/**
|
|
1381
|
+
* This function finds the clause that answers an effect (D146). Without it, the effect goes on to the host. An effect
|
|
1382
|
+
* without a capability gets no clause, thus `record` gives its `#CAP!` (HANDLE.NARROW). A clause of a frame that has
|
|
1383
|
+
* ended gives `#CAP!`, because the capability came from that frame (HANDLE.ENDED).
|
|
1384
|
+
*/
|
|
1385
|
+
private clauseOf(given: Context, start: Frame, kind: Cap, place: readonly Key[]): Clause | Result | undefined {
|
|
1386
|
+
const ctx = inEpoch(given, this.epoch) ? granted(given) : ungranted(given, this.epoch);
|
|
1387
|
+
const keys = typeof kind === "string" ? kind.split(".") : kind;
|
|
1388
|
+
if (!this.permitted(ctx, keys)) return undefined;
|
|
1389
|
+
const found = clauseFor(start, keys);
|
|
1390
|
+
if (found === undefined) return undefined;
|
|
1391
|
+
if (found.frame.ended || found.frame.owner !== this) return fail("#CAP!", `The handler of the effect ${keys.join(".")} has ended.`, { place });
|
|
1392
|
+
return found.clause;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1291
1395
|
effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
|
|
1292
1396
|
const text = typeof kind === "string" ? kind : kind.join(".");
|
|
1293
1397
|
if (this.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
|
|
1398
|
+
const frame = frameOf(ctx);
|
|
1399
|
+
const clause = frame === undefined ? undefined : this.clauseOf(ctx, frame, kind, place);
|
|
1400
|
+
if (clause !== undefined) return "ok" in clause ? clause : drive(clause.answer(payload), text);
|
|
1294
1401
|
const made = this.record(ctx, kind, payload, place);
|
|
1295
1402
|
return "ok" in made ? made : this.answer(made, place);
|
|
1296
1403
|
}
|
|
1297
1404
|
|
|
1298
1405
|
*runEffect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
|
|
1406
|
+
const frame = frameOf(ctx);
|
|
1407
|
+
const clause = frame === undefined ? undefined : this.clauseOf(ctx, frame, kind, place);
|
|
1408
|
+
if (clause !== undefined) return "ok" in clause ? clause : yield* clause.answer(payload);
|
|
1299
1409
|
const made = this.record(ctx, kind, payload, place);
|
|
1300
1410
|
if ("ok" in made) return made;
|
|
1301
1411
|
const perform = this.perform;
|
|
@@ -1771,7 +1881,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1771
1881
|
if (runner !== undefined) {
|
|
1772
1882
|
let inner: RunCode;
|
|
1773
1883
|
try {
|
|
1774
|
-
inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
|
|
1884
|
+
inner = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill, kind?.caps?.some((rule) => rule.rule === "grant") === true);
|
|
1775
1885
|
} catch (error) {
|
|
1776
1886
|
return failingWith(c, meta, placed(thrown(error, `the runner ${name}`), place));
|
|
1777
1887
|
}
|
|
@@ -2316,7 +2426,25 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
|
|
|
2316
2426
|
};
|
|
2317
2427
|
}
|
|
2318
2428
|
|
|
2319
|
-
|
|
2429
|
+
/**
|
|
2430
|
+
* This function runs a part under a new frame (D146). The part gets the capabilities of the document and the kinds of
|
|
2431
|
+
* the clauses (CAP.GRANT), with the grant of the kernel. A rule around an effect narrows them as it narrows any other
|
|
2432
|
+
* capability (HANDLE.NARROW). The frame ends with the steps.
|
|
2433
|
+
*/
|
|
2434
|
+
function* handling(given: Context, clauses: readonly Clause[], rt: Runtime, run: (ctx: Context) => Steps): Steps {
|
|
2435
|
+
const ctx = granted(given);
|
|
2436
|
+
const frame: Frame = { clauses, outer: frameOf(ctx), owner: rt, ended: false };
|
|
2437
|
+
const caps = [...ctx.caps];
|
|
2438
|
+
for (const clause of clauses) if (!permits(caps, clause.op)) caps.push(clause.op);
|
|
2439
|
+
const inner: Granted & Framed = { ...ctx, caps, [GRANT]: caps, [EPOCH]: epochOf(ctx), [FRAME]: frame };
|
|
2440
|
+
try {
|
|
2441
|
+
return yield* run(inner);
|
|
2442
|
+
} finally {
|
|
2443
|
+
frame.ended = true;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown, grants: boolean): RunCode {
|
|
2320
2448
|
const { kind, typePath, doc, data, place, machine } = input;
|
|
2321
2449
|
const refill = input.fill;
|
|
2322
2450
|
const compiled = input.parts;
|
|
@@ -2327,6 +2455,8 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
|
|
|
2327
2455
|
path: part.path,
|
|
2328
2456
|
...(part.name === undefined ? {} : { name: part.name }),
|
|
2329
2457
|
evaluate: (override?: Context) => part.run(override === undefined ? ctx : handed(ctx, override), rt),
|
|
2458
|
+
// Only a kind with a rule grant pays for this function: most runners stay as fast as before.
|
|
2459
|
+
...(grants ? { handled: (clauses: readonly Clause[]) => handling(ctx, clauses, rt, (inner) => part.run(inner, rt)) } : {}),
|
|
2330
2460
|
}));
|
|
2331
2461
|
return fn({
|
|
2332
2462
|
kind,
|
package/src/index.ts
CHANGED
|
@@ -161,6 +161,7 @@ export {
|
|
|
161
161
|
type RunFn,
|
|
162
162
|
type RunInput,
|
|
163
163
|
type RunOptions,
|
|
164
|
+
type Clause,
|
|
164
165
|
type RunPart,
|
|
165
166
|
type Runner,
|
|
166
167
|
type Runtime,
|
|
@@ -177,6 +178,7 @@ export {
|
|
|
177
178
|
narrow,
|
|
178
179
|
nodeAt,
|
|
179
180
|
partCaps,
|
|
181
|
+
partGrants,
|
|
180
182
|
permits,
|
|
181
183
|
planner,
|
|
182
184
|
prepare,
|
|
@@ -267,6 +269,8 @@ export {
|
|
|
267
269
|
type SyntaxCalls,
|
|
268
270
|
type SyntaxConstructs,
|
|
269
271
|
type SyntaxEnvelopes,
|
|
272
|
+
type SyntaxFinding,
|
|
273
|
+
type SyntaxFindingCode,
|
|
270
274
|
type SyntaxFixity,
|
|
271
275
|
type SyntaxForm,
|
|
272
276
|
type SyntaxFormArgs,
|
|
@@ -279,6 +283,7 @@ export {
|
|
|
279
283
|
type SyntaxRefs,
|
|
280
284
|
MAX_SYNTAX_ISSUES,
|
|
281
285
|
STD_SYNTAX,
|
|
286
|
+
checkSyntax,
|
|
282
287
|
parse,
|
|
283
288
|
print,
|
|
284
289
|
syntaxFormat,
|
package/src/reference.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
|
|
|
21
21
|
import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
22
22
|
import {
|
|
23
23
|
type AllOptions,
|
|
24
|
+
type Clause,
|
|
24
25
|
type Effect,
|
|
25
26
|
type EvalFn,
|
|
26
27
|
type EvalInput,
|
|
@@ -295,6 +296,44 @@ function freeIn(root: Context, ctx: Context, path: readonly unknown[]): boolean
|
|
|
295
296
|
return bound === Object.hasOwn(root.names, key) && (!bound || Object.is(ctx.names[key], root.names[key]));
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
/** The clauses of one active `handle`, as SPEC tells: the frame around it, and if its body has ended. */
|
|
300
|
+
interface RefFrame {
|
|
301
|
+
readonly clauses: readonly Clause[];
|
|
302
|
+
readonly outer: RefFrame | undefined;
|
|
303
|
+
/** The list of the effects of the evaluation: a frame of another evaluation has ended. */
|
|
304
|
+
readonly owner: unknown;
|
|
305
|
+
ended: boolean;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The key of the frame of a context. */
|
|
309
|
+
const REF_FRAME = /* @__PURE__ */ Symbol("coeval.reference.frame");
|
|
310
|
+
|
|
311
|
+
/** This function gives the frame of a context. */
|
|
312
|
+
function refFrame(ctx: Context): RefFrame | undefined {
|
|
313
|
+
return (ctx as Context & { readonly [REF_FRAME]?: RefFrame })[REF_FRAME];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* This function finds the clause for an effect (HANDLE.DO, HANDLE.FORWARD). An effect without a capability gets no clause
|
|
318
|
+
* (HANDLE.NARROW). A clause of a frame that has ended gives `#CAP!` (HANDLE.ENDED).
|
|
319
|
+
*/
|
|
320
|
+
function clauseAt(env: Env, ctx: Context, kind: Cap, place: readonly Key[]): Clause | Failed | undefined {
|
|
321
|
+
let frame = refFrame(ctx);
|
|
322
|
+
if (frame === undefined) return undefined;
|
|
323
|
+
const keys = typeof kind === "string" ? kind.split(".") : kind;
|
|
324
|
+
const machine = env.machine;
|
|
325
|
+
const allowed = machine.permits === undefined ? permits(ctx.caps, keys) : machine.permits(ctx.caps, keys, ctx);
|
|
326
|
+
if (!allowed) return undefined;
|
|
327
|
+
for (; frame !== undefined; frame = frame.outer) {
|
|
328
|
+
for (const clause of frame.clauses) {
|
|
329
|
+
if (!permits([clause.op], keys)) continue;
|
|
330
|
+
if (frame.ended || frame.owner !== env.effects) return fail("#CAP!", `The handler of the effect ${keys.join(".")} has ended.`, { place });
|
|
331
|
+
return clause;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
298
337
|
/**
|
|
299
338
|
* This function causes an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). It records the effect with its
|
|
300
339
|
* address (RUN.ADDRESS) and its outcome (RUN.OUTCOME). The mock answers the effect at once (RUN.MOCK). Without a
|
|
@@ -314,6 +353,8 @@ function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: reado
|
|
|
314
353
|
function causeFromEvaluator(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
|
|
315
354
|
const text = typeof kind === "string" ? kind : kind.join(".");
|
|
316
355
|
if (env.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
|
|
356
|
+
const clause = clauseAt(env, ctx, kind, place);
|
|
357
|
+
if (clause !== undefined) return "ok" in clause ? clause : drive(clause.answer(payload), place, text);
|
|
317
358
|
return cause(env, ctx, kind, payload, place);
|
|
318
359
|
}
|
|
319
360
|
|
|
@@ -323,6 +364,8 @@ function causeFromEvaluator(env: Env, ctx: Context, kind: Cap, payload: unknown,
|
|
|
323
364
|
* (RUN.OUTCOME).
|
|
324
365
|
*/
|
|
325
366
|
function* causeAndWait(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
|
|
367
|
+
const clause = clauseAt(env, ctx, kind, place);
|
|
368
|
+
if (clause !== undefined) return "ok" in clause ? clause : yield* clause.answer(payload);
|
|
326
369
|
if (!env.running) return cause(env, ctx, kind, payload, place);
|
|
327
370
|
const effect = recordEffect(env, ctx, kind, payload, place);
|
|
328
371
|
if ("ok" in effect) return effect;
|
|
@@ -480,6 +523,17 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
|
|
|
480
523
|
};
|
|
481
524
|
}
|
|
482
525
|
|
|
526
|
+
/** This function evaluates a part under a frame: the part also gets the kinds of the clauses (CAP.GRANT). */
|
|
527
|
+
function* handledAt(env: Env, part: At, clauses: readonly Clause[]): Steps {
|
|
528
|
+
const frame: RefFrame = { clauses, outer: refFrame(part.ctx), owner: env.effects, ended: false };
|
|
529
|
+
const ctx = { ...part.ctx, caps: [...part.ctx.caps, ...clauses.map((clause) => clause.op)], [REF_FRAME]: frame };
|
|
530
|
+
try {
|
|
531
|
+
return yield* stepsAt(env, { ...part, ctx });
|
|
532
|
+
} finally {
|
|
533
|
+
frame.ended = true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
483
537
|
function runInput(env: Env, seen: Seen, at: At): RunInput {
|
|
484
538
|
const { split, doc } = seen;
|
|
485
539
|
const parts: RunPart[] = split.parts.map((slot) => {
|
|
@@ -489,6 +543,7 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
|
|
|
489
543
|
path: slot.path,
|
|
490
544
|
...(name === undefined ? {} : { name }),
|
|
491
545
|
evaluate: (given?: Context) => stepsAt(env, partAt(seen, at, slot, given)),
|
|
546
|
+
handled: (clauses: readonly Clause[]) => handledAt(env, partAt(seen, at, slot, undefined), clauses),
|
|
492
547
|
};
|
|
493
548
|
});
|
|
494
549
|
const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));
|
package/src/static.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { answerOf, ask } from "./ladder.ts";
|
|
|
4
4
|
import { type PartSlot, partsOf, rulesCover, scopesOf } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
|
|
6
6
|
import type { ErrorCode } from "./errors.ts";
|
|
7
|
-
import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
|
|
7
|
+
import { type Machine, partCaps, partGrants, permits, waitedReason } from "./evaluate.ts";
|
|
8
8
|
import type { ValueType } from "./types.ts";
|
|
9
9
|
import { type RootTypes, type TypeNode, type Typed, machineTypeIssues, typePass } from "./typing.ts";
|
|
10
10
|
import { cycleIssues } from "./instances.ts";
|
|
@@ -41,6 +41,8 @@ export interface DocVisit {
|
|
|
41
41
|
* around it (`Kind.caps`). A walk without capabilities gives none here.
|
|
42
42
|
*/
|
|
43
43
|
readonly caps?: readonly Cap[] | undefined;
|
|
44
|
+
/** The kinds of effects that a handler around the document answers (CAP.GRANT, D146). Such an effect does not reach the host. */
|
|
45
|
+
readonly handled?: readonly Cap[] | undefined;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/** A position that a walk cannot read as a document. */
|
|
@@ -114,6 +116,7 @@ class Visit implements DocVisit {
|
|
|
114
116
|
readonly typePath: readonly Key[] | undefined;
|
|
115
117
|
readonly address: true | undefined;
|
|
116
118
|
readonly caps: readonly Cap[] | undefined;
|
|
119
|
+
readonly handled: readonly Cap[] | undefined;
|
|
117
120
|
readonly #place: Trail | undefined;
|
|
118
121
|
readonly #at: Trail | undefined;
|
|
119
122
|
readonly #bound: readonly string[];
|
|
@@ -135,6 +138,7 @@ class Visit implements DocVisit {
|
|
|
135
138
|
caps: readonly Cap[] | undefined,
|
|
136
139
|
bound: readonly string[],
|
|
137
140
|
expect: ValueType | undefined,
|
|
141
|
+
handled: readonly Cap[] | undefined,
|
|
138
142
|
) {
|
|
139
143
|
this.doc = doc;
|
|
140
144
|
this.declared = declared;
|
|
@@ -147,6 +151,7 @@ class Visit implements DocVisit {
|
|
|
147
151
|
this.typePath = typePath;
|
|
148
152
|
this.address = address;
|
|
149
153
|
this.caps = caps;
|
|
154
|
+
this.handled = handled;
|
|
150
155
|
this.#place = place;
|
|
151
156
|
this.#at = at;
|
|
152
157
|
this.#bound = bound;
|
|
@@ -199,6 +204,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
199
204
|
readonly caps: readonly Cap[] | undefined;
|
|
200
205
|
readonly bound: readonly string[];
|
|
201
206
|
readonly expect: ValueType | undefined;
|
|
207
|
+
readonly handled: readonly Cap[] | undefined;
|
|
202
208
|
}
|
|
203
209
|
const stack: Item[] = [
|
|
204
210
|
{
|
|
@@ -213,6 +219,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
213
219
|
slot: [],
|
|
214
220
|
parent: undefined,
|
|
215
221
|
caps: options.caps,
|
|
222
|
+
handled: undefined,
|
|
216
223
|
bound: [],
|
|
217
224
|
expect: undefined,
|
|
218
225
|
},
|
|
@@ -241,6 +248,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
241
248
|
item.caps,
|
|
242
249
|
item.bound,
|
|
243
250
|
item.expect,
|
|
251
|
+
item.handled,
|
|
244
252
|
);
|
|
245
253
|
const answer = visit(here);
|
|
246
254
|
if (answer === "skip" || kind === undefined) continue;
|
|
@@ -254,6 +262,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
254
262
|
const slot = split.parts[index] as PartSlot;
|
|
255
263
|
const extra = sees[index] ?? [];
|
|
256
264
|
const hidden = later[index] ?? [];
|
|
265
|
+
const grants = partGrants(kind.caps, slot.path, doc.value);
|
|
257
266
|
stack.push({
|
|
258
267
|
value: slot.item,
|
|
259
268
|
place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
|
|
@@ -265,7 +274,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
265
274
|
address: slot.address === true,
|
|
266
275
|
slot: slot.path,
|
|
267
276
|
parent: here,
|
|
268
|
-
caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
|
|
277
|
+
caps: item.caps === undefined || kind.caps === undefined ? item.caps : [...partCaps(kind.caps, slot.path, item.caps), ...grants],
|
|
278
|
+
handled: grants.length === 0 ? item.handled : [...(item.handled ?? []), ...grants],
|
|
269
279
|
bound: extra,
|
|
270
280
|
expect: slot.expect,
|
|
271
281
|
});
|
|
@@ -770,6 +780,7 @@ const KEEPS_SELF: ReadonlySet<string> = /* @__PURE__ */ new Set([
|
|
|
770
780
|
"control.seq",
|
|
771
781
|
"control.pure",
|
|
772
782
|
"control.if",
|
|
783
|
+
"control.handle",
|
|
773
784
|
"list.all",
|
|
774
785
|
"effect.log",
|
|
775
786
|
"effect.write",
|
package/src/stdlib.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type Json, type Key, docEquals, field, isRecord } from "./doc.ts";
|
|
1
|
+
import { type Json, type Key, docEquals, field, isKey, isRecord } from "./doc.ts";
|
|
2
2
|
import {
|
|
3
3
|
type EvalInput,
|
|
4
4
|
type Evaluator,
|
|
5
|
+
type Clause,
|
|
5
6
|
type InputWithRuntime,
|
|
6
7
|
type Machine,
|
|
7
8
|
type Plan,
|
|
@@ -144,6 +145,27 @@ export const STD_WORLD: World = {
|
|
|
144
145
|
lazy: [{ after: 1 }],
|
|
145
146
|
summary: "It evaluates its part without capabilities, thus an effect in it fails.",
|
|
146
147
|
},
|
|
148
|
+
handle: {
|
|
149
|
+
verbs: { eval: "control.handle", view: "view.op" },
|
|
150
|
+
parts: {
|
|
151
|
+
shape: "record",
|
|
152
|
+
fields: {
|
|
153
|
+
with: { shape: "list", item: { shape: "record", fields: { op: { shape: "none" }, do: { shape: "term" }, abort: { shape: "term" } }, required: ["op"], closed: true } },
|
|
154
|
+
return: { shape: "term" },
|
|
155
|
+
body: { shape: "term" },
|
|
156
|
+
},
|
|
157
|
+
required: ["with", "body"],
|
|
158
|
+
closed: true,
|
|
159
|
+
},
|
|
160
|
+
scope: [
|
|
161
|
+
{ names: { fixed: ["payload"] }, in: [["with", "*", "do"], ["with", "*", "abort"]] },
|
|
162
|
+
{ names: { fixed: ["result"] }, in: [["return"]] },
|
|
163
|
+
],
|
|
164
|
+
caps: [{ rule: "grant", from: ["with", "*", "op"], in: [["body"]] }],
|
|
165
|
+
// A clause runs at an effect of the body, and the clause return runs only after a body without a failure.
|
|
166
|
+
lazy: [{ in: [["with", "*", "do"], ["with", "*", "abort"], ["return"]] }],
|
|
167
|
+
summary: "It answers the effects of its body with its clauses, in place, thus a mock, a retry or a log is a document.",
|
|
168
|
+
},
|
|
147
169
|
log: {
|
|
148
170
|
verbs: { eval: "effect.log", view: "view.op" },
|
|
149
171
|
parts: { shape: "list", of: "text" },
|
|
@@ -1493,6 +1515,18 @@ function* inOrder(parts: readonly RunPart[]): Generator<Promise<unknown>, { read
|
|
|
1493
1515
|
return { values };
|
|
1494
1516
|
}
|
|
1495
1517
|
|
|
1518
|
+
/** The value of a `handle` document, with a part in the place of each part. */
|
|
1519
|
+
interface HandleValue {
|
|
1520
|
+
readonly with: readonly { readonly op: Cap; readonly do?: RunPart; readonly abort?: RunPart }[];
|
|
1521
|
+
readonly return?: RunPart;
|
|
1522
|
+
readonly body?: RunPart;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/** This function tells if a value names a kind of effects: a text, or a list of keys. */
|
|
1526
|
+
function isOp(value: unknown): value is Cap {
|
|
1527
|
+
return typeof value === "string" || (Array.isArray(value) && value.every(isKey));
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1496
1530
|
/**
|
|
1497
1531
|
* The runners of the standard world, by name: the kinds that can hold effects. Each one evaluates its parts in
|
|
1498
1532
|
* order. In a run, an effect ends before the next step starts.
|
|
@@ -1516,6 +1550,32 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
|
1516
1550
|
if (only === undefined || parts.length > 1) return fail("#VALUE!", "pure needs exactly one part.");
|
|
1517
1551
|
return yield* only.evaluate();
|
|
1518
1552
|
},
|
|
1553
|
+
// A clause gets the context of the handle document, thus it has no more authority than its parent (HANDLE.CLAUSE).
|
|
1554
|
+
"control.handle": function* ({ value, context }) {
|
|
1555
|
+
const { with: list, return: after, body } = value as HandleValue;
|
|
1556
|
+
if (body?.handled === undefined) return fail("#VALUE!", "handle needs a body.");
|
|
1557
|
+
let stopped: Result | undefined;
|
|
1558
|
+
const clauses: Clause[] = [];
|
|
1559
|
+
for (const item of list) {
|
|
1560
|
+
const part = item.do ?? item.abort;
|
|
1561
|
+
if (!isOp(item.op)) return fail("#VALUE!", "The op of a clause of handle must be a text or a list of keys.");
|
|
1562
|
+
if (part === undefined || (item.do !== undefined && item.abort !== undefined)) return fail("#VALUE!", "Each clause of handle needs exactly one of do and abort.");
|
|
1563
|
+
const stops = item.do === undefined;
|
|
1564
|
+
clauses.push({
|
|
1565
|
+
op: item.op,
|
|
1566
|
+
answer: function* (payload) {
|
|
1567
|
+
const result = yield* part.evaluate({ ...context, names: { ...context.names, payload } });
|
|
1568
|
+
if (!stops) return result;
|
|
1569
|
+
stopped ??= result;
|
|
1570
|
+
return fail("#CALC!", "A clause abort of handle stopped the body.");
|
|
1571
|
+
},
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
const result = yield* body.handled(clauses);
|
|
1575
|
+
if (stopped !== undefined) return stopped;
|
|
1576
|
+
if (!result.ok || after === undefined) return result;
|
|
1577
|
+
return yield* after.evaluate({ ...context, names: { ...context.names, result: result.value } });
|
|
1578
|
+
},
|
|
1519
1579
|
"control.if": function* ({ parts }) {
|
|
1520
1580
|
const test = parts.find((part) => part.key === "test");
|
|
1521
1581
|
if (test === undefined) return fail("#VALUE!", "if needs a test.");
|