@turing-machine-js/machine 6.4.0 → 7.0.0-alpha.2
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/CHANGELOG.md +120 -0
- package/README.md +137 -96
- package/dist/classes/State.d.ts +3 -3
- package/dist/index.cjs +723 -134
- package/dist/index.mjs +723 -134
- package/dist/utilities/graph.d.ts +6 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -349,7 +349,7 @@ var __classPrivateFieldGet$2 = (undefined && undefined.__classPrivateFieldGet) |
|
|
|
349
349
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
350
350
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
351
351
|
};
|
|
352
|
-
var _a, _TapeBlock_symbolToPatternListMap, _TapeBlock_lock, _TapeBlock_tapes, _TapeBlock_generateSymbolHint, _TapeBlock_buildPatternList, _TapeBlock_getSymbolForPatternList, _TapeBlock_symbol;
|
|
352
|
+
var _a$1, _TapeBlock_symbolToPatternListMap, _TapeBlock_lock, _TapeBlock_tapes, _TapeBlock_generateSymbolHint, _TapeBlock_buildPatternList, _TapeBlock_getSymbolForPatternList, _TapeBlock_symbol;
|
|
353
353
|
const symbolToPatternListMapSymbol = Symbol('symbol for symbolToPatternListMap setter');
|
|
354
354
|
const lockSymbol = Symbol('capture symbol');
|
|
355
355
|
class TapeBlock {
|
|
@@ -388,7 +388,7 @@ class TapeBlock {
|
|
|
388
388
|
symbol = storedPatternListSymbol;
|
|
389
389
|
}
|
|
390
390
|
else {
|
|
391
|
-
symbol = Symbol(__classPrivateFieldGet$2(_a, _a, "f", _TapeBlock_generateSymbolHint).call(_a, patternList));
|
|
391
|
+
symbol = Symbol(__classPrivateFieldGet$2(_a$1, _a$1, "f", _TapeBlock_generateSymbolHint).call(_a$1, patternList));
|
|
392
392
|
__classPrivateFieldGet$2(this, _TapeBlock_symbolToPatternListMap, "f").set(symbol, patternList);
|
|
393
393
|
}
|
|
394
394
|
return symbol;
|
|
@@ -488,10 +488,10 @@ class TapeBlock {
|
|
|
488
488
|
clone(cloneTapes = false) {
|
|
489
489
|
let tapeBlock;
|
|
490
490
|
if (cloneTapes) {
|
|
491
|
-
tapeBlock = _a.fromTapes(this.tapes.map((tape) => new Tape(tape)));
|
|
491
|
+
tapeBlock = _a$1.fromTapes(this.tapes.map((tape) => new Tape(tape)));
|
|
492
492
|
}
|
|
493
493
|
else {
|
|
494
|
-
tapeBlock = _a.fromAlphabets(this.alphabets);
|
|
494
|
+
tapeBlock = _a$1.fromAlphabets(this.alphabets);
|
|
495
495
|
}
|
|
496
496
|
tapeBlock[symbolToPatternListMapSymbol] = __classPrivateFieldGet$2(this, _TapeBlock_symbolToPatternListMap, "f");
|
|
497
497
|
return tapeBlock;
|
|
@@ -520,12 +520,12 @@ class TapeBlock {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
|
-
_a = TapeBlock;
|
|
523
|
+
_a$1 = TapeBlock;
|
|
524
524
|
TapeBlock.fromAlphabets = (alphabets) => {
|
|
525
|
-
return new _a({ alphabets });
|
|
525
|
+
return new _a$1({ alphabets });
|
|
526
526
|
};
|
|
527
527
|
TapeBlock.fromTapes = (tapes) => {
|
|
528
|
-
return new _a({ tapes });
|
|
528
|
+
return new _a$1({ tapes });
|
|
529
529
|
};
|
|
530
530
|
_TapeBlock_generateSymbolHint = { value: (patternList) => JSON.stringify(patternList
|
|
531
531
|
.map((pattern) => pattern
|
|
@@ -537,30 +537,40 @@ const movementDescriptionToLabel = {
|
|
|
537
537
|
'do not move carer': 'S',
|
|
538
538
|
};
|
|
539
539
|
const symbolCommandDescriptionToLabel = {
|
|
540
|
-
'keep symbol command': '
|
|
541
|
-
'erase symbol command': '
|
|
540
|
+
'keep symbol command': 'K',
|
|
541
|
+
'erase symbol command': 'E',
|
|
542
542
|
};
|
|
543
543
|
// Reserved characters in the encoded pattern string:
|
|
544
|
-
// '*' per-cell ifOtherSymbol
|
|
545
|
-
//
|
|
544
|
+
// '*' ASCII asterisk (U+002A) — per-cell ifOtherSymbol, matches any symbol
|
|
545
|
+
// on that tape. ASCII (not a fancier glyph like U+1F7B0) so it renders
|
|
546
|
+
// in every Mermaid environment and every monospace font. A literal `*`
|
|
547
|
+
// in the alphabet is unambiguous from the marker because it's quoted
|
|
548
|
+
// (`'*'`).
|
|
549
|
+
// 'B' the tape's blank symbol shorthand (in read patterns). A literal `B`
|
|
550
|
+
// in the alphabet is unambiguous from the marker because it's quoted
|
|
551
|
+
// (`'B'`).
|
|
546
552
|
// ',' separates per-tape cells inside one pattern
|
|
547
553
|
// '|' separates alternative patterns
|
|
548
|
-
// '
|
|
549
|
-
//
|
|
554
|
+
// "'" surrounds a literal alphabet symbol — e.g. `'0'` for literal `0`,
|
|
555
|
+
// `'X'` for literal `X`. The quoting is what visually separates literal
|
|
556
|
+
// symbols from the convention markers `*` / `B` and from the write
|
|
557
|
+
// commands `K` / `E`.
|
|
558
|
+
// '\\' escape prefix — to represent any of '*', 'B', ',', '|', "'", or '\\'
|
|
559
|
+
// as a *literal* alphabet symbol *inside* the quotes (e.g. `'\''` for
|
|
560
|
+
// a literal apostrophe).
|
|
561
|
+
const IF_OTHER_MARKER = '*';
|
|
562
|
+
const BLANK_MARKER = 'B';
|
|
550
563
|
function escapeAlphabetSymbol(s) {
|
|
551
564
|
return s
|
|
552
565
|
.replace(/\\/g, '\\\\')
|
|
553
|
-
.replace(
|
|
554
|
-
.replace(/-/g, '\\-')
|
|
555
|
-
.replace(/,/g, '\\,')
|
|
556
|
-
.replace(/\|/g, '\\|');
|
|
566
|
+
.replace(/'/g, "\\'");
|
|
557
567
|
}
|
|
558
568
|
function decodePatternDescription(description, alphabets) {
|
|
559
569
|
if (!description) {
|
|
560
570
|
return '?';
|
|
561
571
|
}
|
|
562
572
|
if (description === 'other symbol') {
|
|
563
|
-
return
|
|
573
|
+
return IF_OTHER_MARKER;
|
|
564
574
|
}
|
|
565
575
|
try {
|
|
566
576
|
const patternList = JSON.parse(description);
|
|
@@ -568,12 +578,12 @@ function decodePatternDescription(description, alphabets) {
|
|
|
568
578
|
.map((pattern) => pattern
|
|
569
579
|
.map((s, tapeIx) => {
|
|
570
580
|
if (s === null) {
|
|
571
|
-
return
|
|
581
|
+
return IF_OTHER_MARKER;
|
|
572
582
|
}
|
|
573
583
|
if (s === alphabets[tapeIx]?.[0]) {
|
|
574
|
-
return
|
|
584
|
+
return BLANK_MARKER;
|
|
575
585
|
}
|
|
576
|
-
return escapeAlphabetSymbol(s)
|
|
586
|
+
return `'${escapeAlphabetSymbol(s)}'`;
|
|
577
587
|
})
|
|
578
588
|
.join(','))
|
|
579
589
|
.join('|');
|
|
@@ -611,19 +621,24 @@ function splitUnescaped(s, sep) {
|
|
|
611
621
|
return parts;
|
|
612
622
|
}
|
|
613
623
|
function parsePatternString(s, alphabets) {
|
|
614
|
-
if (s ===
|
|
624
|
+
if (s === IF_OTHER_MARKER) {
|
|
615
625
|
return null;
|
|
616
626
|
}
|
|
617
627
|
const alternatives = splitUnescaped(s, '|');
|
|
618
628
|
return alternatives.map((alt) => {
|
|
619
629
|
const cells = splitUnescaped(alt, ',');
|
|
620
630
|
return cells.map((cell, tapeIx) => {
|
|
621
|
-
if (cell ===
|
|
631
|
+
if (cell === IF_OTHER_MARKER) {
|
|
622
632
|
return null;
|
|
623
633
|
}
|
|
624
|
-
if (cell ===
|
|
634
|
+
if (cell === BLANK_MARKER) {
|
|
625
635
|
return alphabets[tapeIx]?.[0] ?? cell;
|
|
626
636
|
}
|
|
637
|
+
// Literal alphabet symbols are wrapped in single quotes by
|
|
638
|
+
// `decodePatternDescription` — strip them on the way back.
|
|
639
|
+
if (cell.length >= 2 && cell.startsWith("'") && cell.endsWith("'")) {
|
|
640
|
+
return cell.slice(1, -1);
|
|
641
|
+
}
|
|
627
642
|
return cell;
|
|
628
643
|
});
|
|
629
644
|
});
|
|
@@ -641,12 +656,17 @@ function parseMovementLabel(label) {
|
|
|
641
656
|
return m;
|
|
642
657
|
}
|
|
643
658
|
function parseWriteSymbolLabel(label) {
|
|
644
|
-
if (label === '
|
|
659
|
+
if (label === 'K') {
|
|
645
660
|
return symbolCommands.keep;
|
|
646
661
|
}
|
|
647
|
-
if (label === '
|
|
662
|
+
if (label === 'E') {
|
|
648
663
|
return symbolCommands.erase;
|
|
649
664
|
}
|
|
665
|
+
// Literal alphabet symbols are wrapped in single quotes by
|
|
666
|
+
// `decodeWriteSymbol` — strip them on the way back.
|
|
667
|
+
if (label.length >= 2 && label.startsWith("'") && label.endsWith("'")) {
|
|
668
|
+
return label.slice(1, -1);
|
|
669
|
+
}
|
|
650
670
|
return label;
|
|
651
671
|
}
|
|
652
672
|
function decodeWriteSymbol(symbol) {
|
|
@@ -654,7 +674,7 @@ function decodeWriteSymbol(symbol) {
|
|
|
654
674
|
const description = symbol.description ?? '?';
|
|
655
675
|
return symbolCommandDescriptionToLabel[description] ?? description;
|
|
656
676
|
}
|
|
657
|
-
return symbol
|
|
677
|
+
return `'${symbol}'`;
|
|
658
678
|
}
|
|
659
679
|
// Format converters (toMermaid / fromMermaid) live in ./graphFormats.
|
|
660
680
|
|
|
@@ -669,7 +689,7 @@ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) |
|
|
|
669
689
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
670
690
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
671
691
|
};
|
|
672
|
-
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_id, _State_name,
|
|
692
|
+
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef;
|
|
673
693
|
const ifOtherSymbol = Symbol('other symbol');
|
|
674
694
|
// Module-private symbol used by DebugConfig setters to call State's validator
|
|
675
695
|
// without exposing the validator on the public surface.
|
|
@@ -714,10 +734,18 @@ _DebugConfig_ownerState = new WeakMap(), _DebugConfig_before = new WeakMap(), _D
|
|
|
714
734
|
class State {
|
|
715
735
|
constructor(stateDefinition = null, name) {
|
|
716
736
|
_State_id.set(this, id(this));
|
|
737
|
+
// Not `readonly` because `withOverriddenHaltState` and `fromGraph` set the
|
|
738
|
+
// composed name on a no-arg `new State()` to bypass the constructor's
|
|
739
|
+
// user-facing name validation (composite names contain `(` and `)`).
|
|
717
740
|
_State_name.set(this, void 0);
|
|
718
|
-
|
|
741
|
+
_State_overriddenHaltState.set(this, null);
|
|
742
|
+
// For wrapper states (produced by `withOverriddenHaltState`), points at the
|
|
743
|
+
// State whose transition map was wrapped. `null` on bare/atomic states.
|
|
744
|
+
// Used by `toGraph` to collapse the wrapper-and-its-bare pair into a single
|
|
745
|
+
// "wrapped bare" graph node — see the v7 emit redesign for #138.
|
|
746
|
+
_State_bareState.set(this, null);
|
|
719
747
|
_State_symbolToDataMap.set(this, new Map());
|
|
720
|
-
// Shared mutable cell —
|
|
748
|
+
// Shared mutable cell — withOverriddenHaltState wrappers reference the same
|
|
721
749
|
// object so that `state.debug = ...` (and nullings) propagate across them.
|
|
722
750
|
// Note: toGraph / fromGraph deliberately do not serialize debug — debug is
|
|
723
751
|
// a runtime concern, not part of the structural graph.
|
|
@@ -734,7 +762,7 @@ class State {
|
|
|
734
762
|
symbols.forEach((symbol) => {
|
|
735
763
|
const { nextState } = stateDefinition[symbol];
|
|
736
764
|
const nextStateLocal = nextState ?? this;
|
|
737
|
-
if (!(nextStateLocal instanceof
|
|
765
|
+
if (!(nextStateLocal instanceof _a) && !(nextStateLocal instanceof Reference)) {
|
|
738
766
|
throw new Error('invalid nextState');
|
|
739
767
|
}
|
|
740
768
|
let { command } = stateDefinition[symbol];
|
|
@@ -763,6 +791,9 @@ class State {
|
|
|
763
791
|
});
|
|
764
792
|
});
|
|
765
793
|
}
|
|
794
|
+
if (name !== undefined && /[()]/.test(name)) {
|
|
795
|
+
throw new Error(`invalid state name "${name}": must not contain '(' or ')' (reserved as wrapper-composition delimiters in withOverriddenHaltState)`);
|
|
796
|
+
}
|
|
766
797
|
__classPrivateFieldSet$1(this, _State_name, name ?? `id:${__classPrivateFieldGet$1(this, _State_id, "f")}`, "f");
|
|
767
798
|
}
|
|
768
799
|
get id() {
|
|
@@ -774,8 +805,8 @@ class State {
|
|
|
774
805
|
get isHalt() {
|
|
775
806
|
return __classPrivateFieldGet$1(this, _State_id, "f") === 0;
|
|
776
807
|
}
|
|
777
|
-
get
|
|
778
|
-
return __classPrivateFieldGet$1(this,
|
|
808
|
+
get overriddenHaltState() {
|
|
809
|
+
return __classPrivateFieldGet$1(this, _State_overriddenHaltState, "f");
|
|
779
810
|
}
|
|
780
811
|
get ref() {
|
|
781
812
|
return this;
|
|
@@ -803,7 +834,7 @@ class State {
|
|
|
803
834
|
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, value);
|
|
804
835
|
}
|
|
805
836
|
/** @internal — invoked by DebugConfig setters via module-private symbol. */
|
|
806
|
-
[(_State_id = new WeakMap(), _State_name = new WeakMap(),
|
|
837
|
+
[(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), validateDebugFilter)](fieldName, filter) {
|
|
807
838
|
if (filter === undefined)
|
|
808
839
|
return;
|
|
809
840
|
// #108 part 2: `.after` on haltState has no semantic anchor — halt is
|
|
@@ -850,11 +881,41 @@ class State {
|
|
|
850
881
|
}
|
|
851
882
|
throw new Error(`No nextState for symbol at state named ${__classPrivateFieldGet$1(this, _State_id, "f")}`);
|
|
852
883
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
884
|
+
withOverriddenHaltState(overriddenHaltState) {
|
|
885
|
+
// Unwrap `this` if it's itself a wrapper — the chain's inner overrides
|
|
886
|
+
// are dead at runtime anyway (only the outermost `.wohs()`'s override is
|
|
887
|
+
// pushed onto the halt-stack on entry; verified empirically). Composite
|
|
888
|
+
// name reflects runtime behavior, not construction history. See #176.
|
|
889
|
+
const bare = __classPrivateFieldGet$1(this, _State_bareState, "f") ?? this;
|
|
890
|
+
// Memoize by (bare, override) so identical args return the same instance
|
|
891
|
+
// (#175). The cache uses WeakMaps + WeakRefs so cached wrappers can be
|
|
892
|
+
// GC'd when nothing else holds them. Compounds with the chain-collapse
|
|
893
|
+
// above: `A.wohs(t1).wohs(t2)` keys as (A, t2) after the unwrap, hitting
|
|
894
|
+
// the same cache slot as a direct `A.wohs(t2)`.
|
|
895
|
+
let innerCache = __classPrivateFieldGet$1(_a, _a, "f", _State_wrapperCache).get(bare);
|
|
896
|
+
if (innerCache !== undefined) {
|
|
897
|
+
const ref = innerCache.get(overriddenHaltState);
|
|
898
|
+
if (ref !== undefined) {
|
|
899
|
+
const cached = ref.deref();
|
|
900
|
+
if (cached !== undefined) {
|
|
901
|
+
return cached;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
innerCache = new WeakMap();
|
|
907
|
+
__classPrivateFieldGet$1(_a, _a, "f", _State_wrapperCache).set(bare, innerCache);
|
|
908
|
+
}
|
|
909
|
+
// Cache miss — construct with no name, then overwrite #name directly
|
|
910
|
+
// (composed names contain `(` and `)` which the constructor's user-facing
|
|
911
|
+
// validation would reject; private-field access bypasses that).
|
|
912
|
+
const state = new _a();
|
|
913
|
+
__classPrivateFieldSet$1(state, _State_name, `${bare.name}(${overriddenHaltState.name})`, "f");
|
|
914
|
+
__classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(bare, _State_symbolToDataMap, "f"), "f");
|
|
915
|
+
__classPrivateFieldSet$1(state, _State_overriddenHaltState, overriddenHaltState, "f");
|
|
916
|
+
__classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(bare, _State_debugRef, "f"), "f");
|
|
917
|
+
__classPrivateFieldSet$1(state, _State_bareState, bare, "f");
|
|
918
|
+
innerCache.set(overriddenHaltState, new WeakRef(state));
|
|
858
919
|
return state;
|
|
859
920
|
}
|
|
860
921
|
// Single-state introspection — no traversal, no tapeBlock required.
|
|
@@ -867,7 +928,7 @@ class State {
|
|
|
867
928
|
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
868
929
|
let target = null;
|
|
869
930
|
try {
|
|
870
|
-
target = nextState instanceof
|
|
931
|
+
target = nextState instanceof _a ? nextState : nextState.ref;
|
|
871
932
|
}
|
|
872
933
|
catch {
|
|
873
934
|
target = null; // unbound Reference
|
|
@@ -885,38 +946,104 @@ class State {
|
|
|
885
946
|
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
886
947
|
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
887
948
|
isHalt: state.isHalt,
|
|
888
|
-
|
|
889
|
-
? { id: __classPrivateFieldGet$1(state,
|
|
949
|
+
overriddenHaltState: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f")
|
|
950
|
+
? { id: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f").id, name: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f").name }
|
|
890
951
|
: null,
|
|
891
952
|
transitions,
|
|
892
953
|
};
|
|
893
954
|
}
|
|
955
|
+
// Walks the State graph and emits a `Graph` data structure. v7 callable-
|
|
956
|
+
// subtree emit shape (#174):
|
|
957
|
+
//
|
|
958
|
+
// Each `withOverriddenHaltState` wrapper produces TWO graph nodes:
|
|
959
|
+
// - A wrapper node (`isWrapper: true`, `[[composite-name]]` shape) — the
|
|
960
|
+
// call site. No transitions of its own. `bareStateId` points to the
|
|
961
|
+
// bare's GraphNode; `overriddenHaltStateId` points to the override
|
|
962
|
+
// target's GraphNode.
|
|
963
|
+
// - A bare node (`isWrapper: false`, regular shape) — the callable body.
|
|
964
|
+
// Has the bare's transitions. Shared across all wrappers that wrap
|
|
965
|
+
// this bare (no per-context duplication).
|
|
966
|
+
//
|
|
967
|
+
// Frames are computed via union-find on bare reachability: two bares whose
|
|
968
|
+
// forward-reachable sets overlap merge into one frame. Each frame contains
|
|
969
|
+
// its bares + body states + a single halt marker (id = `-frameId`). The
|
|
970
|
+
// canonical `frameId` is the smallest bare-id in the component.
|
|
971
|
+
//
|
|
972
|
+
// Halt-bound transitions of any in-frame state are retargeted to the
|
|
973
|
+
// frame's halt marker. The frame's `subtree -. return .-> wrapper` and
|
|
974
|
+
// `subtree -. halt .-> s0` arrows are demand-emitted by `toMermaid` from
|
|
975
|
+
// the frame structure; they're not stored as graph edges.
|
|
894
976
|
static toGraph(initialState, tapeBlock) {
|
|
895
977
|
const nodes = {};
|
|
896
|
-
const queue = [initialState];
|
|
897
978
|
const alphabets = tapeBlock.alphabets.map((alphabet) => alphabet.symbols);
|
|
979
|
+
// Pass 1: BFS-discover all reachable States; emit one GraphNode per State
|
|
980
|
+
// (wrapper or bare/regular). Wrappers and bares are separate nodes.
|
|
981
|
+
const visited = new Set();
|
|
982
|
+
const queue = [initialState];
|
|
983
|
+
const bareIds = new Set(); // ids referenced as a wrapper's bareStateId
|
|
898
984
|
while (queue.length > 0) {
|
|
899
|
-
const
|
|
900
|
-
if (__classPrivateFieldGet$1(
|
|
985
|
+
const state = queue.shift();
|
|
986
|
+
if (visited.has(__classPrivateFieldGet$1(state, _State_id, "f"))) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
visited.add(__classPrivateFieldGet$1(state, _State_id, "f"));
|
|
990
|
+
if (state.isHalt) {
|
|
991
|
+
if (!(0 in nodes)) {
|
|
992
|
+
nodes[0] = {
|
|
993
|
+
id: 0,
|
|
994
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
995
|
+
isHalt: true,
|
|
996
|
+
isHaltMarker: false,
|
|
997
|
+
isWrapper: false,
|
|
998
|
+
bareStateId: null,
|
|
999
|
+
frameId: null,
|
|
1000
|
+
transitions: [],
|
|
1001
|
+
overriddenHaltStateId: null,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
// Wrapper? Emit wrapper node + queue bare and override target.
|
|
1007
|
+
if (__classPrivateFieldGet$1(state, _State_overriddenHaltState, "f") !== null && __classPrivateFieldGet$1(state, _State_bareState, "f") !== null) {
|
|
1008
|
+
const bareState = __classPrivateFieldGet$1(state, _State_bareState, "f");
|
|
1009
|
+
const overrideTarget = __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f");
|
|
1010
|
+
nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = {
|
|
1011
|
+
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1012
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"), // composite name like "A(target)"
|
|
1013
|
+
isHalt: false,
|
|
1014
|
+
isHaltMarker: false,
|
|
1015
|
+
isWrapper: true,
|
|
1016
|
+
bareStateId: __classPrivateFieldGet$1(bareState, _State_id, "f"),
|
|
1017
|
+
frameId: null,
|
|
1018
|
+
transitions: [],
|
|
1019
|
+
overriddenHaltStateId: __classPrivateFieldGet$1(overrideTarget, _State_id, "f"),
|
|
1020
|
+
};
|
|
1021
|
+
bareIds.add(__classPrivateFieldGet$1(bareState, _State_id, "f"));
|
|
1022
|
+
queue.push(bareState);
|
|
1023
|
+
queue.push(overrideTarget);
|
|
901
1024
|
continue;
|
|
902
1025
|
}
|
|
1026
|
+
// Regular (or bare) state — build node with transitions.
|
|
903
1027
|
const node = {
|
|
904
|
-
id: __classPrivateFieldGet$1(
|
|
905
|
-
name: __classPrivateFieldGet$1(
|
|
906
|
-
isHalt:
|
|
1028
|
+
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1029
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1030
|
+
isHalt: false,
|
|
1031
|
+
isHaltMarker: false,
|
|
1032
|
+
isWrapper: false,
|
|
1033
|
+
bareStateId: null,
|
|
1034
|
+
frameId: null,
|
|
907
1035
|
transitions: [],
|
|
908
|
-
|
|
1036
|
+
overriddenHaltStateId: null,
|
|
909
1037
|
};
|
|
910
|
-
nodes[__classPrivateFieldGet$1(
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(current, _State_symbolToDataMap, "f")) {
|
|
1038
|
+
nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = node;
|
|
1039
|
+
let patternIx = 0;
|
|
1040
|
+
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
915
1041
|
let target;
|
|
916
1042
|
try {
|
|
917
|
-
target = nextState instanceof
|
|
1043
|
+
target = nextState instanceof _a ? nextState : nextState.ref;
|
|
918
1044
|
}
|
|
919
1045
|
catch {
|
|
1046
|
+
patternIx += 1;
|
|
920
1047
|
continue;
|
|
921
1048
|
}
|
|
922
1049
|
node.transitions.push({
|
|
@@ -925,26 +1052,173 @@ class State {
|
|
|
925
1052
|
symbol: decodeWriteSymbol(tc.symbol),
|
|
926
1053
|
movement: decodeMovement(tc.movement.description),
|
|
927
1054
|
})),
|
|
928
|
-
nextStateId: target
|
|
1055
|
+
nextStateId: __classPrivateFieldGet$1(target, _State_id, "f"),
|
|
1056
|
+
id: `${__classPrivateFieldGet$1(state, _State_id, "f")}-${patternIx}`,
|
|
929
1057
|
});
|
|
930
1058
|
queue.push(target);
|
|
1059
|
+
patternIx += 1;
|
|
931
1060
|
}
|
|
932
1061
|
}
|
|
1062
|
+
// Always emit real halt as a sentinel, even if no transition targets it.
|
|
1063
|
+
// It anchors the `subtree -. halt .-> s0` frame-level arrow whenever a
|
|
1064
|
+
// frame demand-emits one, and it's the canonical machine-halt singleton.
|
|
1065
|
+
if (!(0 in nodes)) {
|
|
1066
|
+
nodes[0] = {
|
|
1067
|
+
id: 0,
|
|
1068
|
+
name: 'halt',
|
|
1069
|
+
isHalt: true,
|
|
1070
|
+
isHaltMarker: false,
|
|
1071
|
+
isWrapper: false,
|
|
1072
|
+
bareStateId: null,
|
|
1073
|
+
frameId: null,
|
|
1074
|
+
transitions: [],
|
|
1075
|
+
overriddenHaltStateId: null,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
// Pass 2: For each bare, compute its forward-reachable set (following
|
|
1079
|
+
// transitions; stopping at halt and at wrappers — both are frame
|
|
1080
|
+
// boundaries).
|
|
1081
|
+
const computeReach = (startId) => {
|
|
1082
|
+
const reach = new Set();
|
|
1083
|
+
const stack = [startId];
|
|
1084
|
+
while (stack.length > 0) {
|
|
1085
|
+
const id = stack.pop();
|
|
1086
|
+
if (reach.has(id)) {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const node = nodes[id];
|
|
1090
|
+
if (!node || node.isHalt || node.isWrapper) {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
reach.add(id);
|
|
1094
|
+
for (const t of node.transitions) {
|
|
1095
|
+
const target = nodes[t.nextStateId];
|
|
1096
|
+
if (!target || target.isHalt || target.isWrapper) {
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
stack.push(t.nextStateId);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return reach;
|
|
1103
|
+
};
|
|
1104
|
+
const reachByBare = new Map();
|
|
1105
|
+
for (const bareId of bareIds) {
|
|
1106
|
+
reachByBare.set(bareId, computeReach(bareId));
|
|
1107
|
+
}
|
|
1108
|
+
// Pass 3: Union-find on bare overlaps. Two bares merge if their reach
|
|
1109
|
+
// sets share any state. Canonical representative = smallest bare-id in
|
|
1110
|
+
// the component.
|
|
1111
|
+
const ufParent = new Map();
|
|
1112
|
+
const ufFind = (id) => {
|
|
1113
|
+
if (!ufParent.has(id)) {
|
|
1114
|
+
ufParent.set(id, id);
|
|
1115
|
+
}
|
|
1116
|
+
let root = id;
|
|
1117
|
+
while (ufParent.get(root) !== root) {
|
|
1118
|
+
root = ufParent.get(root);
|
|
1119
|
+
}
|
|
1120
|
+
// Path compression
|
|
1121
|
+
let cur = id;
|
|
1122
|
+
while (ufParent.get(cur) !== root) {
|
|
1123
|
+
const next = ufParent.get(cur);
|
|
1124
|
+
ufParent.set(cur, root);
|
|
1125
|
+
cur = next;
|
|
1126
|
+
}
|
|
1127
|
+
return root;
|
|
1128
|
+
};
|
|
1129
|
+
const ufUnion = (a, b) => {
|
|
1130
|
+
const ra = ufFind(a);
|
|
1131
|
+
const rb = ufFind(b);
|
|
1132
|
+
if (ra === rb)
|
|
1133
|
+
return;
|
|
1134
|
+
if (ra < rb) {
|
|
1135
|
+
ufParent.set(rb, ra);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
ufParent.set(ra, rb);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
for (const bareId of bareIds) {
|
|
1142
|
+
ufFind(bareId);
|
|
1143
|
+
}
|
|
1144
|
+
// For each state, collect the bares that reach it; union all bares that
|
|
1145
|
+
// share a state.
|
|
1146
|
+
const stateToReachingBares = new Map();
|
|
1147
|
+
for (const [bareId, reachSet] of reachByBare) {
|
|
1148
|
+
for (const stateId of reachSet) {
|
|
1149
|
+
let bares = stateToReachingBares.get(stateId);
|
|
1150
|
+
if (!bares) {
|
|
1151
|
+
bares = [];
|
|
1152
|
+
stateToReachingBares.set(stateId, bares);
|
|
1153
|
+
}
|
|
1154
|
+
bares.push(bareId);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
for (const bares of stateToReachingBares.values()) {
|
|
1158
|
+
for (let i = 1; i < bares.length; i += 1) {
|
|
1159
|
+
ufUnion(bares[0], bares[i]);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// Assign frameId to each in-reach state.
|
|
1163
|
+
const frameIds = new Set();
|
|
1164
|
+
for (const [stateId, bares] of stateToReachingBares) {
|
|
1165
|
+
const frameId = ufFind(bares[0]);
|
|
1166
|
+
nodes[stateId].frameId = frameId;
|
|
1167
|
+
frameIds.add(frameId);
|
|
1168
|
+
}
|
|
1169
|
+
// Pass 4: Retarget halt-bound transitions for in-frame states to the
|
|
1170
|
+
// frame's halt marker. Out-of-frame states (top-level dispatcher, override
|
|
1171
|
+
// targets, etc.) keep their halt-bound transitions pointing at real halt.
|
|
1172
|
+
for (const node of Object.values(nodes)) {
|
|
1173
|
+
if (node.frameId === null) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const haltMarkerId = -node.frameId;
|
|
1177
|
+
for (const t of node.transitions) {
|
|
1178
|
+
const target = nodes[t.nextStateId];
|
|
1179
|
+
if (target && target.isHalt && !target.isHaltMarker) {
|
|
1180
|
+
t.nextStateId = haltMarkerId;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// Pass 5: Emit one halt marker per frame.
|
|
1185
|
+
for (const frameId of frameIds) {
|
|
1186
|
+
const haltMarkerId = -frameId;
|
|
1187
|
+
nodes[haltMarkerId] = {
|
|
1188
|
+
id: haltMarkerId,
|
|
1189
|
+
name: 'halt',
|
|
1190
|
+
isHalt: true,
|
|
1191
|
+
isHaltMarker: true,
|
|
1192
|
+
isWrapper: false,
|
|
1193
|
+
bareStateId: null,
|
|
1194
|
+
frameId,
|
|
1195
|
+
transitions: [],
|
|
1196
|
+
overriddenHaltStateId: null,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
933
1199
|
return { initialId: __classPrivateFieldGet$1(initialState, _State_id, "f"), alphabets, nodes };
|
|
934
1200
|
}
|
|
935
1201
|
// Inverse of toGraph: rebuilds a State graph (and a fresh TapeBlock with the
|
|
936
1202
|
// graph's alphabets) from a serialized Graph. Round-trips with toGraph in
|
|
937
1203
|
// the sense that running the rebuilt machine on the same input gives the
|
|
938
1204
|
// same output, but the rebuilt State instances have *new* internal IDs.
|
|
1205
|
+
//
|
|
1206
|
+
// Under the v7 callable-subtree model (#174), graph nodes split into:
|
|
1207
|
+
// - Wrapper nodes (`isWrapper: true`, no transitions) — reconstructed via
|
|
1208
|
+
// `bareStates[bareStateId].withOverriddenHaltState(finalStates[overriddenHaltStateId])`.
|
|
1209
|
+
// - Bare/regular nodes — constructed as normal States with transitions.
|
|
1210
|
+
// - Halt + halt-marker nodes — collapse to the singleton `haltState`.
|
|
939
1211
|
static fromGraph(graph) {
|
|
940
1212
|
const alphabetObjs = graph.alphabets.map((syms) => new Alphabet(syms));
|
|
941
1213
|
const tapeBlock = TapeBlock.fromAlphabets(alphabetObjs);
|
|
942
1214
|
const ids = Object.keys(graph.nodes).map(Number);
|
|
943
|
-
// Pass 1: pre-create a Reference for each non-halt node
|
|
944
|
-
//
|
|
1215
|
+
// Pass 1: pre-create a Reference for each non-halt non-halt-marker node
|
|
1216
|
+
// (both wrappers and regulars). Halt and halt-marker nodes collapse to the
|
|
1217
|
+
// singleton `haltState` and need no ref.
|
|
945
1218
|
const refs = {};
|
|
946
1219
|
for (const nodeId of ids) {
|
|
947
|
-
|
|
1220
|
+
const node = graph.nodes[nodeId];
|
|
1221
|
+
if (!node.isHalt) {
|
|
948
1222
|
refs[nodeId] = new Reference();
|
|
949
1223
|
}
|
|
950
1224
|
}
|
|
@@ -961,19 +1235,22 @@ class State {
|
|
|
961
1235
|
}
|
|
962
1236
|
return tapeBlock.symbol(flat);
|
|
963
1237
|
};
|
|
964
|
-
// Pass 2: build a
|
|
965
|
-
//
|
|
1238
|
+
// Pass 2: build a State for each non-wrapper non-halt non-halt-marker
|
|
1239
|
+
// node. Transitions point at refs so cycles work; haltState (and halt
|
|
1240
|
+
// markers, which collapse to haltState) are used directly.
|
|
966
1241
|
const bareStates = {};
|
|
967
1242
|
for (const nodeId of ids) {
|
|
968
1243
|
const node = graph.nodes[nodeId];
|
|
969
|
-
if (node.isHalt) {
|
|
1244
|
+
if (node.isHalt || node.isWrapper) {
|
|
970
1245
|
continue;
|
|
971
1246
|
}
|
|
972
1247
|
const stateDefinition = {};
|
|
973
1248
|
for (const t of node.transitions) {
|
|
974
1249
|
const key = patternToKey(parsePatternString(t.pattern, graph.alphabets));
|
|
975
1250
|
const target = graph.nodes[t.nextStateId];
|
|
976
|
-
const nextState = target
|
|
1251
|
+
const nextState = !target || target.isHalt
|
|
1252
|
+
? haltState
|
|
1253
|
+
: refs[t.nextStateId];
|
|
977
1254
|
stateDefinition[key] = {
|
|
978
1255
|
command: t.command.map((c) => ({
|
|
979
1256
|
symbol: parseWriteSymbolLabel(c.symbol),
|
|
@@ -982,9 +1259,17 @@ class State {
|
|
|
982
1259
|
nextState,
|
|
983
1260
|
};
|
|
984
1261
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1262
|
+
// Graph-sourced names may contain `(` and `)` (composite wrapper names —
|
|
1263
|
+
// although wrappers go through a separate path below, defensive
|
|
1264
|
+
// construction here keeps the bypass uniform). Construct without a name
|
|
1265
|
+
// and assign `#name` directly to skip user-facing name validation.
|
|
1266
|
+
const bare = new _a(stateDefinition);
|
|
1267
|
+
__classPrivateFieldSet$1(bare, _State_name, node.name, "f");
|
|
1268
|
+
bareStates[nodeId] = bare;
|
|
1269
|
+
}
|
|
1270
|
+
// Pass 3: resolve every node to its final State (memoized + cycle-safe).
|
|
1271
|
+
// Wrappers compose lazily via `withOverriddenHaltState` once their bare
|
|
1272
|
+
// and override are resolved.
|
|
988
1273
|
const finalStates = {};
|
|
989
1274
|
const inProgress = new Set();
|
|
990
1275
|
const getFinal = (nodeId) => {
|
|
@@ -992,7 +1277,7 @@ class State {
|
|
|
992
1277
|
return finalStates[nodeId];
|
|
993
1278
|
}
|
|
994
1279
|
const node = graph.nodes[nodeId];
|
|
995
|
-
if (node.isHalt) {
|
|
1280
|
+
if (!node || node.isHalt) {
|
|
996
1281
|
finalStates[nodeId] = haltState;
|
|
997
1282
|
return haltState;
|
|
998
1283
|
}
|
|
@@ -1000,9 +1285,14 @@ class State {
|
|
|
1000
1285
|
throw new Error(`override-halt cycle at state #${nodeId}`);
|
|
1001
1286
|
}
|
|
1002
1287
|
inProgress.add(nodeId);
|
|
1003
|
-
let state
|
|
1004
|
-
if (node.
|
|
1005
|
-
|
|
1288
|
+
let state;
|
|
1289
|
+
if (node.isWrapper) {
|
|
1290
|
+
const bare = getFinal(node.bareStateId);
|
|
1291
|
+
const override = getFinal(node.overriddenHaltStateId);
|
|
1292
|
+
state = bare.withOverriddenHaltState(override);
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
state = bareStates[nodeId];
|
|
1006
1296
|
}
|
|
1007
1297
|
inProgress.delete(nodeId);
|
|
1008
1298
|
finalStates[nodeId] = state;
|
|
@@ -1011,8 +1301,8 @@ class State {
|
|
|
1011
1301
|
for (const nodeId of ids) {
|
|
1012
1302
|
getFinal(nodeId);
|
|
1013
1303
|
}
|
|
1014
|
-
// Pass 4: bind each ref to the
|
|
1015
|
-
//
|
|
1304
|
+
// Pass 4: bind each ref to the resolved final State so cross-node
|
|
1305
|
+
// transitions land on the right instance.
|
|
1016
1306
|
for (const nodeId of ids) {
|
|
1017
1307
|
if (!graph.nodes[nodeId].isHalt) {
|
|
1018
1308
|
refs[nodeId].bind(finalStates[nodeId]);
|
|
@@ -1025,6 +1315,13 @@ class State {
|
|
|
1025
1315
|
};
|
|
1026
1316
|
}
|
|
1027
1317
|
}
|
|
1318
|
+
_a = State;
|
|
1319
|
+
// Memoization cache for `withOverriddenHaltState`. Keyed by
|
|
1320
|
+
// (bare, override) — same args return the same wrapper instance (#175).
|
|
1321
|
+
// Two-level WeakMap so the outer entry is GC'd when the bare is collected;
|
|
1322
|
+
// WeakRef values let wrappers themselves be GC'd when nothing else holds
|
|
1323
|
+
// them, with cache misses simply reconstructing fresh wrappers.
|
|
1324
|
+
_State_wrapperCache = { value: new WeakMap() };
|
|
1028
1325
|
const haltState = new State(null);
|
|
1029
1326
|
|
|
1030
1327
|
var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
@@ -1087,8 +1384,8 @@ class TuringMachine {
|
|
|
1087
1384
|
__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f")[lockSymbol].lock(executionSymbol);
|
|
1088
1385
|
const stack = __classPrivateFieldGet(this, _TuringMachine_stack, "f");
|
|
1089
1386
|
let state = initialState;
|
|
1090
|
-
if (state.
|
|
1091
|
-
stack.push(state.
|
|
1387
|
+
if (state.overriddenHaltState) {
|
|
1388
|
+
stack.push(state.overriddenHaltState);
|
|
1092
1389
|
}
|
|
1093
1390
|
let i = 0;
|
|
1094
1391
|
while (!state.isHalt) {
|
|
@@ -1142,8 +1439,8 @@ class TuringMachine {
|
|
|
1142
1439
|
if (nextState.isHalt && stack.length) {
|
|
1143
1440
|
nextState = stack.pop();
|
|
1144
1441
|
}
|
|
1145
|
-
if (state !== nextState && nextState.
|
|
1146
|
-
stack.push(nextState.
|
|
1442
|
+
if (state !== nextState && nextState.overriddenHaltState) {
|
|
1443
|
+
stack.push(nextState.overriddenHaltState);
|
|
1147
1444
|
}
|
|
1148
1445
|
state = nextState;
|
|
1149
1446
|
}
|
|
@@ -1167,36 +1464,215 @@ _TuringMachine_tapeBlock = new WeakMap(), _TuringMachine_stack = new WeakMap();
|
|
|
1167
1464
|
//
|
|
1168
1465
|
// Currently only Mermaid flowchart syntax is supported. Future formats
|
|
1169
1466
|
// (Graphviz, JSON-LD, custom DSL) belong here too.
|
|
1467
|
+
//
|
|
1468
|
+
// v7 callable-subtree emit (#174):
|
|
1469
|
+
// - Each `withOverriddenHaltState` wrapper produces TWO graph nodes — a
|
|
1470
|
+
// wrapper node (`[[composite-name]]`, OUTSIDE any subgraph) and a bare
|
|
1471
|
+
// node (regular shape, INSIDE its callable subtree subgraph).
|
|
1472
|
+
// - Subgraphs (one per frame): `subgraph w_${frameId}["callable subtree
|
|
1473
|
+
// of NAME"]` (single bare) or `["callable scope: A ∪ B"]` (union).
|
|
1474
|
+
// - Each frame has exactly one halt marker `c${frameId}(((halt)))` inside
|
|
1475
|
+
// its subgraph; halt-bound transitions from in-frame states retarget to
|
|
1476
|
+
// it. Always emitted (orphan signals dead wrapper).
|
|
1477
|
+
// - Arrow conventions:
|
|
1478
|
+
// solid `-->` regular transitions, including wrapper-to-override.
|
|
1479
|
+
// bold `==>` RESERVED for the wrapper-to-bare `call` arrow.
|
|
1480
|
+
// `&` ribbon collapses multi-wrapper-shares-bare.
|
|
1481
|
+
// dotted `-.->` frame-level dispatch (`return`, `halt`, `enter`).
|
|
1482
|
+
// - The `return` arrow (subgraph → wrapper) is demand-emitted iff the
|
|
1483
|
+
// frame's halt marker has at least one incoming edge AND the wrapper
|
|
1484
|
+
// calls into the frame. The `halt` arrow (subgraph → s0) is emitted
|
|
1485
|
+
// iff the halt marker has incoming AND there's at least one non-wrapper
|
|
1486
|
+
// entry into the frame (cross-subgraph solid arrow from outside).
|
|
1487
|
+
// Maps a graph node id to its Mermaid id.
|
|
1488
|
+
// - non-negative id N → "sN"
|
|
1489
|
+
// - negative id -N (halt marker) → "cN"
|
|
1490
|
+
function mermaidIdFor(id) {
|
|
1491
|
+
return id < 0 ? `c${-id}` : `s${id}`;
|
|
1492
|
+
}
|
|
1493
|
+
// Inverse of mermaidIdFor.
|
|
1494
|
+
function parseMermaidId(s) {
|
|
1495
|
+
if (s.startsWith('c')) {
|
|
1496
|
+
return -Number(s.slice(1));
|
|
1497
|
+
}
|
|
1498
|
+
return Number(s.slice(1));
|
|
1499
|
+
}
|
|
1500
|
+
function frameSubgraphId(frameId) {
|
|
1501
|
+
return `w_${frameId}`;
|
|
1502
|
+
}
|
|
1170
1503
|
function toMermaid(graph) {
|
|
1171
1504
|
const lines = [
|
|
1172
1505
|
'flowchart TD',
|
|
1173
1506
|
`%% alphabets: ${JSON.stringify(graph.alphabets)}`,
|
|
1174
1507
|
];
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1508
|
+
// Sort nodes by id ascending — real halt (0) first, then regulars by their
|
|
1509
|
+
// ids, then halt markers (negative) at the end. Deterministic emit lets
|
|
1510
|
+
// toMermaid → fromMermaid → toMermaid round-trip stably (#139).
|
|
1511
|
+
const nodes = Object.values(graph.nodes).slice().sort((a, b) => a.id - b.id);
|
|
1512
|
+
// Bucket nodes for emit order.
|
|
1513
|
+
const topLevelNodes = nodes.filter((n) => n.frameId === null && !n.isWrapper);
|
|
1514
|
+
const wrapperNodes = nodes.filter((n) => n.isWrapper);
|
|
1515
|
+
// Bares-and-bodies inside frames, grouped by frameId.
|
|
1516
|
+
const nodesByFrame = new Map();
|
|
1517
|
+
// Halt-marker per frame (kept separate so it always emits LAST inside the
|
|
1518
|
+
// subgraph for deterministic shape).
|
|
1519
|
+
const haltMarkerByFrame = new Map();
|
|
1520
|
+
for (const node of nodes) {
|
|
1521
|
+
if (node.frameId === null || node.isWrapper)
|
|
1522
|
+
continue;
|
|
1523
|
+
if (node.isHaltMarker) {
|
|
1524
|
+
haltMarkerByFrame.set(node.frameId, node);
|
|
1179
1525
|
}
|
|
1180
|
-
else
|
|
1181
|
-
|
|
1526
|
+
else {
|
|
1527
|
+
let bucket = nodesByFrame.get(node.frameId);
|
|
1528
|
+
if (!bucket) {
|
|
1529
|
+
bucket = [];
|
|
1530
|
+
nodesByFrame.set(node.frameId, bucket);
|
|
1531
|
+
}
|
|
1532
|
+
bucket.push(node);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
// 1. Emit top-level nodes (real halt, non-wrapper regulars outside any frame).
|
|
1536
|
+
for (const node of topLevelNodes) {
|
|
1537
|
+
const mid = mermaidIdFor(node.id);
|
|
1538
|
+
if (node.isHalt) {
|
|
1539
|
+
lines.push(` ${mid}(((halt)))`);
|
|
1182
1540
|
}
|
|
1183
1541
|
else {
|
|
1184
|
-
lines.push(` ${
|
|
1542
|
+
lines.push(` ${mid}["${node.name}"]`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
// 2. Emit wrappers at top level.
|
|
1546
|
+
for (const wrapper of wrapperNodes) {
|
|
1547
|
+
lines.push(` ${mermaidIdFor(wrapper.id)}[["${wrapper.name}"]]`);
|
|
1548
|
+
}
|
|
1549
|
+
// 3. `idle` sentinel.
|
|
1550
|
+
lines.push(' idle([idle])');
|
|
1551
|
+
// 4. Subgraph per frame.
|
|
1552
|
+
const frameIds = [...nodesByFrame.keys()].sort((a, b) => a - b);
|
|
1553
|
+
for (const frameId of frameIds) {
|
|
1554
|
+
const frameBares = (nodesByFrame.get(frameId) ?? []).filter((n) => isFrameBare(n, graph));
|
|
1555
|
+
const frameBareNames = frameBares
|
|
1556
|
+
.slice()
|
|
1557
|
+
.sort((a, b) => a.id - b.id)
|
|
1558
|
+
.map((n) => n.name);
|
|
1559
|
+
const label = frameBareNames.length > 1
|
|
1560
|
+
? `callable scope: ${frameBareNames.join(' ∪ ')}`
|
|
1561
|
+
: `callable subtree of ${frameBareNames[0] ?? frameId}`;
|
|
1562
|
+
lines.push(` subgraph ${frameSubgraphId(frameId)}["${label}"]`);
|
|
1563
|
+
// Inner nodes — sort by id for determinism.
|
|
1564
|
+
for (const node of (nodesByFrame.get(frameId) ?? []).slice().sort((a, b) => a.id - b.id)) {
|
|
1565
|
+
lines.push(` ${mermaidIdFor(node.id)}["${node.name}"]`);
|
|
1566
|
+
}
|
|
1567
|
+
const haltMarker = haltMarkerByFrame.get(frameId);
|
|
1568
|
+
if (haltMarker) {
|
|
1569
|
+
lines.push(` ${mermaidIdFor(haltMarker.id)}(((halt)))`);
|
|
1570
|
+
}
|
|
1571
|
+
lines.push(' end');
|
|
1572
|
+
}
|
|
1573
|
+
// 5. Enter arrow.
|
|
1574
|
+
lines.push(` idle -. enter .-> ${mermaidIdFor(graph.initialId)}`);
|
|
1575
|
+
// 6. `call` arrows — grouped by bare (multi-wrapper-shares-bare collapses
|
|
1576
|
+
// into a single `&` ribbon).
|
|
1577
|
+
const wrappersByBare = new Map();
|
|
1578
|
+
for (const wrapper of wrapperNodes) {
|
|
1579
|
+
if (wrapper.bareStateId === null)
|
|
1580
|
+
continue;
|
|
1581
|
+
let group = wrappersByBare.get(wrapper.bareStateId);
|
|
1582
|
+
if (!group) {
|
|
1583
|
+
group = [];
|
|
1584
|
+
wrappersByBare.set(wrapper.bareStateId, group);
|
|
1585
|
+
}
|
|
1586
|
+
group.push(wrapper);
|
|
1587
|
+
}
|
|
1588
|
+
const sortedBares = [...wrappersByBare.keys()].sort((a, b) => a - b);
|
|
1589
|
+
for (const bareId of sortedBares) {
|
|
1590
|
+
const wrappers = wrappersByBare.get(bareId).slice().sort((a, b) => a.id - b.id);
|
|
1591
|
+
const sources = wrappers.map((w) => mermaidIdFor(w.id)).join(' & ');
|
|
1592
|
+
lines.push(` ${sources} == "call" ==> ${mermaidIdFor(bareId)}`);
|
|
1593
|
+
}
|
|
1594
|
+
// 7. Demand-emit `return` and `halt` arrows per frame.
|
|
1595
|
+
// For each frame: check if its halt marker has incoming transitions.
|
|
1596
|
+
const haltMarkerHasIncoming = new Map();
|
|
1597
|
+
for (const node of nodes) {
|
|
1598
|
+
for (const t of node.transitions) {
|
|
1599
|
+
const target = graph.nodes[t.nextStateId];
|
|
1600
|
+
if (target && target.isHaltMarker && target.frameId !== null) {
|
|
1601
|
+
haltMarkerHasIncoming.set(target.frameId, true);
|
|
1602
|
+
}
|
|
1185
1603
|
}
|
|
1186
1604
|
}
|
|
1187
|
-
|
|
1605
|
+
// For each frame: check if there's at least one non-wrapper entry (a solid
|
|
1606
|
+
// `-->` from OUTSIDE the frame into any node INSIDE).
|
|
1607
|
+
const hasNonWrapperEntry = new Map();
|
|
1608
|
+
for (const node of nodes) {
|
|
1609
|
+
if (node.isWrapper)
|
|
1610
|
+
continue;
|
|
1188
1611
|
for (const t of node.transitions) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1612
|
+
const target = graph.nodes[t.nextStateId];
|
|
1613
|
+
if (target
|
|
1614
|
+
&& target.frameId !== null
|
|
1615
|
+
&& node.frameId !== target.frameId) {
|
|
1616
|
+
hasNonWrapperEntry.set(target.frameId, true);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
for (const frameId of frameIds) {
|
|
1621
|
+
if (!haltMarkerHasIncoming.get(frameId))
|
|
1622
|
+
continue;
|
|
1623
|
+
// Return arrow — collapsed `&` ribbon over all wrappers calling this frame.
|
|
1624
|
+
const callingWrappers = wrapperNodes.filter((w) => {
|
|
1625
|
+
if (w.bareStateId === null)
|
|
1626
|
+
return false;
|
|
1627
|
+
const bare = graph.nodes[w.bareStateId];
|
|
1628
|
+
return !!bare && bare.frameId === frameId;
|
|
1629
|
+
});
|
|
1630
|
+
if (callingWrappers.length > 0) {
|
|
1631
|
+
const targets = callingWrappers
|
|
1632
|
+
.slice()
|
|
1633
|
+
.sort((a, b) => a.id - b.id)
|
|
1634
|
+
.map((w) => mermaidIdFor(w.id))
|
|
1635
|
+
.join(' & ');
|
|
1636
|
+
lines.push(` ${frameSubgraphId(frameId)} -. "return" .-> ${targets}`);
|
|
1193
1637
|
}
|
|
1194
|
-
if (
|
|
1195
|
-
lines.push(`
|
|
1638
|
+
if (hasNonWrapperEntry.get(frameId)) {
|
|
1639
|
+
lines.push(` ${frameSubgraphId(frameId)} -. "halt" .-> s0`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
// 8. Wrapper-to-override arrows (regular solid).
|
|
1643
|
+
for (const wrapper of wrapperNodes) {
|
|
1644
|
+
if (wrapper.overriddenHaltStateId === null)
|
|
1645
|
+
continue;
|
|
1646
|
+
lines.push(` ${mermaidIdFor(wrapper.id)} --> ${mermaidIdFor(wrapper.overriddenHaltStateId)}`);
|
|
1647
|
+
}
|
|
1648
|
+
// 9. Regular transitions for non-wrapper non-halt-marker non-halt nodes.
|
|
1649
|
+
for (const node of nodes) {
|
|
1650
|
+
if (node.isHalt || node.isHaltMarker || node.isWrapper)
|
|
1651
|
+
continue;
|
|
1652
|
+
for (const t of node.transitions) {
|
|
1653
|
+
const alternatives = t.pattern.split('|');
|
|
1654
|
+
const reads = alternatives.map((alt) => `[${alt}]`).join('|');
|
|
1655
|
+
const writes = `[${t.command.map((c) => c.symbol).join(',')}]`;
|
|
1656
|
+
const moves = `[${t.command.map((c) => c.movement).join(',')}]`;
|
|
1657
|
+
const label = `${reads} → ${writes}/${moves}`;
|
|
1658
|
+
lines.push(` ${mermaidIdFor(node.id)} -- "${label}" --> ${mermaidIdFor(t.nextStateId)}`);
|
|
1196
1659
|
}
|
|
1197
1660
|
}
|
|
1198
1661
|
return lines.join('\n');
|
|
1199
1662
|
}
|
|
1663
|
+
// Helper: identify "the bare states" that anchor a frame's name. A bare is a
|
|
1664
|
+
// node referenced as some wrapper's `bareStateId`. Body states (also in-frame
|
|
1665
|
+
// but not bare) are excluded from the frame label.
|
|
1666
|
+
function isFrameBare(node, graph) {
|
|
1667
|
+
if (node.isWrapper || node.isHalt)
|
|
1668
|
+
return false;
|
|
1669
|
+
for (const other of Object.values(graph.nodes)) {
|
|
1670
|
+
if (other.isWrapper && other.bareStateId === node.id) {
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1200
1676
|
// Inverse of toMermaid: parses the Mermaid output produced by toMermaid back
|
|
1201
1677
|
// into a Graph. The parser is strict to the dialect toMermaid emits — it
|
|
1202
1678
|
// recognises the specific node/edge shapes and the leading
|
|
@@ -1208,11 +1684,26 @@ function toMermaid(graph) {
|
|
|
1208
1684
|
// per-tape segments are split on ','. If your alphabet contains '/' or ','
|
|
1209
1685
|
// as literal symbols, the parser cannot disambiguate. Stick to alphabets
|
|
1210
1686
|
// without those characters when round-tripping through Mermaid.
|
|
1211
|
-
const haltNodeRegex = /^
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
1687
|
+
const haltNodeRegex = /^([sc]\d+)\(\(\(halt\)\)\)$/;
|
|
1688
|
+
const regularNodeRegex = /^(s\d+)\["([^"]*)"\]$/;
|
|
1689
|
+
const wrappedNodeRegex = /^(s\d+)\[\["([^"]*)"\]\]$/;
|
|
1690
|
+
const subgraphStartRegex = /^subgraph\s+w_(\d+)\["([^"]*)"\]$/;
|
|
1691
|
+
const subgraphEndRegex = /^end$/;
|
|
1692
|
+
const idleNodeRegex = /^idle\(\[idle\]\)$/;
|
|
1693
|
+
const enterArrowRegex = /^idle\s+-\.\s+enter\s+\.->\s+(s\d+)$/;
|
|
1694
|
+
// Regular labeled transition (solid `-->`).
|
|
1695
|
+
const labeledTransitionRegex = /^([sc]\d+)\s+--\s+"(.*)"\s+-->\s+([sc]\d+)$/;
|
|
1696
|
+
// Wrapper → override (unlabeled solid `-->`).
|
|
1697
|
+
const wrapperOverrideRegex = /^(s\d+)\s+-->\s+([sc]\d+)$/;
|
|
1698
|
+
// Call arrow (bold `==>`), with optional `&`-joined source ribbon.
|
|
1699
|
+
// Ribbon separator is fixed at " & " (single spaces around &) — toMermaid
|
|
1700
|
+
// emits exactly that form, so the parser is strict to it. The literal-space
|
|
1701
|
+
// form avoids CodeQL's polynomial-ReDoS flag on a `\s+&\s+` shape.
|
|
1702
|
+
const callArrowRegex = /^(s\d+(?: & s\d+)*)\s+==\s+"call"\s+==>\s+(s\d+)$/;
|
|
1703
|
+
// Return arrow (`w_N -. return .-> s_W` with optional `&` target ribbon).
|
|
1704
|
+
const returnArrowRegex = /^w_(\d+)\s+-\.\s+"return"\s+\.->\s+(s\d+(?: & s\d+)*)$/;
|
|
1705
|
+
// Halt arrow (`w_N -. halt .-> s0`).
|
|
1706
|
+
const haltArrowRegex = /^w_(\d+)\s+-\.\s+"halt"\s+\.->\s+s0$/;
|
|
1216
1707
|
// First capture char anchored as \S to avoid polynomial backtracking between
|
|
1217
1708
|
// the preceding \s* and a permissive (.+); see CodeQL js/polynomial-redos.
|
|
1218
1709
|
const alphabetsRegex = /^%%\s*alphabets:\s*(\S.*)$/;
|
|
@@ -1221,99 +1712,197 @@ function fromMermaid(text) {
|
|
|
1221
1712
|
let alphabets = [];
|
|
1222
1713
|
let initialId = null;
|
|
1223
1714
|
const nodes = {};
|
|
1715
|
+
let currentFrameId = null;
|
|
1224
1716
|
const ensureNode = (id, opts = {}) => {
|
|
1225
1717
|
if (!nodes[id]) {
|
|
1226
1718
|
nodes[id] = {
|
|
1227
1719
|
id,
|
|
1228
|
-
name: opts.name ??
|
|
1720
|
+
name: opts.name ?? mermaidIdFor(id),
|
|
1229
1721
|
isHalt: opts.isHalt ?? false,
|
|
1722
|
+
isHaltMarker: opts.isHaltMarker ?? false,
|
|
1723
|
+
isWrapper: opts.isWrapper ?? false,
|
|
1724
|
+
bareStateId: opts.bareStateId ?? null,
|
|
1725
|
+
frameId: opts.frameId ?? null,
|
|
1230
1726
|
transitions: [],
|
|
1231
|
-
|
|
1727
|
+
overriddenHaltStateId: null,
|
|
1232
1728
|
};
|
|
1233
1729
|
}
|
|
1234
1730
|
else {
|
|
1235
|
-
if (opts.name !== undefined)
|
|
1731
|
+
if (opts.name !== undefined)
|
|
1236
1732
|
nodes[id].name = opts.name;
|
|
1237
|
-
|
|
1238
|
-
if (opts.isHalt !== undefined) {
|
|
1733
|
+
if (opts.isHalt !== undefined)
|
|
1239
1734
|
nodes[id].isHalt = opts.isHalt;
|
|
1240
|
-
|
|
1735
|
+
if (opts.isHaltMarker !== undefined)
|
|
1736
|
+
nodes[id].isHaltMarker = opts.isHaltMarker;
|
|
1737
|
+
if (opts.isWrapper !== undefined)
|
|
1738
|
+
nodes[id].isWrapper = opts.isWrapper;
|
|
1739
|
+
if (opts.bareStateId !== undefined)
|
|
1740
|
+
nodes[id].bareStateId = opts.bareStateId;
|
|
1741
|
+
if (opts.frameId !== undefined)
|
|
1742
|
+
nodes[id].frameId = opts.frameId;
|
|
1241
1743
|
}
|
|
1242
1744
|
return nodes[id];
|
|
1243
1745
|
};
|
|
1244
|
-
// First pass:
|
|
1746
|
+
// First pass: nodes + alphabets (track subgraph context for frameId).
|
|
1245
1747
|
for (const line of lines) {
|
|
1246
|
-
if (line === 'flowchart TD')
|
|
1748
|
+
if (line === 'flowchart TD')
|
|
1247
1749
|
continue;
|
|
1248
|
-
}
|
|
1249
1750
|
const am = line.match(alphabetsRegex);
|
|
1250
1751
|
if (am) {
|
|
1251
1752
|
alphabets = JSON.parse(am[1]);
|
|
1252
1753
|
continue;
|
|
1253
1754
|
}
|
|
1755
|
+
const sgStart = line.match(subgraphStartRegex);
|
|
1756
|
+
if (sgStart) {
|
|
1757
|
+
currentFrameId = Number(sgStart[1]);
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (subgraphEndRegex.test(line)) {
|
|
1761
|
+
currentFrameId = null;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
if (idleNodeRegex.test(line))
|
|
1765
|
+
continue;
|
|
1254
1766
|
const hm = line.match(haltNodeRegex);
|
|
1255
1767
|
if (hm) {
|
|
1256
|
-
|
|
1768
|
+
const id = parseMermaidId(hm[1]);
|
|
1769
|
+
const isHaltMarker = currentFrameId !== null;
|
|
1770
|
+
ensureNode(id, {
|
|
1771
|
+
name: 'halt',
|
|
1772
|
+
isHalt: true,
|
|
1773
|
+
isHaltMarker,
|
|
1774
|
+
frameId: isHaltMarker ? currentFrameId : null,
|
|
1775
|
+
});
|
|
1257
1776
|
continue;
|
|
1258
1777
|
}
|
|
1259
|
-
const
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1778
|
+
const wm = line.match(wrappedNodeRegex);
|
|
1779
|
+
if (wm) {
|
|
1780
|
+
ensureNode(parseMermaidId(wm[1]), {
|
|
1781
|
+
name: wm[2],
|
|
1782
|
+
isWrapper: true,
|
|
1783
|
+
});
|
|
1264
1784
|
continue;
|
|
1265
1785
|
}
|
|
1266
1786
|
const rm = line.match(regularNodeRegex);
|
|
1267
1787
|
if (rm) {
|
|
1268
|
-
ensureNode(
|
|
1788
|
+
ensureNode(parseMermaidId(rm[1]), {
|
|
1789
|
+
name: rm[2],
|
|
1790
|
+
frameId: currentFrameId,
|
|
1791
|
+
});
|
|
1269
1792
|
continue;
|
|
1270
1793
|
}
|
|
1271
1794
|
}
|
|
1272
1795
|
// Second pass: edges.
|
|
1273
1796
|
for (const line of lines) {
|
|
1274
|
-
const
|
|
1275
|
-
if (
|
|
1276
|
-
|
|
1797
|
+
const em = line.match(enterArrowRegex);
|
|
1798
|
+
if (em) {
|
|
1799
|
+
initialId = parseMermaidId(em[1]);
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
// Return/halt arrows are derivable from frame structure at the next
|
|
1803
|
+
// toMermaid emit; consume but don't persist as graph data.
|
|
1804
|
+
if (returnArrowRegex.test(line) || haltArrowRegex.test(line)) {
|
|
1277
1805
|
continue;
|
|
1278
1806
|
}
|
|
1279
|
-
|
|
1807
|
+
// `call` arrow — sets bareStateId on each source wrapper.
|
|
1808
|
+
const cm = line.match(callArrowRegex);
|
|
1809
|
+
if (cm) {
|
|
1810
|
+
const sources = cm[1].split(' & ');
|
|
1811
|
+
const bareId = parseMermaidId(cm[2]);
|
|
1812
|
+
for (const src of sources) {
|
|
1813
|
+
ensureNode(parseMermaidId(src), { isWrapper: true, bareStateId: bareId });
|
|
1814
|
+
}
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
// Wrapper → override (unlabeled solid `-->`). Only fires if the source
|
|
1818
|
+
// node is a known wrapper (declared as `[[…]]`).
|
|
1819
|
+
const wo = line.match(wrapperOverrideRegex);
|
|
1820
|
+
if (wo) {
|
|
1821
|
+
const fromId = parseMermaidId(wo[1]);
|
|
1822
|
+
const toId = parseMermaidId(wo[2]);
|
|
1823
|
+
if (nodes[fromId] && nodes[fromId].isWrapper) {
|
|
1824
|
+
nodes[fromId].overriddenHaltStateId = toId;
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
// Fall through — unlabeled solid from a non-wrapper is unexpected;
|
|
1828
|
+
// treated as a malformed line and ignored by the labeled-regex below.
|
|
1829
|
+
}
|
|
1830
|
+
const tm = line.match(labeledTransitionRegex);
|
|
1280
1831
|
if (tm) {
|
|
1281
|
-
const fromId =
|
|
1832
|
+
const fromId = parseMermaidId(tm[1]);
|
|
1282
1833
|
const label = tm[2];
|
|
1283
|
-
const toId =
|
|
1834
|
+
const toId = parseMermaidId(tm[3]);
|
|
1284
1835
|
const arrowIx = label.indexOf(' → ');
|
|
1285
1836
|
if (arrowIx === -1) {
|
|
1286
1837
|
throw new Error(`fromMermaid: malformed edge label: "${label}"`);
|
|
1287
1838
|
}
|
|
1288
|
-
const
|
|
1289
|
-
const
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
throw new Error(`fromMermaid: malformed command part: "${part}"`);
|
|
1839
|
+
const readLabel = label.slice(0, arrowIx);
|
|
1840
|
+
const cmdLabel = label.slice(arrowIx + ' → '.length);
|
|
1841
|
+
const stripBrackets = (s) => {
|
|
1842
|
+
if (!s.startsWith('[') || !s.endsWith(']')) {
|
|
1843
|
+
throw new Error(`fromMermaid: malformed bracketed list: "${s}"`);
|
|
1294
1844
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1845
|
+
const inner = s.slice(1, -1);
|
|
1846
|
+
let i = 0;
|
|
1847
|
+
while (i < inner.length) {
|
|
1848
|
+
if (inner[i] === '\\' && i + 1 < inner.length) {
|
|
1849
|
+
i += 2;
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
if (inner[i] === '|') {
|
|
1853
|
+
throw new Error(`fromMermaid: compact in-bracket alternation "${s}" is not supported — `
|
|
1854
|
+
+ 'each alternative must be its own bracketed pattern (e.g. "[\'^\']|[\'1\']").');
|
|
1855
|
+
}
|
|
1856
|
+
i += 1;
|
|
1857
|
+
}
|
|
1858
|
+
return inner;
|
|
1859
|
+
};
|
|
1860
|
+
const blockMatches = readLabel.match(/\[[^\]]*\]/g);
|
|
1861
|
+
if (!blockMatches || blockMatches.length === 0) {
|
|
1862
|
+
throw new Error(`fromMermaid: no bracketed read-list in label: "${label}"`);
|
|
1863
|
+
}
|
|
1864
|
+
const pattern = blockMatches.map(stripBrackets).join('|');
|
|
1865
|
+
const slashIx = cmdLabel.indexOf(']/[');
|
|
1866
|
+
if (slashIx === -1) {
|
|
1867
|
+
throw new Error(`fromMermaid: malformed command label (expected \`[…]/[…]\`): "${cmdLabel}"`);
|
|
1868
|
+
}
|
|
1869
|
+
const writesPart = stripBrackets(cmdLabel.slice(0, slashIx + 1));
|
|
1870
|
+
const movesPart = stripBrackets(cmdLabel.slice(slashIx + 2));
|
|
1871
|
+
const writes = writesPart.split(',');
|
|
1872
|
+
const moves = movesPart.split(',');
|
|
1873
|
+
if (writes.length !== moves.length) {
|
|
1874
|
+
throw new Error(`fromMermaid: write-cells (${writes.length}) and move-cells (${moves.length}) mismatch: "${cmdLabel}"`);
|
|
1875
|
+
}
|
|
1876
|
+
const command = writes.map((symbol, i) => ({ symbol, movement: moves[i] }));
|
|
1877
|
+
const fromNode = ensureNode(fromId);
|
|
1878
|
+
const transitionIx = fromNode.transitions.length;
|
|
1879
|
+
fromNode.transitions.push({
|
|
1880
|
+
pattern,
|
|
1881
|
+
command,
|
|
1882
|
+
nextStateId: toId,
|
|
1883
|
+
id: `${fromId}-${transitionIx}`,
|
|
1299
1884
|
});
|
|
1300
|
-
ensureNode(fromId).transitions.push({ pattern, command, nextStateId: toId });
|
|
1301
1885
|
}
|
|
1302
1886
|
}
|
|
1303
1887
|
if (initialId === null) {
|
|
1304
|
-
throw new Error('fromMermaid: no
|
|
1888
|
+
throw new Error('fromMermaid: no `idle -. enter .-> sN` arrow found');
|
|
1305
1889
|
}
|
|
1306
1890
|
return { initialId, alphabets, nodes };
|
|
1307
1891
|
}
|
|
1308
1892
|
|
|
1309
1893
|
function summarizeGraph(graph) {
|
|
1310
1894
|
const nodes = Object.values(graph.nodes);
|
|
1895
|
+
// `isHaltMarker` nodes are visualization sentinels — one per wrapper context,
|
|
1896
|
+
// all corresponding to the singleton `haltState` at runtime. They don't
|
|
1897
|
+
// count as distinct runtime states; matches the per-algorithm header in
|
|
1898
|
+
// `library-binary-numbers/states.md`.
|
|
1899
|
+
const runtimeStateCount = nodes.filter((n) => !n.isHaltMarker).length;
|
|
1311
1900
|
let transitionCount = 0;
|
|
1312
1901
|
let compositionEdgeCount = 0;
|
|
1313
1902
|
let selfLoopCount = 0;
|
|
1314
1903
|
for (const node of nodes) {
|
|
1315
1904
|
transitionCount += node.transitions.length;
|
|
1316
|
-
if (node.
|
|
1905
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1317
1906
|
compositionEdgeCount += 1;
|
|
1318
1907
|
}
|
|
1319
1908
|
for (const t of node.transitions) {
|
|
@@ -1322,7 +1911,7 @@ function summarizeGraph(graph) {
|
|
|
1322
1911
|
}
|
|
1323
1912
|
}
|
|
1324
1913
|
}
|
|
1325
|
-
// Longest
|
|
1914
|
+
// Longest withOverriddenHaltState chain. Walks node → overriddenHaltState recursively;
|
|
1326
1915
|
// a Set guards against cycles in the override graph (which throw at construction
|
|
1327
1916
|
// time anyway, but being defensive costs little).
|
|
1328
1917
|
const overrideDepthFrom = (id, visited) => {
|
|
@@ -1331,10 +1920,10 @@ function summarizeGraph(graph) {
|
|
|
1331
1920
|
}
|
|
1332
1921
|
visited.add(id);
|
|
1333
1922
|
const node = graph.nodes[id];
|
|
1334
|
-
if (!node || node.
|
|
1923
|
+
if (!node || node.overriddenHaltStateId === null) {
|
|
1335
1924
|
return 0;
|
|
1336
1925
|
}
|
|
1337
|
-
return 1 + overrideDepthFrom(node.
|
|
1926
|
+
return 1 + overrideDepthFrom(node.overriddenHaltStateId, visited);
|
|
1338
1927
|
};
|
|
1339
1928
|
const maxCompositionDepth = nodes.reduce((max, node) => Math.max(max, overrideDepthFrom(node.id, new Set())), 0);
|
|
1340
1929
|
// Cycle detection: tri-color DFS over the transition graph.
|
|
@@ -1377,7 +1966,7 @@ function summarizeGraph(graph) {
|
|
|
1377
1966
|
visit(node.id);
|
|
1378
1967
|
}
|
|
1379
1968
|
return {
|
|
1380
|
-
stateCount:
|
|
1969
|
+
stateCount: runtimeStateCount,
|
|
1381
1970
|
transitionCount,
|
|
1382
1971
|
compositionEdgeCount,
|
|
1383
1972
|
maxCompositionDepth,
|