@mark1russell7/coeval 0.14.0 → 0.15.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/reference.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type Effect,
11
11
  type EvalFn,
12
12
  type EvalInput,
13
+ type Journal,
13
14
  type Machine,
14
15
  type Part,
15
16
  type Perform,
@@ -17,6 +18,8 @@ import {
17
18
  type RunInput,
18
19
  type RunPart,
19
20
  type Steps,
21
+ type StopSignal,
22
+ JOURNAL,
20
23
  makeContext,
21
24
  permits,
22
25
  placeText,
@@ -56,6 +59,17 @@ export interface ReferenceOptions {
56
59
  readonly verb?: string;
57
60
  }
58
61
 
62
+ /** The options of a run of the reference: the options of an evaluation, with the host and a signal (REF.RUN). */
63
+ export interface ReferenceRunOptions extends ReferenceOptions {
64
+ /**
65
+ * The host code that does each effect of a runner. A promise from it makes the run wait. Without it, an effect gives
66
+ * its payload. A journal on the function sets the order of the waits of each `all` (RUN.REPLAY).
67
+ */
68
+ readonly perform?: Perform;
69
+ /** A signal of the host. When it aborts, the run stops with `#ABORT` (RUN.ABORT). */
70
+ readonly signal?: StopSignal;
71
+ }
72
+
59
73
  /** The scope of the addresses of effects (RUN.ADDRESS): the root of the evaluation, or one item of `all`. */
60
74
  interface Scope {
61
75
  /** The text before each address of the scope. */
@@ -73,6 +87,10 @@ interface Env {
73
87
  readonly effects: Effect[];
74
88
  readonly maxDepth: number;
75
89
  readonly perform: Perform | undefined;
90
+ /** The journal on the function `perform`, when it has one. It gets and gives the order of the waits of `all`. */
91
+ readonly journal: Journal | undefined;
92
+ /** `true` in a run: the host does each effect of a runner, and an effect of an evaluator gives `#CALC!`. */
93
+ readonly running: boolean;
76
94
  /** The verb that goes up the ladder at each document. Each part evaluates with the same verb. */
77
95
  readonly verb: string;
78
96
  /** The scope of the code that evaluates at this time. `all` changes it for each item. */
@@ -209,6 +227,49 @@ function resolveFully(machine: Machine, ctx: Context, path: readonly unknown[],
209
227
  * mock, the effect gives its payload (RUN.EVAL).
210
228
  */
211
229
  function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
230
+ const effect = recordEffect(env, ctx, kind, payload, place);
231
+ if ("ok" in effect) return effect;
232
+ effect.outcome = answer(env.perform, effect, place);
233
+ return effect.outcome;
234
+ }
235
+
236
+ /**
237
+ * This function causes the effect of an evaluator. In a run, the host does each effect, thus only a runner can cause
238
+ * one. The effect then gives `#CALC!` before the test of its capability (RUN.NEEDS-RUNNER).
239
+ */
240
+ function causeFromEvaluator(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Result {
241
+ const text = typeof kind === "string" ? kind : kind.join(".");
242
+ if (env.running) return fail("#CALC!", `The effect ${text} needs a runner: in a run, the host does each effect.`, { place });
243
+ return cause(env, ctx, kind, payload, place);
244
+ }
245
+
246
+ /**
247
+ * This function causes the effect of a runner. In an evaluation, the mock answers it at once. In a run, the host does
248
+ * it, and a promise from the host makes the steps wait (RUN.SEQ). An effect whose wait a stop ends keeps no outcome
249
+ * (RUN.OUTCOME).
250
+ */
251
+ function* causeAndWait(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Steps {
252
+ if (!env.running) return cause(env, ctx, kind, payload, place);
253
+ const effect = recordEffect(env, ctx, kind, payload, place);
254
+ if ("ok" in effect) return effect;
255
+ const perform = env.perform;
256
+ if (perform === undefined) return (effect.outcome = ok(payload));
257
+ let given: unknown;
258
+ try {
259
+ given = perform(effect);
260
+ } catch (error) {
261
+ return (effect.outcome = hostError(effect, place, error));
262
+ }
263
+ if (!(given instanceof Promise)) return (effect.outcome = ok(given));
264
+ const outcome = (yield given.then(
265
+ (value: unknown) => ok(value),
266
+ (error: unknown) => hostError(effect, place, error),
267
+ )) as Result;
268
+ return (effect.outcome = outcome);
269
+ }
270
+
271
+ /** This function checks the capability of an effect, and records the effect with its address (RUN.CAP, RUN.ADDRESS). */
272
+ function recordEffect(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Failed | { -readonly [K in keyof Effect]: Effect[K] } {
212
273
  const keys = typeof kind === "string" ? kind.split(".") : kind;
213
274
  const text = keys.join(".");
214
275
  const machine = env.machine;
@@ -229,8 +290,13 @@ function cause(env: Env, ctx: Context, kind: Cap, payload: unknown, place: reado
229
290
  address: `${env.scope.prefix}${at}#${count}`,
230
291
  };
231
292
  env.effects.push(effect);
232
- effect.outcome = answer(env.perform, effect, place);
233
- return effect.outcome;
293
+ return effect;
294
+ }
295
+
296
+ /** This function gives the outcome of an effect that the host failed. A `Failure` gives its own error value (RUN.CAUSE). */
297
+ function hostError(effect: Effect, place: readonly Key[], error: unknown): Result {
298
+ if (error instanceof Failure) return placed({ ok: false, error: error.error }, place);
299
+ return fail("#CALC!", `The host failed the effect ${effect.kind}: ${describe(error)}`, { place, cause: error });
234
300
  }
235
301
 
236
302
  /**
@@ -243,8 +309,7 @@ function answer(perform: Perform | undefined, effect: Effect, place: readonly Ke
243
309
  try {
244
310
  given = perform(effect);
245
311
  } catch (error) {
246
- if (error instanceof Failure) return placed({ ok: false, error: error.error }, place);
247
- return fail("#CALC!", `The host failed the effect ${effect.kind}: ${describe(error)}`, { place, cause: error });
312
+ return hostError(effect, place, error);
248
313
  }
249
314
  if (!(given instanceof Promise)) return ok(given);
250
315
  void given.catch(() => undefined);
@@ -321,7 +386,7 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
321
386
  machine: env.machine,
322
387
  ...(at.address ? { address: true } : {}),
323
388
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
324
- effect: (kind, payload) => cause(env, at.ctx, kind, payload, at.place),
389
+ effect: (kind, payload) => causeFromEvaluator(env, at.ctx, kind, payload, at.place),
325
390
  fill,
326
391
  };
327
392
  }
@@ -349,67 +414,124 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
349
414
  place: at.place,
350
415
  machine: env.machine,
351
416
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
352
- effect: function* (kind, payload) {
353
- return cause(env, at.ctx, kind, payload, at.place);
354
- },
417
+ effect: (kind, payload) => causeAndWait(env, at.ctx, kind, payload, at.place),
355
418
  all: (list, options) => together(env, list, options),
356
419
  fill,
357
420
  };
358
421
  }
359
422
 
423
+ /** A part of `all` that waits, and the promise that gives its next input. */
424
+ interface Waiter {
425
+ readonly index: number;
426
+ readonly steps: Steps;
427
+ next: Promise<Resumed>;
428
+ }
429
+
430
+ /** The input of a part after its wait: the value of the promise, or its rejection. */
431
+ interface Resumed {
432
+ readonly waiter: Waiter;
433
+ readonly value?: unknown;
434
+ readonly error?: unknown;
435
+ readonly threw: boolean;
436
+ }
437
+
360
438
  /**
361
439
  * This function evaluates parts for `all` (RUN.ALL). The parts start in their order, up to the limit, and each part
362
- * goes on until it ends or waits. In an evaluation, a part that waits does not end, thus it keeps its place in the
363
- * limit. With `failFast`, the first failure stops the parts that wait, and the later parts do not start
364
- * (RUN.FAIL-FAST). When no part failed and a part waits, the steps wait, and the drive gives `#CALC!`.
440
+ * goes on until it ends or waits. A part that waits keeps its place in the limit. With `failFast`, the first failure
441
+ * stops the parts that wait, and the later parts do not start (RUN.FAIL-FAST).
442
+ *
443
+ * When a part waits, the steps wait for the next part that settles. The journal of the host can choose that part
444
+ * (RUN.REPLAY), and it gets each part that settled (RUN.RECORD). In an evaluation, the drive stops at the first wait
445
+ * and gives `#CALC!`. In a run, the host settles the wait, and the part goes on in its own scope.
365
446
  *
366
447
  * Each part has its own scope of addresses. Its prefix names the use of `all` in the scope around it, and the part
367
448
  * (RUN.ADDRESS).
368
449
  */
369
450
  function* together(env: Env, list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
370
451
  const limit = Math.max(1, options.concurrency ?? list.length);
452
+ const failFast = options.failFast === true;
371
453
  const results: (Result | undefined)[] = list.map(() => undefined);
372
- const waiting: { readonly steps: Steps; readonly promise: Promise<unknown> }[] = [];
454
+ const scopes: Scope[] = [];
455
+ const waiting: Waiter[] = [];
373
456
  const outer = env.scope;
374
457
  const address = `${outer.prefix}all${outer.alls}`;
375
458
  outer.alls += 1;
376
459
  let failed = false;
377
- for (let index = 0; index < list.length && !failed && waiting.length < limit; index += 1) {
378
- let result: Result | undefined;
379
- env.scope = { prefix: `${address}.${index}/`, counts: new Map(), alls: 0 };
460
+ let started = 0;
461
+ const end = (index: number, result: Result): void => {
462
+ results[index] = result;
463
+ if (failFast && !result.ok) failed = true;
464
+ };
465
+ /** This function gives a part its input in its own scope, and keeps it in the list when it waits again. */
466
+ const goOn = (index: number, steps: Steps, input: Omit<Resumed, "waiter">): void => {
467
+ const around = env.scope;
468
+ env.scope = scopes[index] as Scope;
469
+ let step: IteratorResult<Promise<unknown>, Result>;
380
470
  try {
381
- const steps = (list[index] as RunPart).evaluate();
382
- const step = steps.next();
383
- if (step.done === true) result = step.value;
384
- else waiting.push({ steps, promise: step.value });
471
+ step = input.threw ? steps.throw(input.error) : steps.next(input.value);
385
472
  } catch (error) {
386
- result = thrown(error, "a part");
473
+ end(index, thrown(error, "a part"));
474
+ return;
387
475
  } finally {
388
- env.scope = outer;
476
+ env.scope = around;
389
477
  }
390
- results[index] = result;
391
- if (result !== undefined && options.failFast === true && !result.ok) failed = true;
392
- }
393
- if (failed) {
394
- const stopped = fail("#ABORT", "The part stopped: a part before it failed, and the parts run with failFast.");
395
- for (const item of waiting) {
478
+ if (step.done === true) {
479
+ end(index, step.value);
480
+ return;
481
+ }
482
+ const waiter: Waiter = { index, steps, next: Promise.resolve() as unknown as Promise<Resumed> };
483
+ waiter.next = step.value.then(
484
+ (value: unknown): Resumed => ({ waiter, value, threw: false }),
485
+ (error: unknown): Resumed => ({ waiter, error, threw: true }),
486
+ );
487
+ waiting.push(waiter);
488
+ };
489
+ /** This function starts the next parts in their order, while the limit permits it and no part failed. */
490
+ const startMore = (): void => {
491
+ while (!failed && waiting.length < limit && started < list.length) {
492
+ const index = started;
493
+ started += 1;
494
+ scopes[index] = { prefix: `${address}.${index}/`, counts: new Map(), alls: 0 };
495
+ const around = env.scope;
496
+ env.scope = scopes[index];
497
+ let steps: Steps;
396
498
  try {
397
- item.steps.return(stopped);
398
- } catch {
399
- // The finally code of a part threw. The part stops with the abort.
499
+ steps = (list[index] as RunPart).evaluate();
500
+ } catch (error) {
501
+ end(index, thrown(error, "a part"));
502
+ continue;
503
+ } finally {
504
+ env.scope = around;
400
505
  }
506
+ goOn(index, steps, { threw: false });
401
507
  }
402
- return results.map((result) => result ?? stopped);
508
+ };
509
+ startMore();
510
+ while (waiting.length > 0 && !failed) {
511
+ const chosen = env.journal?.next?.(address, waiting.map((waiter) => waiter.index));
512
+ const pick = chosen === undefined ? undefined : waiting.find((waiter) => waiter.index === chosen);
513
+ const resumed = (yield pick === undefined ? Promise.race(waiting.map((waiter) => waiter.next)) : pick.next) as Resumed;
514
+ env.journal?.settled?.(address, resumed.waiter.index);
515
+ waiting.splice(waiting.indexOf(resumed.waiter), 1);
516
+ goOn(resumed.waiter.index, resumed.waiter.steps, resumed);
517
+ startMore();
403
518
  }
404
- if (waiting.length > 0) yield Promise.race(waiting.map((item) => item.promise));
405
- // An evaluation stops at the first wait, thus only parts that ended reach this line.
406
- return results as readonly Result[];
519
+ if (!failed) return results as readonly Result[];
520
+ const stopped = fail("#ABORT", "The part stopped: a part before it failed, and the parts run with failFast.");
521
+ for (const waiter of waiting) {
522
+ try {
523
+ waiter.steps.return(stopped);
524
+ } catch {
525
+ // The finally code of a part threw. The part stops with the abort.
526
+ }
527
+ }
528
+ return results.map((result) => result ?? stopped);
407
529
  }
408
530
 
409
531
  /**
410
532
  * This function evaluates a document by the rules, without a compilation. It is the reference of the plans: a plan,
411
- * a traced plan and the second tier of a language must give its results. It covers evaluations, not runs: an effect
412
- * gives the answer of the mock or its payload, and a part that waits gives `#CALC!`.
533
+ * a traced plan and the second tier of a language must give its results. In an evaluation, an effect gives the answer
534
+ * of the mock or its payload, and a part that waits gives `#CALC!`. `referenceRun` reads the rules of a run.
413
535
  *
414
536
  * The verb of the options goes up the ladder at each document, as in `prepare`. Each verb has the same rules: the
415
537
  * ladder, the contract of the kind and the sealed parts (VERB.AGREE).
@@ -421,27 +543,110 @@ function* together(env: Env, list: readonly RunPart[], options: AllOptions = {})
421
543
  * @returns The result.
422
544
  */
423
545
  export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
424
- const env: Env = {
546
+ const env = envOf(machine, options, false);
547
+ try {
548
+ return within(env, () => evaluateAt(env, rootAt(machine, doc, context, options)));
549
+ } catch (error) {
550
+ return placed(thrown(error, "the reference"), []);
551
+ }
552
+ }
553
+
554
+ /**
555
+ * This function starts a run of a document by the rules, without a compilation (REF.RUN). The host does each effect of a runner,
556
+ * and a step waits when the host waits (RUN.SEQ). `all` settles the waits in the order of the host, or in the order
557
+ * of the journal of a replay (RUN.REPLAY). An effect of an evaluator gives `#CALC!` (RUN.NEEDS-RUNNER). A signal that
558
+ * aborts stops the run with `#ABORT` (RUN.ABORT).
559
+ *
560
+ * The part of the run before the first wait happens at once, in the call (RUN.EAGER). `agreeRun` compares the run of
561
+ * a plan with this run.
562
+ *
563
+ * @param machine - The world, the evaluators and the runners.
564
+ * @param doc - The document.
565
+ * @param context - The names and the capabilities at the root.
566
+ * @param options - The options of `reference`, the host and a signal.
567
+ * @returns The result, when the run ends.
568
+ */
569
+ export function referenceRun(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceRunOptions = {}): Promise<Result> {
570
+ const env = envOf(machine, options, true);
571
+ const steps = stepsAt(env, rootAt(machine, doc, context, options));
572
+ return driveRun(env, steps, options.signal).catch((error: unknown) => placed(thrown(error, "the reference"), []));
573
+ }
574
+
575
+ /** This function makes the state of the reference for one evaluation or one run. */
576
+ function envOf(machine: Machine, options: ReferenceOptions, running: boolean): Env {
577
+ const perform = options.perform;
578
+ return {
425
579
  machine,
426
580
  definitions: options.definitions ?? {},
427
581
  effects: options.effects ?? [],
428
582
  maxDepth: machine.maxDepth ?? 1000,
429
- perform: options.perform,
583
+ perform,
584
+ journal: perform === undefined ? undefined : (perform as { readonly [JOURNAL]?: Journal })[JOURNAL],
585
+ running,
430
586
  verb: options.verb ?? "eval",
431
587
  scope: { prefix: "", counts: new Map(), alls: 0 },
432
588
  };
589
+ }
590
+
591
+ /** This function gives the root position of a document. A context without a world gets the world of the machine. */
592
+ function rootAt(machine: Machine, doc: unknown, context: Context, options: ReferenceOptions): At {
433
593
  const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
434
- // A node of a tree reads the definitions of this evaluation for its equations (TREE.LADDER).
594
+ return { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx };
595
+ }
596
+
597
+ /**
598
+ * This function does one synchronous step of an evaluation or a run. A node of a tree reads the definitions of the
599
+ * evaluation for its equations (TREE.LADDER). Thus the state of the evaluation is the innermost one in the step.
600
+ */
601
+ function within<T>(env: Env, step: () => T): T {
435
602
  activeEnvs.push(env);
436
603
  try {
437
- return evaluateAt(env, { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx });
438
- } catch (error) {
439
- return placed(thrown(error, "the reference"), []);
604
+ return step();
440
605
  } finally {
441
606
  activeEnvs.pop();
442
607
  }
443
608
  }
444
609
 
610
+ /** The mark of a signal that aborted. */
611
+ const STOPPED: unique symbol = /* @__PURE__ */ Symbol("stopped");
612
+
613
+ /**
614
+ * This function drives the steps of a run. Each step waits for its promise. A rejection goes into the steps as an
615
+ * exception. A signal that aborts stops the steps with `#ABORT`, also before the first step (RUN.ABORT).
616
+ */
617
+ async function driveRun(env: Env, steps: Steps, signal: StopSignal | undefined): Promise<Result> {
618
+ const stop = (): Result => {
619
+ const why = signal?.reason ?? "no reason";
620
+ const aborted = fail("#ABORT", `The host stopped the run: ${describe(why)}`, { cause: signal?.reason });
621
+ try {
622
+ within(env, () => steps.return(aborted));
623
+ } catch {
624
+ // The finally code of a runner threw. The run stops with the abort.
625
+ }
626
+ return aborted;
627
+ };
628
+ if (signal?.aborted === true) return stop();
629
+ const stopping =
630
+ signal === undefined
631
+ ? undefined
632
+ : new Promise<typeof STOPPED>((resolve) => {
633
+ signal.addEventListener("abort", () => resolve(STOPPED), { once: true });
634
+ });
635
+ let step = within(env, () => steps.next());
636
+ while (step.done !== true) {
637
+ let input: unknown;
638
+ try {
639
+ input = await (stopping === undefined ? step.value : Promise.race([step.value, stopping]));
640
+ } catch (error) {
641
+ step = within(env, () => steps.throw(error));
642
+ continue;
643
+ }
644
+ if (input === STOPPED) return stop();
645
+ step = within(env, () => steps.next(input));
646
+ }
647
+ return step.value;
648
+ }
649
+
445
650
  /** The evaluations of the reference that run at this time, the innermost last. */
446
651
  const activeEnvs: Env[] = [];
447
652