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