@turing-machine-js/machine 6.4.0 → 7.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +130 -95
- package/dist/classes/State.d.ts +3 -3
- package/dist/index.cjs +490 -106
- package/dist/index.mjs +490 -106
- package/dist/utilities/graph.d.ts +4 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -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, _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.
|
|
@@ -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,17 @@ 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
|
-
|
|
882
|
+
withOverriddenHaltState(overriddenHaltState) {
|
|
883
|
+
// Construct with no name, then overwrite #name directly — the composed
|
|
884
|
+
// name contains `(` and `)` by design, which the constructor's user-facing
|
|
885
|
+
// validation would reject. Internal composition bypasses validation via
|
|
886
|
+
// private-field access (legal within the same class).
|
|
887
|
+
const state = new State();
|
|
888
|
+
__classPrivateFieldSet$1(state, _State_name, `${this.name}(${overriddenHaltState.name})`, "f");
|
|
853
889
|
__classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f"), "f");
|
|
854
|
-
__classPrivateFieldSet$1(state,
|
|
890
|
+
__classPrivateFieldSet$1(state, _State_overriddenHaltState, overriddenHaltState, "f");
|
|
855
891
|
__classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(this, _State_debugRef, "f"), "f");
|
|
892
|
+
__classPrivateFieldSet$1(state, _State_bareState, this, "f");
|
|
856
893
|
return state;
|
|
857
894
|
}
|
|
858
895
|
// Single-state introspection — no traversal, no tapeBlock required.
|
|
@@ -883,52 +920,196 @@ class State {
|
|
|
883
920
|
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
884
921
|
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
885
922
|
isHalt: state.isHalt,
|
|
886
|
-
|
|
887
|
-
? { id: __classPrivateFieldGet$1(state,
|
|
923
|
+
overriddenHaltState: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f")
|
|
924
|
+
? { id: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f").id, name: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f").name }
|
|
888
925
|
: null,
|
|
889
926
|
transitions,
|
|
890
927
|
};
|
|
891
928
|
}
|
|
929
|
+
// Walks the State graph and emits a `Graph` data structure. v7 emit shape:
|
|
930
|
+
// wrapper-States (those with `#overriddenHaltState !== null`) are collapsed
|
|
931
|
+
// onto their bare's representation in the graph, with the wrapper's own `#id`
|
|
932
|
+
// used as the graph node id, `isWrapped: true`, and `overriddenHaltStateId`
|
|
933
|
+
// set to the override's collapsed id. A per-wrapper "halt marker" graph node
|
|
934
|
+
// (id = negative-of-the-wrapper-id, `isHalt: true, isHaltMarker: true`) is
|
|
935
|
+
// synthesized; the bare's halt-bound transitions are rewritten to target the
|
|
936
|
+
// halt marker instead of the real one.
|
|
937
|
+
//
|
|
938
|
+
// Halt-marker node ids use the negation of the wrapper's id so they sit in a
|
|
939
|
+
// disjoint integer range from real ids (which are always non-negative). Real
|
|
940
|
+
// halt is always id 0.
|
|
892
941
|
static toGraph(initialState, tapeBlock) {
|
|
893
942
|
const nodes = {};
|
|
894
|
-
const queue = [initialState];
|
|
895
943
|
const alphabets = tapeBlock.alphabets.map((alphabet) => alphabet.symbols);
|
|
944
|
+
// Map from a wrapper-State to the "collapsed" graph node id used to refer
|
|
945
|
+
// to it in transitions. Same as the wrapper's `#id`, recorded for clarity
|
|
946
|
+
// when rewriting transition targets.
|
|
947
|
+
const wrapperGraphId = (s) => __classPrivateFieldGet$1(s, _State_id, "f");
|
|
948
|
+
const haltMarkerIdFor = (wrapper) => -__classPrivateFieldGet$1(wrapper, _State_id, "f");
|
|
949
|
+
// The `initialId` is the user-passed start. If it's a wrapper, the
|
|
950
|
+
// collapsed graph node uses its `#id`; otherwise its own `#id`.
|
|
951
|
+
const initialId = __classPrivateFieldGet$1(initialState, _State_id, "f");
|
|
952
|
+
const queue = [];
|
|
953
|
+
// Decide how to enqueue the start: if it's a wrapper, enqueue its bare with
|
|
954
|
+
// the wrapper as context; otherwise enqueue the state itself.
|
|
955
|
+
if (__classPrivateFieldGet$1(initialState, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(initialState, _State_bareState, "f")) {
|
|
956
|
+
queue.push({ state: __classPrivateFieldGet$1(initialState, _State_bareState, "f"), wrapperContext: initialState });
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
queue.push({ state: initialState, wrapperContext: null });
|
|
960
|
+
}
|
|
896
961
|
while (queue.length > 0) {
|
|
897
|
-
const
|
|
898
|
-
if (
|
|
962
|
+
const { state, wrapperContext } = queue.shift();
|
|
963
|
+
if (state.isHalt) {
|
|
964
|
+
// Real halt — always id 0, single node.
|
|
965
|
+
if (!(0 in nodes)) {
|
|
966
|
+
nodes[0] = {
|
|
967
|
+
id: 0,
|
|
968
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
969
|
+
isHalt: true,
|
|
970
|
+
isHaltMarker: false,
|
|
971
|
+
isWrapped: false,
|
|
972
|
+
transitions: [],
|
|
973
|
+
overriddenHaltStateId: null,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (wrapperContext !== null) {
|
|
979
|
+
// Process `state` (the bare) collapsed under `wrapperContext` (the
|
|
980
|
+
// wrapper). Graph node id = wrapper's id.
|
|
981
|
+
const collapsedId = wrapperGraphId(wrapperContext);
|
|
982
|
+
if (collapsedId in nodes) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const haltMarkerId = haltMarkerIdFor(wrapperContext);
|
|
986
|
+
const overrideTarget = __classPrivateFieldGet$1(wrapperContext, _State_overriddenHaltState, "f");
|
|
987
|
+
// The override target's collapsed id: if the override is itself a
|
|
988
|
+
// wrapper, its graph node id is `overrideTarget.#id` (its own wrapper
|
|
989
|
+
// id); otherwise its own bare id.
|
|
990
|
+
const overrideGraphId = __classPrivateFieldGet$1(overrideTarget, _State_overriddenHaltState, "f")
|
|
991
|
+
? wrapperGraphId(overrideTarget)
|
|
992
|
+
: __classPrivateFieldGet$1(overrideTarget, _State_id, "f");
|
|
993
|
+
// Emit the halt-marker node if not already present (one per wrapper).
|
|
994
|
+
if (!(haltMarkerId in nodes)) {
|
|
995
|
+
nodes[haltMarkerId] = {
|
|
996
|
+
id: haltMarkerId,
|
|
997
|
+
name: 'halt',
|
|
998
|
+
isHalt: true,
|
|
999
|
+
isHaltMarker: true,
|
|
1000
|
+
isWrapped: false,
|
|
1001
|
+
transitions: [],
|
|
1002
|
+
overriddenHaltStateId: null,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
// Build the collapsed node.
|
|
1006
|
+
const collapsedNode = {
|
|
1007
|
+
id: collapsedId,
|
|
1008
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1009
|
+
isHalt: false,
|
|
1010
|
+
isHaltMarker: false,
|
|
1011
|
+
isWrapped: true,
|
|
1012
|
+
transitions: [],
|
|
1013
|
+
overriddenHaltStateId: overrideGraphId,
|
|
1014
|
+
};
|
|
1015
|
+
nodes[collapsedId] = collapsedNode;
|
|
1016
|
+
let patternIx = 0;
|
|
1017
|
+
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
1018
|
+
let target;
|
|
1019
|
+
try {
|
|
1020
|
+
target = nextState instanceof State ? nextState : nextState.ref;
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
patternIx += 1;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
// Retarget transitions per Variant X conventions:
|
|
1027
|
+
// - target == haltState → halt marker (stays inside the subgraph)
|
|
1028
|
+
// - target == bare (self-loop) → the collapsed wrapper id
|
|
1029
|
+
// - target is itself a wrapper → that wrapper's collapsed id
|
|
1030
|
+
// - else → target's own id
|
|
1031
|
+
let nextStateId;
|
|
1032
|
+
if (target.isHalt) {
|
|
1033
|
+
nextStateId = haltMarkerId;
|
|
1034
|
+
}
|
|
1035
|
+
else if (target === state) {
|
|
1036
|
+
nextStateId = collapsedId;
|
|
1037
|
+
}
|
|
1038
|
+
else if (__classPrivateFieldGet$1(target, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(target, _State_bareState, "f")) {
|
|
1039
|
+
nextStateId = wrapperGraphId(target);
|
|
1040
|
+
queue.push({ state: __classPrivateFieldGet$1(target, _State_bareState, "f"), wrapperContext: target });
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
nextStateId = __classPrivateFieldGet$1(target, _State_id, "f");
|
|
1044
|
+
queue.push({ state: target, wrapperContext: null });
|
|
1045
|
+
}
|
|
1046
|
+
collapsedNode.transitions.push({
|
|
1047
|
+
pattern: decodePatternDescription(sym.description, alphabets),
|
|
1048
|
+
command: command.tapesCommands.map((tc) => ({
|
|
1049
|
+
symbol: decodeWriteSymbol(tc.symbol),
|
|
1050
|
+
movement: decodeMovement(tc.movement.description),
|
|
1051
|
+
})),
|
|
1052
|
+
nextStateId,
|
|
1053
|
+
id: `${collapsedId}-${patternIx}`,
|
|
1054
|
+
});
|
|
1055
|
+
patternIx += 1;
|
|
1056
|
+
}
|
|
1057
|
+
// Enqueue the override target so its own node is emitted.
|
|
1058
|
+
if (__classPrivateFieldGet$1(overrideTarget, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(overrideTarget, _State_bareState, "f")) {
|
|
1059
|
+
queue.push({ state: __classPrivateFieldGet$1(overrideTarget, _State_bareState, "f"), wrapperContext: overrideTarget });
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
queue.push({ state: overrideTarget, wrapperContext: null });
|
|
1063
|
+
}
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
// Non-wrapper context: emit `state` as a regular node.
|
|
1067
|
+
if (__classPrivateFieldGet$1(state, _State_id, "f") in nodes) {
|
|
899
1068
|
continue;
|
|
900
1069
|
}
|
|
901
1070
|
const node = {
|
|
902
|
-
id: __classPrivateFieldGet$1(
|
|
903
|
-
name: __classPrivateFieldGet$1(
|
|
904
|
-
isHalt:
|
|
1071
|
+
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1072
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1073
|
+
isHalt: false,
|
|
1074
|
+
isHaltMarker: false,
|
|
1075
|
+
isWrapped: false,
|
|
905
1076
|
transitions: [],
|
|
906
|
-
|
|
1077
|
+
overriddenHaltStateId: null,
|
|
907
1078
|
};
|
|
908
|
-
nodes[__classPrivateFieldGet$1(
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(current, _State_symbolToDataMap, "f")) {
|
|
1079
|
+
nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = node;
|
|
1080
|
+
let patternIx = 0;
|
|
1081
|
+
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
913
1082
|
let target;
|
|
914
1083
|
try {
|
|
915
1084
|
target = nextState instanceof State ? nextState : nextState.ref;
|
|
916
1085
|
}
|
|
917
1086
|
catch {
|
|
1087
|
+
patternIx += 1;
|
|
918
1088
|
continue;
|
|
919
1089
|
}
|
|
1090
|
+
let nextStateId;
|
|
1091
|
+
if (__classPrivateFieldGet$1(target, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(target, _State_bareState, "f")) {
|
|
1092
|
+
// Transition into a wrapper — use its collapsed id.
|
|
1093
|
+
nextStateId = wrapperGraphId(target);
|
|
1094
|
+
queue.push({ state: __classPrivateFieldGet$1(target, _State_bareState, "f"), wrapperContext: target });
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
nextStateId = __classPrivateFieldGet$1(target, _State_id, "f");
|
|
1098
|
+
queue.push({ state: target, wrapperContext: null });
|
|
1099
|
+
}
|
|
920
1100
|
node.transitions.push({
|
|
921
1101
|
pattern: decodePatternDescription(sym.description, alphabets),
|
|
922
1102
|
command: command.tapesCommands.map((tc) => ({
|
|
923
1103
|
symbol: decodeWriteSymbol(tc.symbol),
|
|
924
1104
|
movement: decodeMovement(tc.movement.description),
|
|
925
1105
|
})),
|
|
926
|
-
nextStateId
|
|
1106
|
+
nextStateId,
|
|
1107
|
+
id: `${__classPrivateFieldGet$1(state, _State_id, "f")}-${patternIx}`,
|
|
927
1108
|
});
|
|
928
|
-
|
|
1109
|
+
patternIx += 1;
|
|
929
1110
|
}
|
|
930
1111
|
}
|
|
931
|
-
return { initialId
|
|
1112
|
+
return { initialId, alphabets, nodes };
|
|
932
1113
|
}
|
|
933
1114
|
// Inverse of toGraph: rebuilds a State graph (and a fresh TapeBlock with the
|
|
934
1115
|
// graph's alphabets) from a serialized Graph. Round-trips with toGraph in
|
|
@@ -980,7 +1161,12 @@ class State {
|
|
|
980
1161
|
nextState,
|
|
981
1162
|
};
|
|
982
1163
|
}
|
|
983
|
-
|
|
1164
|
+
// Graph-sourced names may contain `(` and `)` (composite wrapper names
|
|
1165
|
+
// emitted by toGraph). Bypass the constructor's user-facing name
|
|
1166
|
+
// validation by constructing without a name and assigning #name directly.
|
|
1167
|
+
const bare = new State(stateDefinition);
|
|
1168
|
+
__classPrivateFieldSet$1(bare, _State_name, node.name, "f");
|
|
1169
|
+
bareStates[nodeId] = bare;
|
|
984
1170
|
}
|
|
985
1171
|
// Pass 3: apply overrideHaltStates transitively.
|
|
986
1172
|
const finalStates = {};
|
|
@@ -999,8 +1185,8 @@ class State {
|
|
|
999
1185
|
}
|
|
1000
1186
|
inProgress.add(nodeId);
|
|
1001
1187
|
let state = bareStates[nodeId];
|
|
1002
|
-
if (node.
|
|
1003
|
-
state = bareStates[nodeId].
|
|
1188
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1189
|
+
state = bareStates[nodeId].withOverriddenHaltState(getFinal(node.overriddenHaltStateId));
|
|
1004
1190
|
}
|
|
1005
1191
|
inProgress.delete(nodeId);
|
|
1006
1192
|
finalStates[nodeId] = state;
|
|
@@ -1085,8 +1271,8 @@ class TuringMachine {
|
|
|
1085
1271
|
__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f")[lockSymbol].lock(executionSymbol);
|
|
1086
1272
|
const stack = __classPrivateFieldGet(this, _TuringMachine_stack, "f");
|
|
1087
1273
|
let state = initialState;
|
|
1088
|
-
if (state.
|
|
1089
|
-
stack.push(state.
|
|
1274
|
+
if (state.overriddenHaltState) {
|
|
1275
|
+
stack.push(state.overriddenHaltState);
|
|
1090
1276
|
}
|
|
1091
1277
|
let i = 0;
|
|
1092
1278
|
while (!state.isHalt) {
|
|
@@ -1140,8 +1326,8 @@ class TuringMachine {
|
|
|
1140
1326
|
if (nextState.isHalt && stack.length) {
|
|
1141
1327
|
nextState = stack.pop();
|
|
1142
1328
|
}
|
|
1143
|
-
if (state !== nextState && nextState.
|
|
1144
|
-
stack.push(nextState.
|
|
1329
|
+
if (state !== nextState && nextState.overriddenHaltState) {
|
|
1330
|
+
stack.push(nextState.overriddenHaltState);
|
|
1145
1331
|
}
|
|
1146
1332
|
state = nextState;
|
|
1147
1333
|
}
|
|
@@ -1165,32 +1351,131 @@ _TuringMachine_tapeBlock = new WeakMap(), _TuringMachine_stack = new WeakMap();
|
|
|
1165
1351
|
//
|
|
1166
1352
|
// Currently only Mermaid flowchart syntax is supported. Future formats
|
|
1167
1353
|
// (Graphviz, JSON-LD, custom DSL) belong here too.
|
|
1354
|
+
//
|
|
1355
|
+
// v7 emit shape (#138/#139):
|
|
1356
|
+
// - Each wrapper-State collapses onto its bare's representation. The collapsed
|
|
1357
|
+
// graph node has `isWrapped: true` and is emitted as Mermaid `[[…]]`
|
|
1358
|
+
// (subroutine / double-walled-rectangle) shape, inside a `subgraph
|
|
1359
|
+
// w_${id}["halt frame"] … end` block. A synthesized "halt marker" graph
|
|
1360
|
+
// node (with `isHalt: true, isHaltMarker: true`, id = -wrapperId in graph
|
|
1361
|
+
// data) sits inside the subgraph and serves as the local landing point for
|
|
1362
|
+
// the bare's halt-bound transitions. The dotted onHalt edge runs from the
|
|
1363
|
+
// `[[bare]]` directly to the override target, crossing the subgraph border.
|
|
1364
|
+
// - Real halt (id 0) is emitted as `s0(((halt)))` outside any subgraph.
|
|
1365
|
+
// - Halt marker nodes use the Mermaid id `c${absId}` (where `absId = -id`)
|
|
1366
|
+
// since Mermaid IDs must match /[A-Za-z][A-Za-z0-9_]*/ — negative numbers
|
|
1367
|
+
// are not legal syntax.
|
|
1368
|
+
// Maps a graph node id to its Mermaid id.
|
|
1369
|
+
// - non-negative id N → "sN"
|
|
1370
|
+
// - negative id -N (halt marker) → "cN"
|
|
1371
|
+
function mermaidIdFor(id) {
|
|
1372
|
+
return id < 0 ? `c${-id}` : `s${id}`;
|
|
1373
|
+
}
|
|
1374
|
+
// Inverse of mermaidIdFor.
|
|
1375
|
+
function parseMermaidId(s) {
|
|
1376
|
+
if (s.startsWith('c')) {
|
|
1377
|
+
return -Number(s.slice(1));
|
|
1378
|
+
}
|
|
1379
|
+
return Number(s.slice(1));
|
|
1380
|
+
}
|
|
1168
1381
|
function toMermaid(graph) {
|
|
1169
1382
|
const lines = [
|
|
1170
1383
|
'flowchart TD',
|
|
1171
1384
|
`%% alphabets: ${JSON.stringify(graph.alphabets)}`,
|
|
1172
1385
|
];
|
|
1173
|
-
|
|
1174
|
-
|
|
1386
|
+
// Sort nodes by id (ascending — real halt first at 0, regular states next,
|
|
1387
|
+
// negative-id halt markers last). Deterministic emit lets `toMermaid` →
|
|
1388
|
+
// `fromMermaid` → `toMermaid` round-trip stably (regression for #139).
|
|
1389
|
+
const nodes = Object.values(graph.nodes).slice().sort((a, b) => a.id - b.id);
|
|
1390
|
+
const wrappedNodes = nodes.filter((n) => n.isWrapped);
|
|
1391
|
+
// Convention: wrapped node id N → halt marker id -N.
|
|
1392
|
+
const haltMarkerIdFor = (wrappedId) => -wrappedId;
|
|
1393
|
+
// Set of halt-marker ids that belong to some wrapper (= are inside a subgraph).
|
|
1394
|
+
const haltMarkerIds = new Set();
|
|
1395
|
+
for (const w of wrappedNodes) {
|
|
1396
|
+
const haltMarkerId = haltMarkerIdFor(w.id);
|
|
1397
|
+
if (haltMarkerId in graph.nodes) {
|
|
1398
|
+
haltMarkerIds.add(haltMarkerId);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
// Emit non-subgraph nodes first: real halt + regular non-wrapped nodes.
|
|
1402
|
+
// No special round-shape `((…))` for the initial — the `idle -. enter .->`
|
|
1403
|
+
// arrow emitted below is the sole "start here" signal.
|
|
1404
|
+
for (const node of nodes) {
|
|
1405
|
+
if (node.isWrapped || haltMarkerIds.has(node.id)) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
const id = mermaidIdFor(node.id);
|
|
1175
1409
|
if (node.isHalt) {
|
|
1176
1410
|
lines.push(` ${id}(((halt)))`);
|
|
1177
1411
|
}
|
|
1178
|
-
else if (node.id === graph.initialId) {
|
|
1179
|
-
lines.push(` ${id}(("${node.name}"))`);
|
|
1180
|
-
}
|
|
1181
1412
|
else {
|
|
1182
1413
|
lines.push(` ${id}["${node.name}"]`);
|
|
1183
1414
|
}
|
|
1184
1415
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1416
|
+
// `idle` sentinel = pre-execution marker for the machine. Always emitted,
|
|
1417
|
+
// with a labeled dotted arrow `idle -. enter .-> sN` to the initial state.
|
|
1418
|
+
// Symmetric with the `onHalt` dotted convention used by wrapper redirects.
|
|
1419
|
+
// Visual-only — `idle` is not a graph node.
|
|
1420
|
+
lines.push(' idle([idle])');
|
|
1421
|
+
// Emit one subgraph per wrapper, in sorted wrapped-id order.
|
|
1422
|
+
for (const wrapped of wrappedNodes) {
|
|
1423
|
+
const wrappedMid = mermaidIdFor(wrapped.id);
|
|
1424
|
+
const haltMarkerId = haltMarkerIdFor(wrapped.id);
|
|
1425
|
+
const haltMarkerMid = mermaidIdFor(haltMarkerId);
|
|
1426
|
+
lines.push(` subgraph w_${wrapped.id}["halt frame"]`);
|
|
1427
|
+
lines.push(` ${wrappedMid}[["${wrapped.name}"]]`);
|
|
1428
|
+
if (haltMarkerId in graph.nodes) {
|
|
1429
|
+
lines.push(` ${haltMarkerMid}(((halt)))`);
|
|
1430
|
+
}
|
|
1431
|
+
lines.push(' end');
|
|
1432
|
+
}
|
|
1433
|
+
// Enter arrow: emitted after subgraphs so it visually points at the initial
|
|
1434
|
+
// node (whether plain `[…]` or wrapped `[[…]]` inside a subgraph).
|
|
1435
|
+
lines.push(` idle -. enter .-> ${mermaidIdFor(graph.initialId)}`);
|
|
1436
|
+
// Emit transitions per-node in sorted node-id order. Within a node,
|
|
1437
|
+
// transitions emit in their stored array order (which mirrors the source
|
|
1438
|
+
// state's symbol-map insertion order — stable per State instance).
|
|
1439
|
+
for (const node of nodes) {
|
|
1440
|
+
if (node.isHalt && !node.isHaltMarker) {
|
|
1441
|
+
continue;
|
|
1191
1442
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1443
|
+
for (const t of node.transitions) {
|
|
1444
|
+
// Bracketed-tape-block format (v7): each role-list — read alternatives,
|
|
1445
|
+
// writes, movements — wraps in `[…]` to mark "this is a tape-block
|
|
1446
|
+
// reading". Brackets stay even for single-tape machines; the `[…]` is
|
|
1447
|
+
// the tape-block concept indicator.
|
|
1448
|
+
//
|
|
1449
|
+
// Single-tape: ['X'] → [K]/[R]
|
|
1450
|
+
// Single-tape + alternation: ['^']|['1']|['0'] → [K]/[S]
|
|
1451
|
+
// Two-tape: ['0','a'] → [K,'1']/[R,S]
|
|
1452
|
+
// Two-tape + alternation: ['0','a']|['1','b'] → [K,K]/[R,L]
|
|
1453
|
+
//
|
|
1454
|
+
// Alternation is ALWAYS per-pattern-bracket — one full bracketed list
|
|
1455
|
+
// per alternative — regardless of tape count. Pedagogically each
|
|
1456
|
+
// alternative is its own drawn transition; a compact in-bracket form
|
|
1457
|
+
// (`['^'|'1']`) would read as cross-product semantics in multi-tape
|
|
1458
|
+
// (`['0'|'1','a'|'b']` = 4 combos, not 2 paired alternatives), so we
|
|
1459
|
+
// avoid introducing it for the single-tape case too.
|
|
1460
|
+
const alternatives = t.pattern.split('|');
|
|
1461
|
+
const reads = alternatives.map((alt) => `[${alt}]`).join('|');
|
|
1462
|
+
const writes = `[${t.command.map((c) => c.symbol).join(',')}]`;
|
|
1463
|
+
const moves = `[${t.command.map((c) => c.movement).join(',')}]`;
|
|
1464
|
+
const label = `${reads} → ${writes}/${moves}`;
|
|
1465
|
+
// Thicker `==>` arrow when the transition crosses INTO a wrapper —
|
|
1466
|
+
// signals "this transition pushes that wrapper's override onto the
|
|
1467
|
+
// runtime stack" (per `TuringMachine.run` line ~220's
|
|
1468
|
+
// `if (state !== nextState && nextState.overriddenHaltState) push(...)`).
|
|
1469
|
+
// Self-loops (state === nextState) don't push at runtime — keep the
|
|
1470
|
+
// regular `-->` for those even when the target is wrapped.
|
|
1471
|
+
const targetNode = graph.nodes[t.nextStateId];
|
|
1472
|
+
const isEnteringWrapper = targetNode && targetNode.isWrapped && t.nextStateId !== node.id;
|
|
1473
|
+
const lineSegment = isEnteringWrapper ? '==' : '--';
|
|
1474
|
+
const arrowTip = isEnteringWrapper ? '==>' : '-->';
|
|
1475
|
+
lines.push(` ${mermaidIdFor(node.id)} ${lineSegment} "${label}" ${arrowTip} ${mermaidIdFor(t.nextStateId)}`);
|
|
1476
|
+
}
|
|
1477
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1478
|
+
lines.push(` ${mermaidIdFor(node.id)} -. onHalt .-> ${mermaidIdFor(node.overriddenHaltStateId)}`);
|
|
1194
1479
|
}
|
|
1195
1480
|
}
|
|
1196
1481
|
return lines.join('\n');
|
|
@@ -1206,11 +1491,16 @@ function toMermaid(graph) {
|
|
|
1206
1491
|
// per-tape segments are split on ','. If your alphabet contains '/' or ','
|
|
1207
1492
|
// as literal symbols, the parser cannot disambiguate. Stick to alphabets
|
|
1208
1493
|
// without those characters when round-tripping through Mermaid.
|
|
1209
|
-
const haltNodeRegex = /^
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1494
|
+
const haltNodeRegex = /^([sc]\d+)\(\(\(halt\)\)\)$/;
|
|
1495
|
+
const regularNodeRegex = /^(s\d+)\["([^"]*)"\]$/;
|
|
1496
|
+
const wrappedNodeRegex = /^(s\d+)\[\["([^"]*)"\]\]$/;
|
|
1497
|
+
const subgraphStartRegex = /^subgraph\s+w_\d+\["([^"]*)"\]$/;
|
|
1498
|
+
const subgraphEndRegex = /^end$/;
|
|
1499
|
+
const idleNodeRegex = /^idle\(\[idle\]\)$/;
|
|
1500
|
+
const enterArrowRegex = /^idle\s+-\.\s+enter\s+\.->\s+(s\d+)$/;
|
|
1501
|
+
const transitionRegex = /^([sc]\d+)\s+--\s+"(.*)"\s+-->\s+([sc]\d+)$/;
|
|
1502
|
+
const thickTransitionRegex = /^([sc]\d+)\s+==\s+"(.*)"\s+==>\s+([sc]\d+)$/;
|
|
1503
|
+
const onHaltRegex = /^([sc]\d+)\s+-\.\s+onHalt\s+\.->\s+([sc]\d+)$/;
|
|
1214
1504
|
// First capture char anchored as \S to avoid polynomial backtracking between
|
|
1215
1505
|
// the preceding \s* and a permissive (.+); see CodeQL js/polynomial-redos.
|
|
1216
1506
|
const alphabetsRegex = /^%%\s*alphabets:\s*(\S.*)$/;
|
|
@@ -1219,27 +1509,36 @@ function fromMermaid(text) {
|
|
|
1219
1509
|
let alphabets = [];
|
|
1220
1510
|
let initialId = null;
|
|
1221
1511
|
const nodes = {};
|
|
1512
|
+
// Track the halt-marker ids that appeared inside a subgraph — they should be
|
|
1513
|
+
// marked `isHaltMarker: true` even though they share the `(((halt)))` shape
|
|
1514
|
+
// with the real halt at the top level.
|
|
1515
|
+
const haltMarkerIds = new Set();
|
|
1516
|
+
let inSubgraph = false;
|
|
1222
1517
|
const ensureNode = (id, opts = {}) => {
|
|
1223
1518
|
if (!nodes[id]) {
|
|
1224
1519
|
nodes[id] = {
|
|
1225
1520
|
id,
|
|
1226
|
-
name: opts.name ??
|
|
1521
|
+
name: opts.name ?? mermaidIdFor(id),
|
|
1227
1522
|
isHalt: opts.isHalt ?? false,
|
|
1523
|
+
isHaltMarker: opts.isHaltMarker ?? false,
|
|
1524
|
+
isWrapped: opts.isWrapped ?? false,
|
|
1228
1525
|
transitions: [],
|
|
1229
|
-
|
|
1526
|
+
overriddenHaltStateId: null,
|
|
1230
1527
|
};
|
|
1231
1528
|
}
|
|
1232
1529
|
else {
|
|
1233
|
-
if (opts.name !== undefined)
|
|
1530
|
+
if (opts.name !== undefined)
|
|
1234
1531
|
nodes[id].name = opts.name;
|
|
1235
|
-
|
|
1236
|
-
if (opts.isHalt !== undefined) {
|
|
1532
|
+
if (opts.isHalt !== undefined)
|
|
1237
1533
|
nodes[id].isHalt = opts.isHalt;
|
|
1238
|
-
|
|
1534
|
+
if (opts.isHaltMarker !== undefined)
|
|
1535
|
+
nodes[id].isHaltMarker = opts.isHaltMarker;
|
|
1536
|
+
if (opts.isWrapped !== undefined)
|
|
1537
|
+
nodes[id].isWrapped = opts.isWrapped;
|
|
1239
1538
|
}
|
|
1240
1539
|
return nodes[id];
|
|
1241
1540
|
};
|
|
1242
|
-
// First pass: alphabets + nodes.
|
|
1541
|
+
// First pass: alphabets + nodes (track subgraph context to mark halt markers).
|
|
1243
1542
|
for (const line of lines) {
|
|
1244
1543
|
if (line === 'flowchart TD') {
|
|
1245
1544
|
continue;
|
|
@@ -1249,69 +1548,154 @@ function fromMermaid(text) {
|
|
|
1249
1548
|
alphabets = JSON.parse(am[1]);
|
|
1250
1549
|
continue;
|
|
1251
1550
|
}
|
|
1551
|
+
if (subgraphStartRegex.test(line)) {
|
|
1552
|
+
inSubgraph = true;
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
if (subgraphEndRegex.test(line)) {
|
|
1556
|
+
inSubgraph = false;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
// `idle([idle])` sentinel: a visual pre-execution marker. Not a graph
|
|
1560
|
+
// node — skip declaration, parse the `idle -. enter .-> sN` arrow in the
|
|
1561
|
+
// edge pass to set initialId.
|
|
1562
|
+
if (idleNodeRegex.test(line)) {
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1252
1565
|
const hm = line.match(haltNodeRegex);
|
|
1253
1566
|
if (hm) {
|
|
1254
|
-
|
|
1567
|
+
const id = parseMermaidId(hm[1]);
|
|
1568
|
+
const isHaltMarker = inSubgraph || id < 0;
|
|
1569
|
+
ensureNode(id, { name: 'halt', isHalt: true, isHaltMarker });
|
|
1570
|
+
if (isHaltMarker) {
|
|
1571
|
+
haltMarkerIds.add(id);
|
|
1572
|
+
}
|
|
1255
1573
|
continue;
|
|
1256
1574
|
}
|
|
1257
|
-
const
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
1260
|
-
initialId = id;
|
|
1261
|
-
ensureNode(id, { name: im[2] });
|
|
1575
|
+
const wm = line.match(wrappedNodeRegex);
|
|
1576
|
+
if (wm) {
|
|
1577
|
+
ensureNode(parseMermaidId(wm[1]), { name: wm[2], isWrapped: true });
|
|
1262
1578
|
continue;
|
|
1263
1579
|
}
|
|
1264
1580
|
const rm = line.match(regularNodeRegex);
|
|
1265
1581
|
if (rm) {
|
|
1266
|
-
ensureNode(
|
|
1582
|
+
ensureNode(parseMermaidId(rm[1]), { name: rm[2] });
|
|
1267
1583
|
continue;
|
|
1268
1584
|
}
|
|
1269
1585
|
}
|
|
1270
1586
|
// Second pass: edges.
|
|
1271
1587
|
for (const line of lines) {
|
|
1588
|
+
// `idle -. enter .-> sN`: the sole source of initialId.
|
|
1589
|
+
const em = line.match(enterArrowRegex);
|
|
1590
|
+
if (em) {
|
|
1591
|
+
initialId = parseMermaidId(em[1]);
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1272
1594
|
const om = line.match(onHaltRegex);
|
|
1273
1595
|
if (om) {
|
|
1274
|
-
ensureNode(
|
|
1596
|
+
ensureNode(parseMermaidId(om[1])).overriddenHaltStateId = parseMermaidId(om[2]);
|
|
1275
1597
|
continue;
|
|
1276
1598
|
}
|
|
1277
|
-
|
|
1599
|
+
// Thick transition (`==> `) and regular transition (`-->`) share the same
|
|
1600
|
+
// semantics — only the visual differs. Parse both via the same code path.
|
|
1601
|
+
const tm = line.match(transitionRegex) ?? line.match(thickTransitionRegex);
|
|
1278
1602
|
if (tm) {
|
|
1279
|
-
const fromId =
|
|
1603
|
+
const fromId = parseMermaidId(tm[1]);
|
|
1280
1604
|
const label = tm[2];
|
|
1281
|
-
const toId =
|
|
1605
|
+
const toId = parseMermaidId(tm[3]);
|
|
1282
1606
|
const arrowIx = label.indexOf(' → ');
|
|
1283
1607
|
if (arrowIx === -1) {
|
|
1284
1608
|
throw new Error(`fromMermaid: malformed edge label: "${label}"`);
|
|
1285
1609
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1610
|
+
// Bracketed-tape-block format (v7):
|
|
1611
|
+
// [<read-cells>]|[<read-cells>]... → [<write-cells>]/[<move-cells>]
|
|
1612
|
+
// Each bracketed list is a tape-block reading; the outer `|` separates
|
|
1613
|
+
// alternative read patterns. For single-tape machines with alternation,
|
|
1614
|
+
// the compact form `[<alt1>|<alt2>|...]` (one bracket, alternatives
|
|
1615
|
+
// inside) is also accepted; both forms decode to the same pattern
|
|
1616
|
+
// string.
|
|
1617
|
+
const readLabel = label.slice(0, arrowIx);
|
|
1618
|
+
const cmdLabel = label.slice(arrowIx + ' → '.length);
|
|
1619
|
+
// Strict per-pattern bracket form: `|` only between bracketed lists,
|
|
1620
|
+
// never inside. The compact `['^'|'1']` form is rejected by design —
|
|
1621
|
+
// every alternative must be its own bracketed pattern (`['^']|['1']`).
|
|
1622
|
+
// Pedagogically: each transition is drawn explicitly; the compact form
|
|
1623
|
+
// would read as cross-product semantics in multi-tape and confuse
|
|
1624
|
+
// readers (`['0'|'1','a'|'b']` could mean 4 combos, not 2 paired alts).
|
|
1625
|
+
// The rule applies to all bracketed lists — read alternatives, writes,
|
|
1626
|
+
// and movements — because commands and movements have no alternation
|
|
1627
|
+
// semantic either.
|
|
1628
|
+
const stripBrackets = (s) => {
|
|
1629
|
+
if (!s.startsWith('[') || !s.endsWith(']')) {
|
|
1630
|
+
throw new Error(`fromMermaid: malformed bracketed list: "${s}"`);
|
|
1292
1631
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1632
|
+
const inner = s.slice(1, -1);
|
|
1633
|
+
// Walk the inner content; backslash escapes the next char (so `\|`
|
|
1634
|
+
// inside a cell is a literal pipe, not the alternation separator).
|
|
1635
|
+
let i = 0;
|
|
1636
|
+
while (i < inner.length) {
|
|
1637
|
+
if (inner[i] === '\\' && i + 1 < inner.length) {
|
|
1638
|
+
i += 2;
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
if (inner[i] === '|') {
|
|
1642
|
+
throw new Error(`fromMermaid: compact in-bracket alternation "${s}" is not supported — `
|
|
1643
|
+
+ 'each alternative must be its own bracketed pattern (e.g. "[\'^\']|[\'1\']").');
|
|
1644
|
+
}
|
|
1645
|
+
i += 1;
|
|
1646
|
+
}
|
|
1647
|
+
return inner;
|
|
1648
|
+
};
|
|
1649
|
+
// Match `[…]` blocks in the read label. Inner content is a tape-block
|
|
1650
|
+
// reading (possibly with `|` for compact single-tape alternation).
|
|
1651
|
+
// `[^\]]*` is the simple non-greedy match — works because cell content
|
|
1652
|
+
// doesn't typically contain literal `]`.
|
|
1653
|
+
const blockMatches = readLabel.match(/\[[^\]]*\]/g);
|
|
1654
|
+
if (!blockMatches || blockMatches.length === 0) {
|
|
1655
|
+
throw new Error(`fromMermaid: no bracketed read-list in label: "${label}"`);
|
|
1656
|
+
}
|
|
1657
|
+
const pattern = blockMatches.map(stripBrackets).join('|');
|
|
1658
|
+
const slashIx = cmdLabel.indexOf(']/[');
|
|
1659
|
+
if (slashIx === -1) {
|
|
1660
|
+
throw new Error(`fromMermaid: malformed command label (expected \`[…]/[…]\`): "${cmdLabel}"`);
|
|
1661
|
+
}
|
|
1662
|
+
const writesPart = stripBrackets(cmdLabel.slice(0, slashIx + 1));
|
|
1663
|
+
const movesPart = stripBrackets(cmdLabel.slice(slashIx + 2));
|
|
1664
|
+
const writes = writesPart.split(',');
|
|
1665
|
+
const moves = movesPart.split(',');
|
|
1666
|
+
if (writes.length !== moves.length) {
|
|
1667
|
+
throw new Error(`fromMermaid: write-cells (${writes.length}) and move-cells (${moves.length}) mismatch: "${cmdLabel}"`);
|
|
1668
|
+
}
|
|
1669
|
+
const command = writes.map((symbol, i) => ({ symbol, movement: moves[i] }));
|
|
1670
|
+
const fromNode = ensureNode(fromId);
|
|
1671
|
+
const transitionIx = fromNode.transitions.length;
|
|
1672
|
+
fromNode.transitions.push({
|
|
1673
|
+
pattern,
|
|
1674
|
+
command,
|
|
1675
|
+
nextStateId: toId,
|
|
1676
|
+
id: `${fromId}-${transitionIx}`,
|
|
1297
1677
|
});
|
|
1298
|
-
ensureNode(fromId).transitions.push({ pattern, command, nextStateId: toId });
|
|
1299
1678
|
}
|
|
1300
1679
|
}
|
|
1301
1680
|
if (initialId === null) {
|
|
1302
|
-
throw new Error('fromMermaid: no
|
|
1681
|
+
throw new Error('fromMermaid: no `idle -. enter .-> sN` arrow found');
|
|
1303
1682
|
}
|
|
1304
1683
|
return { initialId, alphabets, nodes };
|
|
1305
1684
|
}
|
|
1306
1685
|
|
|
1307
1686
|
function summarizeGraph(graph) {
|
|
1308
1687
|
const nodes = Object.values(graph.nodes);
|
|
1688
|
+
// `isHaltMarker` nodes are visualization sentinels — one per wrapper context,
|
|
1689
|
+
// all corresponding to the singleton `haltState` at runtime. They don't
|
|
1690
|
+
// count as distinct runtime states; matches the per-algorithm header in
|
|
1691
|
+
// `library-binary-numbers/states.md`.
|
|
1692
|
+
const runtimeStateCount = nodes.filter((n) => !n.isHaltMarker).length;
|
|
1309
1693
|
let transitionCount = 0;
|
|
1310
1694
|
let compositionEdgeCount = 0;
|
|
1311
1695
|
let selfLoopCount = 0;
|
|
1312
1696
|
for (const node of nodes) {
|
|
1313
1697
|
transitionCount += node.transitions.length;
|
|
1314
|
-
if (node.
|
|
1698
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1315
1699
|
compositionEdgeCount += 1;
|
|
1316
1700
|
}
|
|
1317
1701
|
for (const t of node.transitions) {
|
|
@@ -1320,7 +1704,7 @@ function summarizeGraph(graph) {
|
|
|
1320
1704
|
}
|
|
1321
1705
|
}
|
|
1322
1706
|
}
|
|
1323
|
-
// Longest
|
|
1707
|
+
// Longest withOverriddenHaltState chain. Walks node → overriddenHaltState recursively;
|
|
1324
1708
|
// a Set guards against cycles in the override graph (which throw at construction
|
|
1325
1709
|
// time anyway, but being defensive costs little).
|
|
1326
1710
|
const overrideDepthFrom = (id, visited) => {
|
|
@@ -1329,10 +1713,10 @@ function summarizeGraph(graph) {
|
|
|
1329
1713
|
}
|
|
1330
1714
|
visited.add(id);
|
|
1331
1715
|
const node = graph.nodes[id];
|
|
1332
|
-
if (!node || node.
|
|
1716
|
+
if (!node || node.overriddenHaltStateId === null) {
|
|
1333
1717
|
return 0;
|
|
1334
1718
|
}
|
|
1335
|
-
return 1 + overrideDepthFrom(node.
|
|
1719
|
+
return 1 + overrideDepthFrom(node.overriddenHaltStateId, visited);
|
|
1336
1720
|
};
|
|
1337
1721
|
const maxCompositionDepth = nodes.reduce((max, node) => Math.max(max, overrideDepthFrom(node.id, new Set())), 0);
|
|
1338
1722
|
// Cycle detection: tri-color DFS over the transition graph.
|
|
@@ -1375,7 +1759,7 @@ function summarizeGraph(graph) {
|
|
|
1375
1759
|
visit(node.id);
|
|
1376
1760
|
}
|
|
1377
1761
|
return {
|
|
1378
|
-
stateCount:
|
|
1762
|
+
stateCount: runtimeStateCount,
|
|
1379
1763
|
transitionCount,
|
|
1380
1764
|
compositionEdgeCount,
|
|
1381
1765
|
maxCompositionDepth,
|