@mark1russell7/coeval 0.6.2 → 0.6.3
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 +6 -4
- package/build/index.d.ts +189 -22
- package/build/index.d.ts.map +1 -1
- package/build/index.js +370 -85
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +43 -54
- package/src/deref.ts +14 -3
- package/src/doc.ts +23 -2
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +192 -21
- package/src/index.ts +6 -1
- package/src/reference.ts +83 -12
- package/src/replay.ts +159 -0
- package/src/same.ts +42 -0
- package/src/stdlib.ts +4 -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.6.
|
|
4
|
+
"version": "0.6.3",
|
|
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/agree.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type Key, docEquals,
|
|
1
|
+
import { type Key, docEquals, equalWith } from "./doc.ts";
|
|
2
2
|
import type { Context } from "./deref.ts";
|
|
3
3
|
import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
|
|
4
|
-
import { type Effect, type Machine, evaluate, makeContext, prepare } from "./evaluate.ts";
|
|
4
|
+
import { type Effect, type Machine, type Perform, evaluate, makeContext, prepare } from "./evaluate.ts";
|
|
5
5
|
import { Held, isClosure } from "./formula.ts";
|
|
6
6
|
import { type Definitions, reference } from "./reference.ts";
|
|
7
7
|
|
|
@@ -21,6 +21,12 @@ export interface TierResult {
|
|
|
21
21
|
readonly effects?: readonly Effect[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** What `agree` gives the second tier of a language beside the document and the context. */
|
|
25
|
+
export interface TierInput {
|
|
26
|
+
/** The mock of the host, when `agree` has one (AGREE.MOCK). The tier answers each effect with it. */
|
|
27
|
+
readonly perform?: Perform;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
/** The options of `agree`. */
|
|
25
31
|
export interface AgreeOptions {
|
|
26
32
|
/** The type of a bare root value. */
|
|
@@ -34,9 +40,15 @@ export interface AgreeOptions {
|
|
|
34
40
|
*
|
|
35
41
|
* @param doc - The document.
|
|
36
42
|
* @param context - The context.
|
|
43
|
+
* @param input - The mock of the host, when `agree` has one.
|
|
37
44
|
* @returns The result, or the result with the effects.
|
|
38
45
|
*/
|
|
39
|
-
readonly other?: (doc: unknown, context: Context) => Result | TierResult;
|
|
46
|
+
readonly other?: (doc: unknown, context: Context, input: TierInput) => Result | TierResult;
|
|
47
|
+
/**
|
|
48
|
+
* The mock of the host (RUN.MOCK, AGREE.MOCK). Each tier gets it, thus the tiers agree on documents with effects. A
|
|
49
|
+
* mock must give the same answer to the same effect, because each tier asks it again.
|
|
50
|
+
*/
|
|
51
|
+
readonly perform?: Perform;
|
|
40
52
|
/**
|
|
41
53
|
* This function compares two values that the rules of `sameValue` do not know, for example two dates.
|
|
42
54
|
*
|
|
@@ -80,8 +92,9 @@ export type Agreement =
|
|
|
80
92
|
* errors have the same code and place. Each other object is the same only as itself, unless the rule of the caller
|
|
81
93
|
* tells otherwise.
|
|
82
94
|
*
|
|
83
|
-
* `docEquals`
|
|
84
|
-
*
|
|
95
|
+
* `docEquals` and `sameResult` use the same walk (D20). `docEquals` has no rules for closures, for `Held` values and
|
|
96
|
+
* for a rule of the caller. `sameResult` is for an early cutoff, thus a closure is the same there only as itself, and
|
|
97
|
+
* two errors also need the same reason.
|
|
85
98
|
*
|
|
86
99
|
* @param a - The first value.
|
|
87
100
|
* @param b - The second value.
|
|
@@ -89,47 +102,14 @@ export type Agreement =
|
|
|
89
102
|
* @returns `true` when the values are the same.
|
|
90
103
|
*/
|
|
91
104
|
export function sameValue(a: unknown, b: unknown, equal?: (a: unknown, b: unknown) => boolean | undefined): boolean {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
if (typeof x !== "object" && typeof x !== "function") return false;
|
|
103
|
-
if (typeof y !== "object" && typeof y !== "function") return false;
|
|
104
|
-
if (x === null || y === null) return false;
|
|
105
|
-
// A pair that the comparison already visits is the same, unless another pair shows a difference.
|
|
106
|
-
const pairs = seen.get(x) ?? new Set<object>();
|
|
107
|
-
if (pairs.has(y)) continue;
|
|
108
|
-
pairs.add(y);
|
|
109
|
-
seen.set(x, pairs);
|
|
110
|
-
if (isClosure(x) || isClosure(y)) {
|
|
111
|
-
if (!(isClosure(x) && isClosure(y))) return false;
|
|
112
|
-
stack.push([closureParams(x), closureParams(y)]);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (x instanceof Held || y instanceof Held) {
|
|
116
|
-
if (!(x instanceof Held && y instanceof Held) || errorDiffers(x.error, y.error, false) !== undefined) return false;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (Array.isArray(x) || Array.isArray(y)) {
|
|
120
|
-
if (!Array.isArray(x) || !Array.isArray(y) || x.length !== y.length) return false;
|
|
121
|
-
for (let index = 0; index < x.length; index += 1) stack.push([x[index], y[index]]);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (!isRecord(x) || !isRecord(y)) return false;
|
|
125
|
-
const keys = Object.keys(x);
|
|
126
|
-
if (keys.length !== Object.keys(y).length) return false;
|
|
127
|
-
for (const key of keys) {
|
|
128
|
-
if (!Object.hasOwn(y, key)) return false;
|
|
129
|
-
stack.push([x[key], y[key]]);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return true;
|
|
105
|
+
return equalWith(a, b, equal === undefined ? sameInTiers : (x, y) => equal(x, y) ?? sameInTiers(x, y));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The rules of `sameValue` for the values that two tiers make again: closures and held errors. */
|
|
109
|
+
function sameInTiers(x: unknown, y: unknown): boolean | undefined {
|
|
110
|
+
if (isClosure(x) || isClosure(y)) return isClosure(x) && isClosure(y) && docEquals(closureParams(x), closureParams(y));
|
|
111
|
+
if (x instanceof Held || y instanceof Held) return x instanceof Held && y instanceof Held && errorDiffers(x.error, y.error, false) === undefined;
|
|
112
|
+
return undefined;
|
|
133
113
|
}
|
|
134
114
|
|
|
135
115
|
const CLOSURE = /* @__PURE__ */ Symbol.for("coeval.closure");
|
|
@@ -165,7 +145,10 @@ function resultDiffers(a: Result, b: Result, options: AgreeOptions): string | un
|
|
|
165
145
|
return a.ok ? `The plan gives a value, and the tier gives ${codeAndPlace((b as { readonly error: EvalError }).error)}.` : `The plan gives ${codeAndPlace(a.error)}, and the tier gives a value.`;
|
|
166
146
|
}
|
|
167
147
|
|
|
168
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* This function gives the difference of two lists of effects, or `undefined` when they agree (EFFECT.ORDER). When both
|
|
150
|
+
* tiers record the address or the outcome of an effect, these must agree too (AGREE.MOCK).
|
|
151
|
+
*/
|
|
169
152
|
function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: AgreeOptions): string | undefined {
|
|
170
153
|
if (a.length !== b.length) return `The plan causes ${a.length} effects, and the tier causes ${b.length}.`;
|
|
171
154
|
for (let index = 0; index < a.length; index += 1) {
|
|
@@ -174,6 +157,11 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
|
|
|
174
157
|
if (x.kind !== y.kind || !docEquals(x.place as readonly Key[], y.place as readonly Key[]) || !sameValue(x.payload, y.payload, options.equal)) {
|
|
175
158
|
return `The effect ${index} differs: ${x.kind} at [${x.place.join(", ")}] in the plan, and ${y.kind} at [${y.place.join(", ")}] in the tier.`;
|
|
176
159
|
}
|
|
160
|
+
if (x.address !== undefined && y.address !== undefined && x.address !== y.address) {
|
|
161
|
+
return `The effect ${index} has the address ${x.address} in the plan, and ${y.address} in the tier.`;
|
|
162
|
+
}
|
|
163
|
+
const why = x.outcome !== undefined && y.outcome !== undefined ? resultDiffers(x.outcome, y.outcome, options) : undefined;
|
|
164
|
+
if (why !== undefined) return `The outcome of the effect ${index} differs. ${why}`;
|
|
177
165
|
}
|
|
178
166
|
return undefined;
|
|
179
167
|
}
|
|
@@ -182,21 +170,22 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
|
|
|
182
170
|
* This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
|
|
183
171
|
* results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
|
|
184
172
|
* both have one. The effects must have the same kinds, places and payloads, in the same order. A language gives its
|
|
185
|
-
* own tier with the option `other`.
|
|
173
|
+
* own tier with the option `other`. Each tier gets the mock of the option `perform` (AGREE.MOCK).
|
|
186
174
|
*
|
|
187
175
|
* @param machine - The world and the code of the evaluators.
|
|
188
176
|
* @param doc - The document.
|
|
189
177
|
* @param context - The names and the capabilities at the root.
|
|
190
|
-
* @param options - The tiers, the definitions for the reference, the rules of the comparison
|
|
178
|
+
* @param options - The tiers, the definitions for the reference, the rules of the comparison, the second tier and the mock.
|
|
191
179
|
* @returns The result of the plan, or the first tier that disagrees and the reason.
|
|
192
180
|
*/
|
|
193
181
|
export function agree(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeOptions = {}): Agreement {
|
|
194
182
|
const declared = options.declaredType === undefined ? {} : { declaredType: options.declaredType };
|
|
183
|
+
const mock = options.perform === undefined ? {} : { perform: options.perform };
|
|
195
184
|
const observe = options.observe;
|
|
196
185
|
observe?.();
|
|
197
186
|
const plan = prepare(machine, doc, declared);
|
|
198
187
|
const effects: Effect[] = [];
|
|
199
|
-
const result = plan.evaluate(context, { effects });
|
|
188
|
+
const result = plan.evaluate(context, { effects, ...mock });
|
|
200
189
|
const saw = observe?.();
|
|
201
190
|
|
|
202
191
|
const differs = (other: Result, otherEffects: readonly Effect[] | undefined): string | undefined => {
|
|
@@ -210,13 +199,13 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
210
199
|
let other: Result;
|
|
211
200
|
let otherEffects: Effect[] = [];
|
|
212
201
|
if (tier === "reference") {
|
|
213
|
-
other = reference(machine, doc, context, { ...declared, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
|
|
202
|
+
other = reference(machine, doc, context, { ...declared, ...mock, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
|
|
214
203
|
} else if (tier === "traced") {
|
|
215
|
-
const traced = evaluate(machine, doc, context,
|
|
204
|
+
const traced = evaluate(machine, doc, context, { ...declared, ...mock });
|
|
216
205
|
other = traced.result;
|
|
217
206
|
otherEffects = [...traced.effects];
|
|
218
207
|
} else {
|
|
219
|
-
other = plan.evaluate(context, { effects: otherEffects });
|
|
208
|
+
other = plan.evaluate(context, { effects: otherEffects, ...mock });
|
|
220
209
|
}
|
|
221
210
|
const why = differs(other, otherEffects);
|
|
222
211
|
if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
|
|
@@ -225,7 +214,7 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
225
214
|
if (options.other !== undefined) {
|
|
226
215
|
let given: Result | TierResult;
|
|
227
216
|
try {
|
|
228
|
-
given = options.other(doc, context);
|
|
217
|
+
given = options.other(doc, context, mock);
|
|
229
218
|
} catch (error) {
|
|
230
219
|
given = thrown(error, "the tier");
|
|
231
220
|
}
|
package/src/deref.ts
CHANGED
|
@@ -17,12 +17,22 @@ export interface HostThing {
|
|
|
17
17
|
/** A short text for a reader, for example `node card`. */
|
|
18
18
|
readonly label: string;
|
|
19
19
|
/**
|
|
20
|
-
* This function answers one step.
|
|
20
|
+
* This function answers one step. A field that holds `undefined` also gives `undefined`. The function `has` tells
|
|
21
|
+
* the two cases apart (DEREF.HOST-UNDEFINED).
|
|
21
22
|
*
|
|
22
23
|
* @param key - The key of the step.
|
|
23
24
|
* @returns What the step reaches, or `undefined` when it reaches nothing.
|
|
24
25
|
*/
|
|
25
26
|
deref(key: Key): Thing | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* This optional function tells if the thing has a field for a key. The kernel asks it only when `deref` gives
|
|
29
|
+
* `undefined`. Then `true` tells that the field holds `undefined`. A thing without `has` reads `undefined` as a
|
|
30
|
+
* missing field.
|
|
31
|
+
*
|
|
32
|
+
* @param key - The key of the step.
|
|
33
|
+
* @returns `true` when the thing has the field.
|
|
34
|
+
*/
|
|
35
|
+
has?(key: Key): boolean;
|
|
26
36
|
/**
|
|
27
37
|
* This function gives the thing as a value, when a reference ends on it.
|
|
28
38
|
*
|
|
@@ -34,7 +44,7 @@ export interface HostThing {
|
|
|
34
44
|
* path without one call for each step.
|
|
35
45
|
*
|
|
36
46
|
* @param keys - The keys.
|
|
37
|
-
* @returns What the keys reach, or `undefined` when they reach nothing.
|
|
47
|
+
* @returns What the keys reach, or `undefined` when they reach nothing. `{ thing: undefined }` tells that the keys reach a field that holds `undefined`.
|
|
38
48
|
*/
|
|
39
49
|
resolve?(keys: readonly Key[]): { readonly thing: Thing } | undefined;
|
|
40
50
|
}
|
|
@@ -183,7 +193,8 @@ function plainStep(thing: unknown, key: Key): { readonly found: true; readonly v
|
|
|
183
193
|
export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly thing: Thing; readonly by: HopBy } | undefined {
|
|
184
194
|
if (isHostThing(thing)) {
|
|
185
195
|
const reached = thing.deref(key);
|
|
186
|
-
|
|
196
|
+
// DEREF.HOST-UNDEFINED: `has` tells a field that holds undefined from a missing field.
|
|
197
|
+
return reached !== undefined || thing.has?.(key) === true ? { thing: reached, by: "own" } : undefined;
|
|
187
198
|
}
|
|
188
199
|
if (nav.plain !== true && isEnvelope(thing)) {
|
|
189
200
|
const envelope = thing as JsonRecord & Envelope;
|
package/src/doc.ts
CHANGED
|
@@ -230,20 +230,41 @@ export function fromJson(value: unknown): Envelope | undefined {
|
|
|
230
230
|
* with this function does not keep a stale host value.
|
|
231
231
|
*
|
|
232
232
|
* The function does not use the stack for the depth, thus a deep document cannot overflow it. A cycle of lists or
|
|
233
|
-
* records does not make it loop. `sameValue` of `agree`
|
|
234
|
-
*
|
|
233
|
+
* records does not make it loop. `sameValue` of `agree` and `sameResult` use the same walk, and each adds its rules
|
|
234
|
+
* for other values (D20).
|
|
235
235
|
*
|
|
236
236
|
* @param a - The first value.
|
|
237
237
|
* @param b - The second value.
|
|
238
238
|
* @returns `true` when the values are equal.
|
|
239
239
|
*/
|
|
240
240
|
export function docEquals(a: unknown, b: unknown): boolean {
|
|
241
|
+
return equalWith(a, b, undefined);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A rule for two values that are not the same by `Object.is`: `true`, `false`, or `undefined` for the next rule. */
|
|
245
|
+
export type SameRule = (a: unknown, b: unknown) => boolean | undefined;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* This function is the one walk of `docEquals`, `sameValue` and `sameResult` (D20). It compares two values by the
|
|
249
|
+
* rules of `docEquals`. A rule of the caller answers first for each pair that is not the same by `Object.is`.
|
|
250
|
+
*
|
|
251
|
+
* @param a - The first value.
|
|
252
|
+
* @param b - The second value.
|
|
253
|
+
* @param rule - The rule of the caller. Its `undefined` gives the rules of `docEquals`.
|
|
254
|
+
* @returns `true` when the values are equal.
|
|
255
|
+
*/
|
|
256
|
+
export function equalWith(a: unknown, b: unknown, rule: SameRule | undefined): boolean {
|
|
241
257
|
const stack: [unknown, unknown][] = [[a, b]];
|
|
242
258
|
// The pairs of containers that the comparison already visits. It is made at the first pair of containers.
|
|
243
259
|
let seen: Map<object, Set<object>> | undefined;
|
|
244
260
|
while (stack.length > 0) {
|
|
245
261
|
const [x, y] = stack.pop() as [unknown, unknown];
|
|
246
262
|
if (Object.is(x, y)) continue;
|
|
263
|
+
const told = rule?.(x, y);
|
|
264
|
+
if (told !== undefined) {
|
|
265
|
+
if (told) continue;
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
247
268
|
const list = Array.isArray(x);
|
|
248
269
|
if (list !== Array.isArray(y)) return false;
|
|
249
270
|
// An object that is not a list and not a plain record is a host value: Object.is above compared it.
|
package/src/errors.ts
CHANGED
|
@@ -16,7 +16,8 @@ export type KernelCode =
|
|
|
16
16
|
| "#CAP!"
|
|
17
17
|
| "#CALC!"
|
|
18
18
|
| "#DEPTH!"
|
|
19
|
-
| "#ABORT"
|
|
19
|
+
| "#ABORT"
|
|
20
|
+
| "#REPLAY!";
|
|
20
21
|
|
|
21
22
|
/** The code of an error value. A language can add its own codes. Each code starts with `#`. */
|
|
22
23
|
export type ErrorCode = KernelCode | `#${string}`;
|
package/src/evaluate.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName
|
|
|
2
2
|
import { type Split, partsOf, pathMatches } from "./parts.ts";
|
|
3
3
|
import { ask, type Answer } from "./ladder.ts";
|
|
4
4
|
import type { CapsRule, Kind, Side, World } from "./world.ts";
|
|
5
|
-
import { type ErrorCode, type EvalError, type Result, fail, ok, placed, thrown, describe } from "./errors.ts";
|
|
5
|
+
import { type ErrorCode, type EvalError, type Result, Failure, fail, ok, placed, thrown, describe } from "./errors.ts";
|
|
6
6
|
import {
|
|
7
7
|
type Cap,
|
|
8
8
|
type Context,
|
|
@@ -135,6 +135,7 @@ export type Steps = Generator<Promise<unknown>, Result, unknown>;
|
|
|
135
135
|
|
|
136
136
|
/** An effect that an evaluation caused, in the order of the effects. */
|
|
137
137
|
export interface Effect {
|
|
138
|
+
/** The number of effects before this effect. Under `all`, this number follows the order of the host. */
|
|
138
139
|
readonly index: number;
|
|
139
140
|
/** The kind of the effect, as a text with dots. */
|
|
140
141
|
readonly kind: string;
|
|
@@ -149,11 +150,100 @@ export interface Effect {
|
|
|
149
150
|
readonly place: readonly Key[];
|
|
150
151
|
/** The context of the document that caused the effect: its names, its node and its capabilities. */
|
|
151
152
|
readonly context: Context;
|
|
153
|
+
/**
|
|
154
|
+
* The stable address of the effect (RUN.ADDRESS). The order of the host does not change it. The address has the
|
|
155
|
+
* keys of the place, a `#` and the count of the earlier effects at that place in its scope. An example is
|
|
156
|
+
* `1/bind/x#0`. Each item of `all` is a scope, and its prefix comes first: `all0.1/` is the item 1 of the first `all`
|
|
157
|
+
* of the scope. The kernel gives each effect its address.
|
|
158
|
+
*/
|
|
159
|
+
readonly address?: string;
|
|
160
|
+
/**
|
|
161
|
+
* The outcome of the effect: the result that it gave its document (RUN.OUTCOME). The kernel sets it when the answer
|
|
162
|
+
* comes. It stays `undefined` when the run stops first, for example at an abort.
|
|
163
|
+
*/
|
|
164
|
+
readonly outcome?: Result;
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* The host side of a run: it does one effect, and gives its result. A throw or a rejection is an error value, and a
|
|
169
|
+
* `Failure` gives its own error value. In an evaluation, this function is a mock: it answers at once, and a promise
|
|
170
|
+
* from it gives `#CALC!` (RUN.MOCK).
|
|
171
|
+
*/
|
|
155
172
|
export type Perform = (effect: Effect) => unknown;
|
|
156
173
|
|
|
174
|
+
/** The key of the journal of a recorder or of a replayer, on its function. Two copies of the kernel share it. */
|
|
175
|
+
export const JOURNAL: unique symbol = /* @__PURE__ */ Symbol.for("coeval.journal") as never;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The journal of a recorder or of a replayer, on its function `perform`. The kernel tells it the order in which the
|
|
179
|
+
* waits of each `all` settle. In a replay, the kernel asks the journal for that order (RUN.RECORD, RUN.REPLAY).
|
|
180
|
+
*/
|
|
181
|
+
export interface Journal {
|
|
182
|
+
/**
|
|
183
|
+
* This function gets the item whose wait settled.
|
|
184
|
+
*
|
|
185
|
+
* @param all - The address of the `all`, for example `all0` or `all0.1/all0`.
|
|
186
|
+
* @param item - The index of the item.
|
|
187
|
+
*/
|
|
188
|
+
readonly settled?: (all: string, item: number) => void;
|
|
189
|
+
/**
|
|
190
|
+
* This function gives the item whose wait settles next.
|
|
191
|
+
*
|
|
192
|
+
* @param all - The address of the `all`.
|
|
193
|
+
* @param waiting - The indices of the items that wait.
|
|
194
|
+
* @returns The index of an item that waits, or `undefined` for the order of the host.
|
|
195
|
+
*/
|
|
196
|
+
readonly next?: (all: string, waiting: readonly number[]) => number | undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** This function gives the journal of a function `perform`, when it has one. */
|
|
200
|
+
function journalOf(perform: Perform | undefined): Journal | undefined {
|
|
201
|
+
return perform === undefined ? undefined : (perform as { readonly [JOURNAL]?: Journal })[JOURNAL];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* This function gives the outcome of an effect that the host failed. A `Failure` gives its own error value, for
|
|
206
|
+
* example the recorded error of a replay or `#REPLAY!`. Each other exception gives `#CALC!`, with the exception as the
|
|
207
|
+
* cause (RUN.CAUSE).
|
|
208
|
+
*/
|
|
209
|
+
function hostFailed(kind: string, place: readonly Key[], error: unknown): Result {
|
|
210
|
+
if (error instanceof Failure) return placed({ ok: false, error: error.error }, place);
|
|
211
|
+
return fail("#CALC!", `The host failed the effect ${kind}: ${describe(error)}`, { place, cause: error });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The scope of the addresses of effects (RUN.ADDRESS). The root of an evaluation is one scope, and each item of `all`
|
|
216
|
+
* is a scope. The code of one scope does not interleave with itself, thus its counts do not depend on the host.
|
|
217
|
+
*/
|
|
218
|
+
interface Scope {
|
|
219
|
+
/** The text before each address of the scope, for example `all0.1/`. */
|
|
220
|
+
readonly prefix: string;
|
|
221
|
+
/** The number of earlier effects at each place, by the text of the place. It is made at the first effect. */
|
|
222
|
+
counts: Map<string, number> | undefined;
|
|
223
|
+
/** The number of earlier uses of `all` in the scope. */
|
|
224
|
+
alls: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* This function gives the text of a place in an address: each key, with a slash between two keys. A text key with
|
|
229
|
+
* other characters than letters, digits, `_`, `$` and `-` gets quotes.
|
|
230
|
+
*
|
|
231
|
+
* @param place - The keys from the root document to a document.
|
|
232
|
+
* @returns The text of the place.
|
|
233
|
+
*/
|
|
234
|
+
export function placeText(place: readonly Key[]): string {
|
|
235
|
+
let text = "";
|
|
236
|
+
for (let index = 0; index < place.length; index += 1) {
|
|
237
|
+
const key = place[index] as Key;
|
|
238
|
+
if (index > 0) text += "/";
|
|
239
|
+
text += typeof key === "number" || /^[\w$-]+$/.test(key) ? String(key) : JSON.stringify(key);
|
|
240
|
+
}
|
|
241
|
+
return text;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** An effect while the kernel waits for its outcome. */
|
|
245
|
+
type Recorded = { -readonly [K in keyof Effect]: Effect[K] };
|
|
246
|
+
|
|
157
247
|
/**
|
|
158
248
|
* The runtime of one evaluation or run. Compiled code gets it with the context, and asks it to resolve paths and
|
|
159
249
|
* to cause effects.
|
|
@@ -179,7 +269,7 @@ export interface Runtime {
|
|
|
179
269
|
* @param kind - The kind of the effect.
|
|
180
270
|
* @param payload - The data of the effect.
|
|
181
271
|
* @param place - The place of the document.
|
|
182
|
-
* @returns The payload, or an error value.
|
|
272
|
+
* @returns The answer of the mock, the payload without a mock, or an error value.
|
|
183
273
|
*/
|
|
184
274
|
effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result;
|
|
185
275
|
/**
|
|
@@ -316,7 +406,7 @@ export interface EvalInput {
|
|
|
316
406
|
*
|
|
317
407
|
* @param kind - The kind of the effect, for example `log`.
|
|
318
408
|
* @param payload - The data of the effect.
|
|
319
|
-
* @returns The payload, or
|
|
409
|
+
* @returns The answer of the mock, the payload without a mock, or an error value, for example `#CAP!`.
|
|
320
410
|
*/
|
|
321
411
|
effect(kind: Cap, payload: unknown): Result;
|
|
322
412
|
/**
|
|
@@ -360,7 +450,7 @@ export interface RunInput {
|
|
|
360
450
|
resolve(path: readonly unknown[], options?: ResolveOptions): Resolution;
|
|
361
451
|
/**
|
|
362
452
|
* This function causes an effect. In a run, the host does the effect, and the steps wait when the host waits. In
|
|
363
|
-
* an evaluation, the effect gives its payload. Take its result with `yield*`.
|
|
453
|
+
* an evaluation, a mock answers the effect at once, or the effect gives its payload. Take its result with `yield*`.
|
|
364
454
|
*
|
|
365
455
|
* @param kind - The kind of the effect, for example `log`.
|
|
366
456
|
* @param payload - The data of the effect.
|
|
@@ -547,6 +637,19 @@ export interface RunOptions {
|
|
|
547
637
|
export interface EvalOptions {
|
|
548
638
|
/** A list that gets each effect, in its order. */
|
|
549
639
|
readonly effects?: Effect[];
|
|
640
|
+
/**
|
|
641
|
+
* The mock of the host (RUN.MOCK). It answers each effect at once, and its answer is the result of the effect.
|
|
642
|
+
* Without it, an effect gives its payload (RUN.EVAL).
|
|
643
|
+
*/
|
|
644
|
+
readonly perform?: Perform;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** The options of `evaluate`. */
|
|
648
|
+
export interface EvaluateOptions {
|
|
649
|
+
/** The type of a bare root value. */
|
|
650
|
+
readonly declaredType?: string;
|
|
651
|
+
/** The mock of the host (RUN.MOCK). Without it, an effect gives its payload (RUN.EVAL). */
|
|
652
|
+
readonly perform?: Perform;
|
|
550
653
|
}
|
|
551
654
|
|
|
552
655
|
/** The options of a resolution without a record of hops, and with one. One object of each serves each call. */
|
|
@@ -561,6 +664,11 @@ class Session implements Runtime {
|
|
|
561
664
|
nextId = 0;
|
|
562
665
|
current: Building | undefined;
|
|
563
666
|
readonly holder: Building | undefined;
|
|
667
|
+
/**
|
|
668
|
+
* The scope of the addresses of the code that runs at this time (RUN.ADDRESS). `all` changes it for each item. The
|
|
669
|
+
* root scope is made at the first effect or `all`, thus an evaluation without effects makes no scope.
|
|
670
|
+
*/
|
|
671
|
+
private scope: Scope | undefined;
|
|
564
672
|
|
|
565
673
|
constructor(
|
|
566
674
|
readonly machine: Machine,
|
|
@@ -589,45 +697,90 @@ class Session implements Runtime {
|
|
|
589
697
|
return this.machine.permits === undefined ? permits(ctx.caps, keys) : this.machine.permits(ctx.caps, keys, ctx);
|
|
590
698
|
}
|
|
591
699
|
|
|
592
|
-
|
|
700
|
+
/** This function checks the capability of an effect, and records the effect with its address (RUN.ADDRESS). */
|
|
701
|
+
private record(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result | Recorded {
|
|
593
702
|
const keys = typeof kind === "string" ? kind.split(".") : kind;
|
|
594
703
|
const text = keys.join(".");
|
|
595
704
|
if (!this.permitted(ctx, keys)) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
|
|
596
705
|
const node = this.current;
|
|
597
706
|
const list = this.effects;
|
|
598
|
-
const
|
|
707
|
+
const scope = (this.scope ??= { prefix: "", counts: undefined, alls: 0 });
|
|
708
|
+
const at = placeText(place);
|
|
709
|
+
const counts = (scope.counts ??= new Map<string, number>());
|
|
710
|
+
const count = counts.get(at) ?? 0;
|
|
711
|
+
counts.set(at, count + 1);
|
|
712
|
+
const effect: Recorded = {
|
|
713
|
+
index: list.length,
|
|
714
|
+
kind: text,
|
|
715
|
+
path: keys,
|
|
716
|
+
payload,
|
|
717
|
+
node: node?.id ?? -1,
|
|
718
|
+
at: this.tick,
|
|
719
|
+
place,
|
|
720
|
+
context: ctx,
|
|
721
|
+
address: `${scope.prefix}${at}#${count}`,
|
|
722
|
+
};
|
|
599
723
|
list.push(effect);
|
|
600
724
|
if (node !== undefined) node.effects.push(effect.index);
|
|
601
725
|
return effect;
|
|
602
726
|
}
|
|
603
727
|
|
|
728
|
+
/**
|
|
729
|
+
* This function answers an effect at once, without a wait: the mock gives the result, or the effect gives its
|
|
730
|
+
* payload without a mock (RUN.MOCK, RUN.EVAL). A promise from the mock gives `#CALC!`.
|
|
731
|
+
*/
|
|
732
|
+
private answer(effect: Recorded, place: readonly Key[]): Result {
|
|
733
|
+
const perform = this.perform;
|
|
734
|
+
if (perform === undefined) return (effect.outcome = ok(effect.payload));
|
|
735
|
+
let done: unknown;
|
|
736
|
+
try {
|
|
737
|
+
done = perform(effect);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
return (effect.outcome = hostFailed(effect.kind, place, error));
|
|
740
|
+
}
|
|
741
|
+
if (!(done instanceof Promise)) return (effect.outcome = ok(done));
|
|
742
|
+
// The evaluation does not read the promise, thus its rejection must not be unhandled.
|
|
743
|
+
void done.catch(() => undefined);
|
|
744
|
+
return (effect.outcome = fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place }));
|
|
745
|
+
}
|
|
746
|
+
|
|
604
747
|
effect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
|
|
605
748
|
const text = typeof kind === "string" ? kind : kind.join(".");
|
|
606
749
|
if (this.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
|
|
607
750
|
const made = this.record(ctx, kind, payload, place);
|
|
608
|
-
return "ok" in made ? made :
|
|
751
|
+
return "ok" in made ? made : this.answer(made, place);
|
|
609
752
|
}
|
|
610
753
|
|
|
611
754
|
*runEffect(ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
|
|
612
755
|
const made = this.record(ctx, kind, payload, place);
|
|
613
756
|
if ("ok" in made) return made;
|
|
614
757
|
const perform = this.perform;
|
|
615
|
-
if (!this.running || perform === undefined) return
|
|
616
|
-
const failed = (error: unknown): Result => fail("#CALC!", `The host failed the effect ${made.kind}: ${describe(error)}`, { place, cause: error });
|
|
758
|
+
if (!this.running || perform === undefined) return this.answer(made, place);
|
|
617
759
|
let done: unknown;
|
|
618
760
|
try {
|
|
619
761
|
done = perform(made);
|
|
620
762
|
} catch (error) {
|
|
621
|
-
return
|
|
763
|
+
return (made.outcome = hostFailed(made.kind, place, error));
|
|
622
764
|
}
|
|
623
|
-
if (!(done instanceof Promise)) return ok(done);
|
|
624
|
-
|
|
765
|
+
if (!(done instanceof Promise)) return (made.outcome = ok(done));
|
|
766
|
+
const result = (yield done.then(
|
|
767
|
+
(value: unknown) => ok(value),
|
|
768
|
+
(error: unknown) => hostFailed(made.kind, place, error),
|
|
769
|
+
)) as Result;
|
|
770
|
+
// A run that stops at this wait does not reach this line, thus the effect keeps no outcome (RUN.OUTCOME).
|
|
771
|
+
return (made.outcome = result);
|
|
625
772
|
}
|
|
626
773
|
|
|
627
774
|
*all(list: readonly (() => Steps)[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
|
|
628
775
|
const results: Result[] = new Array<Result>(list.length);
|
|
629
776
|
const limit = Math.max(1, options.concurrency ?? list.length);
|
|
630
777
|
const failFast = options.failFast === true;
|
|
778
|
+
// The address of this call, and a scope for each item (RUN.ADDRESS). The journal of a replay reads the address.
|
|
779
|
+
const outer = (this.scope ??= { prefix: "", counts: undefined, alls: 0 });
|
|
780
|
+
const address = `${outer.prefix}all${outer.alls}`;
|
|
781
|
+
outer.alls += 1;
|
|
782
|
+
const scopes: Scope[] = new Array<Scope>(list.length);
|
|
783
|
+
const journal = journalOf(this.perform);
|
|
631
784
|
let failed = false;
|
|
632
785
|
interface Active {
|
|
633
786
|
readonly index: number;
|
|
@@ -642,11 +795,15 @@ class Session implements Runtime {
|
|
|
642
795
|
};
|
|
643
796
|
const advance = (index: number, steps: Steps, input: { readonly value?: unknown; readonly error?: unknown; readonly threw: boolean }): void => {
|
|
644
797
|
let step: IteratorResult<Promise<unknown>, Result>;
|
|
798
|
+
const before = this.scope;
|
|
799
|
+
this.scope = scopes[index] as Scope;
|
|
645
800
|
try {
|
|
646
801
|
step = input.threw ? steps.throw(input.error) : steps.next(input.value);
|
|
647
802
|
} catch (error) {
|
|
648
803
|
settle(index, thrown(error, "a part"));
|
|
649
804
|
return;
|
|
805
|
+
} finally {
|
|
806
|
+
this.scope = before;
|
|
650
807
|
}
|
|
651
808
|
if (step.done === true) {
|
|
652
809
|
settle(index, step.value);
|
|
@@ -663,25 +820,35 @@ class Session implements Runtime {
|
|
|
663
820
|
while (!failed && active.length < limit && next < list.length) {
|
|
664
821
|
const index = next;
|
|
665
822
|
next += 1;
|
|
823
|
+
const scope: Scope = { prefix: `${address}.${index}/`, counts: undefined, alls: 0 };
|
|
824
|
+
scopes[index] = scope;
|
|
666
825
|
const begin = list[index] as () => Steps;
|
|
667
826
|
let steps: Steps;
|
|
827
|
+
const before = this.scope;
|
|
828
|
+
this.scope = scope;
|
|
668
829
|
try {
|
|
669
830
|
steps = begin();
|
|
670
831
|
} catch (error) {
|
|
671
832
|
settle(index, thrown(error, "a part"));
|
|
672
833
|
continue;
|
|
834
|
+
} finally {
|
|
835
|
+
this.scope = before;
|
|
673
836
|
}
|
|
674
837
|
advance(index, steps, { threw: false });
|
|
675
838
|
}
|
|
676
839
|
};
|
|
677
840
|
start();
|
|
678
841
|
while (active.length > 0 && !failed) {
|
|
679
|
-
|
|
842
|
+
// A replay settles the waits in the recorded order. Each other run takes the first wait that settles.
|
|
843
|
+
const chosen = journal?.next?.(address, active.map((item) => item.index));
|
|
844
|
+
const pick = chosen === undefined ? undefined : active.find((item) => item.index === chosen);
|
|
845
|
+
const settled = (yield pick === undefined ? Promise.race(active.map((item) => item.pending)) : pick.pending) as {
|
|
680
846
|
readonly item: Active;
|
|
681
847
|
readonly value?: unknown;
|
|
682
848
|
readonly error?: unknown;
|
|
683
849
|
readonly threw: boolean;
|
|
684
850
|
};
|
|
851
|
+
journal?.settled?.(address, settled.item.index);
|
|
685
852
|
active.splice(active.indexOf(settled.item), 1);
|
|
686
853
|
advance(settled.item.index, settled.item.steps, settled);
|
|
687
854
|
start();
|
|
@@ -819,10 +986,11 @@ export interface Plan {
|
|
|
819
986
|
/** The compiled root document. */
|
|
820
987
|
readonly root: CompiledPart;
|
|
821
988
|
/**
|
|
822
|
-
* This function evaluates the document. It does not wait, and the host does no effect.
|
|
989
|
+
* This function evaluates the document. It does not wait, and the host does no effect. A mock can answer each
|
|
990
|
+
* effect at once (RUN.MOCK).
|
|
823
991
|
*
|
|
824
992
|
* @param context - The names and the capabilities at the root.
|
|
825
|
-
* @param options - A list that gets the effects.
|
|
993
|
+
* @param options - A list that gets the effects, and the mock of the host.
|
|
826
994
|
* @returns The result.
|
|
827
995
|
*/
|
|
828
996
|
evaluate(context?: Context, options?: EvalOptions): Result;
|
|
@@ -1161,7 +1329,7 @@ function planOf(machine: Machine, traced: boolean, root: CompiledPart): Plan {
|
|
|
1161
1329
|
traced,
|
|
1162
1330
|
root,
|
|
1163
1331
|
evaluate(context: Context = makeContext(), evalOptions?: EvalOptions): Result {
|
|
1164
|
-
const s = new Session(machine, false, traced,
|
|
1332
|
+
const s = new Session(machine, false, traced, evalOptions?.perform, undefined, evalOptions?.effects);
|
|
1165
1333
|
return root.code(rootContext(machine, context), s);
|
|
1166
1334
|
},
|
|
1167
1335
|
run(context: Context = makeContext(), runOptions: RunOptions = {}): Promise<Result> {
|
|
@@ -1212,17 +1380,20 @@ function record(s: Session, result: Result): Run {
|
|
|
1212
1380
|
*
|
|
1213
1381
|
* The verb `eval` goes up the ladder for each document. The kind of the document tells where its parts are. The
|
|
1214
1382
|
* evaluator gets each part sealed: it can evaluate the part, and it cannot read its value. A failure gives an error
|
|
1215
|
-
* value, and the function does not throw. An effect is recorded in its order, and
|
|
1383
|
+
* value, and the function does not throw. An effect is recorded in its order, and the host does not do it. A mock
|
|
1384
|
+
* can answer each effect at once (RUN.MOCK).
|
|
1216
1385
|
*
|
|
1217
1386
|
* @param machine - The world and the code of the evaluators.
|
|
1218
1387
|
* @param doc - The document, as JSON.
|
|
1219
1388
|
* @param context - The names and the capabilities at the root.
|
|
1220
|
-
* @param
|
|
1389
|
+
* @param options - The type of a bare root value, or the options: that type and the mock of the host.
|
|
1221
1390
|
* @returns The result, the trace and the effects in their order.
|
|
1222
1391
|
*/
|
|
1223
|
-
export function evaluate(machine: Machine, doc: unknown, context: Context = makeContext(),
|
|
1392
|
+
export function evaluate(machine: Machine, doc: unknown, context: Context = makeContext(), options?: string | EvaluateOptions): Run {
|
|
1393
|
+
const given: EvaluateOptions = typeof options === "string" ? { declaredType: options } : (options ?? {});
|
|
1394
|
+
const declaredType = given.declaredType;
|
|
1224
1395
|
const plan = prepare(machine, doc, { trace: true, ...(declaredType === undefined ? {} : { declaredType }) });
|
|
1225
|
-
const s = new Session(machine, false, true,
|
|
1396
|
+
const s = new Session(machine, false, true, given.perform, undefined, undefined);
|
|
1226
1397
|
const result = plan.root.code(rootContext(machine, context), s);
|
|
1227
1398
|
return record(s, result);
|
|
1228
1399
|
}
|