@mark1russell7/coeval 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/build/index.d.ts +151 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +689 -34
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/index.ts +5 -0
- package/src/instances.ts +33 -5
- package/src/reference.ts +142 -12
- package/src/static.ts +8 -2
- package/src/stdlib.ts +367 -23
- package/src/steps.ts +181 -0
- package/src/world.ts +59 -0
package/src/stdlib.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Json, type Key, field, isRecord } from "./doc.ts";
|
|
1
|
+
import { type Json, type Key, docEquals, field, isRecord } from "./doc.ts";
|
|
2
2
|
import {
|
|
3
3
|
type EvalInput,
|
|
4
4
|
type Evaluator,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { Failure } from "./errors.ts";
|
|
19
19
|
import { type Cap, type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
|
|
20
20
|
import type { World } from "./ladder.ts";
|
|
21
|
-
import { type AttributeDeclaration, type TreeContract, equationType, lookupKind } from "./world.ts";
|
|
21
|
+
import { type AttributeDeclaration, type CircularRule, type TreeContract, circularOf, equationType, lookupKind } from "./world.ts";
|
|
22
22
|
import { STD_DOMAINS } from "./types.ts";
|
|
23
23
|
import type { SyntaxProfile } from "./syntax.ts";
|
|
24
24
|
|
|
@@ -270,12 +270,8 @@ export interface AttributeTrace {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
/** The state of a node behind its brand. */
|
|
273
|
-
interface NodeState {
|
|
273
|
+
interface NodeState extends LoopNode {
|
|
274
274
|
readonly build: TreeBuild;
|
|
275
|
-
readonly type: string;
|
|
276
|
-
readonly place: readonly Key[];
|
|
277
|
-
/** The tree contract of the kind of the node (D42). */
|
|
278
|
-
readonly tree: TreeContract | undefined;
|
|
279
275
|
/**
|
|
280
276
|
* This function gives an attribute through its ladder (TREE.LADDER).
|
|
281
277
|
*
|
|
@@ -328,9 +324,12 @@ export interface AttributeAnswer {
|
|
|
328
324
|
* This function gives the memo cell of one attribute of one node, for a host that keeps its cells between evaluations
|
|
329
325
|
* (TREE.CELL, D108). The kit uses it at each demand of an attribute in an evaluation.
|
|
330
326
|
*
|
|
331
|
-
* The array `place` is one object for each
|
|
332
|
-
* plan.
|
|
333
|
-
*
|
|
327
|
+
* The array `place` is one object for each node of a plan in its scope. It stays the same across the evaluations of
|
|
328
|
+
* that plan. The scope of a node is the node around it, or the demand that made its tree (TREE.CELL-DOCUMENT,
|
|
329
|
+
* D115). Thus a cached plan, for example the plan of an equation, gives a new array for the nodes of each document.
|
|
330
|
+
*
|
|
331
|
+
* A new plan gives new arrays. Thus a host keeps its cells by the identity of `place` and by `attribute`.
|
|
332
|
+
* A host that keeps its cells by the keys of `place` can give the answer of another node or a stale answer.
|
|
334
333
|
*
|
|
335
334
|
* The function gives the answer of `compute`, or a kept answer. A host with signals gives a computed signal: a change
|
|
336
335
|
* of its store then makes only the attributes that read the store compute again. Each demand gives a new `compute`, and
|
|
@@ -339,7 +338,7 @@ export interface AttributeAnswer {
|
|
|
339
338
|
* The kit marks an attribute in evaluation before it uses the cell. Thus a cycle gives `#CYCLE!` before it reaches the
|
|
340
339
|
* cell again. An exception of the function that `compute` did not throw gives `#CYCLE!` with the chain of the demands.
|
|
341
340
|
*
|
|
342
|
-
* @param place - The keys from the root document to the document of the node.
|
|
341
|
+
* @param place - The key of the node. Its items are the keys from the root document to the document of the node.
|
|
343
342
|
* @param attribute - The name of the attribute.
|
|
344
343
|
* @param compute - The function that visits the rungs of the ladder.
|
|
345
344
|
* @returns The answer.
|
|
@@ -428,14 +427,60 @@ const BUSY: Cell = /* @__PURE__ */ Object.freeze({ busy: true });
|
|
|
428
427
|
const NO_ANSWER: AttributeAnswer = /* @__PURE__ */ Object.freeze({ result: undefined, by: undefined });
|
|
429
428
|
|
|
430
429
|
/**
|
|
431
|
-
* The marks of the attributes in evaluation, for each
|
|
432
|
-
* two evaluations share their
|
|
430
|
+
* The marks of the attributes in evaluation, for each key of a node, with the cells of a host (TREE.CELL). Nodes of
|
|
431
|
+
* two evaluations share their key and their kept cells, thus they share their marks. Nodes of two documents have
|
|
432
|
+
* different keys, thus a nested evaluation gets its own marks (TREE.CELL-NESTED, D115).
|
|
433
433
|
*/
|
|
434
434
|
const marksByPlace = /* @__PURE__ */ new WeakMap<readonly Key[], Map<string, Cell>>();
|
|
435
435
|
|
|
436
|
-
/** The trees of the cells of a host, for each
|
|
436
|
+
/** The trees of the cells of a host, for each key of a root. Nodes of two evaluations stay in one tree. */
|
|
437
437
|
const buildsByPlace = /* @__PURE__ */ new WeakMap<readonly Key[], TreeBuild>();
|
|
438
438
|
|
|
439
|
+
/** The keys of the nodes with the cells of a host, for each scope and each place of a plan (D115). */
|
|
440
|
+
const keysByScope = /* @__PURE__ */ new WeakMap<object, WeakMap<readonly Key[], readonly Key[]>>();
|
|
441
|
+
|
|
442
|
+
/** The scopes of the roots that a demand made, for each marks of a node and each name of an attribute (D115). */
|
|
443
|
+
const demandScopes = /* @__PURE__ */ new WeakMap<Map<string, Cell>, Map<string, object>>();
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* This function gives the key of a node with the cells of a host (TREE.CELL-DOCUMENT, D115). The key of the node
|
|
447
|
+
* around the node is its scope. A root without such a node has the scope of the open demand, or no scope.
|
|
448
|
+
* Without a scope, the key is the place of the plan. Thus a cached plan gives a new key for the nodes of each
|
|
449
|
+
* document, and each evaluation of a document gives the same keys.
|
|
450
|
+
*
|
|
451
|
+
* @param place - The place of the document of the node in its plan.
|
|
452
|
+
* @param above - The state of the node around the node, or `undefined`.
|
|
453
|
+
* @returns The key: an array with the keys of `place`.
|
|
454
|
+
*/
|
|
455
|
+
function cellPlace(place: readonly Key[], above: NodeState | undefined): readonly Key[] {
|
|
456
|
+
let scope: object | undefined = above?.place;
|
|
457
|
+
if (scope === undefined) {
|
|
458
|
+
const demand = demands[demands.length - 1];
|
|
459
|
+
if (demand === undefined) return place;
|
|
460
|
+
let byName = demandScopes.get(demand.cells);
|
|
461
|
+
if (byName === undefined) {
|
|
462
|
+
byName = new Map();
|
|
463
|
+
demandScopes.set(demand.cells, byName);
|
|
464
|
+
}
|
|
465
|
+
scope = byName.get(demand.key);
|
|
466
|
+
if (scope === undefined) {
|
|
467
|
+
scope = {};
|
|
468
|
+
byName.set(demand.key, scope);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
let keys = keysByScope.get(scope);
|
|
472
|
+
if (keys === undefined) {
|
|
473
|
+
keys = new WeakMap();
|
|
474
|
+
keysByScope.set(scope, keys);
|
|
475
|
+
}
|
|
476
|
+
let key = keys.get(place);
|
|
477
|
+
if (key === undefined) {
|
|
478
|
+
key = Object.freeze([...place]);
|
|
479
|
+
keys.set(place, key);
|
|
480
|
+
}
|
|
481
|
+
return key;
|
|
482
|
+
}
|
|
483
|
+
|
|
439
484
|
/** This function gives the value of a weak map, or puts a new value in it. */
|
|
440
485
|
function keptAt<T>(map: WeakMap<readonly Key[], T>, place: readonly Key[], make: () => T): T {
|
|
441
486
|
let value = map.get(place);
|
|
@@ -459,6 +504,293 @@ interface Demand {
|
|
|
459
504
|
/** The attributes in evaluation, in the order of their demands. An evaluation does not wait, thus one list serves. */
|
|
460
505
|
const demands: Demand[] = [];
|
|
461
506
|
|
|
507
|
+
/** One value of an attribute in an open fixed point (D113). */
|
|
508
|
+
interface LoopValue {
|
|
509
|
+
readonly result: Result | undefined;
|
|
510
|
+
readonly by: AttributeRung | undefined;
|
|
511
|
+
/** The round of an approximation. The round `0` marks a final answer of a node with the cells of a host. */
|
|
512
|
+
readonly round: number;
|
|
513
|
+
/** The plain memo of the node. A stable fixed point writes its final value there. A node with host cells has none. */
|
|
514
|
+
readonly memo: Map<string, Cell> | undefined;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* A fixed point in evaluation (TREE.CIRCULAR, D113). The circular attribute that a demand reaches first leads it. Each
|
|
519
|
+
* circular attribute that a demand reaches during its rounds joins it, thus only one fixed point is open at a time.
|
|
520
|
+
*/
|
|
521
|
+
interface FixedPoint {
|
|
522
|
+
/** The index of the demand of the leader in `demands`. */
|
|
523
|
+
readonly depth: number;
|
|
524
|
+
round: number;
|
|
525
|
+
/** `true` when a circular attribute got a new value in this round. */
|
|
526
|
+
changed: boolean;
|
|
527
|
+
/** The values of the fixed point, by the marks of a node and by the name of the attribute. */
|
|
528
|
+
readonly values: Map<Map<string, Cell>, Map<string, LoopValue>>;
|
|
529
|
+
/** The demands of circular attributes, from the leader up. */
|
|
530
|
+
readonly circular: Set<Demand>;
|
|
531
|
+
/** The demands above the leader that read an approximation. Their results are not final. */
|
|
532
|
+
readonly tentative: Set<Demand>;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** The open fixed point, or `undefined`. */
|
|
536
|
+
let openPoint: FixedPoint | undefined;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* What the code of a fixed point needs of a node (D113). The state of each node holds it, thus the functions of a
|
|
540
|
+
* fixed point are module functions, and a node makes no closure for them.
|
|
541
|
+
*/
|
|
542
|
+
interface LoopNode {
|
|
543
|
+
readonly type: string;
|
|
544
|
+
readonly place: readonly Key[];
|
|
545
|
+
/** The tree contract of the kind of the node (D42). */
|
|
546
|
+
readonly tree: TreeContract | undefined;
|
|
547
|
+
/** The marks of the attributes in evaluation. Without the cells of a host, they are the plain memo. */
|
|
548
|
+
readonly marks: Map<string, Cell>;
|
|
549
|
+
/** The plain memo of the node. With the cells of a host, it keeps the last answer only for `has` and `trace`. */
|
|
550
|
+
readonly cells: Map<string, Cell>;
|
|
551
|
+
readonly host: AttributeCell | undefined;
|
|
552
|
+
/** This function visits the rungs of the ladder of an attribute in their order, until one answers (TREE.LADDER). */
|
|
553
|
+
readonly climb: (key: string) => AttributeAnswer | undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** For each table `tree.attributes`, `true` when an attribute of it declares `circular`. */
|
|
557
|
+
const circularTables = /* @__PURE__ */ new WeakMap<object, boolean>();
|
|
558
|
+
|
|
559
|
+
/** For each declaration `circular` that is an object, its rule. */
|
|
560
|
+
const circularRules = /* @__PURE__ */ new WeakMap<object, CircularRule>();
|
|
561
|
+
|
|
562
|
+
/** This function tells if a tree contract declares a circular attribute. The answer for a table is computed one time. */
|
|
563
|
+
function hasCircular(tree: TreeContract | undefined): boolean {
|
|
564
|
+
const table = tree?.attributes;
|
|
565
|
+
if (table === undefined) return false;
|
|
566
|
+
let known = circularTables.get(table);
|
|
567
|
+
if (known === undefined) {
|
|
568
|
+
known = Object.keys(table).some((key) => {
|
|
569
|
+
const declared: unknown = entryOf(table, key);
|
|
570
|
+
return typeof declared === "object" && declared !== null && (declared as AttributeDeclaration).circular !== undefined;
|
|
571
|
+
});
|
|
572
|
+
circularTables.set(table, known);
|
|
573
|
+
}
|
|
574
|
+
return known;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** This function gives the declaration `circular` of an attribute of a tree contract (D113), or `undefined`. */
|
|
578
|
+
function circularRule(tree: TreeContract | undefined, key: string): CircularRule | undefined {
|
|
579
|
+
const declared: unknown = entryOf(tree?.attributes, key)?.circular;
|
|
580
|
+
if (declared === undefined) return undefined;
|
|
581
|
+
if (typeof declared !== "object" || declared === null) return circularOf(declared);
|
|
582
|
+
let rule = circularRules.get(declared);
|
|
583
|
+
if (rule === undefined) {
|
|
584
|
+
rule = circularOf(declared);
|
|
585
|
+
circularRules.set(declared, rule);
|
|
586
|
+
}
|
|
587
|
+
return rule;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** This function gives the values of a fixed point for the marks of a node. */
|
|
591
|
+
function loopValues(loop: FixedPoint, marks: Map<string, Cell>): Map<string, LoopValue> {
|
|
592
|
+
let values = loop.values.get(marks);
|
|
593
|
+
if (values === undefined) {
|
|
594
|
+
values = new Map();
|
|
595
|
+
loop.values.set(marks, values);
|
|
596
|
+
}
|
|
597
|
+
return values;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** This function marks each demand above the leader: each one read an approximation, thus its result is not final. */
|
|
601
|
+
function markTentative(loop: FixedPoint): void {
|
|
602
|
+
for (let index = loop.depth + 1; index < demands.length; index += 1) loop.tentative.add(demands[index] as Demand);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** This function tells if two approximations are the same: two values by DOC.EQUALS, two errors by code and reason. */
|
|
606
|
+
function sameApproximation(a: Result | undefined, b: Result | undefined): boolean {
|
|
607
|
+
if (a === undefined || b === undefined) return a === b;
|
|
608
|
+
if (a.ok && b.ok) return docEquals(a.value, b.value);
|
|
609
|
+
if (!a.ok && !b.ok) return a.error.code === b.error.code && a.error.reason === b.error.reason;
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** This function writes the final value of each circular attribute of the last round in the plain memo of its node. */
|
|
614
|
+
function settle(loop: FixedPoint): void {
|
|
615
|
+
for (const values of loop.values.values()) {
|
|
616
|
+
for (const [key, value] of values) {
|
|
617
|
+
if (value.memo !== undefined && value.round === loop.round) value.memo.set(key, { busy: false, result: value.result, by: value.by });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** The text of the fixed point of a circular attribute that does not become stable (TREE.CIRCULAR-LIMIT). */
|
|
623
|
+
function limitReason(text: string, limit: number): string {
|
|
624
|
+
return `The cycle of the circular attribute ${text} does not become stable within its limit of ${limit} ${limit === 1 ? "round" : "rounds"}.`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* This function answers a demand that reached an attribute in evaluation in an open fixed point (TREE.CYCLE). A chain
|
|
629
|
+
* of circular attributes gives the current approximation (TREE.CIRCULAR). Each other chain gives `#CYCLE!`.
|
|
630
|
+
*/
|
|
631
|
+
function reachedIn(loop: FixedPoint, n: LoopNode, key: string): Result | undefined {
|
|
632
|
+
const first = demands.findIndex((demand) => demand.cells === n.marks && demand.key === key);
|
|
633
|
+
let circular = first >= loop.depth;
|
|
634
|
+
for (let index = first; circular && index < demands.length; index += 1) circular = loop.circular.has(demands[index] as Demand);
|
|
635
|
+
if (!circular) return cycleAt(n.marks, key, attributeText(key, n.type, n.place));
|
|
636
|
+
const rule = circularRule(n.tree, key);
|
|
637
|
+
const kept = loop.values.get(n.marks)?.get(key);
|
|
638
|
+
markTentative(loop);
|
|
639
|
+
return kept !== undefined ? kept.result : ok(rule?.ok === true ? rule.start : null);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* This function evaluates the leader of a fixed point (TREE.CIRCULAR, D113). Its demand is the last one. Each round
|
|
644
|
+
* visits the ladder again, and the rounds end when no circular attribute gets a new value. An error ends the rounds
|
|
645
|
+
* at once. After the last round of the limit, the leader gives `#CYCLE!` at the place of its node.
|
|
646
|
+
*/
|
|
647
|
+
function roundsOf(n: LoopNode, key: string, rule: CircularRule & { readonly ok: true }): AttributeAnswer {
|
|
648
|
+
const leader = demands[demands.length - 1] as Demand;
|
|
649
|
+
const loop: FixedPoint = { depth: demands.length - 1, round: 0, changed: false, values: new Map(), circular: new Set([leader]), tentative: new Set() };
|
|
650
|
+
openPoint = loop;
|
|
651
|
+
try {
|
|
652
|
+
let before: Result | undefined = ok(rule.start);
|
|
653
|
+
for (;;) {
|
|
654
|
+
loop.round += 1;
|
|
655
|
+
loop.changed = false;
|
|
656
|
+
const answer = n.climb(key) ?? NO_ANSWER;
|
|
657
|
+
// An error is not a value of the fixed point. The other attributes of the cycle evaluate again at their next demand.
|
|
658
|
+
if (answer.result !== undefined && !answer.result.ok) return answer;
|
|
659
|
+
if (!sameApproximation(before, answer.result)) loop.changed = true;
|
|
660
|
+
before = answer.result;
|
|
661
|
+
loopValues(loop, n.marks).set(key, { result: answer.result, by: answer.by, round: loop.round, memo: undefined });
|
|
662
|
+
if (!loop.changed) {
|
|
663
|
+
settle(loop);
|
|
664
|
+
return answer;
|
|
665
|
+
}
|
|
666
|
+
if (loop.round >= rule.limit) return { result: fail("#CYCLE!", limitReason(attributeText(key, n.type, n.place), rule.limit), { place: n.place }), by: answer.by };
|
|
667
|
+
}
|
|
668
|
+
} finally {
|
|
669
|
+
openPoint = undefined;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* This function evaluates a circular attribute in the open fixed point of another leader, one time in each round
|
|
675
|
+
* (D113). A later demand in the same round gets the value of the round. Each value is an approximation.
|
|
676
|
+
*/
|
|
677
|
+
function roundIn(loop: FixedPoint, n: LoopNode, key: string, rule: CircularRule & { readonly ok: true }, kept: LoopValue | undefined): Result | undefined {
|
|
678
|
+
let value = kept;
|
|
679
|
+
if (value === undefined || value.round !== loop.round) {
|
|
680
|
+
const demand: Demand = { cells: n.marks, key, text: attributeText(key, n.type, n.place) };
|
|
681
|
+
n.marks.set(key, BUSY);
|
|
682
|
+
demands.push(demand);
|
|
683
|
+
loop.circular.add(demand);
|
|
684
|
+
let answer: AttributeAnswer;
|
|
685
|
+
try {
|
|
686
|
+
answer = n.climb(key) ?? NO_ANSWER;
|
|
687
|
+
} finally {
|
|
688
|
+
demands.pop();
|
|
689
|
+
n.marks.delete(key);
|
|
690
|
+
}
|
|
691
|
+
if (!sameApproximation(value === undefined ? ok(rule.start) : value.result, answer.result)) loop.changed = true;
|
|
692
|
+
value = { result: answer.result, by: answer.by, round: loop.round, memo: n.host === undefined ? n.cells : undefined };
|
|
693
|
+
loopValues(loop, n.marks).set(key, value);
|
|
694
|
+
}
|
|
695
|
+
markTentative(loop);
|
|
696
|
+
return value.result;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* This function evaluates an attribute without the cell of the host: with the plain memo, or in an open fixed point
|
|
701
|
+
* (D113). A result that read an approximation is not final, thus no memo keeps it.
|
|
702
|
+
*/
|
|
703
|
+
function localIn(n: LoopNode, key: string, rule: (CircularRule & { readonly ok: true }) | undefined): Result | undefined {
|
|
704
|
+
const demand: Demand = { cells: n.marks, key, text: attributeText(key, n.type, n.place) };
|
|
705
|
+
const loop = openPoint;
|
|
706
|
+
n.marks.set(key, BUSY);
|
|
707
|
+
demands.push(demand);
|
|
708
|
+
let answer: AttributeAnswer;
|
|
709
|
+
try {
|
|
710
|
+
answer = rule === undefined ? (n.climb(key) ?? NO_ANSWER) : roundsOf(n, key, rule);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
// An exception of the host, for example an overflow of the stack, leaves the attribute without a result.
|
|
713
|
+
n.marks.delete(key);
|
|
714
|
+
throw error;
|
|
715
|
+
} finally {
|
|
716
|
+
demands.pop();
|
|
717
|
+
}
|
|
718
|
+
if (loop !== undefined && loop.tentative.has(demand)) {
|
|
719
|
+
n.marks.delete(key);
|
|
720
|
+
return answer.result;
|
|
721
|
+
}
|
|
722
|
+
if (n.host !== undefined) {
|
|
723
|
+
// The fixed point keeps a final answer of a node with host cells until it ends (D113).
|
|
724
|
+
n.marks.delete(key);
|
|
725
|
+
if (loop !== undefined) loopValues(loop, n.marks).set(key, { result: answer.result, by: answer.by, round: 0, memo: undefined });
|
|
726
|
+
}
|
|
727
|
+
n.cells.set(key, { busy: false, result: answer.result, by: answer.by });
|
|
728
|
+
return answer.result;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* This function answers a demand that reached an attribute in evaluation (TREE.CYCLE). Outside a fixed point, it gives
|
|
733
|
+
* `#CYCLE!` with the chain. In an open fixed point, `reachedIn` answers (TREE.CIRCULAR).
|
|
734
|
+
*/
|
|
735
|
+
function busyAt(n: LoopNode, key: string): Result | undefined {
|
|
736
|
+
const loop = openPoint;
|
|
737
|
+
return loop === undefined ? cycleAt(n.marks, key, attributeText(key, n.type, n.place)) : reachedIn(loop, n, key);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* This function evaluates the leader of a fixed point through the cell of the host (TREE.CELL, D113). The cell of the
|
|
742
|
+
* leader gets the final value of the fixed point, from a computation that contains each round.
|
|
743
|
+
*/
|
|
744
|
+
function leaderOnHost(n: LoopNode, key: string, rule: CircularRule & { readonly ok: true }, host: AttributeCell): Result | undefined {
|
|
745
|
+
const text = attributeText(key, n.type, n.place);
|
|
746
|
+
n.marks.set(key, BUSY);
|
|
747
|
+
demands.push({ cells: n.marks, key, text });
|
|
748
|
+
let thrown: unknown = NOT_THROWN;
|
|
749
|
+
let answer: AttributeAnswer;
|
|
750
|
+
try {
|
|
751
|
+
answer = host(n.place, key, () => {
|
|
752
|
+
try {
|
|
753
|
+
return roundsOf(n, key, rule);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
thrown = error;
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
} catch (error) {
|
|
760
|
+
// An exception of the kit or of the stack goes on, as without a host. Another exception is a cycle of the host.
|
|
761
|
+
if (error === thrown) throw error;
|
|
762
|
+
const chain = demands.map((demand) => demand.text).join(" -> ");
|
|
763
|
+
answer = { result: fail("#CYCLE!", `The cells of the host found a cycle in the demands: ${chain}.`, { cause: error }), by: undefined };
|
|
764
|
+
} finally {
|
|
765
|
+
demands.pop();
|
|
766
|
+
n.marks.delete(key);
|
|
767
|
+
}
|
|
768
|
+
n.cells.set(key, { busy: false, result: answer.result, by: answer.by });
|
|
769
|
+
return answer.result;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* This function gives an attribute of a node with circular attributes, or an attribute in an open fixed point (D113).
|
|
774
|
+
* The memo and the marks of the node do not hold the attribute. An open fixed point does not use the cells of the host.
|
|
775
|
+
*/
|
|
776
|
+
function circularRead(n: LoopNode, key: string): Result | undefined {
|
|
777
|
+
const rule = circularRule(n.tree, key);
|
|
778
|
+
if (rule !== undefined && !rule.ok) {
|
|
779
|
+
const result = fail("#VALUE!", `The attribute ${attributeText(key, n.type, n.place)} has a bad declaration circular. ${rule.reason}`);
|
|
780
|
+
n.cells.set(key, { busy: false, result, by: undefined });
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
const loop = openPoint;
|
|
784
|
+
if (loop !== undefined) {
|
|
785
|
+
const kept = loop.values.get(n.marks)?.get(key);
|
|
786
|
+
if (rule !== undefined) return roundIn(loop, n, key, rule, kept);
|
|
787
|
+
if (kept !== undefined) return kept.result;
|
|
788
|
+
return localIn(n, key, undefined);
|
|
789
|
+
}
|
|
790
|
+
if (rule === undefined || n.host === undefined) return localIn(n, key, rule);
|
|
791
|
+
return leaderOnHost(n, key, rule, n.host);
|
|
792
|
+
}
|
|
793
|
+
|
|
462
794
|
function attributeText(key: string, type: string, place: readonly Key[]): string {
|
|
463
795
|
return `${key} of ${type} at [${place.join(", ")}]`;
|
|
464
796
|
}
|
|
@@ -528,10 +860,11 @@ function plainOf(value: unknown, path: Set<object>): Result {
|
|
|
528
860
|
* fold or a default, come last. A failed attribute throws `Failure` with its own error, thus a reference to it gives that error.
|
|
529
861
|
* The argument `outer` is the self of the context, that `treeNode` resolved. The node reads no root of the context directly (C4).
|
|
530
862
|
*
|
|
531
|
-
* The argument `
|
|
863
|
+
* The argument `place` is the key of the node. With the cells of a host, `cellPlace` gives it. Else it is the place of
|
|
864
|
+
* the plan. The argument `host` gives the memo cells of the host (TREE.CELL), or `undefined` for the plain memo of the node.
|
|
532
865
|
*/
|
|
533
|
-
function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined, host: AttributeCell | undefined): HostThing {
|
|
534
|
-
const { parts, context, kind: type,
|
|
866
|
+
function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, outer: Thing | undefined, host: AttributeCell | undefined): HostThing {
|
|
867
|
+
const { parts, context, kind: type, machine } = input;
|
|
535
868
|
// The equations evaluate in the evaluation that made the node, thus their reads go in its list (READS.EQUATION).
|
|
536
869
|
const runtime = (input as InputWithRuntime)[RUNTIME];
|
|
537
870
|
const { tree, of } = contractOf(machine, input.typePath);
|
|
@@ -539,8 +872,10 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
|
|
|
539
872
|
// Only a node of the same tree is a parent for the rungs `parent` and `copy`.
|
|
540
873
|
const up = above !== undefined && above.build === build ? above : undefined;
|
|
541
874
|
const cells = new Map<string, Cell>();
|
|
542
|
-
// With the cells of a host, the marks of a
|
|
875
|
+
// With the cells of a host, the marks of a key serve each node of the key (TREE.CELL).
|
|
543
876
|
const marks = host === undefined ? cells : keptAt(marksByPlace, place, () => new Map<string, Cell>());
|
|
877
|
+
// Only a node whose kind declares a circular attribute takes the path of D113 outside a fixed point.
|
|
878
|
+
const circular = hasCircular(tree);
|
|
544
879
|
/** The positions of the items of the list `children`, for the names of the children. */
|
|
545
880
|
let positions: { readonly list: readonly unknown[]; readonly at: Map<unknown, number> } | undefined;
|
|
546
881
|
|
|
@@ -594,8 +929,9 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
|
|
|
594
929
|
? undefined
|
|
595
930
|
: (key: string): Result | undefined => {
|
|
596
931
|
const text = attributeText(key, type, place);
|
|
597
|
-
if (marks.has(key)) return
|
|
932
|
+
if (marks.has(key)) return busyAt(state, key);
|
|
598
933
|
if (tree === undefined && up?.tree === undefined && !parts.some((item) => item.key === key)) return undefined;
|
|
934
|
+
if (circular || openPoint !== undefined) return circularRead(state, key);
|
|
599
935
|
marks.set(key, BUSY);
|
|
600
936
|
demands.push({ cells: marks, key, text });
|
|
601
937
|
let thrown: unknown = NOT_THROWN;
|
|
@@ -625,9 +961,11 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
|
|
|
625
961
|
const read = (key: string): Result | undefined => {
|
|
626
962
|
if (through !== undefined) return through(key);
|
|
627
963
|
const cell = cells.get(key);
|
|
628
|
-
if (cell !== undefined) return cell.busy ?
|
|
964
|
+
if (cell !== undefined) return cell.busy ? busyAt(state, key) : cell.result;
|
|
629
965
|
// A node without a contract has only its parts, as before D42. Thus a missing part needs no cell.
|
|
630
966
|
if (tree === undefined && up?.tree === undefined && !parts.some((item) => item.key === key)) return undefined;
|
|
967
|
+
// A circular attribute and each demand in an open fixed point take the code of D113 (TREE.CIRCULAR).
|
|
968
|
+
if (circular || openPoint !== undefined) return circularRead(state, key);
|
|
631
969
|
cells.set(key, BUSY);
|
|
632
970
|
demands.push({ cells, key, text: attributeText(key, type, place) });
|
|
633
971
|
let answer: { readonly result: Result; readonly by: AttributeRung } | undefined;
|
|
@@ -649,6 +987,10 @@ function treeThing(input: EvalInput, build: TreeBuild, outer: Thing | undefined,
|
|
|
649
987
|
type,
|
|
650
988
|
place,
|
|
651
989
|
tree,
|
|
990
|
+
marks,
|
|
991
|
+
cells,
|
|
992
|
+
host,
|
|
993
|
+
climb,
|
|
652
994
|
read,
|
|
653
995
|
equation(term, names) {
|
|
654
996
|
if (machine === undefined) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
|
|
@@ -759,11 +1101,13 @@ function treeNode(input: EvalInput, host: AttributeCell | undefined): Result {
|
|
|
759
1101
|
const found = input.resolve(SELF_PATH, NO_HOPS);
|
|
760
1102
|
const outer = found.ok ? found.thing : undefined;
|
|
761
1103
|
const above = stateOf(outer);
|
|
762
|
-
|
|
1104
|
+
// With the cells of a host, the key of a node holds its scope (D115). Without them, the place of the plan serves.
|
|
1105
|
+
const place = host === undefined ? input.place : cellPlace(input.place, above);
|
|
1106
|
+
if (above !== undefined && above.build.open) return ok(treeThing(input, place, above.build, outer, host));
|
|
763
1107
|
// With the cells of a host, a kept value can hold a node of an earlier evaluation. Thus that node stays in the tree.
|
|
764
|
-
const build: TreeBuild = host === undefined ? { open: true } : keptAt(buildsByPlace,
|
|
1108
|
+
const build: TreeBuild = host === undefined ? { open: true } : keptAt(buildsByPlace, place, () => ({ open: true }));
|
|
765
1109
|
build.open = true;
|
|
766
|
-
const root = treeThing(input, build, outer, host);
|
|
1110
|
+
const root = treeThing(input, place, build, outer, host);
|
|
767
1111
|
try {
|
|
768
1112
|
return (stateOf(root) as NodeState).json(new Set());
|
|
769
1113
|
} finally {
|
package/src/steps.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The steps of a language (STEPS.*). A step is a level of the power of a language. A low step accepts only programs
|
|
3
|
+
* that end for a known reason, for example loops with a fixed count. A high step accepts more programs. The language
|
|
4
|
+
* declares the step of each construct as data. `stepOf` gives the lowest step that a document needs, with the reason,
|
|
5
|
+
* and `checkStep` compares that step with the highest step that the host accepts. A document cannot raise its own
|
|
6
|
+
* step, because the declaration and the limit come from the language and the host. The ladder of `ask` (`ladder.ts`)
|
|
7
|
+
* is a different thing: it finds the evaluator of a verb.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A key of a path from the root to a node: a field name or a position. */
|
|
11
|
+
export type StepKey = string | number;
|
|
12
|
+
|
|
13
|
+
/** The steps of a language, from the lowest to the highest, and the step that each construct needs. It is data. */
|
|
14
|
+
export interface StepLadder<S extends string = string> {
|
|
15
|
+
/** The names of the steps, from the lowest to the highest. */
|
|
16
|
+
readonly steps: readonly S[];
|
|
17
|
+
/** The step that each construct needs. A construct without an entry needs the lowest step. */
|
|
18
|
+
readonly constructs: Readonly<Record<string, S>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The walk of the documents of a language. The language gives these two functions. */
|
|
22
|
+
export interface StepWalk<N> {
|
|
23
|
+
/** This function gives the name of the construct of a node, or `undefined` for a node without a construct. */
|
|
24
|
+
readonly construct: (node: N) => string | undefined;
|
|
25
|
+
/** This function gives the child nodes of a node in document order, each with the key of its path. */
|
|
26
|
+
readonly children: (node: N) => Iterable<readonly [StepKey, N]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The reason for a step: the first node in document order whose construct needs that step. */
|
|
30
|
+
export interface StepReason<N, S extends string = string> {
|
|
31
|
+
/** The name of the construct. */
|
|
32
|
+
readonly construct: string;
|
|
33
|
+
/** The step that the construct needs. */
|
|
34
|
+
readonly step: S;
|
|
35
|
+
/** The keys from the root to the node. The root has the empty path. */
|
|
36
|
+
readonly path: readonly StepKey[];
|
|
37
|
+
readonly node: N;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The lowest step that a document needs. */
|
|
41
|
+
export interface StepNeed<N, S extends string = string> {
|
|
42
|
+
readonly step: S;
|
|
43
|
+
/** The reason. It is `undefined` when no construct needs more than the lowest step. */
|
|
44
|
+
readonly reason?: StepReason<N, S>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The refusal of a document whose step is higher than the limit of the host. */
|
|
48
|
+
export interface StepRefusal<N, S extends string = string> {
|
|
49
|
+
/** The step that the document needs. */
|
|
50
|
+
readonly needed: S;
|
|
51
|
+
/** The highest step that the host accepts. */
|
|
52
|
+
readonly maxStep: S;
|
|
53
|
+
/** The reason of the step. A need that a host makes without `stepOf` can have no reason. */
|
|
54
|
+
readonly reason?: StepReason<N, S>;
|
|
55
|
+
/** The text of the refusal. It names the needed step and the limit, and the construct and its path of the reason. */
|
|
56
|
+
readonly message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This function makes a ladder of steps, and it checks the declaration (STEPS.LADDER). The list of steps must have one
|
|
61
|
+
* step or more, with no name two times. The step of each construct must be a step of the list.
|
|
62
|
+
*
|
|
63
|
+
* @param steps - The names of the steps, from the lowest to the highest.
|
|
64
|
+
* @param constructs - The step that each construct needs.
|
|
65
|
+
* @returns The ladder, frozen.
|
|
66
|
+
* @throws TypeError for a declaration that is not valid, because it is a defect of the language.
|
|
67
|
+
*/
|
|
68
|
+
export function stepLadder<const S extends string>(steps: readonly S[], constructs: Readonly<Record<string, NoInfer<S>>>): StepLadder<S> {
|
|
69
|
+
if (steps.length === 0) throw new TypeError("a ladder of steps needs one step or more");
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
for (const step of steps) {
|
|
72
|
+
if (typeof step !== "string") throw new TypeError(`the step ${String(step)} is not a name`);
|
|
73
|
+
if (seen.has(step)) throw new TypeError(`the step "${step}" is in the ladder two times`);
|
|
74
|
+
seen.add(step);
|
|
75
|
+
}
|
|
76
|
+
for (const [construct, step] of Object.entries(constructs)) {
|
|
77
|
+
if (!seen.has(step)) throw new TypeError(`the construct "${construct}" needs the step "${String(step)}", which is not a step of the ladder`);
|
|
78
|
+
}
|
|
79
|
+
return Object.freeze({ steps: Object.freeze([...steps]), constructs: Object.freeze({ ...constructs }) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* This function tells if a value is a step of a ladder. A host uses it to check its option for the highest step.
|
|
84
|
+
*
|
|
85
|
+
* @param ladder - The ladder.
|
|
86
|
+
* @param value - The value.
|
|
87
|
+
* @returns `true` when the value is the name of a step of the ladder.
|
|
88
|
+
*/
|
|
89
|
+
export function isStep<S extends string>(ladder: StepLadder<S>, value: unknown): value is S {
|
|
90
|
+
return typeof value === "string" && ladder.steps.includes(value as S);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* This function gives the position of a step in its ladder. The lowest step has the position 0.
|
|
95
|
+
*
|
|
96
|
+
* @param ladder - The ladder.
|
|
97
|
+
* @param step - The step.
|
|
98
|
+
* @returns The position.
|
|
99
|
+
* @throws TypeError for a name that is not a step of the ladder.
|
|
100
|
+
*/
|
|
101
|
+
export function stepRank<S extends string>(ladder: StepLadder<S>, step: S): number {
|
|
102
|
+
const rank = ladder.steps.indexOf(step);
|
|
103
|
+
if (rank < 0) throw new TypeError(`"${String(step)}" is not a step of the ladder. The steps are ${ladder.steps.join(", ")}`);
|
|
104
|
+
return rank;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** An item of the walk: a node, the key of its path and the item of its parent. Thus a path costs nothing until a reason needs it. */
|
|
108
|
+
interface Visit<N> {
|
|
109
|
+
readonly node: N;
|
|
110
|
+
readonly key: StepKey | undefined;
|
|
111
|
+
readonly parent: Visit<N> | undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pathOf<N>(visit: Visit<N>): StepKey[] {
|
|
115
|
+
const path: StepKey[] = [];
|
|
116
|
+
for (let v: Visit<N> | undefined = visit; v?.key !== undefined; v = v.parent) path.push(v.key);
|
|
117
|
+
return path.reverse();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* This function gives the lowest step that a document needs, with the reason (STEPS.LOWEST, STEPS.REASON). The step is
|
|
122
|
+
* the highest step of the constructs of its nodes. The reason is the first node in document order with that step. The
|
|
123
|
+
* walk uses its own stack, thus a deep document does not overflow (STEPS.LINEAR). It visits a shared node one time,
|
|
124
|
+
* and it stops at the first node with the highest step of the ladder.
|
|
125
|
+
*
|
|
126
|
+
* @param ladder - The ladder of the language.
|
|
127
|
+
* @param walk - The construct and the child nodes of a node.
|
|
128
|
+
* @param root - The root node of the document.
|
|
129
|
+
* @returns The step, and the reason when the step is not the lowest step.
|
|
130
|
+
*/
|
|
131
|
+
export function stepOf<N, S extends string>(ladder: StepLadder<S>, walk: StepWalk<N>, root: N): StepNeed<N, S> {
|
|
132
|
+
const top = ladder.steps.length - 1;
|
|
133
|
+
let best = 0;
|
|
134
|
+
let reason: StepReason<N, S> | undefined;
|
|
135
|
+
const seen = new Set<unknown>();
|
|
136
|
+
const todo: Visit<N>[] = [{ node: root, key: undefined, parent: undefined }];
|
|
137
|
+
for (let visit = todo.pop(); visit !== undefined; visit = todo.pop()) {
|
|
138
|
+
const { node } = visit;
|
|
139
|
+
if (typeof node === "object" && node !== null) {
|
|
140
|
+
if (seen.has(node)) continue;
|
|
141
|
+
seen.add(node);
|
|
142
|
+
}
|
|
143
|
+
const construct = walk.construct(node);
|
|
144
|
+
const step = construct === undefined || !Object.hasOwn(ladder.constructs, construct) ? undefined : ladder.constructs[construct];
|
|
145
|
+
const rank = step === undefined ? 0 : ladder.steps.indexOf(step);
|
|
146
|
+
if (rank > best) {
|
|
147
|
+
best = rank;
|
|
148
|
+
reason = { construct: construct as string, step: step as S, path: pathOf(visit), node };
|
|
149
|
+
if (best === top) break;
|
|
150
|
+
}
|
|
151
|
+
const kids = [...walk.children(node)];
|
|
152
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
153
|
+
const [key, child] = kids[i] as readonly [StepKey, N];
|
|
154
|
+
todo.push({ node: child, key, parent: visit });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const step = ladder.steps[best] as S;
|
|
158
|
+
return reason === undefined ? { step } : { step, reason };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* This function compares the step of a document with the highest step that the host accepts (STEPS.MAX). The limit
|
|
163
|
+
* changes only which documents the host accepts. The language evaluates an accepted document as before, thus a limit
|
|
164
|
+
* does not change a value.
|
|
165
|
+
*
|
|
166
|
+
* @param ladder - The ladder of the language.
|
|
167
|
+
* @param need - The step of the document, from `stepOf`.
|
|
168
|
+
* @param maxStep - The highest step that the host accepts.
|
|
169
|
+
* @returns `undefined` when the host accepts the document, or the refusal that names the needed step.
|
|
170
|
+
* @throws TypeError when `maxStep` or the step of `need` is not a step of the ladder.
|
|
171
|
+
*/
|
|
172
|
+
export function checkStep<N, S extends string>(ladder: StepLadder<S>, need: StepNeed<N, S>, maxStep: S): StepRefusal<N, S> | undefined {
|
|
173
|
+
const limit = stepRank(ladder, maxStep);
|
|
174
|
+
const needed = stepRank(ladder, need.step);
|
|
175
|
+
if (needed <= limit) return undefined;
|
|
176
|
+
const head = `the document needs the step ${need.step}, and the host accepts the steps up to ${maxStep}`;
|
|
177
|
+
const reason = need.reason;
|
|
178
|
+
if (reason === undefined) return { needed: need.step, maxStep, message: head };
|
|
179
|
+
const where = reason.path.length === 0 ? "at the root" : `at the path ${JSON.stringify(reason.path)}`;
|
|
180
|
+
return { needed: need.step, maxStep, reason, message: `${head}. The construct "${reason.construct}" ${where} needs the step ${reason.step}` };
|
|
181
|
+
}
|