@mark1russell7/coeval 0.14.0 → 0.16.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/build/index.d.ts +471 -190
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1209 -182
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +175 -7
- package/src/deref.ts +27 -9
- package/src/evaluate.ts +258 -12
- package/src/formula.ts +17 -3
- package/src/index.ts +12 -2
- package/src/meaning.ts +472 -0
- package/src/reads.ts +9 -2
- package/src/reference.ts +247 -42
- package/src/schedule.ts +14 -4
- package/src/stdlib.ts +26 -4
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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) =>
|
|
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:
|
|
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.
|
|
363
|
-
*
|
|
364
|
-
*
|
|
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
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
473
|
+
end(index, thrown(error, "a part"));
|
|
474
|
+
return;
|
|
387
475
|
} finally {
|
|
388
|
-
env.scope =
|
|
476
|
+
env.scope = around;
|
|
389
477
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
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
|
-
|
|
398
|
-
} catch {
|
|
399
|
-
|
|
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
|
-
|
|
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 (
|
|
405
|
-
|
|
406
|
-
|
|
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.
|
|
412
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/schedule.ts
CHANGED
|
@@ -166,12 +166,12 @@ const NO_KEYS: readonly Key[] = /* @__PURE__ */ Object.freeze([]);
|
|
|
166
166
|
const NO_EDGES: readonly NodeEdge[] = /* @__PURE__ */ Object.freeze([]);
|
|
167
167
|
|
|
168
168
|
/** This function gives the error of a node on a cycle. It names only the node, thus each order of demands gives the same error. */
|
|
169
|
-
function cycleOf(id: string): Failed {
|
|
169
|
+
export function cycleOf(id: string): Failed {
|
|
170
170
|
return fail("#CYCLE!", `The node ${id} is on a cycle of reads.`);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/** This function gives the error of a read of a node on the run side. */
|
|
174
|
-
function runSide(id: string): Failed {
|
|
174
|
+
export function runSide(id: string): Failed {
|
|
175
175
|
return fail("#REF!", `The node ${id} is on the run side, thus it has no value for a reader.`);
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -354,10 +354,12 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
354
354
|
|
|
355
355
|
/**
|
|
356
356
|
* This function gives the state of the current data epoch. Each data epoch has a new host thing at the root
|
|
357
|
-
* `world`. Thus a read that reaches the whole root changes at each change of data (READS.KEPT).
|
|
357
|
+
* `world`. Thus a read that reaches the whole root changes at each change of data (READS.KEPT). A kept read of a
|
|
358
|
+
* closure reads a node through `known`, without a demand (SCHED.CLOSURE).
|
|
358
359
|
*/
|
|
359
360
|
const current = (): Epoch => {
|
|
360
361
|
if (now !== undefined) return now;
|
|
362
|
+
const values = new Map<string, Result>();
|
|
361
363
|
const nodesThing: HostThing = {
|
|
362
364
|
[THING]: true,
|
|
363
365
|
label: "nodes",
|
|
@@ -371,10 +373,18 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
371
373
|
},
|
|
372
374
|
has: (key) => docs.has(String(key)),
|
|
373
375
|
toJson: record,
|
|
376
|
+
// A demand from a kept read can reach the node in evaluation, and that cycle is false (SCHED.ONE-MEANING).
|
|
377
|
+
// Thus only a node with its result in this epoch gives a value. Each such value is final (D31).
|
|
378
|
+
known(key) {
|
|
379
|
+
const result = values.get(String(key));
|
|
380
|
+
if (result === undefined) return undefined;
|
|
381
|
+
if (!result.ok) throw new Failure(result.error);
|
|
382
|
+
return { thing: result.value };
|
|
383
|
+
},
|
|
374
384
|
};
|
|
375
385
|
// The context has a world, thus `rootContext` only gives it its grant (CAP.NARROW-CODE). Each evaluation of the
|
|
376
386
|
// epoch then uses this object with no copy.
|
|
377
|
-
now = { context: rootContext(machine, { ...base, world: nodesThing }), values
|
|
387
|
+
now = { context: rootContext(machine, { ...base, world: nodesThing }), values };
|
|
378
388
|
return now;
|
|
379
389
|
};
|
|
380
390
|
|
package/src/stdlib.ts
CHANGED
|
@@ -560,6 +560,18 @@ export function attributeTrace(node: Thing, attribute: string): AttributeTrace |
|
|
|
560
560
|
return stateOf(node)?.trace(attribute);
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
/**
|
|
564
|
+
* This function gives the place of a node of the decorated tree: the keys from the root document to the document of
|
|
565
|
+
* the node (HOP.PLACE). A view puts each hop of a resolution on its node with it. The node keeps its place already,
|
|
566
|
+
* thus the function adds no record.
|
|
567
|
+
*
|
|
568
|
+
* @param thing - A thing, for example the thing of a hop.
|
|
569
|
+
* @returns The keys of the place, or `undefined` for a thing that is not a node of the decorated tree.
|
|
570
|
+
*/
|
|
571
|
+
export function placeOf(thing: Thing): readonly Key[] | undefined {
|
|
572
|
+
return stateOf(thing)?.place;
|
|
573
|
+
}
|
|
574
|
+
|
|
563
575
|
/** The path of the root `self`. A node of the tree resolves it, thus the list of reads holds the read (C4). */
|
|
564
576
|
const SELF_PATH: readonly Json[] = /* @__PURE__ */ Object.freeze(["self"]);
|
|
565
577
|
|
|
@@ -597,6 +609,11 @@ export interface NearestOptions {
|
|
|
597
609
|
readonly matches?: (node: Thing, wanted: Json | undefined) => boolean;
|
|
598
610
|
/** The most hops of one walk. The default is 256. */
|
|
599
611
|
readonly maxHops?: number;
|
|
612
|
+
/**
|
|
613
|
+
* `true`: the walk records one hop for each node on the way before the node that it reaches (HOP.EACH-STEP). Each
|
|
614
|
+
* such hop has the followed key as its step. The default is `false`: one hop for the whole walk.
|
|
615
|
+
*/
|
|
616
|
+
readonly eachStep?: boolean;
|
|
600
617
|
}
|
|
601
618
|
|
|
602
619
|
function hostType(node: Thing, wanted: Json | undefined): boolean {
|
|
@@ -606,23 +623,28 @@ function hostType(node: Thing, wanted: Json | undefined): boolean {
|
|
|
606
623
|
/**
|
|
607
624
|
* This function makes a walker `nearest`. The walker follows a key (the default is `parent`) until the node that it
|
|
608
625
|
* reaches has the wanted type. With `self: true`, the node where the walk starts is the first candidate. A node
|
|
609
|
-
* without that key ends the walk with an error.
|
|
626
|
+
* without that key ends the walk with an error. With `eachStep`, a resolution with hops gets a hop for each node on the way.
|
|
610
627
|
*
|
|
611
|
-
* @param options - The test of a type and the
|
|
628
|
+
* @param options - The test of a type, the most hops and the record of each step.
|
|
612
629
|
* @returns The walker.
|
|
613
630
|
*/
|
|
614
631
|
export function nearestWalker(options: NearestOptions = {}): Walker {
|
|
615
632
|
const matches = options.matches ?? hostType;
|
|
616
633
|
const maxHops = options.maxHops ?? 256;
|
|
617
|
-
|
|
634
|
+
const eachStep = options.eachStep === true;
|
|
635
|
+
return ({ data, here, deref, visit }) => {
|
|
618
636
|
const follow = isRecord(data) && typeof field(data, "follow") === "string" ? String(field(data, "follow")) : "parent";
|
|
619
637
|
const wanted = isRecord(data) ? field(data, "type") : undefined;
|
|
620
638
|
const fromSelf = isRecord(data) && field(data, "self") === true;
|
|
639
|
+
const record = eachStep ? visit : undefined;
|
|
621
640
|
let at: Thing | undefined = here;
|
|
622
641
|
for (let hops = 0; hops < maxHops; hops += 1) {
|
|
623
|
-
|
|
642
|
+
const stepped = hops > 0 || !fromSelf;
|
|
643
|
+
if (stepped) at = at === undefined || at === null ? undefined : deref(at, follow);
|
|
624
644
|
if (at === undefined || at === null) return { ok: false, reason: `No node of the type ${String(wanted)} is on the way.` };
|
|
625
645
|
if (matches(at, wanted)) return { ok: true, thing: at };
|
|
646
|
+
// The hop of the whole walk records the node that the walk reaches. Each node before it gets its own hop.
|
|
647
|
+
if (stepped) record?.(follow, at);
|
|
626
648
|
}
|
|
627
649
|
return { ok: false, reason: "The walk is too long." };
|
|
628
650
|
};
|