@mark1russell7/coeval 0.25.0 → 0.26.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/src/stepper.ts ADDED
@@ -0,0 +1,1150 @@
1
+ import { type Envelope, type Json, type Key, type TypeRef, docEquals, field, isEnvelope, isRecord, typeName } from "./doc.ts";
2
+ import { type Cap, type Context, type Resolution, type ResolveOptions, type Thing, isHostThing, worldThing } from "./deref.ts";
3
+ import { type EvalError, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
4
+ import { type Effect, type EvalInput, type Machine, type Part, type Perform, intersectCaps, makeContext, permits, placeText } from "./evaluate.ts";
5
+ import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
6
+ import type { World } from "./ladder.ts";
7
+ import { pathMatches } from "./parts.ts";
8
+ import type { Definitions } from "./reference.ts";
9
+ import { type CapsRule, type Kind, type Shape, type WorldEntry, isAlias, isKind, isNamespace } from "./world.ts";
10
+
11
+ /**
12
+ * The third tier (K95, D158): a stepper from the functional correspondence. It is a CPS transformation and a
13
+ * defunctionalization of the reference for the kernel and the formula kit. The continuation is a stack of frames, and
14
+ * each frame is data. One step is a function from one configuration to the next.
15
+ *
16
+ * The stepper reads the judgments 1 to 3 of `docs/SEMANTICS.md` itself: the lookup, the ladder, the split and the steps
17
+ * of a path. It does not use `partsOf`, `lookupKind`, `answerOf` or `resolve`. Thus `agree` also tests the code that
18
+ * the plan and the reference share.
19
+ */
20
+
21
+ /** One part of a value: the rule SPLIT-TERM gives it. */
22
+ interface Slot {
23
+ readonly path: readonly Key[];
24
+ readonly key: string;
25
+ readonly item: Json;
26
+ readonly of?: string;
27
+ readonly address?: true;
28
+ }
29
+
30
+ /** What the frames of one document share. */
31
+ interface Site {
32
+ /** The name of the evaluator. An exception of a rule of the profile names it. */
33
+ readonly name: string;
34
+ readonly depth: number;
35
+ readonly ctx: Context;
36
+ readonly rules?: readonly CapsRule[];
37
+ }
38
+
39
+ /** The part of a frame that runs at this time. The path of each frame on the stack gives the place (H-PLACE). */
40
+ interface DocFrame {
41
+ at: readonly Key[];
42
+ readonly site: Site;
43
+ }
44
+
45
+ /** The work after the parts of a document: the name of a construct. */
46
+ type After = "list" | "record" | "op" | "ref" | "step" | "args";
47
+
48
+ interface ValuesFrame extends DocFrame {
49
+ readonly f: "values";
50
+ readonly then: After;
51
+ readonly slots: readonly Slot[];
52
+ readonly strict: boolean;
53
+ next: number;
54
+ values: unknown[];
55
+ errors: EvalError[];
56
+ /** The indexes of the fields that the rule `omit` of the profile leaves out. */
57
+ skipped: number[];
58
+ readonly doc?: Envelope;
59
+ readonly name?: string;
60
+ readonly target?: unknown;
61
+ readonly address?: boolean;
62
+ }
63
+
64
+ interface LetFrame extends DocFrame {
65
+ readonly f: "let";
66
+ readonly slots: readonly Slot[];
67
+ readonly body: Slot;
68
+ next: number;
69
+ names: Record<string, unknown>;
70
+ }
71
+
72
+ interface CallFrame extends DocFrame {
73
+ readonly f: "call";
74
+ readonly args: readonly Slot[];
75
+ }
76
+
77
+ interface IfFrame extends DocFrame {
78
+ readonly f: "if";
79
+ readonly yes: Slot;
80
+ readonly no?: Slot;
81
+ }
82
+
83
+ interface LogicFrame extends DocFrame {
84
+ readonly f: "logic";
85
+ readonly stop: boolean;
86
+ readonly what: string;
87
+ readonly slots: readonly Slot[];
88
+ next: number;
89
+ }
90
+
91
+ /**
92
+ * One frame of the continuation: the rest of one document, for example "the items of a list after the item 2".
93
+ *
94
+ * - `tail`: the last part of a document. Only the place reads it.
95
+ * - `return`: the end of one application of a function.
96
+ * - `base`: the end of a nested stepper of a host handle.
97
+ */
98
+ export type Frame =
99
+ | ValuesFrame
100
+ | LetFrame
101
+ | CallFrame
102
+ | IfFrame
103
+ | LogicFrame
104
+ | { readonly f: "tail"; at: readonly Key[] }
105
+ | { readonly f: "return"; readonly place: readonly Key[] }
106
+ | { readonly f: "base"; readonly place: readonly Key[] };
107
+
108
+ interface Shared {
109
+ readonly stack: readonly Frame[];
110
+ /** The count of the active applications of functions (FORMULA.CALL-DEPTH). */
111
+ readonly applying: number;
112
+ /** `true` after a closure or a held error goes in a value. Only then does a value need a change for the host. */
113
+ readonly marked: boolean;
114
+ }
115
+
116
+ /**
117
+ * One configuration of the stepper. It is data: for a pure document of the formula kit, it survives `JSON.stringify` and
118
+ * `JSON.parse` (STEPPER.DATA).
119
+ *
120
+ * - `evaluate`: the document `value` starts, with a new evaluation at its depth.
121
+ * - `resume`: a document gave `result`, and the top frame takes it.
122
+ * - `done`: the evaluation ends with `result`.
123
+ */
124
+ export type Configuration =
125
+ | (Shared & {
126
+ readonly mode: "evaluate";
127
+ readonly value: unknown;
128
+ readonly declared?: string | undefined;
129
+ readonly depth: number;
130
+ readonly address: boolean;
131
+ readonly ctx: Context;
132
+ })
133
+ | (Shared & { readonly mode: "resume"; readonly result: Result })
134
+ | (Shared & { readonly mode: "done"; readonly result: Result });
135
+
136
+ type Evaluating = Extract<Configuration, { readonly mode: "evaluate" }>;
137
+ type Resuming = Extract<Configuration, { readonly mode: "resume" }>;
138
+
139
+ /** The options of `stepper`. */
140
+ export interface StepperOptions {
141
+ /** The type of a bare root value. */
142
+ readonly declaredType?: string;
143
+ /** The verb that goes up the ladder at each document. The default is `eval`. */
144
+ readonly verb?: string;
145
+ /** The operations and the rules of the formula kit of the machine. */
146
+ readonly profile?: FormulaProfile;
147
+ /** The definitional code of other compiled evaluators, as for `reference`. */
148
+ readonly definitions?: Definitions;
149
+ /** A list that gets each effect of a host evaluator, in its order. */
150
+ readonly effects?: Effect[];
151
+ /** The mock of the host (RUN.MOCK). It answers each effect at once. Without it, an effect gives its payload. */
152
+ readonly perform?: Perform;
153
+ }
154
+
155
+ /** The code that a configuration does not hold: the machine and the profile. */
156
+ interface Program {
157
+ readonly machine: Machine;
158
+ readonly profile: FormulaProfile;
159
+ readonly definitions: Definitions;
160
+ readonly verb: string;
161
+ readonly maxDepth: number;
162
+ readonly hosts: WeakMap<object, unknown>;
163
+ readonly effects: Effect[];
164
+ readonly perform: Perform | undefined;
165
+ /** The number of earlier effects at each place, by the text of the place (RUN.ADDRESS). */
166
+ readonly counts: Map<string, number>;
167
+ /** The count of the applications at the last call of host code. */
168
+ applying: number;
169
+ }
170
+
171
+ const CLOSURE_MARK = "coeval.closure";
172
+ const HELD_MARK = "coeval.held";
173
+ const CLOSURE: unique symbol = /* @__PURE__ */ Symbol.for("coeval.closure") as never;
174
+ const MAX_ALIASES = 32;
175
+ const MAX_COLLAPSE = 64;
176
+ const ROOTS: readonly string[] = /* @__PURE__ */ Object.freeze(["self", "names", "world", "event", "caps"]);
177
+ const NO_KEYS: readonly Key[] = /* @__PURE__ */ Object.freeze([]);
178
+
179
+ /** A function of the formula kit as data (F-FN). */
180
+ interface Closure {
181
+ readonly params: readonly string[];
182
+ readonly body: Slot;
183
+ readonly site: Site;
184
+ readonly place: readonly Key[];
185
+ }
186
+
187
+ function own<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
188
+ return table !== undefined && Object.hasOwn(table, name) ? table[name] : undefined;
189
+ }
190
+
191
+ function setField(target: Record<string, unknown>, key: string, value: unknown): void {
192
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
193
+ }
194
+
195
+ function closureOf(value: unknown): Closure | undefined {
196
+ return isRecord(value) && Object.hasOwn(value, CLOSURE_MARK) ? (value[CLOSURE_MARK] as unknown as Closure) : undefined;
197
+ }
198
+
199
+ function heldOf(value: unknown): EvalError | undefined {
200
+ if (value instanceof Held) return value.error;
201
+ return isRecord(value) && Object.hasOwn(value, HELD_MARK) ? (value[HELD_MARK] as unknown as EvalError) : undefined;
202
+ }
203
+
204
+ /** The judgment 2: K-START and K-COMPUTED. */
205
+ function keysOf(type: TypeRef): readonly Key[] | undefined {
206
+ if (typeof type === "string") return [type];
207
+ if (Array.isArray(type)) return type as readonly Key[];
208
+ const steps = (type as { readonly value: readonly unknown[] }).value.slice(1);
209
+ return steps.every(isKey) ? (steps as readonly Key[]) : undefined;
210
+ }
211
+
212
+ /** A key is a text, or a whole number from 0. */
213
+ function isKey(step: unknown): step is Key {
214
+ return typeof step === "string" || (typeof step === "number" && Number.isInteger(step) && step >= 0);
215
+ }
216
+
217
+ /** The judgment 2: the lookup of a kind, with the rules K-FALLBACK to K-NOT-A-KIND. */
218
+ function lookup(world: World, type: TypeRef): { readonly kind: Kind; readonly path: readonly Key[] } | undefined {
219
+ const start = keysOf(type);
220
+ if (start === undefined) return undefined;
221
+ let keys = start;
222
+ let aliases = 0;
223
+ for (;;) {
224
+ let entries: Readonly<Record<string, WorldEntry>> = world.kinds;
225
+ let fallback = world.fallback;
226
+ let followed = false;
227
+ if (keys.length === 0) return undefined;
228
+ for (let index = 0; index < keys.length; index += 1) {
229
+ const key = String(keys[index]);
230
+ const entry = own(entries, key);
231
+ if (entry === undefined) return fallback === undefined ? undefined : { kind: fallback, path: keys };
232
+ if (isAlias(entry)) {
233
+ aliases += 1;
234
+ const target = entry.value.slice(1);
235
+ if (aliases > MAX_ALIASES || entry.value[0] !== "world" || !target.every(isKey)) return undefined;
236
+ keys = [...(target as readonly Key[]), ...keys.slice(index + 1)];
237
+ followed = true;
238
+ break;
239
+ }
240
+ const last = index === keys.length - 1;
241
+ if (last) return isKind(entry) ? { kind: entry, path: keys } : undefined;
242
+ if (!isNamespace(entry)) return undefined;
243
+ entries = entry.kinds;
244
+ fallback = entry.fallback;
245
+ }
246
+ if (!followed) return undefined;
247
+ }
248
+ }
249
+
250
+ /** The result of the ladder for each rung: a name, `null` for L-STOP, or `undefined` for L-NONE. */
251
+ function ladder(world: World, entries: unknown, kind: Kind | undefined, known: boolean, verb: string, has: (name: string) => boolean): string | null | undefined {
252
+ const mine = isRecord(entries) ? own(entries as Readonly<Record<string, string>>, verb) : undefined;
253
+ if (mine !== undefined && has(mine)) return mine;
254
+ if (!known) return null;
255
+ const named = own(kind?.verbs, verb);
256
+ if (named !== undefined && has(named)) return named;
257
+ const fallback = own(world.defaults, verb);
258
+ return fallback !== undefined && has(fallback) ? fallback : undefined;
259
+ }
260
+
261
+ class Bad {
262
+ readonly reason: string;
263
+ readonly at: readonly Key[];
264
+ constructor(reason: string, at: readonly Key[]) {
265
+ this.reason = reason;
266
+ this.at = at;
267
+ }
268
+ }
269
+
270
+ /** The result of the walk for a part: the field is absent from the data (SPLIT-FILL). */
271
+ const PART: unique symbol = /* @__PURE__ */ Symbol("part") as never;
272
+
273
+ function isDataShape(shape: Shape): boolean {
274
+ return shape.shape === "none" || shape.shape === "text" || shape.shape === "number" || shape.shape === "one";
275
+ }
276
+
277
+ /** SPLIT-DATA: the reason why a value is not a member of a data shape. */
278
+ function notMember(shape: Extract<Shape, { readonly shape: "text" | "number" | "one" }>, value: Json): string | undefined {
279
+ if (shape.shape === "text") return typeof value === "string" ? undefined : "The value must be a text.";
280
+ if (shape.shape === "one") return shape.values.some((item) => docEquals(item, value)) ? undefined : `The value must be one of ${JSON.stringify(shape.values)}.`;
281
+ if (typeof value !== "number") return "The value must be a number.";
282
+ if (shape.whole === true && !Number.isInteger(value)) return "The value must be a whole number.";
283
+ if (shape.min !== undefined && !(value >= shape.min)) return `The value must be ${String(shape.min)} or more.`;
284
+ if (shape.max !== undefined && !(value <= shape.max)) return `The value must be ${String(shape.max)} or less.`;
285
+ return undefined;
286
+ }
287
+
288
+ function checkKinds(kinds: readonly (readonly Key[])[], item: Json, of: string | undefined, path: readonly Key[]): void {
289
+ const type = isEnvelope(item) ? keysOf(item.type) : of === undefined ? undefined : [of];
290
+ const fits = type !== undefined && kinds.some((kind) => kind.length <= type.length && kind.every((key, index) => String(key) === String(type[index])));
291
+ if (fits) return;
292
+ const what = isEnvelope(item) ? `The kind ${typeName(item.type)} is not permitted here.` : "A value without an envelope is not permitted here.";
293
+ throw new Bad(`${what} The permitted kinds are ${kinds.map((kind) => kind.join(".")).join(", ")}.`, path);
294
+ }
295
+
296
+ /** The judgment 1: the rules SPLIT-NONE to SPLIT-DEEP. The result holds the parts and the data of SPLIT-FILL. */
297
+ function split(shape: Shape | undefined, value: Json): { readonly slots: readonly Slot[]; readonly data: Json } | Bad {
298
+ const slots: Slot[] = [];
299
+ const slot = (item: Json, path: readonly Key[], of: string | undefined, address: boolean): typeof PART => {
300
+ slots.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }), ...(address ? { address: true as const } : {}) });
301
+ return PART;
302
+ };
303
+ const term = (of: string | undefined): Shape => (of === undefined ? { shape: "term" } : { shape: "term", of });
304
+ const listData = (items: readonly (Json | typeof PART)[]): Json => (items.every((item) => item === PART) ? null : items.map((item) => (item === PART ? null : item)));
305
+ const deep = (item: Json, path: readonly Key[], of: string | undefined): Json | typeof PART => {
306
+ if (isEnvelope(item)) return slot(item, path, of, false);
307
+ const before = slots.length;
308
+ if (Array.isArray(item)) {
309
+ const items = (item as readonly Json[]).map((child, index) => deep(child, [...path, index], of));
310
+ return slots.length === before ? item : listData(items);
311
+ }
312
+ if (!isRecord(item)) return item;
313
+ const out: Record<string, unknown> = {};
314
+ for (const key of Object.keys(item)) {
315
+ const kept = deep(item[key] as Json, [...path, key], of);
316
+ if (kept !== PART) setField(out, key, kept);
317
+ }
318
+ return slots.length === before ? item : (out as Json);
319
+ };
320
+ const walk = (spec: Shape | undefined, item: Json, path: readonly Key[]): Json | typeof PART => {
321
+ if (spec === undefined || spec.shape === "none") return item;
322
+ if (spec.shape === "text" || spec.shape === "number" || spec.shape === "one") {
323
+ const reason = notMember(spec, item);
324
+ if (reason !== undefined) throw new Bad(reason, path);
325
+ return item;
326
+ }
327
+ if (spec.shape === "term") {
328
+ if (spec.kinds !== undefined) checkKinds(spec.kinds, item, spec.of, path);
329
+ return slot(item, path, spec.of, spec.address === true);
330
+ }
331
+ if (spec.shape === "deep") return deep(item, path, spec.of);
332
+ if (spec.shape === "list") {
333
+ if (!Array.isArray(item)) throw new Bad("The value must be a list.", path);
334
+ const list = item as readonly Json[];
335
+ if (spec.distinct === true) {
336
+ const twice = list.findIndex((child, index) => list.slice(0, index).some((before) => docEquals(before, child)));
337
+ if (twice !== -1) throw new Bad("The items of the list must be different.", [...path, twice]);
338
+ }
339
+ const each = spec.item ?? term(spec.of);
340
+ const items = list.map((child, index) => walk(each, child, [...path, index]));
341
+ return isDataShape(each) ? item : listData(items);
342
+ }
343
+ if (!isRecord(item)) throw new Bad("The value must be a record.", path);
344
+ const out: Record<string, unknown> = {};
345
+ const keep = (key: string, kept: Json | typeof PART): void => {
346
+ if (kept !== PART) setField(out, key, kept);
347
+ };
348
+ if (spec.shape === "map") {
349
+ for (const key of Object.keys(item)) keep(key, walk(spec.item ?? term(spec.of), item[key] as Json, [...path, key]));
350
+ return out as Json;
351
+ }
352
+ for (const name of spec.required ?? []) {
353
+ if (!Object.hasOwn(item, name)) throw new Bad(`The field ${name} is missing.`, path);
354
+ }
355
+ for (const key of Object.keys(item)) {
356
+ const named = Array.isArray(spec.fields) ? (spec.fields as readonly string[]).includes(key) : Object.hasOwn(spec.fields, key);
357
+ if (spec.closed === true && !named) throw new Bad(`The field ${key} is not a field of this kind.`, [...path, key]);
358
+ const fieldShape = !named ? spec.rest : Array.isArray(spec.fields) ? term(spec.of) : (spec.fields as Readonly<Record<string, Shape>>)[key];
359
+ keep(key, walk(fieldShape, item[key] as Json, [...path, key]));
360
+ }
361
+ return out as Json;
362
+ };
363
+ try {
364
+ const data = walk(shape, value, []);
365
+ return { slots, data: data === PART ? null : data };
366
+ } catch (error) {
367
+ if (error instanceof Bad) return error;
368
+ throw error;
369
+ }
370
+ }
371
+
372
+ function copyOf(container: unknown): Record<string | number, unknown> {
373
+ if (Array.isArray(container)) return [...container] as never;
374
+ const out: Record<string, unknown> = {};
375
+ for (const key of Object.keys(container as object)) setField(out, key, (container as Record<string, unknown>)[key]);
376
+ return out;
377
+ }
378
+
379
+ /** SPLIT-FILL: the value with the item `i` at the path of the slot `i`. The containers on the way are new. */
380
+ function fillIn(value: unknown, slots: readonly Slot[], items: readonly unknown[]): unknown {
381
+ const first = slots[0];
382
+ if (first === undefined) return value;
383
+ if (first.path.length === 0) return items[0];
384
+ const root = copyOf(value);
385
+ const fresh = new Set<unknown>([root]);
386
+ slots.forEach((part, index) => {
387
+ let here = root;
388
+ for (let step = 0; step < part.path.length - 1; step += 1) {
389
+ const key = part.path[step] as Key;
390
+ let next = here[key];
391
+ if (!fresh.has(next)) {
392
+ next = copyOf(next);
393
+ fresh.add(next);
394
+ setField(here, String(key), next);
395
+ }
396
+ here = next as Record<string | number, unknown>;
397
+ }
398
+ setField(here, String(part.path[part.path.length - 1]), items[index]);
399
+ });
400
+ return root;
401
+ }
402
+
403
+ /** H-CONTEXT: the capabilities of a rule list for one part. */
404
+ function capsOf(rules: readonly CapsRule[] | undefined, path: readonly Key[], caps: readonly Cap[]): readonly Cap[] {
405
+ let out = caps;
406
+ for (const rule of rules ?? []) {
407
+ if (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(path, pattern))) continue;
408
+ if (rule.rule === "mask") return [];
409
+ if (rule.rule === "only") out = intersectCaps(out, rule.caps);
410
+ }
411
+ return out;
412
+ }
413
+
414
+ /** H-CONTEXT: the context of a part. */
415
+ function contextOf(site: Site, slot: Slot, given: Context | undefined): Context {
416
+ const parent = site.ctx;
417
+ const narrow = (context: Context): Context => (context.caps.every((cap) => permits(parent.caps, cap)) ? context : { ...context, caps: intersectCaps(parent.caps, context.caps) });
418
+ const base = given === undefined || given === parent ? parent : narrow(given);
419
+ const caps = capsOf(site.rules, slot.path, base.caps);
420
+ return caps === base.caps ? base : { ...base, caps };
421
+ }
422
+
423
+ /** H-PLACE: the place of the document on top of the stack. */
424
+ function placeOf(stack: readonly Frame[]): readonly Key[] {
425
+ const pieces: (readonly Key[])[] = [];
426
+ let base = NO_KEYS;
427
+ for (let index = stack.length - 1; index >= 0; index -= 1) {
428
+ const frame = stack[index] as Frame;
429
+ if (frame.f === "base" || frame.f === "return") {
430
+ base = frame.place;
431
+ break;
432
+ }
433
+ pieces.push(frame.at);
434
+ }
435
+ const out = [...base];
436
+ for (let index = pieces.length - 1; index >= 0; index -= 1) out.push(...(pieces[index] as readonly Key[]));
437
+ return out;
438
+ }
439
+
440
+ function back(c: Configuration, result: Result): Resuming {
441
+ const placedResult = result.ok || result.error.place !== undefined ? result : placed(result, placeOf(c.stack));
442
+ return { mode: "resume", result: placedResult, stack: c.stack, applying: c.applying, marked: c.marked };
443
+ }
444
+
445
+ function pushed(c: Configuration, frame: Frame): void {
446
+ (c.stack as Frame[]).push(frame);
447
+ }
448
+
449
+ function popped(c: Configuration): void {
450
+ (c.stack as Frame[]).pop();
451
+ }
452
+
453
+ /** H-SEAL: the evaluation of a part, with the frame that waits for it on top of the stack. */
454
+ function enter(c: Configuration, frame: { at: readonly Key[] }, site: Site, slot: Slot, given?: Context, marked: boolean = c.marked): Evaluating {
455
+ frame.at = slot.path;
456
+ return {
457
+ mode: "evaluate",
458
+ value: slot.item,
459
+ declared: slot.of,
460
+ depth: site.depth + 1,
461
+ address: slot.address === true,
462
+ ctx: contextOf(site, slot, given),
463
+ stack: c.stack,
464
+ applying: c.applying,
465
+ marked,
466
+ };
467
+ }
468
+
469
+ function partNamed(slots: readonly Slot[], key: string): Slot | undefined {
470
+ return slots.find((slot) => slot.path.length === 1 && slot.path[0] === key);
471
+ }
472
+
473
+ function partsUnder(slots: readonly Slot[], key: string): readonly Slot[] {
474
+ return slots.filter((slot) => slot.path.length === 2 && slot.path[0] === key);
475
+ }
476
+
477
+ function truthOf(p: Program, c: Configuration, value: unknown): boolean | undefined {
478
+ const given = forHost(p, c, value);
479
+ return p.profile.truthy === undefined ? (typeof given === "boolean" ? given : undefined) : p.profile.truthy(given);
480
+ }
481
+
482
+ function opOf(p: Program, name: string): OpFn | undefined {
483
+ const { ops } = p.profile;
484
+ return typeof ops === "function" ? ops(name) : own(ops, name);
485
+ }
486
+
487
+ function startValues(p: Program, c: Configuration, site: Site, then: After, slots: readonly Slot[], strict: boolean, more: Partial<ValuesFrame> = {}): Configuration {
488
+ const frame: ValuesFrame = { f: "values", at: NO_KEYS, site, then, slots, strict, next: 1, values: [], errors: [], skipped: [], ...more };
489
+ const first = slots[0];
490
+ if (first === undefined) return finish(p, c, frame);
491
+ pushed(c, frame);
492
+ return enter(c, frame, site, first);
493
+ }
494
+
495
+ /** F-REF, after the values of the steps. */
496
+ function finishRef(p: Program, c: Configuration, doc: Envelope, steps: readonly unknown[], site: Site, address: boolean): Configuration {
497
+ const path = doc.value as readonly Json[];
498
+ const head = path[0] === "names" && typeof path[1] === "string" ? path[1] : undefined;
499
+ const bound = head === undefined ? undefined : heldOf(own(site.ctx.names, head));
500
+ if (bound !== undefined) return back(c, { ok: false, error: bound });
501
+ const found = walkPath(p, site.ctx, steps);
502
+ if (!found.ok) return back(c, fail(found.code as never, found.reason));
503
+ if (address) return back(c, ok(found.thing));
504
+ const reached = isHostThing(found.thing) ? found.thing.toJson() : found.thing;
505
+ const held = heldOf(reached);
506
+ if (held !== undefined) return back(c, { ok: false, error: held });
507
+ if (p.profile.missing?.(forHost(p, c, reached)) === true) return back(c, fail("#N/A", "The path reaches no value."));
508
+ return back(c, ok(reached));
509
+ }
510
+
511
+ /** The work of a construct after the values of its parts. */
512
+ function finish(p: Program, c: Configuration, frame: ValuesFrame): Configuration {
513
+ const { values, slots } = frame;
514
+ switch (frame.then) {
515
+ case "list":
516
+ return back(c, ok(values));
517
+ case "record": {
518
+ const out: Record<string, unknown> = {};
519
+ slots.forEach((slot, index) => {
520
+ if (!frame.skipped.includes(index)) setField(out, String(slot.path[0]), values[index]);
521
+ });
522
+ return back(c, ok(out));
523
+ }
524
+ case "op": {
525
+ const name = frame.name as string;
526
+ try {
527
+ const out = (opOf(p, name) as (...args: unknown[]) => unknown)(...(forHost(p, c, values) as readonly unknown[]));
528
+ return back(c, p.profile.check?.(out, name) ?? ok(out));
529
+ } catch (error) {
530
+ return back(c, thrown(error, `the operation ${name}`));
531
+ }
532
+ }
533
+ case "step": {
534
+ const doc = frame.doc as Envelope;
535
+ return back(c, ok(slots.length === 0 ? doc : { ...doc, value: fillIn(doc.value, slots, values) as Json }));
536
+ }
537
+ case "ref": {
538
+ const doc = frame.doc as Envelope;
539
+ return finishRef(p, c, doc, fillIn(doc.value, slots, values) as readonly unknown[], frame.site, frame.address === true);
540
+ }
541
+ case "args":
542
+ return apply(p, c, frame.target, values);
543
+ }
544
+ }
545
+
546
+ /** F-CALL and F-FN: an application of a function. */
547
+ function apply(p: Program, c: Configuration, target: unknown, args: readonly unknown[]): Configuration {
548
+ const closure = closureOf(target);
549
+ if (closure === undefined) {
550
+ try {
551
+ return back(c, ok((target as (...items: unknown[]) => unknown)(...(forHost(p, c, args) as readonly unknown[]))));
552
+ } catch (error) {
553
+ return back(c, thrown(error, "the function of call"));
554
+ }
555
+ }
556
+ const limit = Math.ceil(p.maxDepth / 4);
557
+ if (c.applying >= limit) return back(c, fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`));
558
+ const frame: Frame = { f: "return", place: closure.place.concat(closure.body.path) };
559
+ pushed(c, frame);
560
+ const start = enter(c, { at: NO_KEYS }, closure.site, closure.body, scopeOf(closure, args));
561
+ return { ...start, applying: c.applying + 1 };
562
+ }
563
+
564
+ function scopeOf(closure: Closure, args: readonly unknown[]): Context {
565
+ const { ctx } = closure.site;
566
+ const scope: Record<string, unknown> = {};
567
+ for (const key of Object.keys(ctx.names)) setField(scope, key, ctx.names[key]);
568
+ closure.params.forEach((name, index) => setField(scope, name, args[index]));
569
+ return { ...ctx, names: scope };
570
+ }
571
+
572
+ /** This function makes the host function of a closure. A host calls it, thus it runs a nested stepper. */
573
+ function hostClosure(p: Program, value: object, closure: Closure): unknown {
574
+ const known = p.hosts.get(value);
575
+ if (known !== undefined) return known;
576
+ const fn = (...args: readonly unknown[]): unknown => {
577
+ const limit = Math.ceil(p.maxDepth / 4);
578
+ if (p.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
579
+ p.applying += 1;
580
+ try {
581
+ const place = closure.place.concat(closure.body.path);
582
+ const c: Configuration = { mode: "resume", result: ok(null), stack: [{ f: "base", place }], applying: p.applying, marked: true };
583
+ const end = drive(p, enter(c, { at: NO_KEYS }, closure.site, closure.body, scopeOf(closure, args)));
584
+ if (!end.result.ok) throw new Failure(end.result.error);
585
+ return forHost(p, end, end.result.value);
586
+ } finally {
587
+ p.applying -= 1;
588
+ }
589
+ };
590
+ Object.defineProperty(fn, CLOSURE, { value: { params: closure.params } });
591
+ p.hosts.set(value, fn);
592
+ return fn;
593
+ }
594
+
595
+ /** This function gives a value to host code: a closure is a function, and a held error is a `Held`. */
596
+ function forHost(p: Program, c: Configuration, value: unknown): unknown {
597
+ p.applying = c.applying;
598
+ return c.marked ? hostValue(p, value) : value;
599
+ }
600
+
601
+ function hostValue(p: Program, value: unknown): unknown {
602
+ if (Array.isArray(value)) {
603
+ const items = (value as readonly unknown[]).map((item) => hostValue(p, item));
604
+ return items.some((item, index) => item !== value[index]) ? items : value;
605
+ }
606
+ if (!isRecord(value)) return value;
607
+ const closure = closureOf(value);
608
+ if (closure !== undefined) return hostClosure(p, value, closure);
609
+ const held = heldOf(value);
610
+ if (held !== undefined) return new Held(held);
611
+ let out: Record<string, unknown> | undefined;
612
+ for (const key of Object.keys(value)) {
613
+ const item = hostValue(p, value[key]);
614
+ if (item !== value[key]) {
615
+ out ??= copyOf(value);
616
+ setField(out, key, item);
617
+ }
618
+ }
619
+ return out ?? value;
620
+ }
621
+
622
+ function hostContext(p: Program, c: Configuration, ctx: Context): Context {
623
+ const names = forHost(p, c, ctx.names) as Context["names"];
624
+ return names === ctx.names ? ctx : { ...ctx, names };
625
+ }
626
+
627
+ /** The judgment 3: the steps of a path, with R-NO-ROOT to C-DONE. A failure holds a code and a reason. */
628
+ function walkPath(p: Program, ctx: Context, path: readonly unknown[]): Resolution {
629
+ const { machine } = p;
630
+ const adopt = machine.adopt ?? ((value: unknown): Thing => value);
631
+ const failed = (code: string, reason: string): Resolution => ({ ok: false, code, reason, hops: [] }) as never;
632
+ const open: object[] = [];
633
+ let collapses = 0;
634
+ const collapse = (thing: Thing): Resolution => {
635
+ let here = thing;
636
+ if (machine.plain === true) return { ok: true, thing: here, hops: [] };
637
+ while (isReference(here)) {
638
+ if (open.includes(here as object)) return failed("#CYCLE!", "The references form a cycle.");
639
+ collapses += 1;
640
+ if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", "The path follows more than 64 references. One resolution follows at most 64.");
641
+ open.push(here as object);
642
+ const inner = walk((here as Envelope & { readonly value: readonly unknown[] }).value);
643
+ open.pop();
644
+ if (!inner.ok) return inner;
645
+ here = adopt(inner.thing);
646
+ }
647
+ return { ok: true, thing: here, hops: [] };
648
+ };
649
+ const walk = (steps: readonly unknown[]): Resolution => {
650
+ const root = steps[0];
651
+ if (typeof root !== "string" || !ROOTS.includes(root)) return failed("#REF!", `A path starts with a root: ${ROOTS.join(", ")}.`);
652
+ if (machine.roots !== undefined && !(machine.roots as readonly string[]).includes(root)) return failed("#REF!", `The root ${root} is not open here. The open roots are ${machine.roots.join(", ")}.`);
653
+ const first = collapse(adopt(ctx[root as keyof Context]));
654
+ if (!first.ok) return first;
655
+ let here = first.thing;
656
+ for (let index = 1; index < steps.length; index += 1) {
657
+ const step = steps[index];
658
+ const rest = steps.slice(index);
659
+ if (isHostThing(here) && here.resolve !== undefined && rest.every(isKey)) {
660
+ const reached = here.resolve(rest);
661
+ if (reached === undefined) return failed("#N/A", `The path ${rest.join(".")} reaches nothing.`);
662
+ return collapse(adopt(reached.thing));
663
+ }
664
+ const hop = isKey(step) ? keyStep(p, here, step) : undefined;
665
+ if (isKey(step)) {
666
+ if (hop === undefined) {
667
+ if (root === "names" && index === 1) return failed("#NAME?", `The context has no name ${String(step)}.`);
668
+ return failed("#N/A", `The step ${String(step)} reaches nothing.`);
669
+ }
670
+ here = adopt(hop.thing);
671
+ } else {
672
+ const walker = walkerFor(p, step);
673
+ if (walker === undefined) return failed("#STEP?", "A step is a key, or a document of a kind with the verb step.");
674
+ const deref = (thing: Thing, key: Key): Thing | undefined => {
675
+ const reached = keyStep(p, thing, key);
676
+ return reached === undefined ? undefined : adopt(reached.thing);
677
+ };
678
+ const has = (thing: Thing, key: Key): boolean => keyStep(p, thing, key) !== undefined;
679
+ const result = walker({ data: (step as Envelope).value, here, context: ctx, deref, has });
680
+ if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
681
+ here = adopt(result.thing);
682
+ }
683
+ const next = collapse(here);
684
+ if (!next.ok) return next;
685
+ here = next.thing;
686
+ }
687
+ return { ok: true, thing: here, hops: [] };
688
+ };
689
+ return path.length === 0 ? failed("#REF!", "A path has at least one step: its root.") : walk(path);
690
+ }
691
+
692
+ function isReference(thing: unknown): boolean {
693
+ if (!isEnvelope(thing) || !Array.isArray(thing.value)) return false;
694
+ const keys = keysOf(thing.type);
695
+ return keys !== undefined && keys.length === 1 && keys[0] === "ref";
696
+ }
697
+
698
+ /** R-WALKER: the code of a step document. */
699
+ function walkerFor(p: Program, step: unknown): NonNullable<Machine["walkers"]>[string] | undefined {
700
+ const { walkers, world } = p.machine;
701
+ if (walkers === undefined || !isEnvelope(step)) return undefined;
702
+ const found = lookup(world, step.type);
703
+ const name = ladder(world, field(step as never, "own"), found?.kind, found !== undefined, "step", (item) => own(walkers, item) !== undefined);
704
+ return typeof name === "string" ? own(walkers, name) : undefined;
705
+ }
706
+
707
+ /** FIELD: the rule of a step in a value. */
708
+ function fieldOf(value: unknown, key: Key): { readonly thing: Thing } | undefined {
709
+ if (Array.isArray(value)) {
710
+ if (key === "length") return { thing: value.length };
711
+ const index = typeof key === "number" ? key : /^\d+$/.test(key) ? Number(key) : -1;
712
+ return index >= 0 && index < value.length ? { thing: value[index] as Thing } : undefined;
713
+ }
714
+ if (typeof value !== "object" || value === null || !Object.hasOwn(value, String(key))) return undefined;
715
+ return { thing: (value as Readonly<Record<string, unknown>>)[String(key)] };
716
+ }
717
+
718
+ /** The rules S-HOST to S-FIELD: a step with a key. The result is `undefined` for `miss`. */
719
+ function keyStep(p: Program, thing: Thing, key: Key): { readonly thing: Thing } | undefined {
720
+ const { machine } = p;
721
+ if (isHostThing(thing)) {
722
+ const reached = thing.deref(key);
723
+ return reached !== undefined || thing.has?.(key) === true ? { thing: reached } : undefined;
724
+ }
725
+ if (machine.plain === true || !isEnvelope(thing)) return fieldOf(thing, key);
726
+ const { derefs, world } = machine;
727
+ const found = lookup(world, thing.type);
728
+ const name = derefs === undefined ? undefined : ladder(world, field(thing as never, "own"), found?.kind, found !== undefined, "deref", (item) => own(derefs, item) !== undefined);
729
+ const code = typeof name === "string" ? own(derefs, name) : undefined;
730
+ if (code === undefined) return fieldOf(thing.value, key);
731
+ const reached = code(thing as Envelope, key);
732
+ return reached !== undefined || code.has?.(thing as Envelope, key) === true ? { thing: reached } : undefined;
733
+ }
734
+
735
+ /** A construct of the formula kit, with its compiled evaluator in the machine. */
736
+ const CONSTRUCTS: Readonly<Record<string, true>> = {
737
+ "formula.lit": true,
738
+ "formula.ref": true,
739
+ "formula.step": true,
740
+ "formula.record": true,
741
+ "formula.list": true,
742
+ "formula.let": true,
743
+ "formula.fn": true,
744
+ "formula.call": true,
745
+ "formula.if": true,
746
+ "formula.and": true,
747
+ "formula.or": true,
748
+ "formula.op": true,
749
+ "formula.ext": true,
750
+ };
751
+
752
+ /** The rules F-LIT to F-EXT: the start of a construct. */
753
+ function construct(p: Program, c: Evaluating, name: string, doc: Envelope, keys: readonly Key[], slots: readonly Slot[], site: Site): Configuration {
754
+ const { profile, machine } = p;
755
+ switch (name) {
756
+ case "formula.lit":
757
+ return back(c, ok(doc.value));
758
+ case "formula.ref": {
759
+ const path = doc.value;
760
+ if (!Array.isArray(path) || path.length === 0) return back(c, fail("#VALUE!", 'The value of ref is a path: a list that starts with a root, for example ["names", "a"].'));
761
+ if (slots.length === 0) return finishRef(p, c, doc, path as readonly unknown[], site, c.address);
762
+ return startValues(p, c, site, "ref", slots, true, { doc, address: c.address });
763
+ }
764
+ case "formula.step":
765
+ return startValues(p, c, site, "step", slots, true, { doc });
766
+ case "formula.record":
767
+ return startValues(p, c, site, "record", slots, false);
768
+ case "formula.list":
769
+ return startValues(p, c, site, "list", slots, false);
770
+ case "formula.let": {
771
+ const bindings = partsUnder(slots, "bind");
772
+ const body = partNamed(slots, "body") as Slot;
773
+ const names: Record<string, unknown> = {};
774
+ for (const key of Object.keys(site.ctx.names)) setField(names, key, site.ctx.names[key]);
775
+ const first = bindings[0];
776
+ if (first === undefined) {
777
+ const tail: Frame = { f: "tail", at: NO_KEYS };
778
+ pushed(c, tail);
779
+ return enter(c, tail, site, body, { ...site.ctx, names });
780
+ }
781
+ const frame: LetFrame = { f: "let", at: NO_KEYS, site, slots: bindings, body, next: 1, names };
782
+ pushed(c, frame);
783
+ return enter(c, frame, site, first);
784
+ }
785
+ case "formula.fn": {
786
+ const params = isRecord(doc.value) ? doc.value["params"] : undefined;
787
+ if (!Array.isArray(params) || !params.every((item) => typeof item === "string") || new Set(params).size !== params.length) {
788
+ return back(c, fail("#VALUE!", "The params of fn are a list of different texts."));
789
+ }
790
+ const closure: Closure = { params: params as readonly string[], body: partNamed(slots, "body") as Slot, site, place: placeOf(c.stack) };
791
+ return back({ ...c, marked: true }, ok({ [CLOSURE_MARK]: closure }));
792
+ }
793
+ case "formula.call": {
794
+ const frame: CallFrame = { f: "call", at: NO_KEYS, site, args: partsUnder(slots, "args") };
795
+ pushed(c, frame);
796
+ return enter(c, frame, site, partNamed(slots, "fn") as Slot);
797
+ }
798
+ case "formula.if": {
799
+ const no = partNamed(slots, "else");
800
+ const frame: IfFrame = { f: "if", at: NO_KEYS, site, yes: partNamed(slots, "then") as Slot, ...(no === undefined ? {} : { no }) };
801
+ pushed(c, frame);
802
+ return enter(c, frame, site, partNamed(slots, "test") as Slot);
803
+ }
804
+ case "formula.and":
805
+ case "formula.or": {
806
+ const stop = name === "formula.or";
807
+ const first = slots[0];
808
+ if (first === undefined) return back(c, ok(!stop));
809
+ const frame: LogicFrame = { f: "logic", at: NO_KEYS, site, stop, what: stop ? "term of or" : "term of and", slots, next: 1 };
810
+ pushed(c, frame);
811
+ return enter(c, frame, site, first);
812
+ }
813
+ case "formula.op": {
814
+ const op = keys.slice(1).join(".");
815
+ const lazy = own(profile.lazy, op);
816
+ if (lazy !== undefined) {
817
+ p.applying = c.applying;
818
+ try {
819
+ return back(c, lazy(slots.map((slot) => () => nested(p, c, site, slot))));
820
+ } catch (error) {
821
+ return back(c, thrown(error, `the operation ${op}`));
822
+ }
823
+ }
824
+ if (opOf(p, op) === undefined) return back(c, fail("#NAME?", `The language has no operation ${op}.`));
825
+ return startValues(p, c, site, "op", slots, false, { name: op });
826
+ }
827
+ default: {
828
+ const ext = keys.slice(1).join(".");
829
+ const handler = own(profile.ext, ext) ?? own(STD_EXT, ext);
830
+ if (handler === undefined) return back(c, fail("#NAME?", `The language has no extension ${ext}.`));
831
+ try {
832
+ return back(c, handler(doc.value, hostContext(p, c, site.ctx), machine.domains));
833
+ } catch (error) {
834
+ return back(c, thrown(error, `the extension ${ext}`));
835
+ }
836
+ }
837
+ }
838
+ }
839
+
840
+ /** H-SEAL for host code: a handle starts a nested stepper, on the stack of the host. */
841
+ function nested(p: Program, c: Configuration, site: Site, slot: Slot, given?: Context): Result {
842
+ const stack: Frame[] = [{ f: "base", place: placeOf(c.stack).concat(slot.path) }];
843
+ const start: Configuration = { mode: "resume", result: ok(null), stack, applying: p.applying, marked: c.marked };
844
+ const end = drive(p, enter(start, { at: NO_KEYS }, site, slot, given));
845
+ return end.result.ok ? ok(forHost(p, end, end.result.value)) : end.result;
846
+ }
847
+
848
+ /** RUN.EVAL: an effect of a host evaluator, with RUN.CAP, RUN.ADDRESS and RUN.MOCK. */
849
+ function cause(p: Program, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
850
+ const keys = typeof kind === "string" ? kind.split(".") : kind;
851
+ const text = keys.join(".");
852
+ const { machine, perform, counts, effects } = p;
853
+ const allowed = machine.permits === undefined ? permits(ctx.caps, keys) : machine.permits(ctx.caps, keys, ctx);
854
+ if (!allowed) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
855
+ const at = placeText(place);
856
+ const count = counts.get(at) ?? 0;
857
+ counts.set(at, count + 1);
858
+ const effect: { -readonly [K in keyof Effect]: Effect[K] } = {
859
+ index: effects.length,
860
+ kind: text,
861
+ path: keys,
862
+ payload,
863
+ node: -1,
864
+ at: 0,
865
+ place,
866
+ context: ctx,
867
+ address: `${at}#${count}`,
868
+ };
869
+ effects.push(effect);
870
+ effect.outcome = answer(perform, effect, place);
871
+ return effect.outcome;
872
+ }
873
+
874
+ function answer(perform: Perform | undefined, effect: Effect, place: readonly Key[]): Result {
875
+ if (perform === undefined) return ok(effect.payload);
876
+ let given: unknown;
877
+ try {
878
+ given = perform(effect);
879
+ } catch (error) {
880
+ if (error instanceof Failure) return placed({ ok: false, error: error.error }, place);
881
+ return fail("#CALC!", `The host failed the effect ${effect.kind}: ${describe(error)}`, { place, cause: error });
882
+ }
883
+ if (!(given instanceof Promise)) return ok(given);
884
+ void given.catch(() => undefined);
885
+ return fail("#CALC!", `The mock gave a promise for the effect ${effect.kind}, and an evaluation does not wait.`, { place });
886
+ }
887
+
888
+ /** E-APPLY for host code: one step of the stepper starts the evaluator. */
889
+ function hostEvaluator(p: Program, c: Evaluating, name: string, evaluator: (input: EvalInput) => Result, doc: Envelope, keys: readonly Key[], data: Json, slots: readonly Slot[], site: Site): Configuration {
890
+ const place = placeOf(c.stack);
891
+ const parts: Part[] = slots.map((slot) => {
892
+ const header = isEnvelope(slot.item) ? field(slot.item as never, "name") : undefined;
893
+ return { key: slot.key, path: slot.path, ...(typeof header === "string" ? { name: header } : {}), evaluate: (given?: Context) => nested(p, c, site, slot, given) };
894
+ });
895
+ const fill = (items: readonly unknown[]): unknown => fillIn(doc.value, slots, items);
896
+ p.applying = c.applying;
897
+ const input: EvalInput = {
898
+ kind: typeName(doc.type),
899
+ typePath: keys,
900
+ doc,
901
+ value: fill(parts),
902
+ data,
903
+ parts,
904
+ context: hostContext(p, c, site.ctx),
905
+ place,
906
+ machine: p.machine,
907
+ ...(c.address ? { address: true } : {}),
908
+ resolve: (path: readonly unknown[], _options?: ResolveOptions) => walkPath(p, site.ctx, path),
909
+ effect: (kind, payload) => cause(p, site.ctx, kind, payload, place),
910
+ fill,
911
+ };
912
+ try {
913
+ return back(c, evaluator(input));
914
+ } catch (error) {
915
+ return back(c, thrown(error, `the evaluator ${name}`));
916
+ }
917
+ }
918
+
919
+ /** The judgment 4: the rules E-PART to E-APPLY for one document, in their order. */
920
+ function evaluateDoc(p: Program, c: Evaluating): Configuration {
921
+ const { value, declared, depth } = c;
922
+ const { machine, verb } = p;
923
+ const { world } = machine;
924
+ const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
925
+ if (envelope === undefined && declared === undefined) return back(c, fail("#PART?", "The value has no envelope, and its position declares no type."));
926
+ const doc: Envelope = envelope ?? { type: declared as string, value: value as Json };
927
+ const found = lookup(world, doc.type);
928
+ if (depth > p.maxDepth) return back(c, fail("#DEPTH!", `The nesting is deeper than ${p.maxDepth} documents.`));
929
+ const kindText = typeName(doc.type);
930
+ const name = ladder(world, envelope === undefined ? undefined : field(envelope as never, "own"), found?.kind, found !== undefined, verb, () => true);
931
+ if (name === null) return back(c, fail("#KIND?", `The world has no kind ${kindText}.`));
932
+ const code = typeof name === "string" ? (own(machine.runners, name) ?? own(machine.evaluators, name)) : undefined;
933
+ if (name === undefined || code === undefined) return back(c, fail("#NOEVAL", `No rung gives an evaluator for ${kindText}.`));
934
+ const read = split(found?.kind.parts, doc.value);
935
+ if (read instanceof Bad) {
936
+ const where = read.at.length === 0 ? "" : ` (at ${read.at.join(".")})`;
937
+ return back(c, fail("#VALUE!", `${read.reason}${where} The kind is ${kindText}.`));
938
+ }
939
+ if (own(machine.runners, name) !== undefined) return back(c, fail("#NOEVAL", `The stepper has no runner ${name}.`));
940
+ const rules = found?.kind.caps;
941
+ const site: Site = { name, depth, ctx: c.ctx, ...(rules === undefined ? {} : { rules }) };
942
+ const keys = found?.path ?? [];
943
+ if (typeof code === "function") return hostEvaluator(p, c, name, code as never, doc, keys, read.data, read.slots, site);
944
+ if (own(CONSTRUCTS, name) === true) {
945
+ try {
946
+ return construct(p, c, name, doc, keys, read.slots, site);
947
+ } catch (error) {
948
+ return back(c, thrown(error, `the evaluator ${name}`));
949
+ }
950
+ }
951
+ const defined = own(p.definitions.evaluators, name);
952
+ if (defined !== undefined) return hostEvaluator(p, c, name, defined, doc, keys, read.data, read.slots, site);
953
+ return back(c, fail("#NOEVAL", `The stepper has no definition of the compiled evaluator ${name}.`));
954
+ }
955
+
956
+ /** The next part of a frame with a list of parts. */
957
+ function nextOf(frame: { next: number; readonly slots: readonly Slot[] }): Slot | undefined {
958
+ const slot = frame.slots[frame.next];
959
+ if (slot !== undefined) frame.next += 1;
960
+ return slot;
961
+ }
962
+
963
+ function resumeValues(p: Program, c: Resuming, frame: ValuesFrame): Configuration {
964
+ const { result } = c;
965
+ const { combine, omit } = p.profile;
966
+ if (result.ok) frame.values.push(result.value);
967
+ else if (frame.then === "record" && omit?.(result.error) === true) {
968
+ frame.skipped.push(frame.values.length);
969
+ frame.values.push(null);
970
+ } else if (frame.strict || combine === undefined) {
971
+ popped(c);
972
+ return c;
973
+ } else frame.errors.push(result.error);
974
+ const slot = nextOf(frame);
975
+ if (slot !== undefined) return enter(c, frame, frame.site, slot);
976
+ popped(c);
977
+ const first = frame.errors[0];
978
+ if (first === undefined) return finish(p, c, frame);
979
+ return back(c, { ok: false, error: frame.errors.length === 1 ? first : (combine as NonNullable<typeof combine>)(frame.errors) });
980
+ }
981
+
982
+ /** The rule `resume(k, r)`: the top frame takes a result. An exception of a rule of the profile ends its document. */
983
+ function resume(p: Program, c: Resuming): Configuration {
984
+ const stack = c.stack as Frame[];
985
+ const frame = stack[stack.length - 1];
986
+ if (frame === undefined) return { mode: "done", result: c.result, stack, applying: c.applying, marked: c.marked };
987
+ try {
988
+ return take(p, c, frame);
989
+ } catch (error) {
990
+ if (stack[stack.length - 1] === frame) popped(c);
991
+ return back(c, thrown(error, `the evaluator ${"site" in frame ? frame.site.name : "of the part"}`));
992
+ }
993
+ }
994
+
995
+ function take(p: Program, c: Resuming, frame: Frame): Configuration {
996
+ const stack = c.stack as Frame[];
997
+ const { result } = c;
998
+ switch (frame.f) {
999
+ case "base":
1000
+ popped(c);
1001
+ return { mode: "done", result, stack, applying: c.applying, marked: c.marked };
1002
+ case "tail":
1003
+ popped(c);
1004
+ return c;
1005
+ case "return":
1006
+ popped(c);
1007
+ return { ...c, applying: c.applying - 1 };
1008
+ case "values":
1009
+ return resumeValues(p, c, frame);
1010
+ case "let": {
1011
+ const key = String((frame.slots[frame.next - 1] as Slot).path[1]);
1012
+ setField(frame.names, key, result.ok ? result.value : { [HELD_MARK]: result.error });
1013
+ const marked = c.marked || !result.ok;
1014
+ const slot = nextOf(frame);
1015
+ if (slot !== undefined) return enter(c, frame, frame.site, slot, undefined, marked);
1016
+ const tail: Frame = { f: "tail", at: NO_KEYS };
1017
+ stack[stack.length - 1] = tail;
1018
+ return enter(c, tail, frame.site, frame.body, { ...frame.site.ctx, names: frame.names }, marked);
1019
+ }
1020
+ case "call": {
1021
+ popped(c);
1022
+ if (!result.ok) return c;
1023
+ if (closureOf(result.value) === undefined && typeof result.value !== "function") return back(c, fail("#VALUE!", "The fn of call must give a function."));
1024
+ return startValues(p, c, frame.site, "args", frame.args, true, { target: result.value });
1025
+ }
1026
+ case "if": {
1027
+ if (!result.ok) {
1028
+ popped(c);
1029
+ return c;
1030
+ }
1031
+ const truth = truthOf(p, c, result.value);
1032
+ if (truth === undefined) {
1033
+ popped(c);
1034
+ return back(c, fail("#VALUE!", "The test of if must give a truth value."));
1035
+ }
1036
+ const branch = truth ? frame.yes : frame.no;
1037
+ if (branch === undefined) {
1038
+ popped(c);
1039
+ return back(c, ok(Object.hasOwn(p.profile, "otherwise") ? p.profile.otherwise : null));
1040
+ }
1041
+ const tail: Frame = { f: "tail", at: NO_KEYS };
1042
+ stack[stack.length - 1] = tail;
1043
+ return enter(c, tail, frame.site, branch);
1044
+ }
1045
+ case "logic": {
1046
+ if (!result.ok) {
1047
+ popped(c);
1048
+ return c;
1049
+ }
1050
+ const truth = truthOf(p, c, result.value);
1051
+ const value = p.profile.logic === "value";
1052
+ if (truth === undefined) {
1053
+ popped(c);
1054
+ return back(c, fail("#VALUE!", `The ${frame.what} must give a truth value.`));
1055
+ }
1056
+ if (truth === frame.stop) {
1057
+ popped(c);
1058
+ return back(c, value ? result : ok(frame.stop));
1059
+ }
1060
+ const slot = nextOf(frame);
1061
+ if (slot !== undefined) return enter(c, frame, frame.site, slot);
1062
+ popped(c);
1063
+ return back(c, value ? result : ok(!frame.stop));
1064
+ }
1065
+ }
1066
+ }
1067
+
1068
+ function advance(p: Program, c: Configuration): Configuration {
1069
+ if (c.mode === "evaluate") return evaluateDoc(p, c);
1070
+ return c.mode === "resume" ? resume(p, c) : c;
1071
+ }
1072
+
1073
+ function drive(p: Program, start: Configuration): Extract<Configuration, { readonly mode: "done" }> {
1074
+ let now = start;
1075
+ while (now.mode !== "done") now = advance(p, now);
1076
+ return now;
1077
+ }
1078
+
1079
+ function programOf(machine: Machine, options: StepperOptions): Program {
1080
+ return {
1081
+ machine,
1082
+ profile: options.profile ?? {},
1083
+ definitions: options.definitions ?? {},
1084
+ verb: options.verb ?? "eval",
1085
+ maxDepth: machine.maxDepth ?? 1000,
1086
+ hosts: new WeakMap(),
1087
+ effects: options.effects ?? [],
1088
+ perform: options.perform,
1089
+ counts: new Map(),
1090
+ applying: 0,
1091
+ };
1092
+ }
1093
+
1094
+ /**
1095
+ * This function gives the first configuration of a document: `evaluate(d, Γ, [], [])`.
1096
+ *
1097
+ * @param machine - The world, the evaluators and the other code.
1098
+ * @param doc - The document.
1099
+ * @param context - The names and the capabilities at the root. A context without a world gets the world of the machine.
1100
+ * @param options - The type of a bare root value.
1101
+ * @returns The configuration.
1102
+ */
1103
+ export function stepperStart(machine: Machine, doc: unknown, context: Context = makeContext(), options: StepperOptions = {}): Configuration {
1104
+ const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
1105
+ return { mode: "evaluate", value: doc, declared: options.declaredType, depth: 0, address: false, ctx, stack: [], applying: 0, marked: false };
1106
+ }
1107
+
1108
+ /**
1109
+ * This function does one step. It is pure: the configuration stays as it is, and a host can keep it, store it as JSON and
1110
+ * step it again (STEPPER.DATA). The result of `done` holds a closure as data. The function `stepper` gives a host
1111
+ * function.
1112
+ *
1113
+ * @param machine - The machine of the configuration.
1114
+ * @param configuration - The configuration.
1115
+ * @param options - The profile and the verb of the configuration.
1116
+ * @returns The next configuration, or the same configuration at `done`.
1117
+ */
1118
+ export function stepperNext(machine: Machine, configuration: Configuration, options: StepperOptions = {}): Configuration {
1119
+ const stack = [...configuration.stack];
1120
+ const top = stack[stack.length - 1];
1121
+ if (top !== undefined) {
1122
+ const { f } = top;
1123
+ if (f === "values") stack[stack.length - 1] = { ...top, values: [...top.values], errors: [...top.errors], skipped: [...top.skipped] };
1124
+ else if (f === "let") stack[stack.length - 1] = { ...top, names: { ...top.names } };
1125
+ else stack[stack.length - 1] = { ...top };
1126
+ }
1127
+ return advance(programOf(machine, options), { ...configuration, stack });
1128
+ }
1129
+
1130
+ /**
1131
+ * This function evaluates a document with an explicit stack (K95, D158). It gives the result of `reference` for each
1132
+ * document of the kernel and the formula kit (AGREE.STEPPER). The nesting of a document does not use the stack of the
1133
+ * host (STEPPER.DEEP). A host evaluator is opaque: one step starts it, and each of its handles starts a nested
1134
+ * stepper. The stepper does no effects and no runs.
1135
+ *
1136
+ * @param machine - The world, the evaluators and the other code.
1137
+ * @param doc - The document.
1138
+ * @param context - The names and the capabilities at the root.
1139
+ * @param options - The profile of the formula kit, the verb and the type of a bare root value.
1140
+ * @returns The result.
1141
+ */
1142
+ export function stepper(machine: Machine, doc: unknown, context: Context = makeContext(), options: StepperOptions = {}): Result {
1143
+ const p = programOf(machine, options);
1144
+ try {
1145
+ const end = drive(p, stepperStart(machine, doc, context, options));
1146
+ return end.result.ok ? ok(forHost(p, end, end.result.value)) : end.result;
1147
+ } catch (error) {
1148
+ return placed(thrown(error, "the stepper"), []);
1149
+ }
1150
+ }