@turing-machine-js/machine 6.3.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 +160 -0
- package/README.md +140 -111
- package/dist/classes/State.d.ts +3 -3
- package/dist/classes/TuringMachine.d.ts +21 -10
- package/dist/index.cjs +495 -108
- package/dist/index.mjs +495 -108
- package/dist/utilities/graph.d.ts +4 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -537,30 +537,40 @@ const movementDescriptionToLabel = {
|
|
|
537
537
|
'do not move carer': 'S',
|
|
538
538
|
};
|
|
539
539
|
const symbolCommandDescriptionToLabel = {
|
|
540
|
-
'keep symbol command': '
|
|
541
|
-
'erase symbol command': '
|
|
540
|
+
'keep symbol command': 'K',
|
|
541
|
+
'erase symbol command': 'E',
|
|
542
542
|
};
|
|
543
543
|
// Reserved characters in the encoded pattern string:
|
|
544
|
-
// '*' per-cell ifOtherSymbol
|
|
545
|
-
//
|
|
544
|
+
// '*' ASCII asterisk (U+002A) — per-cell ifOtherSymbol, matches any symbol
|
|
545
|
+
// on that tape. ASCII (not a fancier glyph like U+1F7B0) so it renders
|
|
546
|
+
// in every Mermaid environment and every monospace font. A literal `*`
|
|
547
|
+
// in the alphabet is unambiguous from the marker because it's quoted
|
|
548
|
+
// (`'*'`).
|
|
549
|
+
// 'B' the tape's blank symbol shorthand (in read patterns). A literal `B`
|
|
550
|
+
// in the alphabet is unambiguous from the marker because it's quoted
|
|
551
|
+
// (`'B'`).
|
|
546
552
|
// ',' separates per-tape cells inside one pattern
|
|
547
553
|
// '|' separates alternative patterns
|
|
548
|
-
// '
|
|
549
|
-
//
|
|
554
|
+
// "'" surrounds a literal alphabet symbol — e.g. `'0'` for literal `0`,
|
|
555
|
+
// `'X'` for literal `X`. The quoting is what visually separates literal
|
|
556
|
+
// symbols from the convention markers `*` / `B` and from the write
|
|
557
|
+
// commands `K` / `E`.
|
|
558
|
+
// '\\' escape prefix — to represent any of '*', 'B', ',', '|', "'", or '\\'
|
|
559
|
+
// as a *literal* alphabet symbol *inside* the quotes (e.g. `'\''` for
|
|
560
|
+
// a literal apostrophe).
|
|
561
|
+
const IF_OTHER_MARKER = '*';
|
|
562
|
+
const BLANK_MARKER = 'B';
|
|
550
563
|
function escapeAlphabetSymbol(s) {
|
|
551
564
|
return s
|
|
552
565
|
.replace(/\\/g, '\\\\')
|
|
553
|
-
.replace(
|
|
554
|
-
.replace(/-/g, '\\-')
|
|
555
|
-
.replace(/,/g, '\\,')
|
|
556
|
-
.replace(/\|/g, '\\|');
|
|
566
|
+
.replace(/'/g, "\\'");
|
|
557
567
|
}
|
|
558
568
|
function decodePatternDescription(description, alphabets) {
|
|
559
569
|
if (!description) {
|
|
560
570
|
return '?';
|
|
561
571
|
}
|
|
562
572
|
if (description === 'other symbol') {
|
|
563
|
-
return
|
|
573
|
+
return IF_OTHER_MARKER;
|
|
564
574
|
}
|
|
565
575
|
try {
|
|
566
576
|
const patternList = JSON.parse(description);
|
|
@@ -568,12 +578,12 @@ function decodePatternDescription(description, alphabets) {
|
|
|
568
578
|
.map((pattern) => pattern
|
|
569
579
|
.map((s, tapeIx) => {
|
|
570
580
|
if (s === null) {
|
|
571
|
-
return
|
|
581
|
+
return IF_OTHER_MARKER;
|
|
572
582
|
}
|
|
573
583
|
if (s === alphabets[tapeIx]?.[0]) {
|
|
574
|
-
return
|
|
584
|
+
return BLANK_MARKER;
|
|
575
585
|
}
|
|
576
|
-
return escapeAlphabetSymbol(s)
|
|
586
|
+
return `'${escapeAlphabetSymbol(s)}'`;
|
|
577
587
|
})
|
|
578
588
|
.join(','))
|
|
579
589
|
.join('|');
|
|
@@ -611,19 +621,24 @@ function splitUnescaped(s, sep) {
|
|
|
611
621
|
return parts;
|
|
612
622
|
}
|
|
613
623
|
function parsePatternString(s, alphabets) {
|
|
614
|
-
if (s ===
|
|
624
|
+
if (s === IF_OTHER_MARKER) {
|
|
615
625
|
return null;
|
|
616
626
|
}
|
|
617
627
|
const alternatives = splitUnescaped(s, '|');
|
|
618
628
|
return alternatives.map((alt) => {
|
|
619
629
|
const cells = splitUnescaped(alt, ',');
|
|
620
630
|
return cells.map((cell, tapeIx) => {
|
|
621
|
-
if (cell ===
|
|
631
|
+
if (cell === IF_OTHER_MARKER) {
|
|
622
632
|
return null;
|
|
623
633
|
}
|
|
624
|
-
if (cell ===
|
|
634
|
+
if (cell === BLANK_MARKER) {
|
|
625
635
|
return alphabets[tapeIx]?.[0] ?? cell;
|
|
626
636
|
}
|
|
637
|
+
// Literal alphabet symbols are wrapped in single quotes by
|
|
638
|
+
// `decodePatternDescription` — strip them on the way back.
|
|
639
|
+
if (cell.length >= 2 && cell.startsWith("'") && cell.endsWith("'")) {
|
|
640
|
+
return cell.slice(1, -1);
|
|
641
|
+
}
|
|
627
642
|
return cell;
|
|
628
643
|
});
|
|
629
644
|
});
|
|
@@ -641,12 +656,17 @@ function parseMovementLabel(label) {
|
|
|
641
656
|
return m;
|
|
642
657
|
}
|
|
643
658
|
function parseWriteSymbolLabel(label) {
|
|
644
|
-
if (label === '
|
|
659
|
+
if (label === 'K') {
|
|
645
660
|
return symbolCommands.keep;
|
|
646
661
|
}
|
|
647
|
-
if (label === '
|
|
662
|
+
if (label === 'E') {
|
|
648
663
|
return symbolCommands.erase;
|
|
649
664
|
}
|
|
665
|
+
// Literal alphabet symbols are wrapped in single quotes by
|
|
666
|
+
// `decodeWriteSymbol` — strip them on the way back.
|
|
667
|
+
if (label.length >= 2 && label.startsWith("'") && label.endsWith("'")) {
|
|
668
|
+
return label.slice(1, -1);
|
|
669
|
+
}
|
|
650
670
|
return label;
|
|
651
671
|
}
|
|
652
672
|
function decodeWriteSymbol(symbol) {
|
|
@@ -654,7 +674,7 @@ function decodeWriteSymbol(symbol) {
|
|
|
654
674
|
const description = symbol.description ?? '?';
|
|
655
675
|
return symbolCommandDescriptionToLabel[description] ?? description;
|
|
656
676
|
}
|
|
657
|
-
return symbol
|
|
677
|
+
return `'${symbol}'`;
|
|
658
678
|
}
|
|
659
679
|
// Format converters (toMermaid / fromMermaid) live in ./graphFormats.
|
|
660
680
|
|
|
@@ -669,7 +689,7 @@ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) |
|
|
|
669
689
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
670
690
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
671
691
|
};
|
|
672
|
-
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_id, _State_name,
|
|
692
|
+
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef;
|
|
673
693
|
const ifOtherSymbol = Symbol('other symbol');
|
|
674
694
|
// Module-private symbol used by DebugConfig setters to call State's validator
|
|
675
695
|
// without exposing the validator on the public surface.
|
|
@@ -714,10 +734,18 @@ _DebugConfig_ownerState = new WeakMap(), _DebugConfig_before = new WeakMap(), _D
|
|
|
714
734
|
class State {
|
|
715
735
|
constructor(stateDefinition = null, name) {
|
|
716
736
|
_State_id.set(this, id(this));
|
|
737
|
+
// Not `readonly` because `withOverriddenHaltState` and `fromGraph` set the
|
|
738
|
+
// composed name on a no-arg `new State()` to bypass the constructor's
|
|
739
|
+
// user-facing name validation (composite names contain `(` and `)`).
|
|
717
740
|
_State_name.set(this, void 0);
|
|
718
|
-
|
|
741
|
+
_State_overriddenHaltState.set(this, null);
|
|
742
|
+
// For wrapper states (produced by `withOverriddenHaltState`), points at the
|
|
743
|
+
// State whose transition map was wrapped. `null` on bare/atomic states.
|
|
744
|
+
// Used by `toGraph` to collapse the wrapper-and-its-bare pair into a single
|
|
745
|
+
// "wrapped bare" graph node — see the v7 emit redesign for #138.
|
|
746
|
+
_State_bareState.set(this, null);
|
|
719
747
|
_State_symbolToDataMap.set(this, new Map());
|
|
720
|
-
// Shared mutable cell —
|
|
748
|
+
// Shared mutable cell — withOverriddenHaltState wrappers reference the same
|
|
721
749
|
// object so that `state.debug = ...` (and nullings) propagate across them.
|
|
722
750
|
// Note: toGraph / fromGraph deliberately do not serialize debug — debug is
|
|
723
751
|
// a runtime concern, not part of the structural graph.
|
|
@@ -763,6 +791,9 @@ class State {
|
|
|
763
791
|
});
|
|
764
792
|
});
|
|
765
793
|
}
|
|
794
|
+
if (name !== undefined && /[()]/.test(name)) {
|
|
795
|
+
throw new Error(`invalid state name "${name}": must not contain '(' or ')' (reserved as wrapper-composition delimiters in withOverriddenHaltState)`);
|
|
796
|
+
}
|
|
766
797
|
__classPrivateFieldSet$1(this, _State_name, name ?? `id:${__classPrivateFieldGet$1(this, _State_id, "f")}`, "f");
|
|
767
798
|
}
|
|
768
799
|
get id() {
|
|
@@ -774,8 +805,8 @@ class State {
|
|
|
774
805
|
get isHalt() {
|
|
775
806
|
return __classPrivateFieldGet$1(this, _State_id, "f") === 0;
|
|
776
807
|
}
|
|
777
|
-
get
|
|
778
|
-
return __classPrivateFieldGet$1(this,
|
|
808
|
+
get overriddenHaltState() {
|
|
809
|
+
return __classPrivateFieldGet$1(this, _State_overriddenHaltState, "f");
|
|
779
810
|
}
|
|
780
811
|
get ref() {
|
|
781
812
|
return this;
|
|
@@ -803,7 +834,7 @@ class State {
|
|
|
803
834
|
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, value);
|
|
804
835
|
}
|
|
805
836
|
/** @internal — invoked by DebugConfig setters via module-private symbol. */
|
|
806
|
-
[(_State_id = new WeakMap(), _State_name = new WeakMap(),
|
|
837
|
+
[(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), validateDebugFilter)](fieldName, filter) {
|
|
807
838
|
if (filter === undefined)
|
|
808
839
|
return;
|
|
809
840
|
// #108 part 2: `.after` on haltState has no semantic anchor — halt is
|
|
@@ -850,11 +881,17 @@ class State {
|
|
|
850
881
|
}
|
|
851
882
|
throw new Error(`No nextState for symbol at state named ${__classPrivateFieldGet$1(this, _State_id, "f")}`);
|
|
852
883
|
}
|
|
853
|
-
|
|
854
|
-
|
|
884
|
+
withOverriddenHaltState(overriddenHaltState) {
|
|
885
|
+
// Construct with no name, then overwrite #name directly — the composed
|
|
886
|
+
// name contains `(` and `)` by design, which the constructor's user-facing
|
|
887
|
+
// validation would reject. Internal composition bypasses validation via
|
|
888
|
+
// private-field access (legal within the same class).
|
|
889
|
+
const state = new State();
|
|
890
|
+
__classPrivateFieldSet$1(state, _State_name, `${this.name}(${overriddenHaltState.name})`, "f");
|
|
855
891
|
__classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f"), "f");
|
|
856
|
-
__classPrivateFieldSet$1(state,
|
|
892
|
+
__classPrivateFieldSet$1(state, _State_overriddenHaltState, overriddenHaltState, "f");
|
|
857
893
|
__classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(this, _State_debugRef, "f"), "f");
|
|
894
|
+
__classPrivateFieldSet$1(state, _State_bareState, this, "f");
|
|
858
895
|
return state;
|
|
859
896
|
}
|
|
860
897
|
// Single-state introspection — no traversal, no tapeBlock required.
|
|
@@ -885,52 +922,196 @@ class State {
|
|
|
885
922
|
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
886
923
|
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
887
924
|
isHalt: state.isHalt,
|
|
888
|
-
|
|
889
|
-
? { id: __classPrivateFieldGet$1(state,
|
|
925
|
+
overriddenHaltState: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f")
|
|
926
|
+
? { id: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f").id, name: __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f").name }
|
|
890
927
|
: null,
|
|
891
928
|
transitions,
|
|
892
929
|
};
|
|
893
930
|
}
|
|
931
|
+
// Walks the State graph and emits a `Graph` data structure. v7 emit shape:
|
|
932
|
+
// wrapper-States (those with `#overriddenHaltState !== null`) are collapsed
|
|
933
|
+
// onto their bare's representation in the graph, with the wrapper's own `#id`
|
|
934
|
+
// used as the graph node id, `isWrapped: true`, and `overriddenHaltStateId`
|
|
935
|
+
// set to the override's collapsed id. A per-wrapper "halt marker" graph node
|
|
936
|
+
// (id = negative-of-the-wrapper-id, `isHalt: true, isHaltMarker: true`) is
|
|
937
|
+
// synthesized; the bare's halt-bound transitions are rewritten to target the
|
|
938
|
+
// halt marker instead of the real one.
|
|
939
|
+
//
|
|
940
|
+
// Halt-marker node ids use the negation of the wrapper's id so they sit in a
|
|
941
|
+
// disjoint integer range from real ids (which are always non-negative). Real
|
|
942
|
+
// halt is always id 0.
|
|
894
943
|
static toGraph(initialState, tapeBlock) {
|
|
895
944
|
const nodes = {};
|
|
896
|
-
const queue = [initialState];
|
|
897
945
|
const alphabets = tapeBlock.alphabets.map((alphabet) => alphabet.symbols);
|
|
946
|
+
// Map from a wrapper-State to the "collapsed" graph node id used to refer
|
|
947
|
+
// to it in transitions. Same as the wrapper's `#id`, recorded for clarity
|
|
948
|
+
// when rewriting transition targets.
|
|
949
|
+
const wrapperGraphId = (s) => __classPrivateFieldGet$1(s, _State_id, "f");
|
|
950
|
+
const haltMarkerIdFor = (wrapper) => -__classPrivateFieldGet$1(wrapper, _State_id, "f");
|
|
951
|
+
// The `initialId` is the user-passed start. If it's a wrapper, the
|
|
952
|
+
// collapsed graph node uses its `#id`; otherwise its own `#id`.
|
|
953
|
+
const initialId = __classPrivateFieldGet$1(initialState, _State_id, "f");
|
|
954
|
+
const queue = [];
|
|
955
|
+
// Decide how to enqueue the start: if it's a wrapper, enqueue its bare with
|
|
956
|
+
// the wrapper as context; otherwise enqueue the state itself.
|
|
957
|
+
if (__classPrivateFieldGet$1(initialState, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(initialState, _State_bareState, "f")) {
|
|
958
|
+
queue.push({ state: __classPrivateFieldGet$1(initialState, _State_bareState, "f"), wrapperContext: initialState });
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
queue.push({ state: initialState, wrapperContext: null });
|
|
962
|
+
}
|
|
898
963
|
while (queue.length > 0) {
|
|
899
|
-
const
|
|
900
|
-
if (
|
|
964
|
+
const { state, wrapperContext } = queue.shift();
|
|
965
|
+
if (state.isHalt) {
|
|
966
|
+
// Real halt — always id 0, single node.
|
|
967
|
+
if (!(0 in nodes)) {
|
|
968
|
+
nodes[0] = {
|
|
969
|
+
id: 0,
|
|
970
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
971
|
+
isHalt: true,
|
|
972
|
+
isHaltMarker: false,
|
|
973
|
+
isWrapped: false,
|
|
974
|
+
transitions: [],
|
|
975
|
+
overriddenHaltStateId: null,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (wrapperContext !== null) {
|
|
981
|
+
// Process `state` (the bare) collapsed under `wrapperContext` (the
|
|
982
|
+
// wrapper). Graph node id = wrapper's id.
|
|
983
|
+
const collapsedId = wrapperGraphId(wrapperContext);
|
|
984
|
+
if (collapsedId in nodes) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const haltMarkerId = haltMarkerIdFor(wrapperContext);
|
|
988
|
+
const overrideTarget = __classPrivateFieldGet$1(wrapperContext, _State_overriddenHaltState, "f");
|
|
989
|
+
// The override target's collapsed id: if the override is itself a
|
|
990
|
+
// wrapper, its graph node id is `overrideTarget.#id` (its own wrapper
|
|
991
|
+
// id); otherwise its own bare id.
|
|
992
|
+
const overrideGraphId = __classPrivateFieldGet$1(overrideTarget, _State_overriddenHaltState, "f")
|
|
993
|
+
? wrapperGraphId(overrideTarget)
|
|
994
|
+
: __classPrivateFieldGet$1(overrideTarget, _State_id, "f");
|
|
995
|
+
// Emit the halt-marker node if not already present (one per wrapper).
|
|
996
|
+
if (!(haltMarkerId in nodes)) {
|
|
997
|
+
nodes[haltMarkerId] = {
|
|
998
|
+
id: haltMarkerId,
|
|
999
|
+
name: 'halt',
|
|
1000
|
+
isHalt: true,
|
|
1001
|
+
isHaltMarker: true,
|
|
1002
|
+
isWrapped: false,
|
|
1003
|
+
transitions: [],
|
|
1004
|
+
overriddenHaltStateId: null,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
// Build the collapsed node.
|
|
1008
|
+
const collapsedNode = {
|
|
1009
|
+
id: collapsedId,
|
|
1010
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1011
|
+
isHalt: false,
|
|
1012
|
+
isHaltMarker: false,
|
|
1013
|
+
isWrapped: true,
|
|
1014
|
+
transitions: [],
|
|
1015
|
+
overriddenHaltStateId: overrideGraphId,
|
|
1016
|
+
};
|
|
1017
|
+
nodes[collapsedId] = collapsedNode;
|
|
1018
|
+
let patternIx = 0;
|
|
1019
|
+
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
1020
|
+
let target;
|
|
1021
|
+
try {
|
|
1022
|
+
target = nextState instanceof State ? nextState : nextState.ref;
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
patternIx += 1;
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
// Retarget transitions per Variant X conventions:
|
|
1029
|
+
// - target == haltState → halt marker (stays inside the subgraph)
|
|
1030
|
+
// - target == bare (self-loop) → the collapsed wrapper id
|
|
1031
|
+
// - target is itself a wrapper → that wrapper's collapsed id
|
|
1032
|
+
// - else → target's own id
|
|
1033
|
+
let nextStateId;
|
|
1034
|
+
if (target.isHalt) {
|
|
1035
|
+
nextStateId = haltMarkerId;
|
|
1036
|
+
}
|
|
1037
|
+
else if (target === state) {
|
|
1038
|
+
nextStateId = collapsedId;
|
|
1039
|
+
}
|
|
1040
|
+
else if (__classPrivateFieldGet$1(target, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(target, _State_bareState, "f")) {
|
|
1041
|
+
nextStateId = wrapperGraphId(target);
|
|
1042
|
+
queue.push({ state: __classPrivateFieldGet$1(target, _State_bareState, "f"), wrapperContext: target });
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
nextStateId = __classPrivateFieldGet$1(target, _State_id, "f");
|
|
1046
|
+
queue.push({ state: target, wrapperContext: null });
|
|
1047
|
+
}
|
|
1048
|
+
collapsedNode.transitions.push({
|
|
1049
|
+
pattern: decodePatternDescription(sym.description, alphabets),
|
|
1050
|
+
command: command.tapesCommands.map((tc) => ({
|
|
1051
|
+
symbol: decodeWriteSymbol(tc.symbol),
|
|
1052
|
+
movement: decodeMovement(tc.movement.description),
|
|
1053
|
+
})),
|
|
1054
|
+
nextStateId,
|
|
1055
|
+
id: `${collapsedId}-${patternIx}`,
|
|
1056
|
+
});
|
|
1057
|
+
patternIx += 1;
|
|
1058
|
+
}
|
|
1059
|
+
// Enqueue the override target so its own node is emitted.
|
|
1060
|
+
if (__classPrivateFieldGet$1(overrideTarget, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(overrideTarget, _State_bareState, "f")) {
|
|
1061
|
+
queue.push({ state: __classPrivateFieldGet$1(overrideTarget, _State_bareState, "f"), wrapperContext: overrideTarget });
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
queue.push({ state: overrideTarget, wrapperContext: null });
|
|
1065
|
+
}
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
// Non-wrapper context: emit `state` as a regular node.
|
|
1069
|
+
if (__classPrivateFieldGet$1(state, _State_id, "f") in nodes) {
|
|
901
1070
|
continue;
|
|
902
1071
|
}
|
|
903
1072
|
const node = {
|
|
904
|
-
id: __classPrivateFieldGet$1(
|
|
905
|
-
name: __classPrivateFieldGet$1(
|
|
906
|
-
isHalt:
|
|
1073
|
+
id: __classPrivateFieldGet$1(state, _State_id, "f"),
|
|
1074
|
+
name: __classPrivateFieldGet$1(state, _State_name, "f"),
|
|
1075
|
+
isHalt: false,
|
|
1076
|
+
isHaltMarker: false,
|
|
1077
|
+
isWrapped: false,
|
|
907
1078
|
transitions: [],
|
|
908
|
-
|
|
1079
|
+
overriddenHaltStateId: null,
|
|
909
1080
|
};
|
|
910
|
-
nodes[__classPrivateFieldGet$1(
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(current, _State_symbolToDataMap, "f")) {
|
|
1081
|
+
nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = node;
|
|
1082
|
+
let patternIx = 0;
|
|
1083
|
+
for (const [sym, { command, nextState }] of __classPrivateFieldGet$1(state, _State_symbolToDataMap, "f")) {
|
|
915
1084
|
let target;
|
|
916
1085
|
try {
|
|
917
1086
|
target = nextState instanceof State ? nextState : nextState.ref;
|
|
918
1087
|
}
|
|
919
1088
|
catch {
|
|
1089
|
+
patternIx += 1;
|
|
920
1090
|
continue;
|
|
921
1091
|
}
|
|
1092
|
+
let nextStateId;
|
|
1093
|
+
if (__classPrivateFieldGet$1(target, _State_overriddenHaltState, "f") && __classPrivateFieldGet$1(target, _State_bareState, "f")) {
|
|
1094
|
+
// Transition into a wrapper — use its collapsed id.
|
|
1095
|
+
nextStateId = wrapperGraphId(target);
|
|
1096
|
+
queue.push({ state: __classPrivateFieldGet$1(target, _State_bareState, "f"), wrapperContext: target });
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
nextStateId = __classPrivateFieldGet$1(target, _State_id, "f");
|
|
1100
|
+
queue.push({ state: target, wrapperContext: null });
|
|
1101
|
+
}
|
|
922
1102
|
node.transitions.push({
|
|
923
1103
|
pattern: decodePatternDescription(sym.description, alphabets),
|
|
924
1104
|
command: command.tapesCommands.map((tc) => ({
|
|
925
1105
|
symbol: decodeWriteSymbol(tc.symbol),
|
|
926
1106
|
movement: decodeMovement(tc.movement.description),
|
|
927
1107
|
})),
|
|
928
|
-
nextStateId
|
|
1108
|
+
nextStateId,
|
|
1109
|
+
id: `${__classPrivateFieldGet$1(state, _State_id, "f")}-${patternIx}`,
|
|
929
1110
|
});
|
|
930
|
-
|
|
1111
|
+
patternIx += 1;
|
|
931
1112
|
}
|
|
932
1113
|
}
|
|
933
|
-
return { initialId
|
|
1114
|
+
return { initialId, alphabets, nodes };
|
|
934
1115
|
}
|
|
935
1116
|
// Inverse of toGraph: rebuilds a State graph (and a fresh TapeBlock with the
|
|
936
1117
|
// graph's alphabets) from a serialized Graph. Round-trips with toGraph in
|
|
@@ -982,7 +1163,12 @@ class State {
|
|
|
982
1163
|
nextState,
|
|
983
1164
|
};
|
|
984
1165
|
}
|
|
985
|
-
|
|
1166
|
+
// Graph-sourced names may contain `(` and `)` (composite wrapper names
|
|
1167
|
+
// emitted by toGraph). Bypass the constructor's user-facing name
|
|
1168
|
+
// validation by constructing without a name and assigning #name directly.
|
|
1169
|
+
const bare = new State(stateDefinition);
|
|
1170
|
+
__classPrivateFieldSet$1(bare, _State_name, node.name, "f");
|
|
1171
|
+
bareStates[nodeId] = bare;
|
|
986
1172
|
}
|
|
987
1173
|
// Pass 3: apply overrideHaltStates transitively.
|
|
988
1174
|
const finalStates = {};
|
|
@@ -1001,8 +1187,8 @@ class State {
|
|
|
1001
1187
|
}
|
|
1002
1188
|
inProgress.add(nodeId);
|
|
1003
1189
|
let state = bareStates[nodeId];
|
|
1004
|
-
if (node.
|
|
1005
|
-
state = bareStates[nodeId].
|
|
1190
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1191
|
+
state = bareStates[nodeId].withOverriddenHaltState(getFinal(node.overriddenHaltStateId));
|
|
1006
1192
|
}
|
|
1007
1193
|
inProgress.delete(nodeId);
|
|
1008
1194
|
finalStates[nodeId] = state;
|
|
@@ -1060,7 +1246,7 @@ class TuringMachine {
|
|
|
1060
1246
|
get tapeBlock() {
|
|
1061
1247
|
return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
|
|
1062
1248
|
}
|
|
1063
|
-
async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
|
|
1249
|
+
async run({ initialState, stepsLimit = 1e5, onStep, onPause, onIter, debug = true, }) {
|
|
1064
1250
|
const generator = this.runStepByStep({ initialState, stepsLimit });
|
|
1065
1251
|
for (const machineState of generator) {
|
|
1066
1252
|
// Per-iter lifecycle: before → step → after. All three operate on the
|
|
@@ -1069,12 +1255,15 @@ class TuringMachine {
|
|
|
1069
1255
|
if (debug && machineState.debugBreak?.before && onPause) {
|
|
1070
1256
|
await onPause({ ...machineState, debugBreak: { before: true } });
|
|
1071
1257
|
}
|
|
1072
|
-
if (onStep
|
|
1258
|
+
if (onStep) {
|
|
1073
1259
|
onStep(machineState);
|
|
1074
1260
|
}
|
|
1075
1261
|
if (debug && machineState.debugBreak?.after && onPause) {
|
|
1076
1262
|
await onPause({ ...machineState, debugBreak: { after: true } });
|
|
1077
1263
|
}
|
|
1264
|
+
if (onIter) {
|
|
1265
|
+
await onIter(machineState);
|
|
1266
|
+
}
|
|
1078
1267
|
}
|
|
1079
1268
|
}
|
|
1080
1269
|
*runStepByStep({ initialState, stepsLimit = 1e5 }) {
|
|
@@ -1084,8 +1273,8 @@ class TuringMachine {
|
|
|
1084
1273
|
__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f")[lockSymbol].lock(executionSymbol);
|
|
1085
1274
|
const stack = __classPrivateFieldGet(this, _TuringMachine_stack, "f");
|
|
1086
1275
|
let state = initialState;
|
|
1087
|
-
if (state.
|
|
1088
|
-
stack.push(state.
|
|
1276
|
+
if (state.overriddenHaltState) {
|
|
1277
|
+
stack.push(state.overriddenHaltState);
|
|
1089
1278
|
}
|
|
1090
1279
|
let i = 0;
|
|
1091
1280
|
while (!state.isHalt) {
|
|
@@ -1139,8 +1328,8 @@ class TuringMachine {
|
|
|
1139
1328
|
if (nextState.isHalt && stack.length) {
|
|
1140
1329
|
nextState = stack.pop();
|
|
1141
1330
|
}
|
|
1142
|
-
if (state !== nextState && nextState.
|
|
1143
|
-
stack.push(nextState.
|
|
1331
|
+
if (state !== nextState && nextState.overriddenHaltState) {
|
|
1332
|
+
stack.push(nextState.overriddenHaltState);
|
|
1144
1333
|
}
|
|
1145
1334
|
state = nextState;
|
|
1146
1335
|
}
|
|
@@ -1164,32 +1353,131 @@ _TuringMachine_tapeBlock = new WeakMap(), _TuringMachine_stack = new WeakMap();
|
|
|
1164
1353
|
//
|
|
1165
1354
|
// Currently only Mermaid flowchart syntax is supported. Future formats
|
|
1166
1355
|
// (Graphviz, JSON-LD, custom DSL) belong here too.
|
|
1356
|
+
//
|
|
1357
|
+
// v7 emit shape (#138/#139):
|
|
1358
|
+
// - Each wrapper-State collapses onto its bare's representation. The collapsed
|
|
1359
|
+
// graph node has `isWrapped: true` and is emitted as Mermaid `[[…]]`
|
|
1360
|
+
// (subroutine / double-walled-rectangle) shape, inside a `subgraph
|
|
1361
|
+
// w_${id}["halt frame"] … end` block. A synthesized "halt marker" graph
|
|
1362
|
+
// node (with `isHalt: true, isHaltMarker: true`, id = -wrapperId in graph
|
|
1363
|
+
// data) sits inside the subgraph and serves as the local landing point for
|
|
1364
|
+
// the bare's halt-bound transitions. The dotted onHalt edge runs from the
|
|
1365
|
+
// `[[bare]]` directly to the override target, crossing the subgraph border.
|
|
1366
|
+
// - Real halt (id 0) is emitted as `s0(((halt)))` outside any subgraph.
|
|
1367
|
+
// - Halt marker nodes use the Mermaid id `c${absId}` (where `absId = -id`)
|
|
1368
|
+
// since Mermaid IDs must match /[A-Za-z][A-Za-z0-9_]*/ — negative numbers
|
|
1369
|
+
// are not legal syntax.
|
|
1370
|
+
// Maps a graph node id to its Mermaid id.
|
|
1371
|
+
// - non-negative id N → "sN"
|
|
1372
|
+
// - negative id -N (halt marker) → "cN"
|
|
1373
|
+
function mermaidIdFor(id) {
|
|
1374
|
+
return id < 0 ? `c${-id}` : `s${id}`;
|
|
1375
|
+
}
|
|
1376
|
+
// Inverse of mermaidIdFor.
|
|
1377
|
+
function parseMermaidId(s) {
|
|
1378
|
+
if (s.startsWith('c')) {
|
|
1379
|
+
return -Number(s.slice(1));
|
|
1380
|
+
}
|
|
1381
|
+
return Number(s.slice(1));
|
|
1382
|
+
}
|
|
1167
1383
|
function toMermaid(graph) {
|
|
1168
1384
|
const lines = [
|
|
1169
1385
|
'flowchart TD',
|
|
1170
1386
|
`%% alphabets: ${JSON.stringify(graph.alphabets)}`,
|
|
1171
1387
|
];
|
|
1172
|
-
|
|
1173
|
-
|
|
1388
|
+
// Sort nodes by id (ascending — real halt first at 0, regular states next,
|
|
1389
|
+
// negative-id halt markers last). Deterministic emit lets `toMermaid` →
|
|
1390
|
+
// `fromMermaid` → `toMermaid` round-trip stably (regression for #139).
|
|
1391
|
+
const nodes = Object.values(graph.nodes).slice().sort((a, b) => a.id - b.id);
|
|
1392
|
+
const wrappedNodes = nodes.filter((n) => n.isWrapped);
|
|
1393
|
+
// Convention: wrapped node id N → halt marker id -N.
|
|
1394
|
+
const haltMarkerIdFor = (wrappedId) => -wrappedId;
|
|
1395
|
+
// Set of halt-marker ids that belong to some wrapper (= are inside a subgraph).
|
|
1396
|
+
const haltMarkerIds = new Set();
|
|
1397
|
+
for (const w of wrappedNodes) {
|
|
1398
|
+
const haltMarkerId = haltMarkerIdFor(w.id);
|
|
1399
|
+
if (haltMarkerId in graph.nodes) {
|
|
1400
|
+
haltMarkerIds.add(haltMarkerId);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
// Emit non-subgraph nodes first: real halt + regular non-wrapped nodes.
|
|
1404
|
+
// No special round-shape `((…))` for the initial — the `idle -. enter .->`
|
|
1405
|
+
// arrow emitted below is the sole "start here" signal.
|
|
1406
|
+
for (const node of nodes) {
|
|
1407
|
+
if (node.isWrapped || haltMarkerIds.has(node.id)) {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
const id = mermaidIdFor(node.id);
|
|
1174
1411
|
if (node.isHalt) {
|
|
1175
1412
|
lines.push(` ${id}(((halt)))`);
|
|
1176
1413
|
}
|
|
1177
|
-
else if (node.id === graph.initialId) {
|
|
1178
|
-
lines.push(` ${id}(("${node.name}"))`);
|
|
1179
|
-
}
|
|
1180
1414
|
else {
|
|
1181
1415
|
lines.push(` ${id}["${node.name}"]`);
|
|
1182
1416
|
}
|
|
1183
1417
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1418
|
+
// `idle` sentinel = pre-execution marker for the machine. Always emitted,
|
|
1419
|
+
// with a labeled dotted arrow `idle -. enter .-> sN` to the initial state.
|
|
1420
|
+
// Symmetric with the `onHalt` dotted convention used by wrapper redirects.
|
|
1421
|
+
// Visual-only — `idle` is not a graph node.
|
|
1422
|
+
lines.push(' idle([idle])');
|
|
1423
|
+
// Emit one subgraph per wrapper, in sorted wrapped-id order.
|
|
1424
|
+
for (const wrapped of wrappedNodes) {
|
|
1425
|
+
const wrappedMid = mermaidIdFor(wrapped.id);
|
|
1426
|
+
const haltMarkerId = haltMarkerIdFor(wrapped.id);
|
|
1427
|
+
const haltMarkerMid = mermaidIdFor(haltMarkerId);
|
|
1428
|
+
lines.push(` subgraph w_${wrapped.id}["halt frame"]`);
|
|
1429
|
+
lines.push(` ${wrappedMid}[["${wrapped.name}"]]`);
|
|
1430
|
+
if (haltMarkerId in graph.nodes) {
|
|
1431
|
+
lines.push(` ${haltMarkerMid}(((halt)))`);
|
|
1432
|
+
}
|
|
1433
|
+
lines.push(' end');
|
|
1434
|
+
}
|
|
1435
|
+
// Enter arrow: emitted after subgraphs so it visually points at the initial
|
|
1436
|
+
// node (whether plain `[…]` or wrapped `[[…]]` inside a subgraph).
|
|
1437
|
+
lines.push(` idle -. enter .-> ${mermaidIdFor(graph.initialId)}`);
|
|
1438
|
+
// Emit transitions per-node in sorted node-id order. Within a node,
|
|
1439
|
+
// transitions emit in their stored array order (which mirrors the source
|
|
1440
|
+
// state's symbol-map insertion order — stable per State instance).
|
|
1441
|
+
for (const node of nodes) {
|
|
1442
|
+
if (node.isHalt && !node.isHaltMarker) {
|
|
1443
|
+
continue;
|
|
1190
1444
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1445
|
+
for (const t of node.transitions) {
|
|
1446
|
+
// Bracketed-tape-block format (v7): each role-list — read alternatives,
|
|
1447
|
+
// writes, movements — wraps in `[…]` to mark "this is a tape-block
|
|
1448
|
+
// reading". Brackets stay even for single-tape machines; the `[…]` is
|
|
1449
|
+
// the tape-block concept indicator.
|
|
1450
|
+
//
|
|
1451
|
+
// Single-tape: ['X'] → [K]/[R]
|
|
1452
|
+
// Single-tape + alternation: ['^']|['1']|['0'] → [K]/[S]
|
|
1453
|
+
// Two-tape: ['0','a'] → [K,'1']/[R,S]
|
|
1454
|
+
// Two-tape + alternation: ['0','a']|['1','b'] → [K,K]/[R,L]
|
|
1455
|
+
//
|
|
1456
|
+
// Alternation is ALWAYS per-pattern-bracket — one full bracketed list
|
|
1457
|
+
// per alternative — regardless of tape count. Pedagogically each
|
|
1458
|
+
// alternative is its own drawn transition; a compact in-bracket form
|
|
1459
|
+
// (`['^'|'1']`) would read as cross-product semantics in multi-tape
|
|
1460
|
+
// (`['0'|'1','a'|'b']` = 4 combos, not 2 paired alternatives), so we
|
|
1461
|
+
// avoid introducing it for the single-tape case too.
|
|
1462
|
+
const alternatives = t.pattern.split('|');
|
|
1463
|
+
const reads = alternatives.map((alt) => `[${alt}]`).join('|');
|
|
1464
|
+
const writes = `[${t.command.map((c) => c.symbol).join(',')}]`;
|
|
1465
|
+
const moves = `[${t.command.map((c) => c.movement).join(',')}]`;
|
|
1466
|
+
const label = `${reads} → ${writes}/${moves}`;
|
|
1467
|
+
// Thicker `==>` arrow when the transition crosses INTO a wrapper —
|
|
1468
|
+
// signals "this transition pushes that wrapper's override onto the
|
|
1469
|
+
// runtime stack" (per `TuringMachine.run` line ~220's
|
|
1470
|
+
// `if (state !== nextState && nextState.overriddenHaltState) push(...)`).
|
|
1471
|
+
// Self-loops (state === nextState) don't push at runtime — keep the
|
|
1472
|
+
// regular `-->` for those even when the target is wrapped.
|
|
1473
|
+
const targetNode = graph.nodes[t.nextStateId];
|
|
1474
|
+
const isEnteringWrapper = targetNode && targetNode.isWrapped && t.nextStateId !== node.id;
|
|
1475
|
+
const lineSegment = isEnteringWrapper ? '==' : '--';
|
|
1476
|
+
const arrowTip = isEnteringWrapper ? '==>' : '-->';
|
|
1477
|
+
lines.push(` ${mermaidIdFor(node.id)} ${lineSegment} "${label}" ${arrowTip} ${mermaidIdFor(t.nextStateId)}`);
|
|
1478
|
+
}
|
|
1479
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1480
|
+
lines.push(` ${mermaidIdFor(node.id)} -. onHalt .-> ${mermaidIdFor(node.overriddenHaltStateId)}`);
|
|
1193
1481
|
}
|
|
1194
1482
|
}
|
|
1195
1483
|
return lines.join('\n');
|
|
@@ -1205,11 +1493,16 @@ function toMermaid(graph) {
|
|
|
1205
1493
|
// per-tape segments are split on ','. If your alphabet contains '/' or ','
|
|
1206
1494
|
// as literal symbols, the parser cannot disambiguate. Stick to alphabets
|
|
1207
1495
|
// without those characters when round-tripping through Mermaid.
|
|
1208
|
-
const haltNodeRegex = /^
|
|
1209
|
-
const
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1496
|
+
const haltNodeRegex = /^([sc]\d+)\(\(\(halt\)\)\)$/;
|
|
1497
|
+
const regularNodeRegex = /^(s\d+)\["([^"]*)"\]$/;
|
|
1498
|
+
const wrappedNodeRegex = /^(s\d+)\[\["([^"]*)"\]\]$/;
|
|
1499
|
+
const subgraphStartRegex = /^subgraph\s+w_\d+\["([^"]*)"\]$/;
|
|
1500
|
+
const subgraphEndRegex = /^end$/;
|
|
1501
|
+
const idleNodeRegex = /^idle\(\[idle\]\)$/;
|
|
1502
|
+
const enterArrowRegex = /^idle\s+-\.\s+enter\s+\.->\s+(s\d+)$/;
|
|
1503
|
+
const transitionRegex = /^([sc]\d+)\s+--\s+"(.*)"\s+-->\s+([sc]\d+)$/;
|
|
1504
|
+
const thickTransitionRegex = /^([sc]\d+)\s+==\s+"(.*)"\s+==>\s+([sc]\d+)$/;
|
|
1505
|
+
const onHaltRegex = /^([sc]\d+)\s+-\.\s+onHalt\s+\.->\s+([sc]\d+)$/;
|
|
1213
1506
|
// First capture char anchored as \S to avoid polynomial backtracking between
|
|
1214
1507
|
// the preceding \s* and a permissive (.+); see CodeQL js/polynomial-redos.
|
|
1215
1508
|
const alphabetsRegex = /^%%\s*alphabets:\s*(\S.*)$/;
|
|
@@ -1218,27 +1511,36 @@ function fromMermaid(text) {
|
|
|
1218
1511
|
let alphabets = [];
|
|
1219
1512
|
let initialId = null;
|
|
1220
1513
|
const nodes = {};
|
|
1514
|
+
// Track the halt-marker ids that appeared inside a subgraph — they should be
|
|
1515
|
+
// marked `isHaltMarker: true` even though they share the `(((halt)))` shape
|
|
1516
|
+
// with the real halt at the top level.
|
|
1517
|
+
const haltMarkerIds = new Set();
|
|
1518
|
+
let inSubgraph = false;
|
|
1221
1519
|
const ensureNode = (id, opts = {}) => {
|
|
1222
1520
|
if (!nodes[id]) {
|
|
1223
1521
|
nodes[id] = {
|
|
1224
1522
|
id,
|
|
1225
|
-
name: opts.name ??
|
|
1523
|
+
name: opts.name ?? mermaidIdFor(id),
|
|
1226
1524
|
isHalt: opts.isHalt ?? false,
|
|
1525
|
+
isHaltMarker: opts.isHaltMarker ?? false,
|
|
1526
|
+
isWrapped: opts.isWrapped ?? false,
|
|
1227
1527
|
transitions: [],
|
|
1228
|
-
|
|
1528
|
+
overriddenHaltStateId: null,
|
|
1229
1529
|
};
|
|
1230
1530
|
}
|
|
1231
1531
|
else {
|
|
1232
|
-
if (opts.name !== undefined)
|
|
1532
|
+
if (opts.name !== undefined)
|
|
1233
1533
|
nodes[id].name = opts.name;
|
|
1234
|
-
|
|
1235
|
-
if (opts.isHalt !== undefined) {
|
|
1534
|
+
if (opts.isHalt !== undefined)
|
|
1236
1535
|
nodes[id].isHalt = opts.isHalt;
|
|
1237
|
-
|
|
1536
|
+
if (opts.isHaltMarker !== undefined)
|
|
1537
|
+
nodes[id].isHaltMarker = opts.isHaltMarker;
|
|
1538
|
+
if (opts.isWrapped !== undefined)
|
|
1539
|
+
nodes[id].isWrapped = opts.isWrapped;
|
|
1238
1540
|
}
|
|
1239
1541
|
return nodes[id];
|
|
1240
1542
|
};
|
|
1241
|
-
// First pass: alphabets + nodes.
|
|
1543
|
+
// First pass: alphabets + nodes (track subgraph context to mark halt markers).
|
|
1242
1544
|
for (const line of lines) {
|
|
1243
1545
|
if (line === 'flowchart TD') {
|
|
1244
1546
|
continue;
|
|
@@ -1248,69 +1550,154 @@ function fromMermaid(text) {
|
|
|
1248
1550
|
alphabets = JSON.parse(am[1]);
|
|
1249
1551
|
continue;
|
|
1250
1552
|
}
|
|
1553
|
+
if (subgraphStartRegex.test(line)) {
|
|
1554
|
+
inSubgraph = true;
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
if (subgraphEndRegex.test(line)) {
|
|
1558
|
+
inSubgraph = false;
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
// `idle([idle])` sentinel: a visual pre-execution marker. Not a graph
|
|
1562
|
+
// node — skip declaration, parse the `idle -. enter .-> sN` arrow in the
|
|
1563
|
+
// edge pass to set initialId.
|
|
1564
|
+
if (idleNodeRegex.test(line)) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1251
1567
|
const hm = line.match(haltNodeRegex);
|
|
1252
1568
|
if (hm) {
|
|
1253
|
-
|
|
1569
|
+
const id = parseMermaidId(hm[1]);
|
|
1570
|
+
const isHaltMarker = inSubgraph || id < 0;
|
|
1571
|
+
ensureNode(id, { name: 'halt', isHalt: true, isHaltMarker });
|
|
1572
|
+
if (isHaltMarker) {
|
|
1573
|
+
haltMarkerIds.add(id);
|
|
1574
|
+
}
|
|
1254
1575
|
continue;
|
|
1255
1576
|
}
|
|
1256
|
-
const
|
|
1257
|
-
if (
|
|
1258
|
-
|
|
1259
|
-
initialId = id;
|
|
1260
|
-
ensureNode(id, { name: im[2] });
|
|
1577
|
+
const wm = line.match(wrappedNodeRegex);
|
|
1578
|
+
if (wm) {
|
|
1579
|
+
ensureNode(parseMermaidId(wm[1]), { name: wm[2], isWrapped: true });
|
|
1261
1580
|
continue;
|
|
1262
1581
|
}
|
|
1263
1582
|
const rm = line.match(regularNodeRegex);
|
|
1264
1583
|
if (rm) {
|
|
1265
|
-
ensureNode(
|
|
1584
|
+
ensureNode(parseMermaidId(rm[1]), { name: rm[2] });
|
|
1266
1585
|
continue;
|
|
1267
1586
|
}
|
|
1268
1587
|
}
|
|
1269
1588
|
// Second pass: edges.
|
|
1270
1589
|
for (const line of lines) {
|
|
1590
|
+
// `idle -. enter .-> sN`: the sole source of initialId.
|
|
1591
|
+
const em = line.match(enterArrowRegex);
|
|
1592
|
+
if (em) {
|
|
1593
|
+
initialId = parseMermaidId(em[1]);
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1271
1596
|
const om = line.match(onHaltRegex);
|
|
1272
1597
|
if (om) {
|
|
1273
|
-
ensureNode(
|
|
1598
|
+
ensureNode(parseMermaidId(om[1])).overriddenHaltStateId = parseMermaidId(om[2]);
|
|
1274
1599
|
continue;
|
|
1275
1600
|
}
|
|
1276
|
-
|
|
1601
|
+
// Thick transition (`==> `) and regular transition (`-->`) share the same
|
|
1602
|
+
// semantics — only the visual differs. Parse both via the same code path.
|
|
1603
|
+
const tm = line.match(transitionRegex) ?? line.match(thickTransitionRegex);
|
|
1277
1604
|
if (tm) {
|
|
1278
|
-
const fromId =
|
|
1605
|
+
const fromId = parseMermaidId(tm[1]);
|
|
1279
1606
|
const label = tm[2];
|
|
1280
|
-
const toId =
|
|
1607
|
+
const toId = parseMermaidId(tm[3]);
|
|
1281
1608
|
const arrowIx = label.indexOf(' → ');
|
|
1282
1609
|
if (arrowIx === -1) {
|
|
1283
1610
|
throw new Error(`fromMermaid: malformed edge label: "${label}"`);
|
|
1284
1611
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1612
|
+
// Bracketed-tape-block format (v7):
|
|
1613
|
+
// [<read-cells>]|[<read-cells>]... → [<write-cells>]/[<move-cells>]
|
|
1614
|
+
// Each bracketed list is a tape-block reading; the outer `|` separates
|
|
1615
|
+
// alternative read patterns. For single-tape machines with alternation,
|
|
1616
|
+
// the compact form `[<alt1>|<alt2>|...]` (one bracket, alternatives
|
|
1617
|
+
// inside) is also accepted; both forms decode to the same pattern
|
|
1618
|
+
// string.
|
|
1619
|
+
const readLabel = label.slice(0, arrowIx);
|
|
1620
|
+
const cmdLabel = label.slice(arrowIx + ' → '.length);
|
|
1621
|
+
// Strict per-pattern bracket form: `|` only between bracketed lists,
|
|
1622
|
+
// never inside. The compact `['^'|'1']` form is rejected by design —
|
|
1623
|
+
// every alternative must be its own bracketed pattern (`['^']|['1']`).
|
|
1624
|
+
// Pedagogically: each transition is drawn explicitly; the compact form
|
|
1625
|
+
// would read as cross-product semantics in multi-tape and confuse
|
|
1626
|
+
// readers (`['0'|'1','a'|'b']` could mean 4 combos, not 2 paired alts).
|
|
1627
|
+
// The rule applies to all bracketed lists — read alternatives, writes,
|
|
1628
|
+
// and movements — because commands and movements have no alternation
|
|
1629
|
+
// semantic either.
|
|
1630
|
+
const stripBrackets = (s) => {
|
|
1631
|
+
if (!s.startsWith('[') || !s.endsWith(']')) {
|
|
1632
|
+
throw new Error(`fromMermaid: malformed bracketed list: "${s}"`);
|
|
1291
1633
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1634
|
+
const inner = s.slice(1, -1);
|
|
1635
|
+
// Walk the inner content; backslash escapes the next char (so `\|`
|
|
1636
|
+
// inside a cell is a literal pipe, not the alternation separator).
|
|
1637
|
+
let i = 0;
|
|
1638
|
+
while (i < inner.length) {
|
|
1639
|
+
if (inner[i] === '\\' && i + 1 < inner.length) {
|
|
1640
|
+
i += 2;
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (inner[i] === '|') {
|
|
1644
|
+
throw new Error(`fromMermaid: compact in-bracket alternation "${s}" is not supported — `
|
|
1645
|
+
+ 'each alternative must be its own bracketed pattern (e.g. "[\'^\']|[\'1\']").');
|
|
1646
|
+
}
|
|
1647
|
+
i += 1;
|
|
1648
|
+
}
|
|
1649
|
+
return inner;
|
|
1650
|
+
};
|
|
1651
|
+
// Match `[…]` blocks in the read label. Inner content is a tape-block
|
|
1652
|
+
// reading (possibly with `|` for compact single-tape alternation).
|
|
1653
|
+
// `[^\]]*` is the simple non-greedy match — works because cell content
|
|
1654
|
+
// doesn't typically contain literal `]`.
|
|
1655
|
+
const blockMatches = readLabel.match(/\[[^\]]*\]/g);
|
|
1656
|
+
if (!blockMatches || blockMatches.length === 0) {
|
|
1657
|
+
throw new Error(`fromMermaid: no bracketed read-list in label: "${label}"`);
|
|
1658
|
+
}
|
|
1659
|
+
const pattern = blockMatches.map(stripBrackets).join('|');
|
|
1660
|
+
const slashIx = cmdLabel.indexOf(']/[');
|
|
1661
|
+
if (slashIx === -1) {
|
|
1662
|
+
throw new Error(`fromMermaid: malformed command label (expected \`[…]/[…]\`): "${cmdLabel}"`);
|
|
1663
|
+
}
|
|
1664
|
+
const writesPart = stripBrackets(cmdLabel.slice(0, slashIx + 1));
|
|
1665
|
+
const movesPart = stripBrackets(cmdLabel.slice(slashIx + 2));
|
|
1666
|
+
const writes = writesPart.split(',');
|
|
1667
|
+
const moves = movesPart.split(',');
|
|
1668
|
+
if (writes.length !== moves.length) {
|
|
1669
|
+
throw new Error(`fromMermaid: write-cells (${writes.length}) and move-cells (${moves.length}) mismatch: "${cmdLabel}"`);
|
|
1670
|
+
}
|
|
1671
|
+
const command = writes.map((symbol, i) => ({ symbol, movement: moves[i] }));
|
|
1672
|
+
const fromNode = ensureNode(fromId);
|
|
1673
|
+
const transitionIx = fromNode.transitions.length;
|
|
1674
|
+
fromNode.transitions.push({
|
|
1675
|
+
pattern,
|
|
1676
|
+
command,
|
|
1677
|
+
nextStateId: toId,
|
|
1678
|
+
id: `${fromId}-${transitionIx}`,
|
|
1296
1679
|
});
|
|
1297
|
-
ensureNode(fromId).transitions.push({ pattern, command, nextStateId: toId });
|
|
1298
1680
|
}
|
|
1299
1681
|
}
|
|
1300
1682
|
if (initialId === null) {
|
|
1301
|
-
throw new Error('fromMermaid: no
|
|
1683
|
+
throw new Error('fromMermaid: no `idle -. enter .-> sN` arrow found');
|
|
1302
1684
|
}
|
|
1303
1685
|
return { initialId, alphabets, nodes };
|
|
1304
1686
|
}
|
|
1305
1687
|
|
|
1306
1688
|
function summarizeGraph(graph) {
|
|
1307
1689
|
const nodes = Object.values(graph.nodes);
|
|
1690
|
+
// `isHaltMarker` nodes are visualization sentinels — one per wrapper context,
|
|
1691
|
+
// all corresponding to the singleton `haltState` at runtime. They don't
|
|
1692
|
+
// count as distinct runtime states; matches the per-algorithm header in
|
|
1693
|
+
// `library-binary-numbers/states.md`.
|
|
1694
|
+
const runtimeStateCount = nodes.filter((n) => !n.isHaltMarker).length;
|
|
1308
1695
|
let transitionCount = 0;
|
|
1309
1696
|
let compositionEdgeCount = 0;
|
|
1310
1697
|
let selfLoopCount = 0;
|
|
1311
1698
|
for (const node of nodes) {
|
|
1312
1699
|
transitionCount += node.transitions.length;
|
|
1313
|
-
if (node.
|
|
1700
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1314
1701
|
compositionEdgeCount += 1;
|
|
1315
1702
|
}
|
|
1316
1703
|
for (const t of node.transitions) {
|
|
@@ -1319,7 +1706,7 @@ function summarizeGraph(graph) {
|
|
|
1319
1706
|
}
|
|
1320
1707
|
}
|
|
1321
1708
|
}
|
|
1322
|
-
// Longest
|
|
1709
|
+
// Longest withOverriddenHaltState chain. Walks node → overriddenHaltState recursively;
|
|
1323
1710
|
// a Set guards against cycles in the override graph (which throw at construction
|
|
1324
1711
|
// time anyway, but being defensive costs little).
|
|
1325
1712
|
const overrideDepthFrom = (id, visited) => {
|
|
@@ -1328,10 +1715,10 @@ function summarizeGraph(graph) {
|
|
|
1328
1715
|
}
|
|
1329
1716
|
visited.add(id);
|
|
1330
1717
|
const node = graph.nodes[id];
|
|
1331
|
-
if (!node || node.
|
|
1718
|
+
if (!node || node.overriddenHaltStateId === null) {
|
|
1332
1719
|
return 0;
|
|
1333
1720
|
}
|
|
1334
|
-
return 1 + overrideDepthFrom(node.
|
|
1721
|
+
return 1 + overrideDepthFrom(node.overriddenHaltStateId, visited);
|
|
1335
1722
|
};
|
|
1336
1723
|
const maxCompositionDepth = nodes.reduce((max, node) => Math.max(max, overrideDepthFrom(node.id, new Set())), 0);
|
|
1337
1724
|
// Cycle detection: tri-color DFS over the transition graph.
|
|
@@ -1374,7 +1761,7 @@ function summarizeGraph(graph) {
|
|
|
1374
1761
|
visit(node.id);
|
|
1375
1762
|
}
|
|
1376
1763
|
return {
|
|
1377
|
-
stateCount:
|
|
1764
|
+
stateCount: runtimeStateCount,
|
|
1378
1765
|
transitionCount,
|
|
1379
1766
|
compositionEdgeCount,
|
|
1380
1767
|
maxCompositionDepth,
|