@mark1russell7/coeval 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +40 -2
- package/build/index.d.ts +1600 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3378 -0
- package/build/index.js.map +1 -0
- package/package.json +49 -3
- package/src/classify.ts +54 -0
- package/src/context.ts +193 -0
- package/src/deref.ts +401 -0
- package/src/doc.ts +251 -0
- package/src/errors.ts +141 -0
- package/src/evaluate.ts +1069 -0
- package/src/formula.ts +409 -0
- package/src/index.ts +183 -0
- package/src/ladder.ts +141 -0
- package/src/order.ts +111 -0
- package/src/parts.ts +156 -0
- package/src/static.ts +364 -0
- package/src/stdlib.ts +310 -0
- package/src/world.ts +237 -0
package/src/evaluate.ts
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, isRecord, typeName } from "./doc.ts";
|
|
2
|
+
import { splitParts } from "./parts.ts";
|
|
3
|
+
import { ask, type Answer } from "./ladder.ts";
|
|
4
|
+
import { type Side, lookupKind } from "./world.ts";
|
|
5
|
+
import { type ErrorCode, type EvalError, type Result, fail, ok, placed, thrown, describe } from "./errors.ts";
|
|
6
|
+
import {
|
|
7
|
+
type Cap,
|
|
8
|
+
type Context,
|
|
9
|
+
type Navigator,
|
|
10
|
+
type Resolution,
|
|
11
|
+
type ResolveOptions,
|
|
12
|
+
type Thing,
|
|
13
|
+
context as makeRootContext,
|
|
14
|
+
isHostThing,
|
|
15
|
+
resolve,
|
|
16
|
+
worldThing,
|
|
17
|
+
} from "./deref.ts";
|
|
18
|
+
|
|
19
|
+
export type { Context } from "./deref.ts";
|
|
20
|
+
export type { ErrorCode, EvalError, Result } from "./errors.ts";
|
|
21
|
+
export { fail, ok } from "./errors.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function makes a context with names and capabilities. The other roots are empty, and an evaluation sets
|
|
25
|
+
* the world.
|
|
26
|
+
*
|
|
27
|
+
* @param names - The names and their values.
|
|
28
|
+
* @param caps - The effects that the term can cause.
|
|
29
|
+
* @param self - The node of the term, if it has one.
|
|
30
|
+
* @returns The context.
|
|
31
|
+
*/
|
|
32
|
+
export function makeContext(names: Readonly<Record<string, unknown>> = {}, caps: readonly Cap[] = [], self: Thing = null): Context {
|
|
33
|
+
return makeRootContext({ names, caps, self });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function gives the keys of a capability or of a kind of effect: a text is split at its dots.
|
|
38
|
+
*
|
|
39
|
+
* @param kind - The text or the keys.
|
|
40
|
+
* @returns The keys, as texts.
|
|
41
|
+
*/
|
|
42
|
+
export function capKeys(kind: Cap): readonly string[] {
|
|
43
|
+
return typeof kind === "string" ? kind.split(".") : kind.map(String);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* This function tells if the capabilities permit an effect. A capability permits the kind of its name, and each kind
|
|
48
|
+
* under it: `store` permits `store.update`. The comparison is key by key, thus `fs.read` does not permit the
|
|
49
|
+
* kind with the keys `fs` and `read.json`.
|
|
50
|
+
*
|
|
51
|
+
* @param caps - The capabilities of the context.
|
|
52
|
+
* @param kind - The kind of the effect: a text with dots, or its keys.
|
|
53
|
+
* @returns `true` when one capability permits it.
|
|
54
|
+
*/
|
|
55
|
+
export function permits(caps: readonly Cap[], kind: Cap): boolean {
|
|
56
|
+
const want = capKeys(kind);
|
|
57
|
+
return caps.some((cap) => {
|
|
58
|
+
const have = capKeys(cap);
|
|
59
|
+
return have.length <= want.length && have.every((key, index) => key === want[index]);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* This function narrows a context for a part: the part keeps only the capabilities that the context of its parent
|
|
65
|
+
* permits. Thus an evaluator cannot give a part more effects than it has.
|
|
66
|
+
*
|
|
67
|
+
* @param parent - The context of the parent.
|
|
68
|
+
* @param next - The context that the evaluator gives the part.
|
|
69
|
+
* @returns The context of the part.
|
|
70
|
+
*/
|
|
71
|
+
export function narrow(parent: Context, next: Context): Context {
|
|
72
|
+
if (next === parent || next.caps === parent.caps) return next;
|
|
73
|
+
const caps = next.caps.filter((cap) => permits(parent.caps, cap));
|
|
74
|
+
return caps.length === next.caps.length ? next : { ...next, caps };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The steps of an evaluation that can wait. It yields a promise only when the host makes it wait. A runner takes the
|
|
79
|
+
* result of a part with `yield*`, thus a run goes on at once until an effect waits. A runner does not yield a
|
|
80
|
+
* promise itself: it uses `yield*` on the steps that the kernel gives.
|
|
81
|
+
*/
|
|
82
|
+
export type Steps = Generator<Promise<unknown>, Result, unknown>;
|
|
83
|
+
|
|
84
|
+
/** An effect that an evaluation caused, in the order of the effects. */
|
|
85
|
+
export interface Effect {
|
|
86
|
+
readonly index: number;
|
|
87
|
+
/** The kind of the effect, as a text with dots. */
|
|
88
|
+
readonly kind: string;
|
|
89
|
+
/** The keys of the kind of the effect. */
|
|
90
|
+
readonly path: readonly Key[];
|
|
91
|
+
readonly payload: unknown;
|
|
92
|
+
/** The ID of the trace node that caused the effect, or -1 without a trace. */
|
|
93
|
+
readonly node: number;
|
|
94
|
+
/** The number of events before the effect. The effect comes before the end of its node. */
|
|
95
|
+
readonly at: number;
|
|
96
|
+
/** The keys from the root document to the document that caused the effect. */
|
|
97
|
+
readonly place: readonly Key[];
|
|
98
|
+
/** The context of the document that caused the effect: its names, its node and its capabilities. */
|
|
99
|
+
readonly context: Context;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The host side of a run: it does one effect, and gives its result. A throw or a rejection is an error value. */
|
|
103
|
+
export type Perform = (effect: Effect) => unknown;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The runtime of one evaluation or run. Compiled code gets it with the context, and asks it to resolve paths and
|
|
107
|
+
* to cause effects.
|
|
108
|
+
*/
|
|
109
|
+
export interface Runtime {
|
|
110
|
+
readonly machine: Machine;
|
|
111
|
+
/** `true` in a run: the host does each effect, and a part that waits needs a parent that can wait. */
|
|
112
|
+
readonly running: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* This function resolves a path from a context.
|
|
115
|
+
*
|
|
116
|
+
* @param ctx - The context.
|
|
117
|
+
* @param path - The steps. The first step is a root.
|
|
118
|
+
* @param options - `hops: true` records each hop. The default records them only in a traced evaluation.
|
|
119
|
+
* @returns What the path reached, or an error.
|
|
120
|
+
*/
|
|
121
|
+
resolve(ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution;
|
|
122
|
+
/**
|
|
123
|
+
* This function causes an effect from an evaluator. In a run, an evaluator cannot cause an effect: the host does
|
|
124
|
+
* each effect, thus only a runner can wait for it.
|
|
125
|
+
*
|
|
126
|
+
* @param ctx - The context of the document.
|
|
127
|
+
* @param kind - The kind of the effect.
|
|
128
|
+
* @param payload - The data of the effect.
|
|
129
|
+
* @param place - The place of the document.
|
|
130
|
+
* @returns The payload, or an error value.
|
|
131
|
+
*/
|
|
132
|
+
effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result;
|
|
133
|
+
/**
|
|
134
|
+
* This function causes an effect from a runner. In a run, the host does it, and the steps wait when the host
|
|
135
|
+
* waits.
|
|
136
|
+
*
|
|
137
|
+
* @param ctx - The context of the document.
|
|
138
|
+
* @param kind - The kind of the effect.
|
|
139
|
+
* @param payload - The data of the effect.
|
|
140
|
+
* @param place - The place of the document.
|
|
141
|
+
* @returns The steps that give the result of the host, or an error value.
|
|
142
|
+
*/
|
|
143
|
+
runEffect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps;
|
|
144
|
+
/**
|
|
145
|
+
* This function drives several steps at the same time. When one waits, the others go on.
|
|
146
|
+
*
|
|
147
|
+
* @param list - One function for each item. Each function starts the steps of its item.
|
|
148
|
+
* @param options - The most items that run at the same time. The default is each item.
|
|
149
|
+
* @returns The steps that give each result, in the order of the list.
|
|
150
|
+
*/
|
|
151
|
+
all(list: readonly (() => Steps)[], options?: { readonly concurrency?: number }): Generator<Promise<unknown>, readonly Result[], unknown>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Compiled code: the evaluation of a document in a context. */
|
|
155
|
+
export type Code = (ctx: Context, rt: Runtime) => Result;
|
|
156
|
+
|
|
157
|
+
/** Compiled code that can wait: the run of a document in a context. */
|
|
158
|
+
export type RunCode = (ctx: Context, rt: Runtime) => Steps;
|
|
159
|
+
|
|
160
|
+
/** A compiled part: its place in its parent, its binding name, and its code. It has no field with its value. */
|
|
161
|
+
export interface CompiledPart {
|
|
162
|
+
/** The path in the value of the parent as one text, for example `0` or `bind.x`. */
|
|
163
|
+
readonly key: string;
|
|
164
|
+
/** The keys from the value of the parent to the part. */
|
|
165
|
+
readonly path: readonly Key[];
|
|
166
|
+
/** The binding name from the header of the part. */
|
|
167
|
+
readonly name?: string;
|
|
168
|
+
/** The code that evaluates the part. */
|
|
169
|
+
readonly code: Code;
|
|
170
|
+
/** The code that runs the part. Its steps can wait. */
|
|
171
|
+
readonly run: RunCode;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The input of a compiler: the document, its kind, and its parts, compiled. */
|
|
175
|
+
export interface CompileInput {
|
|
176
|
+
/** The text of the type of the document, for example `add` or `math.sum` after its alias. */
|
|
177
|
+
readonly kind: string;
|
|
178
|
+
/** The keys of the kind after each alias. */
|
|
179
|
+
readonly typePath: readonly Key[];
|
|
180
|
+
readonly doc: Envelope;
|
|
181
|
+
/** The value of the document with a compiled part in the place of each part. */
|
|
182
|
+
readonly value: unknown;
|
|
183
|
+
/** The value of the document without its parts. */
|
|
184
|
+
readonly data: Json;
|
|
185
|
+
/** The compiled parts, in the order of the value. */
|
|
186
|
+
readonly parts: readonly CompiledPart[];
|
|
187
|
+
/** The keys from the root document to this document. */
|
|
188
|
+
readonly place: readonly Key[];
|
|
189
|
+
readonly machine: Machine;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A sealed part. An evaluator can read the binding name of a part and evaluate it. It cannot read the value of the
|
|
194
|
+
* part, thus two parts that give the same results can replace each other.
|
|
195
|
+
*/
|
|
196
|
+
export interface Part {
|
|
197
|
+
/** The path in the value of the parent as one text, for example `0` or `bind.x`. */
|
|
198
|
+
readonly key: string;
|
|
199
|
+
/** The keys from the value of the parent to the part. */
|
|
200
|
+
readonly path: readonly Key[];
|
|
201
|
+
/** The binding name from the header of the part. */
|
|
202
|
+
readonly name?: string;
|
|
203
|
+
/**
|
|
204
|
+
* This function evaluates the part. Each call is a new evaluation.
|
|
205
|
+
*
|
|
206
|
+
* @param context - The context for the part. The default is the context of the parent.
|
|
207
|
+
* @returns The result of the part.
|
|
208
|
+
*/
|
|
209
|
+
evaluate(context?: Context): Result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The input of an evaluator. */
|
|
213
|
+
export interface EvalInput {
|
|
214
|
+
/** The text of the type of the document, for example `add` or `math.sum` after its alias. */
|
|
215
|
+
readonly kind: string;
|
|
216
|
+
/** The keys of the kind after each alias. */
|
|
217
|
+
readonly typePath: readonly Key[];
|
|
218
|
+
readonly doc: Envelope;
|
|
219
|
+
/** The value of the document with a sealed part in the place of each part. */
|
|
220
|
+
readonly value: unknown;
|
|
221
|
+
/** The value of the document without its parts. */
|
|
222
|
+
readonly data: Json;
|
|
223
|
+
/** The parts, in the order of the value. */
|
|
224
|
+
readonly parts: readonly Part[];
|
|
225
|
+
/** The context of the document. */
|
|
226
|
+
readonly context: Context;
|
|
227
|
+
/** The keys from the root document to this document. */
|
|
228
|
+
readonly place: readonly Key[];
|
|
229
|
+
/**
|
|
230
|
+
* This function resolves a path from the context of the document.
|
|
231
|
+
*
|
|
232
|
+
* @param path - The steps. The first step is a root.
|
|
233
|
+
* @param options - `hops: true` records each hop.
|
|
234
|
+
* @returns What the path reached, or an error.
|
|
235
|
+
*/
|
|
236
|
+
resolve(path: readonly unknown[], options?: ResolveOptions): Resolution;
|
|
237
|
+
/**
|
|
238
|
+
* This function causes an effect. The context must have the capability for the kind of the effect. In a run, an
|
|
239
|
+
* evaluator cannot cause an effect: the host does each effect, thus only a runner can wait for it.
|
|
240
|
+
*
|
|
241
|
+
* @param kind - The kind of the effect, for example `log`.
|
|
242
|
+
* @param payload - The data of the effect.
|
|
243
|
+
* @returns The payload, or the error `#CAP!`.
|
|
244
|
+
*/
|
|
245
|
+
effect(kind: Cap, payload: unknown): Result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A part for a runner. Its evaluation can wait for the effects in it. */
|
|
249
|
+
export interface RunPart {
|
|
250
|
+
/** The path in the value of the parent as one text. */
|
|
251
|
+
readonly key: string;
|
|
252
|
+
/** The keys from the value of the parent to the part. */
|
|
253
|
+
readonly path: readonly Key[];
|
|
254
|
+
/** The binding name from the header of the part. */
|
|
255
|
+
readonly name?: string;
|
|
256
|
+
/**
|
|
257
|
+
* This function evaluates the part. Each call is a new evaluation. Take its result with `yield*`.
|
|
258
|
+
*
|
|
259
|
+
* @param context - The context for the part. The default is the context of the parent.
|
|
260
|
+
* @returns The steps of the evaluation of the part.
|
|
261
|
+
*/
|
|
262
|
+
evaluate(context?: Context): Steps;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The input of a runner: the input of an evaluator, with parts and effects that can wait. */
|
|
266
|
+
export interface RunInput {
|
|
267
|
+
readonly kind: string;
|
|
268
|
+
readonly typePath: readonly Key[];
|
|
269
|
+
readonly doc: Envelope;
|
|
270
|
+
/** The value of the document with a part for a runner in the place of each part. */
|
|
271
|
+
readonly value: unknown;
|
|
272
|
+
readonly data: Json;
|
|
273
|
+
readonly parts: readonly RunPart[];
|
|
274
|
+
readonly context: Context;
|
|
275
|
+
readonly place: readonly Key[];
|
|
276
|
+
resolve(path: readonly unknown[], options?: ResolveOptions): Resolution;
|
|
277
|
+
/**
|
|
278
|
+
* This function causes an effect. In a run, the host does the effect, and the steps wait when the host waits. In
|
|
279
|
+
* an evaluation, the effect gives its payload. Take its result with `yield*`.
|
|
280
|
+
*
|
|
281
|
+
* @param kind - The kind of the effect, for example `log`.
|
|
282
|
+
* @param payload - The data of the effect.
|
|
283
|
+
* @returns The steps that give the result of the host, or an error value.
|
|
284
|
+
*/
|
|
285
|
+
effect(kind: Cap, payload: unknown): Steps;
|
|
286
|
+
/**
|
|
287
|
+
* This function evaluates parts at the same time. Take the results with `yield*`.
|
|
288
|
+
*
|
|
289
|
+
* @param parts - The parts.
|
|
290
|
+
* @param options - The most parts that run at the same time.
|
|
291
|
+
* @returns The steps that give each result, in the order of the parts.
|
|
292
|
+
*/
|
|
293
|
+
all(parts: readonly RunPart[], options?: { readonly concurrency?: number }): Generator<Promise<unknown>, readonly Result[], unknown>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** An evaluator that is a function of its input. The kernel calls it for each evaluation. */
|
|
297
|
+
export type EvalFn = (input: EvalInput) => Result;
|
|
298
|
+
|
|
299
|
+
/** An evaluator that compiles a document one time into code. The code then evaluates it many times. */
|
|
300
|
+
export interface CompiledEvaluator {
|
|
301
|
+
/**
|
|
302
|
+
* This function compiles a document.
|
|
303
|
+
*
|
|
304
|
+
* @param input - The document, its kind and its compiled parts.
|
|
305
|
+
* @returns The code that evaluates the document.
|
|
306
|
+
*/
|
|
307
|
+
readonly compile: (input: CompileInput) => Code;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** An evaluator: a function of its input, or a compiler. */
|
|
311
|
+
export type Evaluator = EvalFn | CompiledEvaluator;
|
|
312
|
+
|
|
313
|
+
/** A runner that is a generator function of its input. */
|
|
314
|
+
export type RunFn = (input: RunInput) => Steps;
|
|
315
|
+
|
|
316
|
+
/** A runner that compiles a document one time into code that can wait. */
|
|
317
|
+
export interface CompiledRunner {
|
|
318
|
+
/**
|
|
319
|
+
* This function compiles a document.
|
|
320
|
+
*
|
|
321
|
+
* @param input - The document, its kind and its compiled parts.
|
|
322
|
+
* @returns The code that runs the document.
|
|
323
|
+
*/
|
|
324
|
+
readonly compileRun: (input: CompileInput) => RunCode;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* A runner: the code of a kind whose parts or effects can wait. The same runner serves an evaluation, which does
|
|
329
|
+
* not wait at any time, and a run, in which the host does each effect.
|
|
330
|
+
*/
|
|
331
|
+
export type Runner = RunFn | CompiledRunner;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A machine: a world of kinds, and the code of each evaluator name, of each `deref` and of each walker.
|
|
335
|
+
*
|
|
336
|
+
* - `maxDepth`: the deepest nesting of documents. The default is 1000.
|
|
337
|
+
* - `permits`: the rule of the host for an effect. The default is the function `permits` on the capabilities.
|
|
338
|
+
*/
|
|
339
|
+
export interface Machine extends Navigator {
|
|
340
|
+
readonly evaluators: Readonly<Record<string, Evaluator>>;
|
|
341
|
+
/** The runners, by evaluator name. A runner wins over an evaluator of the same name. */
|
|
342
|
+
readonly runners?: Readonly<Record<string, Runner>>;
|
|
343
|
+
readonly maxDepth?: number;
|
|
344
|
+
readonly permits?: (caps: readonly Cap[], kind: readonly Key[], context: Context) => boolean;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** One evaluation in a trace. The children are the evaluations of its parts, in the order that they started. */
|
|
348
|
+
export interface TraceNode {
|
|
349
|
+
readonly id: number;
|
|
350
|
+
/** The keys from the root document to this document, as texts. */
|
|
351
|
+
readonly path: readonly string[];
|
|
352
|
+
/** The document. A bare value has the type that its position declares. */
|
|
353
|
+
readonly doc: Envelope;
|
|
354
|
+
/** `true` when the type came from the container and not from an envelope. */
|
|
355
|
+
readonly declared: boolean;
|
|
356
|
+
/** The answer of the ladder to the verb `eval`. */
|
|
357
|
+
readonly answer: Answer;
|
|
358
|
+
readonly side: Side;
|
|
359
|
+
/** The names that the document could read when its evaluation started. */
|
|
360
|
+
readonly names: readonly string[];
|
|
361
|
+
/** The names and their values when the evaluation started. */
|
|
362
|
+
readonly scope: Readonly<Record<string, unknown>>;
|
|
363
|
+
/** The capabilities that the document had when its evaluation started. */
|
|
364
|
+
readonly caps: readonly Cap[];
|
|
365
|
+
/** A short text for the root `self` when the evaluation started. */
|
|
366
|
+
readonly self: string;
|
|
367
|
+
/** The time of the start. Each start and each end takes one tick. */
|
|
368
|
+
readonly enter: number;
|
|
369
|
+
/** The time of the end. */
|
|
370
|
+
readonly exit: number;
|
|
371
|
+
readonly result: Result;
|
|
372
|
+
readonly children: readonly TraceNode[];
|
|
373
|
+
/** The indices of the effects that this document caused itself. */
|
|
374
|
+
readonly effects: readonly number[];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The record of one evaluation of a document. */
|
|
378
|
+
export interface Run {
|
|
379
|
+
readonly result: Result;
|
|
380
|
+
readonly trace: TraceNode;
|
|
381
|
+
readonly effects: readonly Effect[];
|
|
382
|
+
/** The number of ticks. */
|
|
383
|
+
readonly time: number;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
interface Building {
|
|
387
|
+
id: number;
|
|
388
|
+
path: readonly string[];
|
|
389
|
+
doc: Envelope;
|
|
390
|
+
declared: boolean;
|
|
391
|
+
answer: Answer;
|
|
392
|
+
side: Side;
|
|
393
|
+
names: readonly string[];
|
|
394
|
+
scope: Readonly<Record<string, unknown>>;
|
|
395
|
+
caps: readonly Cap[];
|
|
396
|
+
self: string;
|
|
397
|
+
enter: number;
|
|
398
|
+
exit: number;
|
|
399
|
+
result: Result;
|
|
400
|
+
children: Building[];
|
|
401
|
+
effects: number[];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** What a trace node knows from the compilation. */
|
|
405
|
+
interface Meta {
|
|
406
|
+
readonly path: readonly string[];
|
|
407
|
+
readonly doc: Envelope;
|
|
408
|
+
readonly declared: boolean;
|
|
409
|
+
readonly answer: Answer;
|
|
410
|
+
readonly side: Side;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function selfLabel(self: Thing): string {
|
|
414
|
+
try {
|
|
415
|
+
if (isHostThing(self)) return self.label;
|
|
416
|
+
if (self === null || self === undefined) return "none";
|
|
417
|
+
return (JSON.stringify(self) ?? describe(self)).slice(0, 40);
|
|
418
|
+
} catch {
|
|
419
|
+
return describe(self).slice(0, 40);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const NO_ANSWER: Answer = { verb: "eval", error: "no-answer", steps: [] };
|
|
424
|
+
|
|
425
|
+
/** A signal of the host that stops a run: the shape of the standard `AbortSignal`. */
|
|
426
|
+
export interface StopSignal {
|
|
427
|
+
readonly aborted: boolean;
|
|
428
|
+
readonly reason?: unknown;
|
|
429
|
+
addEventListener(type: "abort", listener: () => void, options?: { readonly once?: boolean }): void;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The options of a run. */
|
|
433
|
+
export interface RunOptions {
|
|
434
|
+
/** The host code that does each effect. Without it, an effect gives its payload, as in an evaluation. */
|
|
435
|
+
readonly perform?: Perform;
|
|
436
|
+
/** The type of a bare root value. */
|
|
437
|
+
readonly declaredType?: string;
|
|
438
|
+
/** A signal of the host. When it aborts, the run stops with `#ABORT`. */
|
|
439
|
+
readonly signal?: StopSignal;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** The options of an evaluation of a plan. */
|
|
443
|
+
export interface EvalOptions {
|
|
444
|
+
/** A list that gets each effect, in its order. */
|
|
445
|
+
readonly effects?: Effect[];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** This class holds the state of one evaluation or run: the machine, the effects, the clock and the trace. */
|
|
449
|
+
class Session implements Runtime {
|
|
450
|
+
readonly effects: Effect[];
|
|
451
|
+
tick = 0;
|
|
452
|
+
nextId = 0;
|
|
453
|
+
current: Building | undefined;
|
|
454
|
+
readonly holder: Building | undefined;
|
|
455
|
+
|
|
456
|
+
constructor(
|
|
457
|
+
readonly machine: Machine,
|
|
458
|
+
readonly running: boolean,
|
|
459
|
+
readonly traced: boolean,
|
|
460
|
+
readonly perform: Perform | undefined,
|
|
461
|
+
readonly signal: StopSignal | undefined,
|
|
462
|
+
effects: Effect[] | undefined,
|
|
463
|
+
) {
|
|
464
|
+
this.effects = effects ?? [];
|
|
465
|
+
this.holder = traced ? holderNode() : undefined;
|
|
466
|
+
this.current = this.holder;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
resolve(ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution {
|
|
470
|
+
return resolve(this.machine, ctx, path, { hops: options?.hops ?? this.traced });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private permitted(ctx: Context, keys: readonly Key[]): boolean {
|
|
474
|
+
return this.machine.permits === undefined ? permits(ctx.caps, keys) : this.machine.permits(ctx.caps, keys, ctx);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private record(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result | Effect {
|
|
478
|
+
const keys = typeof kind === "string" ? kind.split(".") : kind;
|
|
479
|
+
const text = keys.join(".");
|
|
480
|
+
if (!this.permitted(ctx, keys)) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
|
|
481
|
+
const node = this.current;
|
|
482
|
+
const effect: Effect = { index: this.effects.length, kind: text, path: keys, payload, node: node?.id ?? -1, at: this.tick, place, context: ctx };
|
|
483
|
+
this.effects.push(effect);
|
|
484
|
+
if (node !== undefined) node.effects.push(effect.index);
|
|
485
|
+
return effect;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
|
|
489
|
+
const text = typeof kind === "string" ? kind : kind.join(".");
|
|
490
|
+
if (this.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
|
|
491
|
+
const made = this.record(ctx, kind, payload, place);
|
|
492
|
+
return "ok" in made ? made : ok(payload);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
*runEffect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
|
|
496
|
+
const made = this.record(ctx, kind, payload, place);
|
|
497
|
+
if ("ok" in made) return made;
|
|
498
|
+
const perform = this.perform;
|
|
499
|
+
if (!this.running || perform === undefined) return ok(payload);
|
|
500
|
+
const failed = (error: unknown): Result => fail("#CALC!", `The host failed the effect ${made.kind}: ${describe(error)}`, { place, cause: error });
|
|
501
|
+
let done: unknown;
|
|
502
|
+
try {
|
|
503
|
+
done = perform(made);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
return failed(error);
|
|
506
|
+
}
|
|
507
|
+
if (!(done instanceof Promise)) return ok(done);
|
|
508
|
+
return (yield done.then((value: unknown) => ok(value), failed)) as Result;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
*all(list: readonly (() => Steps)[], options: { readonly concurrency?: number } = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
|
|
512
|
+
const results: Result[] = new Array<Result>(list.length);
|
|
513
|
+
const limit = Math.max(1, options.concurrency ?? list.length);
|
|
514
|
+
interface Active {
|
|
515
|
+
readonly index: number;
|
|
516
|
+
readonly steps: Steps;
|
|
517
|
+
pending: Promise<unknown>;
|
|
518
|
+
}
|
|
519
|
+
const active: Active[] = [];
|
|
520
|
+
let next = 0;
|
|
521
|
+
const advance = (index: number, steps: Steps, input: { readonly value?: unknown; readonly error?: unknown; readonly threw: boolean }): void => {
|
|
522
|
+
let step: IteratorResult<Promise<unknown>, Result>;
|
|
523
|
+
try {
|
|
524
|
+
step = input.threw ? steps.throw(input.error) : steps.next(input.value);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
results[index] = thrown(error, "a part");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (step.done === true) {
|
|
530
|
+
results[index] = step.value;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const item: Active = { index, steps, pending: Promise.resolve() };
|
|
534
|
+
item.pending = step.value.then(
|
|
535
|
+
(value) => ({ item, value, threw: false }),
|
|
536
|
+
(error: unknown) => ({ item, error, threw: true }),
|
|
537
|
+
);
|
|
538
|
+
active.push(item);
|
|
539
|
+
};
|
|
540
|
+
const start = (): void => {
|
|
541
|
+
while (active.length < limit && next < list.length) {
|
|
542
|
+
const index = next;
|
|
543
|
+
next += 1;
|
|
544
|
+
const begin = list[index] as () => Steps;
|
|
545
|
+
let steps: Steps;
|
|
546
|
+
try {
|
|
547
|
+
steps = begin();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
results[index] = thrown(error, "a part");
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
advance(index, steps, { threw: false });
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
start();
|
|
556
|
+
while (active.length > 0) {
|
|
557
|
+
const settled = (yield Promise.race(active.map((item) => item.pending))) as {
|
|
558
|
+
readonly item: Active;
|
|
559
|
+
readonly value?: unknown;
|
|
560
|
+
readonly error?: unknown;
|
|
561
|
+
readonly threw: boolean;
|
|
562
|
+
};
|
|
563
|
+
active.splice(active.indexOf(settled.item), 1);
|
|
564
|
+
advance(settled.item.index, settled.item.steps, settled);
|
|
565
|
+
start();
|
|
566
|
+
}
|
|
567
|
+
return results;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
enter(meta: Meta, ctx: Context): Building {
|
|
571
|
+
const node: Building = {
|
|
572
|
+
id: this.nextId,
|
|
573
|
+
path: meta.path,
|
|
574
|
+
doc: meta.doc,
|
|
575
|
+
declared: meta.declared,
|
|
576
|
+
answer: meta.answer,
|
|
577
|
+
side: meta.side,
|
|
578
|
+
names: Object.keys(ctx.names),
|
|
579
|
+
scope: ctx.names,
|
|
580
|
+
caps: ctx.caps,
|
|
581
|
+
self: selfLabel(ctx.self),
|
|
582
|
+
enter: this.tick,
|
|
583
|
+
exit: this.tick,
|
|
584
|
+
result: fail("#NOEVAL", "The evaluation did not end."),
|
|
585
|
+
children: [],
|
|
586
|
+
effects: [],
|
|
587
|
+
};
|
|
588
|
+
this.nextId += 1;
|
|
589
|
+
this.tick += 1;
|
|
590
|
+
(this.current ?? (this.holder as Building)).children.push(node);
|
|
591
|
+
return node;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
exit(node: Building, result: Result): void {
|
|
595
|
+
node.result = result;
|
|
596
|
+
node.exit = this.tick;
|
|
597
|
+
this.tick += 1;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function holderNode(): Building {
|
|
602
|
+
return {
|
|
603
|
+
id: -1,
|
|
604
|
+
path: [],
|
|
605
|
+
doc: { type: "?", value: null },
|
|
606
|
+
declared: false,
|
|
607
|
+
answer: NO_ANSWER,
|
|
608
|
+
side: "derive",
|
|
609
|
+
names: [],
|
|
610
|
+
scope: {},
|
|
611
|
+
caps: [],
|
|
612
|
+
self: "none",
|
|
613
|
+
enter: 0,
|
|
614
|
+
exit: 0,
|
|
615
|
+
result: ok(null),
|
|
616
|
+
children: [],
|
|
617
|
+
effects: [],
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** This function drives steps that cannot wait. */
|
|
622
|
+
function drive(steps: Steps): Result {
|
|
623
|
+
const step = steps.next();
|
|
624
|
+
if (step.done === true) return step.value;
|
|
625
|
+
void step.value.catch(() => undefined);
|
|
626
|
+
void steps.return(fail("#CALC!", "A part waited, and its parent cannot wait."));
|
|
627
|
+
return fail("#CALC!", "A part waited, and its parent cannot wait.");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const ABORTED = Symbol("aborted");
|
|
631
|
+
|
|
632
|
+
/** This function drives steps that can wait. The steps before the first wait happen at once, in the call. */
|
|
633
|
+
async function driveWaiting(steps: Steps, signal: StopSignal | undefined): Promise<Result> {
|
|
634
|
+
const abort = (): Result => {
|
|
635
|
+
const reason = fail("#ABORT", `The host stopped the run: ${describe(signal?.reason ?? "no reason")}`, { cause: signal?.reason });
|
|
636
|
+
try {
|
|
637
|
+
steps.return(reason);
|
|
638
|
+
} catch {
|
|
639
|
+
// The finally code of a runner threw. The run stops with the abort.
|
|
640
|
+
}
|
|
641
|
+
return reason;
|
|
642
|
+
};
|
|
643
|
+
const aborted =
|
|
644
|
+
signal === undefined
|
|
645
|
+
? undefined
|
|
646
|
+
: new Promise<typeof ABORTED>((resolve) => {
|
|
647
|
+
if (signal.aborted) resolve(ABORTED);
|
|
648
|
+
else signal.addEventListener("abort", () => resolve(ABORTED), { once: true });
|
|
649
|
+
});
|
|
650
|
+
if (signal?.aborted === true) return abort();
|
|
651
|
+
let step = steps.next();
|
|
652
|
+
while (step.done !== true) {
|
|
653
|
+
let value: unknown;
|
|
654
|
+
try {
|
|
655
|
+
value = aborted === undefined ? await step.value : await Promise.race([step.value, aborted]);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
step = steps.throw(error);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (value === ABORTED) return abort();
|
|
661
|
+
step = steps.next(value);
|
|
662
|
+
}
|
|
663
|
+
return step.value;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function isCompiledEvaluator(evaluator: Evaluator): evaluator is CompiledEvaluator {
|
|
667
|
+
return typeof evaluator === "object" && evaluator !== null && typeof (evaluator as CompiledEvaluator).compile === "function";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function isCompiledRunner(runner: Runner): runner is CompiledRunner {
|
|
671
|
+
return typeof runner === "object" && runner !== null && typeof (runner as CompiledRunner).compileRun === "function";
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** The options of a compilation. */
|
|
675
|
+
export interface PrepareOptions {
|
|
676
|
+
/** The type of a bare root value. */
|
|
677
|
+
readonly declaredType?: string;
|
|
678
|
+
/** `true`: each evaluation records a trace. The default is `false`. */
|
|
679
|
+
readonly trace?: boolean;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** A compiled document. It evaluates many times, and it does the work of the ladder one time. */
|
|
683
|
+
export interface Plan {
|
|
684
|
+
readonly machine: Machine;
|
|
685
|
+
/** `true` when each evaluation records a trace. */
|
|
686
|
+
readonly traced: boolean;
|
|
687
|
+
/** The compiled root document. */
|
|
688
|
+
readonly root: CompiledPart;
|
|
689
|
+
/**
|
|
690
|
+
* This function evaluates the document. It does not wait, and the host does no effect.
|
|
691
|
+
*
|
|
692
|
+
* @param context - The names and the capabilities at the root.
|
|
693
|
+
* @param options - A list that gets the effects.
|
|
694
|
+
* @returns The result.
|
|
695
|
+
*/
|
|
696
|
+
evaluate(context?: Context, options?: EvalOptions): Result;
|
|
697
|
+
/**
|
|
698
|
+
* This function runs the document: the host does each effect, and the run waits when the host waits.
|
|
699
|
+
*
|
|
700
|
+
* @param context - The names and the capabilities at the root.
|
|
701
|
+
* @param options - The host code that does the effects, and a signal to stop the run.
|
|
702
|
+
* @returns The result, when the run ends.
|
|
703
|
+
*/
|
|
704
|
+
run(context?: Context, options?: RunOptions): Promise<Result>;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
interface Compiler {
|
|
708
|
+
readonly machine: Machine;
|
|
709
|
+
readonly traced: boolean;
|
|
710
|
+
readonly maxDepth: number;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function failing(c: Compiler, meta: Meta, place: readonly Key[], code: ErrorCode, reason: string): { readonly code: Code; readonly run: RunCode } {
|
|
714
|
+
const result = fail(code, reason, { place });
|
|
715
|
+
const plain: Code = () => result;
|
|
716
|
+
return finish(c, meta, plain, function* () {
|
|
717
|
+
return result;
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function finish(c: Compiler, meta: Meta, code: Code, run: RunCode): { readonly code: Code; readonly run: RunCode } {
|
|
722
|
+
if (!c.traced) return { code, run };
|
|
723
|
+
const tracedCode: Code = (ctx, rt) => {
|
|
724
|
+
const s = rt as Session;
|
|
725
|
+
const node = s.enter(meta, ctx);
|
|
726
|
+
const before = s.current;
|
|
727
|
+
s.current = node;
|
|
728
|
+
let result: Result;
|
|
729
|
+
try {
|
|
730
|
+
result = code(ctx, rt);
|
|
731
|
+
} finally {
|
|
732
|
+
s.current = before;
|
|
733
|
+
}
|
|
734
|
+
s.exit(node, result);
|
|
735
|
+
return result;
|
|
736
|
+
};
|
|
737
|
+
const tracedRun: RunCode = function* (ctx, rt) {
|
|
738
|
+
const s = rt as Session;
|
|
739
|
+
const node = s.enter(meta, ctx);
|
|
740
|
+
const steps = run(ctx, rt);
|
|
741
|
+
let input: unknown;
|
|
742
|
+
let threw = false;
|
|
743
|
+
for (;;) {
|
|
744
|
+
const before = s.current;
|
|
745
|
+
s.current = node;
|
|
746
|
+
let step: IteratorResult<Promise<unknown>, Result>;
|
|
747
|
+
try {
|
|
748
|
+
step = threw ? steps.throw(input) : steps.next(input);
|
|
749
|
+
} finally {
|
|
750
|
+
s.current = before;
|
|
751
|
+
}
|
|
752
|
+
if (step.done === true) {
|
|
753
|
+
s.exit(node, step.value);
|
|
754
|
+
return step.value;
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
input = yield step.value;
|
|
758
|
+
threw = false;
|
|
759
|
+
} catch (error) {
|
|
760
|
+
input = error;
|
|
761
|
+
threw = true;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
return { code: tracedCode, run: tracedRun };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number): { readonly code: Code; readonly run: RunCode } {
|
|
769
|
+
const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
|
|
770
|
+
const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
|
|
771
|
+
const path = place.map(String);
|
|
772
|
+
const meta0: Meta = { path, doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" };
|
|
773
|
+
if (envelope === undefined && declared === undefined) {
|
|
774
|
+
return failing(c, meta0, place, "#PART?", "The value has no envelope, and its position declares no type.");
|
|
775
|
+
}
|
|
776
|
+
if (depth > c.maxDepth) return failing(c, meta0, place, "#DEPTH!", `The nesting is deeper than ${c.maxDepth} documents.`);
|
|
777
|
+
|
|
778
|
+
const answer = ask(c.machine.world, envelope ?? {}, "eval", declared === undefined ? {} : { declaredType: declared });
|
|
779
|
+
const found = lookupKind(c.machine.world, doc.type);
|
|
780
|
+
const kind = found.ok ? found.kind : undefined;
|
|
781
|
+
const meta: Meta = { ...meta0, answer, side: kind?.side ?? "derive" };
|
|
782
|
+
const kindText = typeName(doc.type);
|
|
783
|
+
if (answer.error === "unknown-kind") return failing(c, meta, place, "#KIND?", `The world has no kind ${kindText}.`);
|
|
784
|
+
const name = answer.evaluator;
|
|
785
|
+
const runners = c.machine.runners ?? {};
|
|
786
|
+
const runner = name !== undefined && Object.hasOwn(runners, name) ? runners[name] : undefined;
|
|
787
|
+
const evaluator = name !== undefined && Object.hasOwn(c.machine.evaluators, name) ? c.machine.evaluators[name] : undefined;
|
|
788
|
+
if (name === undefined || (runner === undefined && evaluator === undefined)) {
|
|
789
|
+
return failing(c, meta, place, "#NOEVAL", `No rung gives an evaluator for ${kindText}.`);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const split = splitParts(kind?.parts, doc.value);
|
|
793
|
+
if (!split.ok) {
|
|
794
|
+
const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
|
|
795
|
+
return failing(c, meta, place, "#VALUE!", `${split.reason}${at} The kind is ${kindText}.`);
|
|
796
|
+
}
|
|
797
|
+
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
798
|
+
const compiled = compileNode(c, slot.item, [...place, ...slot.path], slot.of, depth + 1);
|
|
799
|
+
const header = isEnvelope(slot.item) && isRecord(slot.item) && typeof slot.item["name"] === "string" ? slot.item["name"] : undefined;
|
|
800
|
+
return { key: slot.key, path: slot.path, ...(header === undefined ? {} : { name: header }), code: compiled.code, run: compiled.run };
|
|
801
|
+
});
|
|
802
|
+
const typePath = found.ok ? found.path : [];
|
|
803
|
+
const input: CompileInput = {
|
|
804
|
+
kind: kindText,
|
|
805
|
+
typePath,
|
|
806
|
+
doc,
|
|
807
|
+
value: split.parts.length === 0 ? doc.value : split.fill(parts),
|
|
808
|
+
data: split.data,
|
|
809
|
+
parts,
|
|
810
|
+
place,
|
|
811
|
+
machine: c.machine,
|
|
812
|
+
};
|
|
813
|
+
const who = `the evaluator ${name}`;
|
|
814
|
+
|
|
815
|
+
if (runner !== undefined) {
|
|
816
|
+
const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
|
|
817
|
+
const run: RunCode = function* (ctx, rt) {
|
|
818
|
+
try {
|
|
819
|
+
return placed(yield* inner(ctx, rt), place);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
return placed(thrown(error, `the runner ${name}`), place);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
const code: Code = (ctx, rt) =>
|
|
825
|
+
rt.running
|
|
826
|
+
? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
|
|
827
|
+
: drive(run(ctx, rt));
|
|
828
|
+
return finish(c, meta, code, run);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const inner: Code = isCompiledEvaluator(evaluator as Evaluator)
|
|
832
|
+
? (evaluator as CompiledEvaluator).compile(input)
|
|
833
|
+
: interpret(evaluator as EvalFn, input, split.fill);
|
|
834
|
+
const code: Code = (ctx, rt) => {
|
|
835
|
+
try {
|
|
836
|
+
return placed(inner(ctx, rt), place);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
return placed(thrown(error, who), place);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
return finish(c, meta, code, function* (ctx, rt) {
|
|
842
|
+
return code(ctx, rt);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
|
|
847
|
+
const { kind, typePath, doc, data, place } = input;
|
|
848
|
+
const compiled = input.parts;
|
|
849
|
+
const hasParts = compiled.length > 0;
|
|
850
|
+
return (ctx, rt) => {
|
|
851
|
+
const parts: Part[] = compiled.map((part) => ({
|
|
852
|
+
key: part.key,
|
|
853
|
+
path: part.path,
|
|
854
|
+
...(part.name === undefined ? {} : { name: part.name }),
|
|
855
|
+
evaluate: (override?: Context) => part.code(override === undefined ? ctx : narrow(ctx, override), rt),
|
|
856
|
+
}));
|
|
857
|
+
return fn({
|
|
858
|
+
kind,
|
|
859
|
+
typePath,
|
|
860
|
+
doc,
|
|
861
|
+
value: hasParts ? fill(parts) : doc.value,
|
|
862
|
+
data,
|
|
863
|
+
parts,
|
|
864
|
+
context: ctx,
|
|
865
|
+
place,
|
|
866
|
+
resolve: (path, options) => rt.resolve(ctx, path, options),
|
|
867
|
+
effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
|
|
868
|
+
});
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): RunCode {
|
|
873
|
+
const { kind, typePath, doc, data, place } = input;
|
|
874
|
+
const compiled = input.parts;
|
|
875
|
+
const hasParts = compiled.length > 0;
|
|
876
|
+
return (ctx, rt) => {
|
|
877
|
+
const parts: RunPart[] = compiled.map((part) => ({
|
|
878
|
+
key: part.key,
|
|
879
|
+
path: part.path,
|
|
880
|
+
...(part.name === undefined ? {} : { name: part.name }),
|
|
881
|
+
evaluate: (override?: Context) => part.run(override === undefined ? ctx : narrow(ctx, override), rt),
|
|
882
|
+
}));
|
|
883
|
+
return fn({
|
|
884
|
+
kind,
|
|
885
|
+
typePath,
|
|
886
|
+
doc,
|
|
887
|
+
value: hasParts ? fill(parts) : doc.value,
|
|
888
|
+
data,
|
|
889
|
+
parts,
|
|
890
|
+
context: ctx,
|
|
891
|
+
place,
|
|
892
|
+
resolve: (path, options) => rt.resolve(ctx, path, options),
|
|
893
|
+
effect: (effectKind, payload) => rt.runEffect(ctx, effectKind, payload, place),
|
|
894
|
+
all: (list, options) => rt.all(list.map((part) => () => part.evaluate()), options),
|
|
895
|
+
});
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function rootContext(machine: Machine, ctx: Context): Context {
|
|
900
|
+
return ctx.world === null ? { ...ctx, world: worldThing(machine.world) } : ctx;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* This function compiles a document into a plan. The ladder, the kinds and the parts are known after it. A plan
|
|
905
|
+
* evaluates many times. A document that does not compile gives a plan that fails with the reason, when it evaluates.
|
|
906
|
+
*
|
|
907
|
+
* @param machine - The world and the code of the evaluators.
|
|
908
|
+
* @param doc - The document, as JSON.
|
|
909
|
+
* @param options - The type of a bare root value, and a trace.
|
|
910
|
+
* @returns The plan.
|
|
911
|
+
*/
|
|
912
|
+
export function prepare(machine: Machine, doc: unknown, options: PrepareOptions = {}): Plan {
|
|
913
|
+
const c: Compiler = { machine, traced: options.trace === true, maxDepth: machine.maxDepth ?? 1000 };
|
|
914
|
+
let root: { readonly code: Code; readonly run: RunCode };
|
|
915
|
+
try {
|
|
916
|
+
root = compileNode(c, doc, [], options.declaredType, 0);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
const result = thrown(error, "the compilation");
|
|
919
|
+
root = {
|
|
920
|
+
code: () => result,
|
|
921
|
+
run: function* () {
|
|
922
|
+
return result;
|
|
923
|
+
},
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
const compiled: CompiledPart = { key: "", path: [], code: root.code, run: root.run };
|
|
927
|
+
return {
|
|
928
|
+
machine,
|
|
929
|
+
traced: c.traced,
|
|
930
|
+
root: compiled,
|
|
931
|
+
evaluate(context: Context = makeContext(), evalOptions: EvalOptions = {}): Result {
|
|
932
|
+
const s = new Session(machine, false, c.traced, undefined, undefined, evalOptions.effects);
|
|
933
|
+
return root.code(rootContext(machine, context), s);
|
|
934
|
+
},
|
|
935
|
+
run(context: Context = makeContext(), runOptions: RunOptions = {}): Promise<Result> {
|
|
936
|
+
const s = new Session(machine, true, c.traced, runOptions.perform, runOptions.signal, undefined);
|
|
937
|
+
return driveWaiting(root.run(rootContext(machine, context), s), runOptions.signal);
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* This function makes a cache of plans for one machine. A document object compiles one time. Use it only for
|
|
944
|
+
* documents that do not change after they are made.
|
|
945
|
+
*
|
|
946
|
+
* @param machine - The machine.
|
|
947
|
+
* @param options - The options of each compilation.
|
|
948
|
+
* @returns A function that gives the plan of a document.
|
|
949
|
+
*/
|
|
950
|
+
export function planner(machine: Machine, options: PrepareOptions = {}): (doc: unknown) => Plan {
|
|
951
|
+
const objects = new WeakMap<object, Plan>();
|
|
952
|
+
const values = new Map<unknown, Plan>();
|
|
953
|
+
return (doc) => {
|
|
954
|
+
if (typeof doc === "object" && doc !== null) {
|
|
955
|
+
let plan = objects.get(doc);
|
|
956
|
+
if (plan === undefined) {
|
|
957
|
+
plan = prepare(machine, doc, options);
|
|
958
|
+
objects.set(doc, plan);
|
|
959
|
+
}
|
|
960
|
+
return plan;
|
|
961
|
+
}
|
|
962
|
+
let plan = values.get(doc);
|
|
963
|
+
if (plan === undefined) {
|
|
964
|
+
plan = prepare(machine, doc, options);
|
|
965
|
+
if (values.size > 1000) values.clear();
|
|
966
|
+
values.set(doc, plan);
|
|
967
|
+
}
|
|
968
|
+
return plan;
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function record(s: Session, result: Result): Run {
|
|
973
|
+
const root = s.holder?.children[0];
|
|
974
|
+
if (root === undefined) throw new Error("The evaluation made no trace.");
|
|
975
|
+
return { result, trace: root as TraceNode, effects: s.effects, time: s.tick };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* This function evaluates a document, and records each evaluation in a trace.
|
|
980
|
+
*
|
|
981
|
+
* The verb `eval` goes up the ladder for each document. The kind of the document tells where its parts are. The
|
|
982
|
+
* evaluator gets each part sealed: it can evaluate the part, and it cannot read its value. A failure gives an error
|
|
983
|
+
* value, and the function does not throw. An effect is recorded in its order, and nothing does it.
|
|
984
|
+
*
|
|
985
|
+
* @param machine - The world and the code of the evaluators.
|
|
986
|
+
* @param doc - The document, as JSON.
|
|
987
|
+
* @param context - The names and the capabilities at the root.
|
|
988
|
+
* @param declaredType - The type of a bare root value.
|
|
989
|
+
* @returns The result, the trace and the effects in their order.
|
|
990
|
+
*/
|
|
991
|
+
export function evaluate(machine: Machine, doc: unknown, context: Context = makeContext(), declaredType?: string): Run {
|
|
992
|
+
const plan = prepare(machine, doc, { trace: true, ...(declaredType === undefined ? {} : { declaredType }) });
|
|
993
|
+
const s = new Session(machine, false, true, undefined, undefined, undefined);
|
|
994
|
+
const result = plan.root.code(rootContext(machine, context), s);
|
|
995
|
+
return record(s, result);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* This function runs a document: an evaluation in which the host does each effect. A runner waits for its
|
|
1000
|
+
* parts and for its effects, thus a step can read the result of the step before. The part of the run before the
|
|
1001
|
+
* first wait happens at once, in the call. The run records a trace.
|
|
1002
|
+
*
|
|
1003
|
+
* @param machine - The world, the evaluators and the runners.
|
|
1004
|
+
* @param doc - The document, as JSON.
|
|
1005
|
+
* @param context - The names and the capabilities at the root.
|
|
1006
|
+
* @param options - The host code that does the effects, the type of a bare root value, and a signal.
|
|
1007
|
+
* @returns The result, the trace and the effects in their order, when the run ends.
|
|
1008
|
+
*/
|
|
1009
|
+
export async function run(machine: Machine, doc: unknown, context: Context = makeContext(), options: RunOptions = {}): Promise<Run> {
|
|
1010
|
+
const plan = prepare(machine, doc, { trace: true, ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }) });
|
|
1011
|
+
const s = new Session(machine, true, true, options.perform, options.signal, undefined);
|
|
1012
|
+
const result = await driveWaiting(plan.root.run(rootContext(machine, context), s), options.signal);
|
|
1013
|
+
return record(s, result);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* This function finds a node of a trace by its path.
|
|
1018
|
+
*
|
|
1019
|
+
* @param trace - The root of the trace.
|
|
1020
|
+
* @param path - The keys from the root.
|
|
1021
|
+
* @returns The first evaluation at that path, or `undefined` when the document did not evaluate.
|
|
1022
|
+
*/
|
|
1023
|
+
export function nodeAt(trace: TraceNode, path: readonly Key[]): TraceNode | undefined {
|
|
1024
|
+
const want = path.map(String);
|
|
1025
|
+
let node: TraceNode | undefined = trace;
|
|
1026
|
+
let at = 0;
|
|
1027
|
+
while (node !== undefined && at < want.length) {
|
|
1028
|
+
const here: TraceNode = node;
|
|
1029
|
+
node = here.children.find((child) => {
|
|
1030
|
+
const tail = child.path.slice(here.path.length);
|
|
1031
|
+
return tail.length > 0 && tail.every((key, index) => key === want[at + index]);
|
|
1032
|
+
});
|
|
1033
|
+
if (node !== undefined) at += node.path.length - here.path.length;
|
|
1034
|
+
}
|
|
1035
|
+
return node;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* This function lists the nodes of a trace in the order of their start.
|
|
1040
|
+
*
|
|
1041
|
+
* @param trace - The root of the trace.
|
|
1042
|
+
* @returns Each node, the root first.
|
|
1043
|
+
*/
|
|
1044
|
+
export function flatten(trace: TraceNode): readonly TraceNode[] {
|
|
1045
|
+
const out: TraceNode[] = [];
|
|
1046
|
+
const stack: TraceNode[] = [trace];
|
|
1047
|
+
while (stack.length > 0) {
|
|
1048
|
+
const node = stack.pop() as TraceNode;
|
|
1049
|
+
out.push(node);
|
|
1050
|
+
for (let index = node.children.length - 1; index >= 0; index -= 1) stack.push(node.children[index] as TraceNode);
|
|
1051
|
+
}
|
|
1052
|
+
return out.sort((a, b) => a.enter - b.enter);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* This function lists the errors of a trace: each node whose result is an error and whose parts did not fail
|
|
1057
|
+
* the same way. These nodes are where each error started.
|
|
1058
|
+
*
|
|
1059
|
+
* @param trace - The root of the trace.
|
|
1060
|
+
* @returns The errors, in the order of their start.
|
|
1061
|
+
*/
|
|
1062
|
+
export function failures(trace: TraceNode): readonly EvalError[] {
|
|
1063
|
+
return flatten(trace)
|
|
1064
|
+
.filter((node) => !node.result.ok && !node.children.some((child) => !child.result.ok && child.result.error === (node.result as { error: EvalError }).error))
|
|
1065
|
+
.map((node) => (node.result as { readonly error: EvalError }).error);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** The type of a document as a reference: re-exported for the compilers. */
|
|
1069
|
+
export type { TypeRef };
|