@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.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;
|
|
@@ -1058,7 +1244,7 @@ class TuringMachine {
|
|
|
1058
1244
|
get tapeBlock() {
|
|
1059
1245
|
return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
|
|
1060
1246
|
}
|
|
1061
|
-
async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
|
|
1247
|
+
async run({ initialState, stepsLimit = 1e5, onStep, onPause, onIter, debug = true, }) {
|
|
1062
1248
|
const generator = this.runStepByStep({ initialState, stepsLimit });
|
|
1063
1249
|
for (const machineState of generator) {
|
|
1064
1250
|
// Per-iter lifecycle: before → step → after. All three operate on the
|
|
@@ -1067,12 +1253,15 @@ class TuringMachine {
|
|
|
1067
1253
|
if (debug && machineState.debugBreak?.before && onPause) {
|
|
1068
1254
|
await onPause({ ...machineState, debugBreak: { before: true } });
|
|
1069
1255
|
}
|
|
1070
|
-
if (onStep
|
|
1256
|
+
if (onStep) {
|
|
1071
1257
|
onStep(machineState);
|
|
1072
1258
|
}
|
|
1073
1259
|
if (debug && machineState.debugBreak?.after && onPause) {
|
|
1074
1260
|
await onPause({ ...machineState, debugBreak: { after: true } });
|
|
1075
1261
|
}
|
|
1262
|
+
if (onIter) {
|
|
1263
|
+
await onIter(machineState);
|
|
1264
|
+
}
|
|
1076
1265
|
}
|
|
1077
1266
|
}
|
|
1078
1267
|
*runStepByStep({ initialState, stepsLimit = 1e5 }) {
|
|
@@ -1082,8 +1271,8 @@ class TuringMachine {
|
|
|
1082
1271
|
__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f")[lockSymbol].lock(executionSymbol);
|
|
1083
1272
|
const stack = __classPrivateFieldGet(this, _TuringMachine_stack, "f");
|
|
1084
1273
|
let state = initialState;
|
|
1085
|
-
if (state.
|
|
1086
|
-
stack.push(state.
|
|
1274
|
+
if (state.overriddenHaltState) {
|
|
1275
|
+
stack.push(state.overriddenHaltState);
|
|
1087
1276
|
}
|
|
1088
1277
|
let i = 0;
|
|
1089
1278
|
while (!state.isHalt) {
|
|
@@ -1137,8 +1326,8 @@ class TuringMachine {
|
|
|
1137
1326
|
if (nextState.isHalt && stack.length) {
|
|
1138
1327
|
nextState = stack.pop();
|
|
1139
1328
|
}
|
|
1140
|
-
if (state !== nextState && nextState.
|
|
1141
|
-
stack.push(nextState.
|
|
1329
|
+
if (state !== nextState && nextState.overriddenHaltState) {
|
|
1330
|
+
stack.push(nextState.overriddenHaltState);
|
|
1142
1331
|
}
|
|
1143
1332
|
state = nextState;
|
|
1144
1333
|
}
|
|
@@ -1162,32 +1351,131 @@ _TuringMachine_tapeBlock = new WeakMap(), _TuringMachine_stack = new WeakMap();
|
|
|
1162
1351
|
//
|
|
1163
1352
|
// Currently only Mermaid flowchart syntax is supported. Future formats
|
|
1164
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
|
+
}
|
|
1165
1381
|
function toMermaid(graph) {
|
|
1166
1382
|
const lines = [
|
|
1167
1383
|
'flowchart TD',
|
|
1168
1384
|
`%% alphabets: ${JSON.stringify(graph.alphabets)}`,
|
|
1169
1385
|
];
|
|
1170
|
-
|
|
1171
|
-
|
|
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);
|
|
1172
1409
|
if (node.isHalt) {
|
|
1173
1410
|
lines.push(` ${id}(((halt)))`);
|
|
1174
1411
|
}
|
|
1175
|
-
else if (node.id === graph.initialId) {
|
|
1176
|
-
lines.push(` ${id}(("${node.name}"))`);
|
|
1177
|
-
}
|
|
1178
1412
|
else {
|
|
1179
1413
|
lines.push(` ${id}["${node.name}"]`);
|
|
1180
1414
|
}
|
|
1181
1415
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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;
|
|
1188
1442
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
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)}`);
|
|
1191
1479
|
}
|
|
1192
1480
|
}
|
|
1193
1481
|
return lines.join('\n');
|
|
@@ -1203,11 +1491,16 @@ function toMermaid(graph) {
|
|
|
1203
1491
|
// per-tape segments are split on ','. If your alphabet contains '/' or ','
|
|
1204
1492
|
// as literal symbols, the parser cannot disambiguate. Stick to alphabets
|
|
1205
1493
|
// without those characters when round-tripping through Mermaid.
|
|
1206
|
-
const haltNodeRegex = /^
|
|
1207
|
-
const
|
|
1208
|
-
const
|
|
1209
|
-
const
|
|
1210
|
-
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+)$/;
|
|
1211
1504
|
// First capture char anchored as \S to avoid polynomial backtracking between
|
|
1212
1505
|
// the preceding \s* and a permissive (.+); see CodeQL js/polynomial-redos.
|
|
1213
1506
|
const alphabetsRegex = /^%%\s*alphabets:\s*(\S.*)$/;
|
|
@@ -1216,27 +1509,36 @@ function fromMermaid(text) {
|
|
|
1216
1509
|
let alphabets = [];
|
|
1217
1510
|
let initialId = null;
|
|
1218
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;
|
|
1219
1517
|
const ensureNode = (id, opts = {}) => {
|
|
1220
1518
|
if (!nodes[id]) {
|
|
1221
1519
|
nodes[id] = {
|
|
1222
1520
|
id,
|
|
1223
|
-
name: opts.name ??
|
|
1521
|
+
name: opts.name ?? mermaidIdFor(id),
|
|
1224
1522
|
isHalt: opts.isHalt ?? false,
|
|
1523
|
+
isHaltMarker: opts.isHaltMarker ?? false,
|
|
1524
|
+
isWrapped: opts.isWrapped ?? false,
|
|
1225
1525
|
transitions: [],
|
|
1226
|
-
|
|
1526
|
+
overriddenHaltStateId: null,
|
|
1227
1527
|
};
|
|
1228
1528
|
}
|
|
1229
1529
|
else {
|
|
1230
|
-
if (opts.name !== undefined)
|
|
1530
|
+
if (opts.name !== undefined)
|
|
1231
1531
|
nodes[id].name = opts.name;
|
|
1232
|
-
|
|
1233
|
-
if (opts.isHalt !== undefined) {
|
|
1532
|
+
if (opts.isHalt !== undefined)
|
|
1234
1533
|
nodes[id].isHalt = opts.isHalt;
|
|
1235
|
-
|
|
1534
|
+
if (opts.isHaltMarker !== undefined)
|
|
1535
|
+
nodes[id].isHaltMarker = opts.isHaltMarker;
|
|
1536
|
+
if (opts.isWrapped !== undefined)
|
|
1537
|
+
nodes[id].isWrapped = opts.isWrapped;
|
|
1236
1538
|
}
|
|
1237
1539
|
return nodes[id];
|
|
1238
1540
|
};
|
|
1239
|
-
// First pass: alphabets + nodes.
|
|
1541
|
+
// First pass: alphabets + nodes (track subgraph context to mark halt markers).
|
|
1240
1542
|
for (const line of lines) {
|
|
1241
1543
|
if (line === 'flowchart TD') {
|
|
1242
1544
|
continue;
|
|
@@ -1246,69 +1548,154 @@ function fromMermaid(text) {
|
|
|
1246
1548
|
alphabets = JSON.parse(am[1]);
|
|
1247
1549
|
continue;
|
|
1248
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
|
+
}
|
|
1249
1565
|
const hm = line.match(haltNodeRegex);
|
|
1250
1566
|
if (hm) {
|
|
1251
|
-
|
|
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
|
+
}
|
|
1252
1573
|
continue;
|
|
1253
1574
|
}
|
|
1254
|
-
const
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
1257
|
-
initialId = id;
|
|
1258
|
-
ensureNode(id, { name: im[2] });
|
|
1575
|
+
const wm = line.match(wrappedNodeRegex);
|
|
1576
|
+
if (wm) {
|
|
1577
|
+
ensureNode(parseMermaidId(wm[1]), { name: wm[2], isWrapped: true });
|
|
1259
1578
|
continue;
|
|
1260
1579
|
}
|
|
1261
1580
|
const rm = line.match(regularNodeRegex);
|
|
1262
1581
|
if (rm) {
|
|
1263
|
-
ensureNode(
|
|
1582
|
+
ensureNode(parseMermaidId(rm[1]), { name: rm[2] });
|
|
1264
1583
|
continue;
|
|
1265
1584
|
}
|
|
1266
1585
|
}
|
|
1267
1586
|
// Second pass: edges.
|
|
1268
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
|
+
}
|
|
1269
1594
|
const om = line.match(onHaltRegex);
|
|
1270
1595
|
if (om) {
|
|
1271
|
-
ensureNode(
|
|
1596
|
+
ensureNode(parseMermaidId(om[1])).overriddenHaltStateId = parseMermaidId(om[2]);
|
|
1272
1597
|
continue;
|
|
1273
1598
|
}
|
|
1274
|
-
|
|
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);
|
|
1275
1602
|
if (tm) {
|
|
1276
|
-
const fromId =
|
|
1603
|
+
const fromId = parseMermaidId(tm[1]);
|
|
1277
1604
|
const label = tm[2];
|
|
1278
|
-
const toId =
|
|
1605
|
+
const toId = parseMermaidId(tm[3]);
|
|
1279
1606
|
const arrowIx = label.indexOf(' → ');
|
|
1280
1607
|
if (arrowIx === -1) {
|
|
1281
1608
|
throw new Error(`fromMermaid: malformed edge label: "${label}"`);
|
|
1282
1609
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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}"`);
|
|
1289
1631
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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}`,
|
|
1294
1677
|
});
|
|
1295
|
-
ensureNode(fromId).transitions.push({ pattern, command, nextStateId: toId });
|
|
1296
1678
|
}
|
|
1297
1679
|
}
|
|
1298
1680
|
if (initialId === null) {
|
|
1299
|
-
throw new Error('fromMermaid: no
|
|
1681
|
+
throw new Error('fromMermaid: no `idle -. enter .-> sN` arrow found');
|
|
1300
1682
|
}
|
|
1301
1683
|
return { initialId, alphabets, nodes };
|
|
1302
1684
|
}
|
|
1303
1685
|
|
|
1304
1686
|
function summarizeGraph(graph) {
|
|
1305
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;
|
|
1306
1693
|
let transitionCount = 0;
|
|
1307
1694
|
let compositionEdgeCount = 0;
|
|
1308
1695
|
let selfLoopCount = 0;
|
|
1309
1696
|
for (const node of nodes) {
|
|
1310
1697
|
transitionCount += node.transitions.length;
|
|
1311
|
-
if (node.
|
|
1698
|
+
if (node.overriddenHaltStateId !== null) {
|
|
1312
1699
|
compositionEdgeCount += 1;
|
|
1313
1700
|
}
|
|
1314
1701
|
for (const t of node.transitions) {
|
|
@@ -1317,7 +1704,7 @@ function summarizeGraph(graph) {
|
|
|
1317
1704
|
}
|
|
1318
1705
|
}
|
|
1319
1706
|
}
|
|
1320
|
-
// Longest
|
|
1707
|
+
// Longest withOverriddenHaltState chain. Walks node → overriddenHaltState recursively;
|
|
1321
1708
|
// a Set guards against cycles in the override graph (which throw at construction
|
|
1322
1709
|
// time anyway, but being defensive costs little).
|
|
1323
1710
|
const overrideDepthFrom = (id, visited) => {
|
|
@@ -1326,10 +1713,10 @@ function summarizeGraph(graph) {
|
|
|
1326
1713
|
}
|
|
1327
1714
|
visited.add(id);
|
|
1328
1715
|
const node = graph.nodes[id];
|
|
1329
|
-
if (!node || node.
|
|
1716
|
+
if (!node || node.overriddenHaltStateId === null) {
|
|
1330
1717
|
return 0;
|
|
1331
1718
|
}
|
|
1332
|
-
return 1 + overrideDepthFrom(node.
|
|
1719
|
+
return 1 + overrideDepthFrom(node.overriddenHaltStateId, visited);
|
|
1333
1720
|
};
|
|
1334
1721
|
const maxCompositionDepth = nodes.reduce((max, node) => Math.max(max, overrideDepthFrom(node.id, new Set())), 0);
|
|
1335
1722
|
// Cycle detection: tri-color DFS over the transition graph.
|
|
@@ -1372,7 +1759,7 @@ function summarizeGraph(graph) {
|
|
|
1372
1759
|
visit(node.id);
|
|
1373
1760
|
}
|
|
1374
1761
|
return {
|
|
1375
|
-
stateCount:
|
|
1762
|
+
stateCount: runtimeStateCount,
|
|
1376
1763
|
transitionCount,
|
|
1377
1764
|
compositionEdgeCount,
|
|
1378
1765
|
maxCompositionDepth,
|