@turing-machine-js/machine 7.0.0-alpha.3 → 7.0.0-alpha.5

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/dist/index.cjs CHANGED
@@ -175,6 +175,153 @@ class Reference {
175
175
  }
176
176
  _Reference_referenceBinding = new WeakMap();
177
177
 
178
+ const movementDescriptionToLabel = {
179
+ 'move caret left command': 'L',
180
+ 'move caret right command': 'R',
181
+ 'do not move carer': 'S',
182
+ };
183
+ const symbolCommandDescriptionToLabel = {
184
+ 'keep symbol command': 'K',
185
+ 'erase symbol command': 'E',
186
+ };
187
+ // Reserved characters in the encoded pattern string:
188
+ // '*' ASCII asterisk (U+002A) — per-cell ifOtherSymbol, matches any symbol
189
+ // on that tape. ASCII (not a fancier glyph like U+1F7B0) so it renders
190
+ // in every Mermaid environment and every monospace font. A literal `*`
191
+ // in the alphabet is unambiguous from the marker because it's quoted
192
+ // (`'*'`).
193
+ // 'B' the tape's blank symbol shorthand (in read patterns). A literal `B`
194
+ // in the alphabet is unambiguous from the marker because it's quoted
195
+ // (`'B'`).
196
+ // ',' separates per-tape cells inside one pattern
197
+ // '|' separates alternative patterns
198
+ // "'" surrounds a literal alphabet symbol — e.g. `'0'` for literal `0`,
199
+ // `'X'` for literal `X`. The quoting is what visually separates literal
200
+ // symbols from the convention markers `*` / `B` and from the write
201
+ // commands `K` / `E`.
202
+ // '\\' escape prefix — to represent any of '*', 'B', ',', '|', "'", or '\\'
203
+ // as a *literal* alphabet symbol *inside* the quotes (e.g. `'\''` for
204
+ // a literal apostrophe).
205
+ const IF_OTHER_MARKER = '*';
206
+ const BLANK_MARKER = 'B';
207
+ function escapeAlphabetSymbol(s) {
208
+ return s
209
+ .replace(/\\/g, '\\\\')
210
+ .replace(/'/g, "\\'");
211
+ }
212
+ function decodePatternDescription(description, alphabets) {
213
+ if (!description) {
214
+ return '?';
215
+ }
216
+ if (description === 'other symbol') {
217
+ return IF_OTHER_MARKER;
218
+ }
219
+ try {
220
+ const patternList = JSON.parse(description);
221
+ return patternList
222
+ .map((pattern) => pattern
223
+ .map((s, tapeIx) => {
224
+ if (s === null) {
225
+ return IF_OTHER_MARKER;
226
+ }
227
+ if (s === alphabets[tapeIx]?.[0]) {
228
+ return BLANK_MARKER;
229
+ }
230
+ return `'${escapeAlphabetSymbol(s)}'`;
231
+ })
232
+ .join(','))
233
+ .join('|');
234
+ }
235
+ catch {
236
+ return description;
237
+ }
238
+ }
239
+ function decodeMovement(description) {
240
+ if (!description) {
241
+ return '?';
242
+ }
243
+ return movementDescriptionToLabel[description] ?? description;
244
+ }
245
+ function splitUnescaped(s, sep) {
246
+ const parts = [];
247
+ let current = '';
248
+ let i = 0;
249
+ while (i < s.length) {
250
+ if (s[i] === '\\' && i + 1 < s.length) {
251
+ current += s[i + 1];
252
+ i += 2;
253
+ }
254
+ else if (s[i] === sep) {
255
+ parts.push(current);
256
+ current = '';
257
+ i += 1;
258
+ }
259
+ else {
260
+ current += s[i];
261
+ i += 1;
262
+ }
263
+ }
264
+ parts.push(current);
265
+ return parts;
266
+ }
267
+ function parsePatternString(s, alphabets) {
268
+ if (s === IF_OTHER_MARKER) {
269
+ return null;
270
+ }
271
+ const alternatives = splitUnescaped(s, '|');
272
+ return alternatives.map((alt) => {
273
+ const cells = splitUnescaped(alt, ',');
274
+ return cells.map((cell, tapeIx) => {
275
+ if (cell === IF_OTHER_MARKER) {
276
+ return null;
277
+ }
278
+ if (cell === BLANK_MARKER) {
279
+ return alphabets[tapeIx]?.[0] ?? cell;
280
+ }
281
+ // Literal alphabet symbols are wrapped in single quotes by
282
+ // `decodePatternDescription` — strip them on the way back.
283
+ if (cell.length >= 2 && cell.startsWith("'") && cell.endsWith("'")) {
284
+ return cell.slice(1, -1);
285
+ }
286
+ return cell;
287
+ });
288
+ });
289
+ }
290
+ const movementLabelToSymbol = {
291
+ L: movements.left,
292
+ R: movements.right,
293
+ S: movements.stay,
294
+ };
295
+ function parseMovementLabel(label) {
296
+ const m = movementLabelToSymbol[label];
297
+ if (!m) {
298
+ throw new Error(`unknown movement label: ${label}`);
299
+ }
300
+ return m;
301
+ }
302
+ function parseWriteSymbolLabel(label) {
303
+ if (label === 'K') {
304
+ return symbolCommands.keep;
305
+ }
306
+ if (label === 'E') {
307
+ return symbolCommands.erase;
308
+ }
309
+ // Literal alphabet symbols are wrapped in single quotes by
310
+ // `decodeWriteSymbol` — strip them on the way back.
311
+ if (label.length >= 2 && label.startsWith("'") && label.endsWith("'")) {
312
+ return label.slice(1, -1);
313
+ }
314
+ return label;
315
+ }
316
+ function decodeWriteSymbol(symbol) {
317
+ if (typeof symbol === 'symbol') {
318
+ const description = symbol.description ?? '?';
319
+ return symbolCommandDescriptionToLabel[description] ?? description;
320
+ }
321
+ return `'${symbol}'`;
322
+ }
323
+ // Format converters (toMermaid / fromMermaid) live in ./graphFormats.
324
+
178
325
  var __classPrivateFieldSet$4 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
179
326
  if (kind === "m") throw new TypeError("Private method is not writable");
180
327
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
@@ -508,6 +655,46 @@ class TapeBlock {
508
655
  .every((everySymbol, ix) => (everySymbol === ifOtherSymbol
509
656
  || everySymbol === currentSymbols[ix])))) ?? false;
510
657
  }
658
+ /**
659
+ * For a Symbol returned by `this.symbol([...])` (or the catch-all
660
+ * `ifOtherSymbol`), returns the per-tape match kind for the
661
+ * **alternative that actually matched** given `currentSymbols`:
662
+ * `'wildcard'` if that tape position was `ifOtherSymbol` in the winning
663
+ * alternative, `'literal'` otherwise. Length always equals the tape
664
+ * count.
665
+ *
666
+ * Used by `TuringMachine.runStepByStep` to populate
667
+ * `MachineState.matchedTransition.matchKinds` for #205. The "winning
668
+ * alternative" disambiguation matters for alternations like
669
+ * `[[ifOtherSymbol, 'c'], ['a', 'b']]` — different alternatives can
670
+ * have different per-tape kinds, and only the alternative that matched
671
+ * the current head symbols is meaningful.
672
+ *
673
+ * - `ifOtherSymbol` (the State's catch-all transition fired): all
674
+ * positions are `'wildcard'`.
675
+ * - Symbol with patternList: find the first alternative that matches
676
+ * `currentSymbols` (same predicate as `isMatched`), return its
677
+ * per-position kinds.
678
+ * - Symbol with no winning alternative under the given `currentSymbols`
679
+ * (defensive — shouldn't happen if the caller resolved the Symbol via
680
+ * the State's normal matching): fall back to all `'literal'`.
681
+ */
682
+ patternKinds(symbol, currentSymbols = this.currentSymbols) {
683
+ const tapeCount = __classPrivateFieldGet$2(this, _TapeBlock_tapes, "f").length;
684
+ if (symbol === ifOtherSymbol) {
685
+ return Array.from({ length: tapeCount }, () => 'wildcard');
686
+ }
687
+ const patternList = __classPrivateFieldGet$2(this, _TapeBlock_symbolToPatternListMap, "f").get(symbol);
688
+ if (patternList === undefined) {
689
+ return Array.from({ length: tapeCount }, () => 'literal');
690
+ }
691
+ const winning = patternList.find((pattern) => (pattern.every((everySymbol, ix) => (everySymbol === ifOtherSymbol
692
+ || everySymbol === currentSymbols[ix]))));
693
+ if (winning === undefined) {
694
+ return Array.from({ length: tapeCount }, () => 'literal');
695
+ }
696
+ return winning.map((everySymbol) => (everySymbol === ifOtherSymbol ? 'wildcard' : 'literal'));
697
+ }
511
698
  replaceTape(tape, tapeIx = 0) {
512
699
  if (__classPrivateFieldGet$2(this, _TapeBlock_tapes, "f")[tapeIx] == null) {
513
700
  throw new Error('invalid tapeIx');
@@ -531,152 +718,523 @@ _TapeBlock_generateSymbolHint = { value: (patternList) => JSON.stringify(pattern
531
718
  .map((pattern) => pattern
532
719
  .map((symbol) => (symbol === ifOtherSymbol ? null : symbol)))) };
533
720
 
534
- const movementDescriptionToLabel = {
535
- 'move caret left command': 'L',
536
- 'move caret right command': 'R',
537
- 'do not move carer': 'S',
538
- };
539
- const symbolCommandDescriptionToLabel = {
540
- 'keep symbol command': 'K',
541
- 'erase symbol command': 'E',
542
- };
543
- // Reserved characters in the encoded pattern string:
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'`).
552
- // ',' separates per-tape cells inside one pattern
553
- // '|' separates alternative patterns
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';
563
- function escapeAlphabetSymbol(s) {
564
- return s
565
- .replace(/\\/g, '\\\\')
566
- .replace(/'/g, "\\'");
567
- }
568
- function decodePatternDescription(description, alphabets) {
569
- if (!description) {
570
- return '?';
571
- }
572
- if (description === 'other symbol') {
573
- return IF_OTHER_MARKER;
574
- }
575
- try {
576
- const patternList = JSON.parse(description);
577
- return patternList
578
- .map((pattern) => pattern
579
- .map((s, tapeIx) => {
580
- if (s === null) {
581
- return IF_OTHER_MARKER;
721
+ // Graph serialization/reconstruction for State graphs. Extracted from
722
+ // `classes/State.ts` (#180) so the State class stays focused on the runtime
723
+ // machinery (transitions, debug, halt-stack composition). Sibling-module
724
+ // private access to State's internals goes through the `STATE_INTERNAL`
725
+ // Symbol re-exported from State.ts — see the @internal JSDoc there.
726
+ //
727
+ // Public surface is preserved: `State.toGraph` and `State.fromGraph` static
728
+ // methods continue to exist as thin delegates to the functions in this
729
+ // module. New consumers (e.g. #195's planned `collectStates`) will live
730
+ // here too and share the BFS-walk shape with `toGraph`.
731
+ /**
732
+ * Walks the reachable graph from `initialState` and returns a serializable
733
+ * `Graph`. The walk is a BFS that visits each State exactly once (keyed by
734
+ * the State's internal id) and emits one `GraphNode` per State plus
735
+ * synthetic halt-marker nodes per callable-subtree frame.
736
+ *
737
+ * Round-trips losslessly with `fromGraph` in the sense that running the
738
+ * rebuilt machine on the same input produces the same output — but State
739
+ * instance identities are NOT preserved across the cycle.
740
+ *
741
+ * See `classes/State.ts` for the runtime model these graph nodes describe;
742
+ * see `utilities/graphFormats.ts` for the Mermaid-flavored serialization
743
+ * built on top of `Graph`.
744
+ */
745
+ function toGraph(initialState, tapeBlock) {
746
+ const nodes = {};
747
+ const alphabets = tapeBlock.alphabets.map((alphabet) => alphabet.symbols);
748
+ // Pass 1: BFS-discover all reachable States; emit one GraphNode per State
749
+ // (wrapper or bare/regular). Wrappers and bares are separate nodes.
750
+ const visited = new Set();
751
+ const queue = [initialState];
752
+ const bareIds = new Set(); // ids referenced as a wrapper's bareStateId
753
+ while (queue.length > 0) {
754
+ const state = queue.shift();
755
+ const stateInternal = state[STATE_INTERNAL]();
756
+ if (visited.has(stateInternal.id)) {
757
+ continue;
758
+ }
759
+ visited.add(stateInternal.id);
760
+ if (state.isHalt) {
761
+ if (!(0 in nodes)) {
762
+ nodes[0] = {
763
+ id: 0,
764
+ name: stateInternal.name,
765
+ isHalt: true,
766
+ isHaltMarker: false,
767
+ isWrapper: false,
768
+ bareStateId: null,
769
+ frameId: null,
770
+ transitions: [],
771
+ overriddenHaltStateId: null,
772
+ tags: [...stateInternal.tags],
773
+ };
582
774
  }
583
- if (s === alphabets[tapeIx]?.[0]) {
584
- return BLANK_MARKER;
775
+ continue;
776
+ }
777
+ // Wrapper? Emit wrapper node + queue bare and override target.
778
+ if (stateInternal.overriddenHaltState !== null && stateInternal.bareState !== null) {
779
+ const bareState = stateInternal.bareState;
780
+ const overrideTarget = stateInternal.overriddenHaltState;
781
+ const bareInternal = bareState[STATE_INTERNAL]();
782
+ const overrideInternal = overrideTarget[STATE_INTERNAL]();
783
+ nodes[stateInternal.id] = {
784
+ id: stateInternal.id,
785
+ name: stateInternal.name, // composite name like "A(target)"
786
+ isHalt: false,
787
+ isHaltMarker: false,
788
+ isWrapper: true,
789
+ bareStateId: bareInternal.id,
790
+ frameId: null,
791
+ transitions: [],
792
+ overriddenHaltStateId: overrideInternal.id,
793
+ tags: [...stateInternal.tags],
794
+ };
795
+ bareIds.add(bareInternal.id);
796
+ queue.push(bareState);
797
+ queue.push(overrideTarget);
798
+ continue;
799
+ }
800
+ // Regular (or bare) state — build node with transitions.
801
+ const node = {
802
+ id: stateInternal.id,
803
+ name: stateInternal.name,
804
+ isHalt: false,
805
+ isHaltMarker: false,
806
+ isWrapper: false,
807
+ bareStateId: null,
808
+ frameId: null,
809
+ transitions: [],
810
+ overriddenHaltStateId: null,
811
+ tags: [...stateInternal.tags],
812
+ };
813
+ nodes[stateInternal.id] = node;
814
+ let patternIx = 0;
815
+ for (const [sym, { command, nextState }] of stateInternal.symbolToDataMap) {
816
+ let target;
817
+ try {
818
+ target = nextState instanceof State ? nextState : nextState.ref;
585
819
  }
586
- return `'${escapeAlphabetSymbol(s)}'`;
587
- })
588
- .join(','))
589
- .join('|');
820
+ catch {
821
+ patternIx += 1;
822
+ continue;
823
+ }
824
+ const targetInternal = target[STATE_INTERNAL]();
825
+ node.transitions.push({
826
+ pattern: decodePatternDescription(sym.description, alphabets),
827
+ command: command.tapesCommands.map((tc) => ({
828
+ symbol: decodeWriteSymbol(tc.symbol),
829
+ movement: decodeMovement(tc.movement.description),
830
+ })),
831
+ nextStateId: targetInternal.id,
832
+ // Transition id format: `${stateId}.${transitionIx}` (#205).
833
+ // Matches `TuringMachine.runStepByStep`'s `MachineState.
834
+ // matchedTransition.id` so consumers can do
835
+ // `graph.nodes[stateId].transitions.find(t => t.id === id)`.
836
+ // Was `${stateId}-${ix}` pre-#205 — the `.` separator avoids
837
+ // the hyphen reading as a minus sign next to negative halt-
838
+ // marker ids in adjacent contexts.
839
+ id: `${stateInternal.id}.${patternIx}`,
840
+ });
841
+ queue.push(target);
842
+ patternIx += 1;
843
+ }
844
+ }
845
+ // Always emit real halt as a sentinel, even if no transition targets it.
846
+ // It anchors the `subtree -. halt .-> s0` frame-level arrow whenever a
847
+ // frame demand-emits one, and it's the canonical machine-halt singleton.
848
+ if (!(0 in nodes)) {
849
+ nodes[0] = {
850
+ id: 0,
851
+ name: 'halt',
852
+ isHalt: true,
853
+ isHaltMarker: false,
854
+ isWrapper: false,
855
+ bareStateId: null,
856
+ frameId: null,
857
+ transitions: [],
858
+ overriddenHaltStateId: null,
859
+ tags: [...haltState[STATE_INTERNAL]().tags],
860
+ };
590
861
  }
591
- catch {
592
- return description;
862
+ // Pass 2: For each bare, compute its forward-reachable set (following
863
+ // transitions; stopping at halt and at wrappers — both are frame
864
+ // boundaries).
865
+ const computeReach = (startId) => {
866
+ const reach = new Set();
867
+ const stack = [startId];
868
+ while (stack.length > 0) {
869
+ const id = stack.pop();
870
+ if (reach.has(id)) {
871
+ continue;
872
+ }
873
+ const node = nodes[id];
874
+ // `nodes[id]` is always populated for `id` that the BFS reached, so
875
+ // a defensive `!node` check would be dead. `isHalt` / `isWrapper`
876
+ // are real boundaries — both stop reach-set expansion.
877
+ /* c8 ignore next 3 — defensive: the push site below already filters
878
+ halt/wrapper targets, and the initial push is always a bare, so
879
+ this branch is unreachable in practice. */
880
+ if (node.isHalt || node.isWrapper) {
881
+ continue;
882
+ }
883
+ reach.add(id);
884
+ for (const t of node.transitions) {
885
+ const target = nodes[t.nextStateId];
886
+ if (!target || target.isHalt || target.isWrapper) {
887
+ continue;
888
+ }
889
+ stack.push(t.nextStateId);
890
+ }
891
+ }
892
+ return reach;
893
+ };
894
+ const reachByBare = new Map();
895
+ for (const bareId of bareIds) {
896
+ reachByBare.set(bareId, computeReach(bareId));
897
+ }
898
+ // Pass 3: Union-find on bare overlaps. Two bares merge if their reach
899
+ // sets share any state. Canonical representative = smallest bare-id in
900
+ // the component.
901
+ const ufParent = new Map();
902
+ // Note: no path compression. The union policy below ("smaller id always
903
+ // becomes root") keeps the tree flat — every union targets bares[0] as
904
+ // the root, so any node's parent IS the root. Walking up never exceeds
905
+ // one step. Path compression would be dead code under this invariant.
906
+ const ufFind = (id) => {
907
+ if (!ufParent.has(id)) {
908
+ ufParent.set(id, id);
909
+ }
910
+ let root = id;
911
+ while (ufParent.get(root) !== root) {
912
+ root = ufParent.get(root);
913
+ }
914
+ return root;
915
+ };
916
+ const ufUnion = (a, b) => {
917
+ const ra = ufFind(a);
918
+ const rb = ufFind(b);
919
+ if (ra === rb)
920
+ return;
921
+ if (ra < rb) {
922
+ ufParent.set(rb, ra);
923
+ }
924
+ else {
925
+ ufParent.set(ra, rb);
926
+ }
927
+ };
928
+ for (const bareId of bareIds) {
929
+ ufFind(bareId);
930
+ }
931
+ // For each state, collect the bares that reach it; union all bares that
932
+ // share a state.
933
+ const stateToReachingBares = new Map();
934
+ for (const [bareId, reachSet] of reachByBare) {
935
+ for (const stateId of reachSet) {
936
+ let bares = stateToReachingBares.get(stateId);
937
+ if (!bares) {
938
+ bares = [];
939
+ stateToReachingBares.set(stateId, bares);
940
+ }
941
+ bares.push(bareId);
942
+ }
943
+ }
944
+ for (const bares of stateToReachingBares.values()) {
945
+ for (let i = 1; i < bares.length; i += 1) {
946
+ ufUnion(bares[0], bares[i]);
947
+ }
948
+ }
949
+ // Assign frameId to each in-reach state.
950
+ const frameIds = new Set();
951
+ for (const [stateId, bares] of stateToReachingBares) {
952
+ const frameId = ufFind(bares[0]);
953
+ nodes[stateId].frameId = frameId;
954
+ frameIds.add(frameId);
955
+ }
956
+ // Pass 4: Retarget halt-bound transitions for in-frame states to the
957
+ // frame's halt marker. Out-of-frame states (top-level dispatcher, override
958
+ // targets, etc.) keep their halt-bound transitions pointing at real halt.
959
+ for (const node of Object.values(nodes)) {
960
+ if (node.frameId === null) {
961
+ continue;
962
+ }
963
+ const haltMarkerId = -node.frameId;
964
+ for (const t of node.transitions) {
965
+ const target = nodes[t.nextStateId];
966
+ if (target && target.isHalt && !target.isHaltMarker) {
967
+ t.nextStateId = haltMarkerId;
968
+ }
969
+ }
593
970
  }
594
- }
595
- function decodeMovement(description) {
596
- if (!description) {
597
- return '?';
971
+ // Pass 5: Emit one halt marker per frame.
972
+ for (const frameId of frameIds) {
973
+ const haltMarkerId = -frameId;
974
+ nodes[haltMarkerId] = {
975
+ id: haltMarkerId,
976
+ name: 'halt',
977
+ isHalt: true,
978
+ isHaltMarker: true,
979
+ isWrapper: false,
980
+ bareStateId: null,
981
+ frameId,
982
+ transitions: [],
983
+ overriddenHaltStateId: null,
984
+ tags: [],
985
+ };
598
986
  }
599
- return movementDescriptionToLabel[description] ?? description;
987
+ return { initialId: initialState[STATE_INTERNAL]().id, alphabets, nodes };
600
988
  }
601
- function splitUnescaped(s, sep) {
602
- const parts = [];
603
- let current = '';
604
- let i = 0;
605
- while (i < s.length) {
606
- if (s[i] === '\\' && i + 1 < s.length) {
607
- current += s[i + 1];
608
- i += 2;
989
+ /**
990
+ * Inverse of `toGraph`: rebuilds a State graph (and a fresh TapeBlock with
991
+ * the graph's alphabets) from a serialized Graph. Round-trips with `toGraph`
992
+ * in the sense that running the rebuilt machine on the same input gives the
993
+ * same output, but the rebuilt State instances have *new* internal IDs.
994
+ *
995
+ * Under the v7 callable-subtree model (#174), graph nodes split into:
996
+ * - Wrapper nodes (`isWrapper: true`, no transitions) — reconstructed via
997
+ * `bareStates[bareStateId].withOverriddenHaltState(finalStates[overriddenHaltStateId])`.
998
+ * - Bare/regular nodes — constructed as normal States with transitions.
999
+ * - Halt + halt-marker nodes — collapse to the singleton `haltState`.
1000
+ */
1001
+ function fromGraph(graph) {
1002
+ const alphabetObjs = graph.alphabets.map((syms) => new Alphabet(syms));
1003
+ const tapeBlock = TapeBlock.fromAlphabets(alphabetObjs);
1004
+ const ids = Object.keys(graph.nodes).map(Number);
1005
+ // Pass 1: pre-create a Reference for each non-halt non-halt-marker node
1006
+ // (both wrappers and regulars). Halt and halt-marker nodes collapse to the
1007
+ // singleton `haltState` and need no ref.
1008
+ const refs = {};
1009
+ for (const nodeId of ids) {
1010
+ const node = graph.nodes[nodeId];
1011
+ if (!node.isHalt) {
1012
+ refs[nodeId] = new Reference();
1013
+ }
1014
+ }
1015
+ // Convert a parsed pattern back to the symbol key the State expects.
1016
+ const patternToKey = (parsed) => {
1017
+ if (parsed === null) {
1018
+ return ifOtherSymbol;
1019
+ }
1020
+ const flat = [];
1021
+ for (const row of parsed) {
1022
+ for (const cell of row) {
1023
+ flat.push(cell === null ? ifOtherSymbol : cell);
1024
+ }
1025
+ }
1026
+ return tapeBlock.symbol(flat);
1027
+ };
1028
+ // Pass 2: build a State for each non-wrapper non-halt non-halt-marker
1029
+ // node. Transitions point at refs so cycles work; haltState (and halt
1030
+ // markers, which collapse to haltState) are used directly.
1031
+ const bareStates = {};
1032
+ for (const nodeId of ids) {
1033
+ const node = graph.nodes[nodeId];
1034
+ if (node.isHalt || node.isWrapper) {
1035
+ continue;
609
1036
  }
610
- else if (s[i] === sep) {
611
- parts.push(current);
612
- current = '';
613
- i += 1;
1037
+ const stateDefinition = {};
1038
+ for (const t of node.transitions) {
1039
+ const key = patternToKey(parsePatternString(t.pattern, graph.alphabets));
1040
+ const target = graph.nodes[t.nextStateId];
1041
+ const nextState = !target || target.isHalt
1042
+ ? haltState
1043
+ : refs[t.nextStateId];
1044
+ stateDefinition[key] = {
1045
+ command: t.command.map((c) => ({
1046
+ symbol: parseWriteSymbolLabel(c.symbol),
1047
+ movement: parseMovementLabel(c.movement),
1048
+ })),
1049
+ nextState,
1050
+ };
1051
+ }
1052
+ // Graph-sourced names may contain `(` and `)` (composite wrapper names —
1053
+ // although wrappers go through a separate path below, defensive
1054
+ // construction here keeps the bypass uniform). Construct without a name
1055
+ // and assign `name` directly through the internal accessor's setter to
1056
+ // skip the constructor's user-facing name validation.
1057
+ const bare = new State(stateDefinition);
1058
+ bare[STATE_INTERNAL]().name = node.name;
1059
+ if (node.tags.length > 0) {
1060
+ bare.tag(...node.tags);
1061
+ }
1062
+ bareStates[nodeId] = bare;
1063
+ }
1064
+ // Pass 3: resolve every node to its final State (memoized + cycle-safe).
1065
+ // Wrappers compose lazily via `withOverriddenHaltState` once their bare
1066
+ // and override are resolved.
1067
+ const finalStates = {};
1068
+ const inProgress = new Set();
1069
+ const getFinal = (nodeId) => {
1070
+ if (finalStates[nodeId]) {
1071
+ return finalStates[nodeId];
1072
+ }
1073
+ const node = graph.nodes[nodeId];
1074
+ if (!node || node.isHalt) {
1075
+ finalStates[nodeId] = haltState;
1076
+ return haltState;
1077
+ }
1078
+ if (inProgress.has(nodeId)) {
1079
+ throw new Error(`override-halt cycle at state #${nodeId}`);
1080
+ }
1081
+ inProgress.add(nodeId);
1082
+ let state;
1083
+ if (node.isWrapper) {
1084
+ const bare = getFinal(node.bareStateId);
1085
+ const override = getFinal(node.overriddenHaltStateId);
1086
+ state = bare.withOverriddenHaltState(override);
1087
+ // Apply wrapper-scoped tags (#186). Tags don't leak across wrappers
1088
+ // sharing a bare — the wrapper instance owns its own tag set, and
1089
+ // engine #175 memoization returns the same instance for the same
1090
+ // (bare, override) pair, so this is idempotent across rebuilds.
1091
+ if (node.tags.length > 0) {
1092
+ state.tag(...node.tags);
1093
+ }
614
1094
  }
615
1095
  else {
616
- current += s[i];
617
- i += 1;
1096
+ state = bareStates[nodeId];
618
1097
  }
1098
+ inProgress.delete(nodeId);
1099
+ finalStates[nodeId] = state;
1100
+ return state;
1101
+ };
1102
+ for (const nodeId of ids) {
1103
+ getFinal(nodeId);
619
1104
  }
620
- parts.push(current);
621
- return parts;
622
- }
623
- function parsePatternString(s, alphabets) {
624
- if (s === IF_OTHER_MARKER) {
625
- return null;
1105
+ // Pass 4: bind each ref to the resolved final State so cross-node
1106
+ // transitions land on the right instance.
1107
+ for (const nodeId of ids) {
1108
+ if (!graph.nodes[nodeId].isHalt) {
1109
+ refs[nodeId].bind(finalStates[nodeId]);
1110
+ }
626
1111
  }
627
- const alternatives = splitUnescaped(s, '|');
628
- return alternatives.map((alt) => {
629
- const cells = splitUnescaped(alt, ',');
630
- return cells.map((cell, tapeIx) => {
631
- if (cell === IF_OTHER_MARKER) {
632
- return null;
633
- }
634
- if (cell === BLANK_MARKER) {
635
- return alphabets[tapeIx]?.[0] ?? cell;
1112
+ return {
1113
+ start: finalStates[graph.initialId],
1114
+ tapeBlock,
1115
+ states: finalStates,
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Returns a `Map<number, {state, transitionSymbols}>` keyed by engine
1120
+ * `GraphNode.id`, giving downstream tooling direct access to the `State`
1121
+ * instance + per-pattern Symbol references for breakpoint setup (#195).
1122
+ *
1123
+ * **Positional alignment contract.** For any `GraphTransition` whose id
1124
+ * is `${N}.${K}` (#205 changed the separator from `-` to `.`),
1125
+ * `result.get(N)!.transitionSymbols[K]` is the Symbol
1126
+ * the transition fires on (reference equality, not structural). The K-th
1127
+ * entry is the K-th key from the source State's `#symbolToDataMap` in
1128
+ * insertion order, including `ifOtherSymbol` when the user wrote one.
1129
+ * Consumers filtering the catch-all path identity-compare against the
1130
+ * engine-exported `ifOtherSymbol`.
1131
+ *
1132
+ * **Unbound-`Reference` slots.** `toGraph` increments `patternIx` even
1133
+ * when a transition's `nextState` is an unresolved `Reference` (it
1134
+ * `continue`s without pushing the GraphTransition). In that case
1135
+ * `transitionSymbols[K]` is still set to the K-th Map key, but no
1136
+ * `Graph.nodes[N].transitions` entry exists with id `${N}.${K}`. Sparse
1137
+ * on the Graph side, dense on the `transitionSymbols` side — same
1138
+ * indexing.
1139
+ *
1140
+ * **Coverage.** Map keys are the State-backed subset of `graph.nodes`:
1141
+ * regulars + bares + wrappers + the halt singleton (id `0`). Synthetic
1142
+ * halt markers (id `-frameId`) are excluded — they all reach the same
1143
+ * `haltState` object at runtime, and the named consumer
1144
+ * ([machines-demo#37](https://github.com/mellonis/machines-demo/issues/37))
1145
+ * surfaces halt-pause via a separate UI control, not via clicks on
1146
+ * halt glyphs. If a future consumer needs uniform-by-id lookup, the
1147
+ * helper can be extended additively.
1148
+ *
1149
+ * **Halt-singleton warning.** `result.get(0)!.state === haltState` — the
1150
+ * process-wide halt. Toggling `.debug` on that entry affects every
1151
+ * machine in the runtime, not just the one this map was built from.
1152
+ */
1153
+ function collectStates(initialState, tapeBlock) {
1154
+ // Anchor on toGraph's authoritative id set — it knows the canonical
1155
+ // ordering of wrapper/bare/regular emission and which nodes are
1156
+ // synthetic halt markers we have to skip. Building our own BFS would
1157
+ // duplicate that logic; reusing the Graph guarantees collectStates'
1158
+ // id keys never drift from toGraph's GraphTransition ids.
1159
+ const graph = toGraph(initialState, tapeBlock);
1160
+ // Walk the State graph to associate each State instance with its
1161
+ // engine id. The shape mirrors toGraph's Pass 1 — visit by id, branch
1162
+ // on halt / wrapper / regular — but only collects the (id → State)
1163
+ // mapping. Lighter than re-running the union-find passes; no
1164
+ // GraphNode construction.
1165
+ const stateById = new Map();
1166
+ const visited = new Set();
1167
+ const queue = [initialState];
1168
+ while (queue.length > 0) {
1169
+ const state = queue.shift();
1170
+ const internal = state[STATE_INTERNAL]();
1171
+ if (visited.has(internal.id))
1172
+ continue;
1173
+ visited.add(internal.id);
1174
+ stateById.set(internal.id, state);
1175
+ if (state.isHalt)
1176
+ continue;
1177
+ if (internal.bareState !== null && internal.overriddenHaltState !== null) {
1178
+ queue.push(internal.bareState);
1179
+ queue.push(internal.overriddenHaltState);
1180
+ continue;
1181
+ }
1182
+ for (const { nextState } of internal.symbolToDataMap.values()) {
1183
+ let target;
1184
+ try {
1185
+ target = nextState instanceof State ? nextState : nextState.ref;
636
1186
  }
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);
1187
+ catch {
1188
+ continue; // unbound Reference skip silently, matches toGraph
641
1189
  }
642
- return cell;
643
- });
644
- });
645
- }
646
- const movementLabelToSymbol = {
647
- L: movements.left,
648
- R: movements.right,
649
- S: movements.stay,
650
- };
651
- function parseMovementLabel(label) {
652
- const m = movementLabelToSymbol[label];
653
- if (!m) {
654
- throw new Error(`unknown movement label: ${label}`);
655
- }
656
- return m;
657
- }
658
- function parseWriteSymbolLabel(label) {
659
- if (label === 'K') {
660
- return symbolCommands.keep;
661
- }
662
- if (label === 'E') {
663
- return symbolCommands.erase;
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);
1190
+ queue.push(target);
1191
+ }
669
1192
  }
670
- return label;
671
- }
672
- function decodeWriteSymbol(symbol) {
673
- if (typeof symbol === 'symbol') {
674
- const description = symbol.description ?? '?';
675
- return symbolCommandDescriptionToLabel[description] ?? description;
1193
+ // Build the result by iterating graph.nodes — the authoritative id set
1194
+ // minus halt markers — and dispatching on node kind. The halt singleton
1195
+ // entry's `state` reads from `stateById` (the BFS visited haltState if
1196
+ // any path reached it) but falls back to the module-level singleton
1197
+ // for graphs whose only halt presence is the always-emitted sentinel.
1198
+ const result = new Map();
1199
+ for (const idStr of Object.keys(graph.nodes)) {
1200
+ const id = Number(idStr);
1201
+ const node = graph.nodes[id];
1202
+ if (node.isHaltMarker)
1203
+ continue; // synthetic; collapses to haltState at id 0
1204
+ if (node.isHalt) {
1205
+ // The real halt — always the engine-wide singleton. Prefer the
1206
+ // BFS-visited instance for identity-equality with whatever the
1207
+ // caller has; fall back to the module singleton when the BFS
1208
+ // didn't reach haltState (toGraph emits id 0 unconditionally).
1209
+ result.set(id, {
1210
+ state: stateById.get(0) ?? haltState,
1211
+ transitionSymbols: [],
1212
+ });
1213
+ continue;
1214
+ }
1215
+ if (node.isWrapper) {
1216
+ result.set(id, {
1217
+ state: stateById.get(id),
1218
+ transitionSymbols: [],
1219
+ });
1220
+ continue;
1221
+ }
1222
+ // Regular or bare State — enumerate `#symbolToDataMap.keys()` for
1223
+ // the patternIx alignment. The K-th key is the Symbol that
1224
+ // `${id}.${K}` GraphTransition fires on (positional contract).
1225
+ const state = stateById.get(id);
1226
+ const transitionSymbols = [...state[STATE_INTERNAL]().symbolToDataMap.keys()];
1227
+ result.set(id, { state, transitionSymbols });
676
1228
  }
677
- return `'${symbol}'`;
1229
+ return result;
678
1230
  }
679
- // Format converters (toMermaid / fromMermaid) live in ./graphFormats.
1231
+ // Note on the import cycle with `State.ts`: stateGraph.ts value-imports
1232
+ // `State`, `STATE_INTERNAL`, `haltState`, and `ifOtherSymbol`; State.ts
1233
+ // value-imports `toGraph` and `fromGraph` for its static-method delegates.
1234
+ // ESM resolves cycles via live bindings — both modules see each other's
1235
+ // exports as long as nothing at module-load reads a binding before its
1236
+ // source module finishes evaluating. All references here live inside
1237
+ // function bodies, so the cycle is safe.
680
1238
 
681
1239
  var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
682
1240
  if (kind === "m") throw new TypeError("Private method is not writable");
@@ -689,11 +1247,33 @@ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) |
689
1247
  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");
690
1248
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
691
1249
  };
692
- var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef, _State_tags;
1250
+ var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_instances, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef, _State_haltDebug, _State_tags, _State_getEntry;
693
1251
  const ifOtherSymbol = Symbol('other symbol');
694
1252
  // Module-private symbol used by DebugConfig setters to call State's validator
695
1253
  // without exposing the validator on the public surface.
696
1254
  const validateDebugFilter = Symbol('validateDebugFilter');
1255
+ /**
1256
+ * @internal
1257
+ *
1258
+ * Package-private accessor key for sibling modules in
1259
+ * `packages/machine/src` (e.g. `utilities/stateGraph.ts`, and the planned
1260
+ * `utilities/stateCollect.ts` for #195). Re-exported from this module so
1261
+ * sibling files can import it; intentionally NOT re-exported from the
1262
+ * package's public `index.ts`, so downstream consumers don't see it on
1263
+ * the supported surface.
1264
+ *
1265
+ * Calling `state[STATE_INTERNAL]()` returns a getter/setter view onto the
1266
+ * State's private fields. Reads are live (they close over `this`), so the
1267
+ * view stays in sync with subsequent mutations on the State. There's one
1268
+ * mutating setter on the view — `name` — used exclusively by
1269
+ * `fromGraph` to assign graph-sourced composite names (e.g. `A(target)`)
1270
+ * that the public name validator would reject; see the JSDoc on the
1271
+ * accessor itself.
1272
+ *
1273
+ * Designed in #180 with #195 in mind so its surface doesn't need to grow
1274
+ * when `collectStates` lands.
1275
+ */
1276
+ const STATE_INTERNAL = Symbol('State.internal');
697
1277
  class DebugConfig {
698
1278
  constructor(ownerState, initial) {
699
1279
  _DebugConfig_ownerState.set(this, void 0);
@@ -733,6 +1313,7 @@ class DebugConfig {
733
1313
  _DebugConfig_ownerState = new WeakMap(), _DebugConfig_before = new WeakMap(), _DebugConfig_after = new WeakMap();
734
1314
  class State {
735
1315
  constructor(stateDefinition = null, name) {
1316
+ _State_instances.add(this);
736
1317
  _State_id.set(this, id(this));
737
1318
  // Not `readonly` because `withOverriddenHaltState` and `fromGraph` set the
738
1319
  // composed name on a no-arg `new State()` to bypass the constructor's
@@ -750,6 +1331,14 @@ class State {
750
1331
  // Note: toGraph / fromGraph deliberately do not serialize debug — debug is
751
1332
  // a runtime concern, not part of the structural graph.
752
1333
  _State_debugRef.set(this, { current: null });
1334
+ // Storage for `haltState.debug` (#207). haltState is a singleton terminal
1335
+ // state — it has no iter of its own, so the per-side `{ before, after }`
1336
+ // DebugConfig shape doesn't model anything meaningful for it. Instead the
1337
+ // halt breakpoint is a single boolean ("enabled / disabled"). The pause
1338
+ // anchors on the iter whose transition LEADS to halt, fired at end-of-iter
1339
+ // (after that iter's own after-pause if armed). Only used when `isHalt`;
1340
+ // ignored on every other State (whose `#debugRef` flow is unchanged).
1341
+ _State_haltDebug.set(this, false);
753
1342
  // Out-of-band tags applied to this State (#186). Tags are visualization
754
1343
  // and debugger-tooling metadata — they don't affect runtime transition
755
1344
  // lookup or `equivalentOn` comparisons. Stored as a Set for de-duplication;
@@ -820,6 +1409,17 @@ class State {
820
1409
  return this;
821
1410
  }
822
1411
  get debug() {
1412
+ // haltState (#207): the canonical access path is the `haltState` singleton
1413
+ // export, which is typed `HaltState` — its `debug` getter is narrowed to
1414
+ // `boolean`. Generic `State` references statically see `DebugConfig` and
1415
+ // (in practice) never refer to haltState — the run loop's `state` is
1416
+ // never haltState because halt is terminal and doesn't iterate. The cast
1417
+ // below makes the runtime boolean return type-compatible with the
1418
+ // declared `DebugConfig` for any rare caller that holds a State
1419
+ // reference happening to be haltState.
1420
+ if (this.isHalt) {
1421
+ return __classPrivateFieldGet$1(this, _State_haltDebug, "f");
1422
+ }
823
1423
  // Lazy-init: `state.debug` is never null at read time, so chained writes
824
1424
  // like `state.debug.before = true` work on a fresh state without a prior
825
1425
  // whole-object assignment. The setter still accepts `null` to reset the
@@ -830,16 +1430,47 @@ class State {
830
1430
  }
831
1431
  return __classPrivateFieldGet$1(this, _State_debugRef, "f").current;
832
1432
  }
1433
+ // TS signature: non-halt callers (generic `State` reference) get the
1434
+ // `DebugConfig | object | null` surface; boolean is rejected statically.
1435
+ // The `HaltState` typed alias on the singleton export overrides this to
1436
+ // `boolean | null` for the canonical halt access path. Runtime checks
1437
+ // below are defensive against type-bypass / mixed-source callers.
833
1438
  set debug(value) {
834
- if (value === null) {
1439
+ // Defensive runtime cast: TS signature excludes boolean for the generic
1440
+ // State surface, but haltState (via the HaltState alias) DOES accept
1441
+ // boolean, and the runtime needs to handle it for the singleton path.
1442
+ const v = value;
1443
+ // haltState (#207): only `boolean | null` is accepted. `null` aliases
1444
+ // to `false` (reset). Any object-shaped write throws at write-time so
1445
+ // misuse surfaces immediately rather than silently no-op'ing — the
1446
+ // `{before, after}` shape doesn't model anything meaningful for halt
1447
+ // (no own iter to anchor on; halt is terminal).
1448
+ if (this.isHalt) {
1449
+ if (v === null || typeof v === 'boolean') {
1450
+ __classPrivateFieldSet$1(this, _State_haltDebug, v === true, "f");
1451
+ return;
1452
+ }
1453
+ throw new Error('haltState.debug only accepts boolean (or null to reset). Use '
1454
+ + '`haltState.debug = true` to enable the halt breakpoint, false to '
1455
+ + 'disable. The pause fires after the iter whose transition leads to '
1456
+ + 'halt (post-iter, before halt processing).');
1457
+ }
1458
+ // Non-halt states: boolean writes are rejected — the per-side
1459
+ // `{before, after}` granularity is the contract. A boolean shortcut
1460
+ // would hide the asymmetry between before / after.
1461
+ if (typeof v === 'boolean') {
1462
+ throw new Error('state.debug only accepts a DebugConfig or `{ before, after }` object '
1463
+ + '(or null to reset). Boolean assignment is reserved for `haltState`.');
1464
+ }
1465
+ if (v === null) {
835
1466
  __classPrivateFieldGet$1(this, _State_debugRef, "f").current = null;
836
1467
  return;
837
1468
  }
838
- if (value instanceof DebugConfig) {
839
- __classPrivateFieldGet$1(this, _State_debugRef, "f").current = value;
1469
+ if (v instanceof DebugConfig) {
1470
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = v;
840
1471
  return;
841
1472
  }
842
- __classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, value);
1473
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, v);
843
1474
  }
844
1475
  /**
845
1476
  * Add one or more tags to this State (#186). Tags are out-of-band metadata
@@ -871,25 +1502,15 @@ class State {
871
1502
  get tags() {
872
1503
  return Object.freeze([...__classPrivateFieldGet$1(this, _State_tags, "f")]);
873
1504
  }
874
- /** @internal — invoked by DebugConfig setters via module-private symbol. */
875
- [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), _State_tags = new WeakMap(), validateDebugFilter)](fieldName, filter) {
1505
+ /** @internal — invoked by DebugConfig setters via module-private symbol.
1506
+ * Per #207, haltState no longer flows through DebugConfig (its `debug`
1507
+ * setter rejects object writes before construction), so the validator
1508
+ * only sees non-halt states here. */
1509
+ [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), _State_haltDebug = new WeakMap(), _State_tags = new WeakMap(), _State_instances = new WeakSet(), validateDebugFilter)](fieldName, filter) {
876
1510
  if (filter === undefined)
877
1511
  return;
878
- // #108 part 2: `.after` on haltState has no semantic anchor — halt is
879
- // terminal, so there is no iteration-after-halt for an after-fire to
880
- // attach to. Reject any truthy assignment (true OR list) at write time
881
- // so misuse surfaces immediately rather than silently no-op'ing.
882
- if (this.isHalt && fieldName === 'after') {
883
- throw new Error('haltState.debug.after is not supported: halt is terminal, so there is '
884
- + 'no iteration-after-halt for an after-fire to anchor on. Use '
885
- + '{ before: true } to pause on halt entry.');
886
- }
887
1512
  if (filter === true)
888
1513
  return;
889
- // haltState has no own transitions; symbol-list filters on `before` are
890
- // silent no-ops at the engine level (spec §8.6), so accept any list shape.
891
- if (this.isHalt)
892
- return;
893
1514
  for (const sym of filter) {
894
1515
  if (sym !== ifOtherSymbol && !__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").has(sym)) {
895
1516
  throw new Error(`State.debug.${fieldName}: symbol is not a transition key of this state `
@@ -908,16 +1529,40 @@ class State {
908
1529
  return ifOtherSymbol;
909
1530
  }
910
1531
  getCommand(symbol) {
911
- if (__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").has(symbol)) {
912
- return __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").get(symbol).command;
913
- }
914
- throw new Error(`No command for symbol at state named ${__classPrivateFieldGet$1(this, _State_name, "f")}`);
1532
+ return __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol).command;
915
1533
  }
916
1534
  getNextState(symbol) {
917
- if (__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").has(symbol)) {
918
- return __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").get(symbol).nextState;
919
- }
920
- throw new Error(`No nextState for symbol at state named ${__classPrivateFieldGet$1(this, _State_id, "f")}`);
1535
+ return __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol).nextState;
1536
+ }
1537
+ /**
1538
+ * Like `getNextState`, but also returns the matched Symbol and its index
1539
+ * in this State's transition declaration order (= the `K` in `toGraph`'s
1540
+ * `${stateId}.${K}` transition ids). Used by `TuringMachine.runStepByStep`
1541
+ * to populate `MachineState.matchedTransition` for #205 — exposes which
1542
+ * transition fired so consumers (UIs, log tools, coverage maps) can
1543
+ * resolve the firing edge without re-deriving from `(source, nextState)`,
1544
+ * which is ambiguous when multiple transitions on the same source go to
1545
+ * the same destination.
1546
+ *
1547
+ * Throws (matching `getNextState` / `getCommand`) when no entry exists for
1548
+ * the symbol. For wrappers (states produced by `withOverriddenHaltState`):
1549
+ * the symbol-to-data map is shared with the bare via `bareState`, so the
1550
+ * returned `ix` is a valid position into BOTH the wrapper's and the
1551
+ * bare's transition iteration order — they're the same map.
1552
+ */
1553
+ getMatchedTransition(symbol) {
1554
+ const entry = __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol);
1555
+ // Iteration order on a Map is insertion order; index lookup is O(N),
1556
+ // acceptable since this fires at most once per iter and N (transitions
1557
+ // per state) is typically tiny. If hot-path measurement ever flags it,
1558
+ // cache as `#symbolToIxMap` mirror.
1559
+ let ix = 0;
1560
+ for (const key of __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").keys()) {
1561
+ if (key === symbol)
1562
+ break;
1563
+ ix += 1;
1564
+ }
1565
+ return { nextState: entry.nextState, matchedSymbol: symbol, ix };
921
1566
  }
922
1567
  withOverriddenHaltState(overriddenHaltState) {
923
1568
  // Unwrap `this` if it's itself a wrapper — the chain's inner overrides
@@ -956,6 +1601,53 @@ class State {
956
1601
  innerCache.set(overriddenHaltState, new WeakRef(state));
957
1602
  return state;
958
1603
  }
1604
+ /**
1605
+ * @internal
1606
+ *
1607
+ * Package-private getter/setter view onto this State's private fields,
1608
+ * for sibling modules in `packages/machine/src` (currently `stateGraph.ts`
1609
+ * for `toGraph` / `fromGraph`, and the planned `stateCollect.ts` for
1610
+ * #195's `collectStates`).
1611
+ *
1612
+ * Read access is live — the getters close over `this`, so the view
1613
+ * stays in sync with subsequent mutations on this State. There's a
1614
+ * single mutating setter on the view, `name`, which exists to let
1615
+ * `fromGraph` assign graph-sourced composite names (e.g. `A(target)`)
1616
+ * to freshly-constructed bare States. The constructor's name validator
1617
+ * rejects parens (reserved as wrapper-composition delimiters in
1618
+ * `withOverriddenHaltState`); the setter intentionally bypasses that
1619
+ * check because the same delimiters appear in legitimate wrapper-bare
1620
+ * names round-tripped through the graph.
1621
+ *
1622
+ * Returns a fresh view object on every call — cheap enough for the
1623
+ * BFS-once-per-build callers, and avoids holding a reference object on
1624
+ * every State instance. Keep this surface tight: callers should only
1625
+ * read what they need. Adding fields here is a deliberate decision —
1626
+ * each adds to the implicit contract sibling modules can rely on.
1627
+ */
1628
+ [(_State_getEntry = function _State_getEntry(symbol) {
1629
+ const entry = __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").get(symbol);
1630
+ if (entry === undefined) {
1631
+ throw new Error(`No transition for symbol at state named ${__classPrivateFieldGet$1(this, _State_name, "f")}`);
1632
+ }
1633
+ return entry;
1634
+ }, STATE_INTERNAL)]() {
1635
+ // Aliasing `this` so the nested object-literal getters/setters below
1636
+ // can read/write the enclosing State's private fields — getters in an
1637
+ // object literal can't be arrow functions, so the standard arrow-
1638
+ // captures-`this` trick doesn't apply here.
1639
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
1640
+ const self = this;
1641
+ return {
1642
+ get id() { return __classPrivateFieldGet$1(self, _State_id, "f"); },
1643
+ get name() { return __classPrivateFieldGet$1(self, _State_name, "f"); },
1644
+ set name(v) { __classPrivateFieldSet$1(self, _State_name, v, "f"); },
1645
+ get bareState() { return __classPrivateFieldGet$1(self, _State_bareState, "f"); },
1646
+ get overriddenHaltState() { return __classPrivateFieldGet$1(self, _State_overriddenHaltState, "f"); },
1647
+ get symbolToDataMap() { return __classPrivateFieldGet$1(self, _State_symbolToDataMap, "f"); },
1648
+ get tags() { return __classPrivateFieldGet$1(self, _State_tags, "f"); },
1649
+ };
1650
+ }
959
1651
  // Single-state introspection — no traversal, no tapeBlock required.
960
1652
  // Returns id, name, halt-status, override-halt target, and the list of
961
1653
  // transitions out of this state with decoded write/movement labels.
@@ -990,382 +1682,36 @@ class State {
990
1682
  transitions,
991
1683
  };
992
1684
  }
993
- // Walks the State graph and emits a `Graph` data structure. v7 callable-
994
- // subtree emit shape (#174):
995
- //
996
- // Each `withOverriddenHaltState` wrapper produces TWO graph nodes:
997
- // - A wrapper node (`isWrapper: true`, `[[composite-name]]` shape) — the
998
- // call site. No transitions of its own. `bareStateId` points to the
999
- // bare's GraphNode; `overriddenHaltStateId` points to the override
1000
- // target's GraphNode.
1001
- // - A bare node (`isWrapper: false`, regular shape) — the callable body.
1002
- // Has the bare's transitions. Shared across all wrappers that wrap
1003
- // this bare (no per-context duplication).
1004
- //
1005
- // Frames are computed via union-find on bare reachability: two bares whose
1006
- // forward-reachable sets overlap merge into one frame. Each frame contains
1007
- // its bares + body states + a single halt marker (id = `-frameId`). The
1008
- // canonical `frameId` is the smallest bare-id in the component.
1009
- //
1010
- // Halt-bound transitions of any in-frame state are retargeted to the
1011
- // frame's halt marker. The frame's `subtree -. return .-> wrapper` and
1012
- // `subtree -. halt .-> s0` arrows are demand-emitted by `toMermaid` from
1013
- // the frame structure; they're not stored as graph edges.
1685
+ /**
1686
+ * Walks the reachable State graph from `initialState` and returns a
1687
+ * serializable `Graph`. Thin delegate to `utilities/stateGraph.ts`'s
1688
+ * `toGraph` (extracted in #180); see that module for the BFS shape and
1689
+ * v7 callable-subtree emit semantics.
1690
+ */
1014
1691
  static toGraph(initialState, tapeBlock) {
1015
- const nodes = {};
1016
- const alphabets = tapeBlock.alphabets.map((alphabet) => alphabet.symbols);
1017
- // Pass 1: BFS-discover all reachable States; emit one GraphNode per State
1018
- // (wrapper or bare/regular). Wrappers and bares are separate nodes.
1019
- const visited = new Set();
1020
- const queue = [initialState];
1021
- const bareIds = new Set(); // ids referenced as a wrapper's bareStateId
1022
- while (queue.length > 0) {
1023
- const state = queue.shift();
1024
- if (visited.has(__classPrivateFieldGet$1(state, _State_id, "f"))) {
1025
- continue;
1026
- }
1027
- visited.add(__classPrivateFieldGet$1(state, _State_id, "f"));
1028
- if (state.isHalt) {
1029
- if (!(0 in nodes)) {
1030
- nodes[0] = {
1031
- id: 0,
1032
- name: __classPrivateFieldGet$1(state, _State_name, "f"),
1033
- isHalt: true,
1034
- isHaltMarker: false,
1035
- isWrapper: false,
1036
- bareStateId: null,
1037
- frameId: null,
1038
- transitions: [],
1039
- overriddenHaltStateId: null,
1040
- tags: [...__classPrivateFieldGet$1(state, _State_tags, "f")],
1041
- };
1042
- }
1043
- continue;
1044
- }
1045
- // Wrapper? Emit wrapper node + queue bare and override target.
1046
- if (__classPrivateFieldGet$1(state, _State_overriddenHaltState, "f") !== null && __classPrivateFieldGet$1(state, _State_bareState, "f") !== null) {
1047
- const bareState = __classPrivateFieldGet$1(state, _State_bareState, "f");
1048
- const overrideTarget = __classPrivateFieldGet$1(state, _State_overriddenHaltState, "f");
1049
- nodes[__classPrivateFieldGet$1(state, _State_id, "f")] = {
1050
- id: __classPrivateFieldGet$1(state, _State_id, "f"),
1051
- name: __classPrivateFieldGet$1(state, _State_name, "f"), // composite name like "A(target)"
1052
- isHalt: false,
1053
- isHaltMarker: false,
1054
- isWrapper: true,
1055
- bareStateId: __classPrivateFieldGet$1(bareState, _State_id, "f"),
1056
- frameId: null,
1057
- transitions: [],
1058
- overriddenHaltStateId: __classPrivateFieldGet$1(overrideTarget, _State_id, "f"),
1059
- tags: [...__classPrivateFieldGet$1(state, _State_tags, "f")],
1060
- };
1061
- bareIds.add(__classPrivateFieldGet$1(bareState, _State_id, "f"));
1062
- queue.push(bareState);
1063
- queue.push(overrideTarget);
1064
- continue;
1065
- }
1066
- // Regular (or bare) state — build node with transitions.
1067
- const node = {
1068
- id: __classPrivateFieldGet$1(state, _State_id, "f"),
1069
- name: __classPrivateFieldGet$1(state, _State_name, "f"),
1070
- isHalt: false,
1071
- isHaltMarker: false,
1072
- isWrapper: false,
1073
- bareStateId: null,
1074
- frameId: null,
1075
- transitions: [],
1076
- overriddenHaltStateId: null,
1077
- tags: [...__classPrivateFieldGet$1(state, _State_tags, "f")],
1078
- };
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")) {
1082
- let target;
1083
- try {
1084
- target = nextState instanceof _a ? nextState : nextState.ref;
1085
- }
1086
- catch {
1087
- patternIx += 1;
1088
- continue;
1089
- }
1090
- node.transitions.push({
1091
- pattern: decodePatternDescription(sym.description, alphabets),
1092
- command: command.tapesCommands.map((tc) => ({
1093
- symbol: decodeWriteSymbol(tc.symbol),
1094
- movement: decodeMovement(tc.movement.description),
1095
- })),
1096
- nextStateId: __classPrivateFieldGet$1(target, _State_id, "f"),
1097
- id: `${__classPrivateFieldGet$1(state, _State_id, "f")}-${patternIx}`,
1098
- });
1099
- queue.push(target);
1100
- patternIx += 1;
1101
- }
1102
- }
1103
- // Always emit real halt as a sentinel, even if no transition targets it.
1104
- // It anchors the `subtree -. halt .-> s0` frame-level arrow whenever a
1105
- // frame demand-emits one, and it's the canonical machine-halt singleton.
1106
- if (!(0 in nodes)) {
1107
- nodes[0] = {
1108
- id: 0,
1109
- name: 'halt',
1110
- isHalt: true,
1111
- isHaltMarker: false,
1112
- isWrapper: false,
1113
- bareStateId: null,
1114
- frameId: null,
1115
- transitions: [],
1116
- overriddenHaltStateId: null,
1117
- tags: [...__classPrivateFieldGet$1(haltState, _State_tags, "f")],
1118
- };
1119
- }
1120
- // Pass 2: For each bare, compute its forward-reachable set (following
1121
- // transitions; stopping at halt and at wrappers — both are frame
1122
- // boundaries).
1123
- const computeReach = (startId) => {
1124
- const reach = new Set();
1125
- const stack = [startId];
1126
- while (stack.length > 0) {
1127
- const id = stack.pop();
1128
- if (reach.has(id)) {
1129
- continue;
1130
- }
1131
- const node = nodes[id];
1132
- // `nodes[id]` is always populated for `id` that the BFS reached, so
1133
- // a defensive `!node` check would be dead. `isHalt` / `isWrapper`
1134
- // are real boundaries — both stop reach-set expansion.
1135
- if (node.isHalt || node.isWrapper) {
1136
- continue;
1137
- }
1138
- reach.add(id);
1139
- for (const t of node.transitions) {
1140
- const target = nodes[t.nextStateId];
1141
- if (!target || target.isHalt || target.isWrapper) {
1142
- continue;
1143
- }
1144
- stack.push(t.nextStateId);
1145
- }
1146
- }
1147
- return reach;
1148
- };
1149
- const reachByBare = new Map();
1150
- for (const bareId of bareIds) {
1151
- reachByBare.set(bareId, computeReach(bareId));
1152
- }
1153
- // Pass 3: Union-find on bare overlaps. Two bares merge if their reach
1154
- // sets share any state. Canonical representative = smallest bare-id in
1155
- // the component.
1156
- const ufParent = new Map();
1157
- // Note: no path compression. The union policy below ("smaller id always
1158
- // becomes root") keeps the tree flat — every union targets bares[0] as
1159
- // the root, so any node's parent IS the root. Walking up never exceeds
1160
- // one step. Path compression would be dead code under this invariant.
1161
- const ufFind = (id) => {
1162
- if (!ufParent.has(id)) {
1163
- ufParent.set(id, id);
1164
- }
1165
- let root = id;
1166
- while (ufParent.get(root) !== root) {
1167
- root = ufParent.get(root);
1168
- }
1169
- return root;
1170
- };
1171
- const ufUnion = (a, b) => {
1172
- const ra = ufFind(a);
1173
- const rb = ufFind(b);
1174
- if (ra === rb)
1175
- return;
1176
- if (ra < rb) {
1177
- ufParent.set(rb, ra);
1178
- }
1179
- else {
1180
- ufParent.set(ra, rb);
1181
- }
1182
- };
1183
- for (const bareId of bareIds) {
1184
- ufFind(bareId);
1185
- }
1186
- // For each state, collect the bares that reach it; union all bares that
1187
- // share a state.
1188
- const stateToReachingBares = new Map();
1189
- for (const [bareId, reachSet] of reachByBare) {
1190
- for (const stateId of reachSet) {
1191
- let bares = stateToReachingBares.get(stateId);
1192
- if (!bares) {
1193
- bares = [];
1194
- stateToReachingBares.set(stateId, bares);
1195
- }
1196
- bares.push(bareId);
1197
- }
1198
- }
1199
- for (const bares of stateToReachingBares.values()) {
1200
- for (let i = 1; i < bares.length; i += 1) {
1201
- ufUnion(bares[0], bares[i]);
1202
- }
1203
- }
1204
- // Assign frameId to each in-reach state.
1205
- const frameIds = new Set();
1206
- for (const [stateId, bares] of stateToReachingBares) {
1207
- const frameId = ufFind(bares[0]);
1208
- nodes[stateId].frameId = frameId;
1209
- frameIds.add(frameId);
1210
- }
1211
- // Pass 4: Retarget halt-bound transitions for in-frame states to the
1212
- // frame's halt marker. Out-of-frame states (top-level dispatcher, override
1213
- // targets, etc.) keep their halt-bound transitions pointing at real halt.
1214
- for (const node of Object.values(nodes)) {
1215
- if (node.frameId === null) {
1216
- continue;
1217
- }
1218
- const haltMarkerId = -node.frameId;
1219
- for (const t of node.transitions) {
1220
- const target = nodes[t.nextStateId];
1221
- if (target && target.isHalt && !target.isHaltMarker) {
1222
- t.nextStateId = haltMarkerId;
1223
- }
1224
- }
1225
- }
1226
- // Pass 5: Emit one halt marker per frame.
1227
- for (const frameId of frameIds) {
1228
- const haltMarkerId = -frameId;
1229
- nodes[haltMarkerId] = {
1230
- id: haltMarkerId,
1231
- name: 'halt',
1232
- isHalt: true,
1233
- isHaltMarker: true,
1234
- isWrapper: false,
1235
- bareStateId: null,
1236
- frameId,
1237
- transitions: [],
1238
- overriddenHaltStateId: null,
1239
- tags: [],
1240
- };
1241
- }
1242
- return { initialId: __classPrivateFieldGet$1(initialState, _State_id, "f"), alphabets, nodes };
1692
+ return toGraph(initialState, tapeBlock);
1243
1693
  }
1244
- // Inverse of toGraph: rebuilds a State graph (and a fresh TapeBlock with the
1245
- // graph's alphabets) from a serialized Graph. Round-trips with toGraph in
1246
- // the sense that running the rebuilt machine on the same input gives the
1247
- // same output, but the rebuilt State instances have *new* internal IDs.
1248
- //
1249
- // Under the v7 callable-subtree model (#174), graph nodes split into:
1250
- // - Wrapper nodes (`isWrapper: true`, no transitions) — reconstructed via
1251
- // `bareStates[bareStateId].withOverriddenHaltState(finalStates[overriddenHaltStateId])`.
1252
- // - Bare/regular nodes — constructed as normal States with transitions.
1253
- // - Halt + halt-marker nodes — collapse to the singleton `haltState`.
1694
+ /**
1695
+ * Inverse of `toGraph`: rebuilds a State graph and a fresh TapeBlock
1696
+ * from a serialized `Graph`. Thin delegate to `utilities/stateGraph.ts`'s
1697
+ * `fromGraph` (extracted in #180); see that module for the
1698
+ * reconstruction pass shape (Reference pre-create, bare build, wrapper
1699
+ * resolution via `withOverriddenHaltState`, ref binding).
1700
+ */
1254
1701
  static fromGraph(graph) {
1255
- const alphabetObjs = graph.alphabets.map((syms) => new Alphabet(syms));
1256
- const tapeBlock = TapeBlock.fromAlphabets(alphabetObjs);
1257
- const ids = Object.keys(graph.nodes).map(Number);
1258
- // Pass 1: pre-create a Reference for each non-halt non-halt-marker node
1259
- // (both wrappers and regulars). Halt and halt-marker nodes collapse to the
1260
- // singleton `haltState` and need no ref.
1261
- const refs = {};
1262
- for (const nodeId of ids) {
1263
- const node = graph.nodes[nodeId];
1264
- if (!node.isHalt) {
1265
- refs[nodeId] = new Reference();
1266
- }
1267
- }
1268
- // Convert a parsed pattern back to the symbol key the State expects.
1269
- const patternToKey = (parsed) => {
1270
- if (parsed === null) {
1271
- return ifOtherSymbol;
1272
- }
1273
- const flat = [];
1274
- for (const row of parsed) {
1275
- for (const cell of row) {
1276
- flat.push(cell === null ? ifOtherSymbol : cell);
1277
- }
1278
- }
1279
- return tapeBlock.symbol(flat);
1280
- };
1281
- // Pass 2: build a State for each non-wrapper non-halt non-halt-marker
1282
- // node. Transitions point at refs so cycles work; haltState (and halt
1283
- // markers, which collapse to haltState) are used directly.
1284
- const bareStates = {};
1285
- for (const nodeId of ids) {
1286
- const node = graph.nodes[nodeId];
1287
- if (node.isHalt || node.isWrapper) {
1288
- continue;
1289
- }
1290
- const stateDefinition = {};
1291
- for (const t of node.transitions) {
1292
- const key = patternToKey(parsePatternString(t.pattern, graph.alphabets));
1293
- const target = graph.nodes[t.nextStateId];
1294
- const nextState = !target || target.isHalt
1295
- ? haltState
1296
- : refs[t.nextStateId];
1297
- stateDefinition[key] = {
1298
- command: t.command.map((c) => ({
1299
- symbol: parseWriteSymbolLabel(c.symbol),
1300
- movement: parseMovementLabel(c.movement),
1301
- })),
1302
- nextState,
1303
- };
1304
- }
1305
- // Graph-sourced names may contain `(` and `)` (composite wrapper names —
1306
- // although wrappers go through a separate path below, defensive
1307
- // construction here keeps the bypass uniform). Construct without a name
1308
- // and assign `#name` directly to skip user-facing name validation.
1309
- const bare = new _a(stateDefinition);
1310
- __classPrivateFieldSet$1(bare, _State_name, node.name, "f");
1311
- if (node.tags.length > 0) {
1312
- bare.tag(...node.tags);
1313
- }
1314
- bareStates[nodeId] = bare;
1315
- }
1316
- // Pass 3: resolve every node to its final State (memoized + cycle-safe).
1317
- // Wrappers compose lazily via `withOverriddenHaltState` once their bare
1318
- // and override are resolved.
1319
- const finalStates = {};
1320
- const inProgress = new Set();
1321
- const getFinal = (nodeId) => {
1322
- if (finalStates[nodeId]) {
1323
- return finalStates[nodeId];
1324
- }
1325
- const node = graph.nodes[nodeId];
1326
- if (!node || node.isHalt) {
1327
- finalStates[nodeId] = haltState;
1328
- return haltState;
1329
- }
1330
- if (inProgress.has(nodeId)) {
1331
- throw new Error(`override-halt cycle at state #${nodeId}`);
1332
- }
1333
- inProgress.add(nodeId);
1334
- let state;
1335
- if (node.isWrapper) {
1336
- const bare = getFinal(node.bareStateId);
1337
- const override = getFinal(node.overriddenHaltStateId);
1338
- state = bare.withOverriddenHaltState(override);
1339
- // Apply wrapper-scoped tags (#186). Tags don't leak across wrappers
1340
- // sharing a bare — the wrapper instance owns its own tag set, and
1341
- // engine #175 memoization returns the same instance for the same
1342
- // (bare, override) pair, so this is idempotent across rebuilds.
1343
- if (node.tags.length > 0) {
1344
- state.tag(...node.tags);
1345
- }
1346
- }
1347
- else {
1348
- state = bareStates[nodeId];
1349
- }
1350
- inProgress.delete(nodeId);
1351
- finalStates[nodeId] = state;
1352
- return state;
1353
- };
1354
- for (const nodeId of ids) {
1355
- getFinal(nodeId);
1356
- }
1357
- // Pass 4: bind each ref to the resolved final State so cross-node
1358
- // transitions land on the right instance.
1359
- for (const nodeId of ids) {
1360
- if (!graph.nodes[nodeId].isHalt) {
1361
- refs[nodeId].bind(finalStates[nodeId]);
1362
- }
1363
- }
1364
- return {
1365
- start: finalStates[graph.initialId],
1366
- tapeBlock,
1367
- states: finalStates,
1368
- };
1702
+ return fromGraph(graph);
1703
+ }
1704
+ /**
1705
+ * Returns a `Map<number, {state, transitionSymbols}>` keyed by engine
1706
+ * `GraphNode.id`, exposing the live `State` instance + per-pattern
1707
+ * Symbol references for each node so downstream tooling can mutate
1708
+ * `state.debug` by numeric id and set per-pattern breakpoints by
1709
+ * `GraphTransition.id` (#195). Thin delegate to
1710
+ * `utilities/stateGraph.ts`'s `collectStates`; see that module for
1711
+ * the alignment contract, coverage rules, and halt-singleton warning.
1712
+ */
1713
+ static collectStates(initialState, tapeBlock) {
1714
+ return collectStates(initialState, tapeBlock);
1369
1715
  }
1370
1716
  }
1371
1717
  _a = State;
@@ -1388,7 +1734,7 @@ var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) ||
1388
1734
  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");
1389
1735
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
1390
1736
  };
1391
- var _TuringMachine_tapeBlock, _TuringMachine_stack;
1737
+ var _TuringMachine_tapeBlock;
1392
1738
  // True iff `filter` matches `symbol` per the DebugConfig semantics.
1393
1739
  // undefined / [] -> never; true -> always; symbol[] -> exact membership.
1394
1740
  function matchFilter(filter, symbol) {
@@ -1401,7 +1747,6 @@ function matchFilter(filter, symbol) {
1401
1747
  class TuringMachine {
1402
1748
  constructor({ tapeBlock, } = {}) {
1403
1749
  _TuringMachine_tapeBlock.set(this, void 0);
1404
- _TuringMachine_stack.set(this, []);
1405
1750
  if (!tapeBlock) {
1406
1751
  throw new Error('invalid tapeBlock');
1407
1752
  }
@@ -1435,7 +1780,14 @@ class TuringMachine {
1435
1780
  try {
1436
1781
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f")[lockSymbol].check(executionSymbol);
1437
1782
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f")[lockSymbol].lock(executionSymbol);
1438
- const stack = __classPrivateFieldGet(this, _TuringMachine_stack, "f");
1783
+ // Halt-stack is run-scoped, not machine-scoped (#196). Declaring it
1784
+ // local makes that lifetime explicit and prevents leftover entries
1785
+ // from a previous `runStepByStep` call (e.g. a build-time peek that
1786
+ // never drained the generator) from being popped during a subsequent
1787
+ // halt-bound transition. Before this change `#stack` was an instance
1788
+ // field and accumulated one extra push per call when the same machine
1789
+ // was reused.
1790
+ const stack = [];
1439
1791
  let state = initialState;
1440
1792
  if (state.overriddenHaltState) {
1441
1793
  stack.push(state.overriddenHaltState);
@@ -1448,14 +1800,43 @@ class TuringMachine {
1448
1800
  i += 1;
1449
1801
  const symbol = state.getSymbol(__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f"));
1450
1802
  const command = state.getCommand(symbol);
1451
- let nextState = state.getNextState(symbol).ref;
1803
+ const matched = state.getMatchedTransition(symbol);
1804
+ let nextState = matched.nextState.ref;
1805
+ // For wrapper-entry iters, the wrapper's transitions in `toGraph`
1806
+ // are empty (wrappers delegate to the bare via shared
1807
+ // `#symbolToDataMap`); the resolvable transition id lives under
1808
+ // the bare's stateId. `bareState` is non-null only when `state`
1809
+ // is a wrapper produced by `withOverriddenHaltState`. Accessed
1810
+ // via the STATE_INTERNAL package-private view (same pattern
1811
+ // `utilities/stateGraph.ts` uses) to avoid widening the public
1812
+ // State API for this internal need.
1813
+ const stateInternal = state[STATE_INTERNAL]();
1814
+ const resolvableStateId = stateInternal.bareState?.id ?? state.id;
1815
+ const matchedTransition = {
1816
+ id: `${resolvableStateId}.${matched.ix}`,
1817
+ matchKinds: __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").patternKinds(matched.matchedSymbol),
1818
+ };
1452
1819
  try {
1453
1820
  // Both before and after refer to THIS iter (#119 / v6.0.0).
1454
1821
  // The halting iter's after-fire just rides along on the iter's
1455
1822
  // own yield — no post-loop drain needed.
1456
- const beforeMatch = matchFilter(state.debug?.before, symbol)
1457
- || (nextState.isHalt && nextState.debug?.before === true);
1458
- const afterMatch = matchFilter(state.debug?.after, symbol);
1823
+ //
1824
+ // #207: `haltState.debug` is now a boolean, and pauses on the
1825
+ // halt-triggering iter's AFTER side (not before). The previous
1826
+ // before-side check (`nextState.debug?.before === true`) was
1827
+ // "early-warning" timing — the user paused before the halt-bound
1828
+ // transition fired, then had to mentally re-derive what would
1829
+ // happen. Now the pause anchors post-step (after the iter's own
1830
+ // after-pause if armed), so consumers see the just-fired halt-
1831
+ // bound transition + diagram cursor still on the triggering state.
1832
+ //
1833
+ // `state` here is always non-halt (halt is terminal — the run
1834
+ // loop never iterates with state === haltState), so `state.debug`
1835
+ // is always `DebugConfig` at runtime. The public getter's return
1836
+ // type matches that.
1837
+ const beforeMatch = matchFilter(state.debug?.before, symbol);
1838
+ const afterMatch = matchFilter(state.debug?.after, symbol)
1839
+ || (nextState === haltState && haltState.debug);
1459
1840
  const nextStateForYield = nextState.isHalt && stack.length
1460
1841
  ? stack.slice(-1)[0]
1461
1842
  : nextState;
@@ -1478,6 +1859,7 @@ class TuringMachine {
1478
1859
  }),
1479
1860
  movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
1480
1861
  nextState: nextStateForYield,
1862
+ matchedTransition,
1481
1863
  };
1482
1864
  if (beforeMatch || afterMatch) {
1483
1865
  const dbg = {};
@@ -1510,7 +1892,7 @@ class TuringMachine {
1510
1892
  }
1511
1893
  }
1512
1894
  }
1513
- _TuringMachine_tapeBlock = new WeakMap(), _TuringMachine_stack = new WeakMap();
1895
+ _TuringMachine_tapeBlock = new WeakMap();
1514
1896
 
1515
1897
  // Format converters between a Graph (the data model produced by State.toGraph
1516
1898
  // and consumed by State.fromGraph) and external string representations.
@@ -1553,6 +1935,81 @@ function parseMermaidId(s) {
1553
1935
  function frameSubgraphId(frameId) {
1554
1936
  return `w_${frameId}`;
1555
1937
  }
1938
+ // User-controlled content (state names, tag names, alphabet symbols inside
1939
+ // edge labels) is interpolated into Mermaid label strings (`"..."` wrappers
1940
+ // on nodes, wrappers, subgraphs, and edges). Mermaid's grammar terminates
1941
+ // the string on a literal `"`, and labels render via HTML/foreignObject so
1942
+ // `<`, `>`, `&` get interpreted as markup. Statement terminators (`\n`,
1943
+ // `\r`), C0 controls (except `\t`), DEL, bidi controls, and lone UTF-16
1944
+ // surrogates are encoded as numeric entities so they can't confuse the
1945
+ // tokenizer or flip text direction silently (#194).
1946
+ //
1947
+ // Printable Unicode (Cyrillic, CJK, emoji, accented Latin, etc.) passes
1948
+ // through unchanged — a tape alphabet of Cyrillic or Brainfuck glyphs
1949
+ // stays readable in the emitted `.mmd`.
1950
+ //
1951
+ // Escape is applied at the leaf — to each user-supplied fragment BEFORE
1952
+ // it's composed into a label. Structural pieces this module emits (`<br>`
1953
+ // tag separator, ` ∪ ` bare-name join, `[`, `]`, `,`, `|`, `/`, ` → `,
1954
+ // the `callable subtree of `/`callable scope: ` prefixes) are NOT escaped;
1955
+ // only user-controlled content is. fromMermaid mirrors with
1956
+ // `unescapeMermaidLabel` on each extracted leaf AFTER structural parsing,
1957
+ // so a literal `<br>` inside a state name (encoded as `&lt;br&gt;`)
1958
+ // survives the tag-split and decodes back at the leaf.
1959
+ const MERMAID_LABEL_ESCAPE_RE = /[&"<>\n\r\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\u202A-\u202E\u2066-\u2069\uD800-\uDFFF]/g;
1960
+ function escapeMermaidLabel(s) {
1961
+ return s.replace(MERMAID_LABEL_ESCAPE_RE, (ch) => {
1962
+ switch (ch) {
1963
+ case '&': return '&amp;';
1964
+ case '"': return '&quot;';
1965
+ case '<': return '&lt;';
1966
+ case '>': return '&gt;';
1967
+ case '\n': return '&#10;';
1968
+ case '\r': return '&#13;';
1969
+ default: return `&#${ch.charCodeAt(0)};`;
1970
+ }
1971
+ });
1972
+ }
1973
+ // Inverse of escapeMermaidLabel. Decodes the four named entities the
1974
+ // encoder emits (`&amp;`, `&quot;`, `&lt;`, `&gt;`) plus arbitrary
1975
+ // numeric entities (`&#NN;`, `&#xHH;`) — the latter to round-trip the
1976
+ // control / bidi / lone-surrogate cases from encode. Other named entities
1977
+ // pass through unchanged: fromMermaid is strict to the dialect toMermaid
1978
+ // emits, and a future-proof full HTML-entity decoder would muddle that.
1979
+ //
1980
+ // Replacement is single-pass: each `&...;` match is consumed once with
1981
+ // no re-scanning of the substitution, so nested-looking inputs like
1982
+ // `&amp;quot;` (literal `&quot;` as user text) decode to `&quot;` not `"`.
1983
+ const MERMAID_LABEL_UNESCAPE_RE = /&(?:(amp|quot|lt|gt)|#(\d+)|#x([0-9a-fA-F]+));/g;
1984
+ function unescapeMermaidLabel(s) {
1985
+ return s.replace(MERMAID_LABEL_UNESCAPE_RE, (match, named, dec, hex) => {
1986
+ switch (named) {
1987
+ case 'amp': return '&';
1988
+ case 'quot': return '"';
1989
+ case 'lt': return '<';
1990
+ case 'gt': return '>';
1991
+ default: {
1992
+ // Code units up to U+FFFF decode via fromCharCode so lone
1993
+ // surrogates we encoded by UTF-16 code unit round-trip exactly.
1994
+ // Hand-edited supplementary code points (`&#x1F600;`) use
1995
+ // fromCodePoint to produce the right surrogate pair — but only
1996
+ // when we didn't emit them ourselves, since encode runs per code
1997
+ // unit.
1998
+ if (dec !== undefined) {
1999
+ const n = Number.parseInt(dec, 10);
2000
+ return n <= 0xFFFF ? String.fromCharCode(n) : String.fromCodePoint(n);
2001
+ }
2002
+ if (hex !== undefined) {
2003
+ const n = Number.parseInt(hex, 16);
2004
+ return n <= 0xFFFF ? String.fromCharCode(n) : String.fromCodePoint(n);
2005
+ }
2006
+ /* c8 ignore next 2 — defensive: the regex shape guarantees one of
2007
+ named / dec / hex is always set, so this fallback is unreachable. */
2008
+ return match;
2009
+ }
2010
+ }
2011
+ });
2012
+ }
1556
2013
  function toMermaid(graph) {
1557
2014
  const lines = [
1558
2015
  'flowchart TD',
@@ -1591,9 +2048,18 @@ function toMermaid(graph) {
1591
2048
  // Mermaid line-break that works across renderers without `classDef`-
1592
2049
  // pseudo-element hacks (#186).
1593
2050
  const labelOf = (node) => {
2051
+ const name = escapeMermaidLabel(node.name);
1594
2052
  if (node.tags.length === 0)
1595
- return node.name;
1596
- return `${node.name}<br>${node.tags.join(', ')}`;
2053
+ return name;
2054
+ // Per-tag escape that ALSO encodes `,` — tags are joined with `, ` and
2055
+ // split on `,` in `splitLabelTags`, so a literal comma in user tag
2056
+ // content would be mistaken for a separator on the way back. `,` isn't
2057
+ // in the base escape set because it's structural in edge labels
2058
+ // (between per-tape cells in `writes`/`moves`), where the encode pass
2059
+ // happens after composition — different context, different escape.
2060
+ const tagFragments = node.tags
2061
+ .map((t) => escapeMermaidLabel(t).replace(/,/g, '&#44;'));
2062
+ return `${name}<br>${tagFragments.join(', ')}`;
1597
2063
  };
1598
2064
  // 1. Emit top-level nodes (real halt, non-wrapper regulars outside any frame).
1599
2065
  for (const node of topLevelNodes) {
@@ -1618,7 +2084,7 @@ function toMermaid(graph) {
1618
2084
  const frameBareNames = frameBares
1619
2085
  .slice()
1620
2086
  .sort((a, b) => a.id - b.id)
1621
- .map((n) => n.name);
2087
+ .map((n) => escapeMermaidLabel(n.name));
1622
2088
  const label = frameBareNames.length > 1
1623
2089
  ? `callable scope: ${frameBareNames.join(' ∪ ')}`
1624
2090
  : `callable subtree of ${frameBareNames[0] ?? frameId}`;
@@ -1720,7 +2186,12 @@ function toMermaid(graph) {
1720
2186
  const reads = alternatives.map((alt) => `[${alt}]`).join('|');
1721
2187
  const writes = `[${t.command.map((c) => c.symbol).join(',')}]`;
1722
2188
  const moves = `[${t.command.map((c) => c.movement).join(',')}]`;
1723
- const label = `${reads} ${writes}/${moves}`;
2189
+ // Escape the WHOLE composed label structural separators ([, ], ,,
2190
+ // |, /, ' → ') are all in our safe ASCII set and pass through
2191
+ // unchanged; only embedded user alphabet symbols inside `'...'` get
2192
+ // entity-encoded. fromMermaid unescapes the captured label as the
2193
+ // first step before structural parsing.
2194
+ const label = escapeMermaidLabel(`${reads} → ${writes}/${moves}`);
1724
2195
  lines.push(` ${mermaidIdFor(node.id)} -- "${label}" --> ${mermaidIdFor(t.nextStateId)}`);
1725
2196
  }
1726
2197
  }
@@ -1851,14 +2322,23 @@ const classAssignTagRegex = /^class ([sc]\d+(?:,[sc]\d+)*) tag_([A-Za-z0-9_-]+)$
1851
2322
  // Labels without `<br>` have no tags. Tags are comma-joined; trimmed of
1852
2323
  // whitespace. The `<br>` is the single source of truth for tag-name parsing —
1853
2324
  // `class` lines are decorative-only and not consulted here.
2325
+ //
2326
+ // Mermaid-label entities (`&lt;`, `&quot;`, etc., #194) are decoded AFTER
2327
+ // structural splitting: the `<br>` separator and `,` tag delimiter survive
2328
+ // encode unchanged, and a user state name / tag containing a literal `<br>`
2329
+ // or `,` was encoded leaf-side so it can't be confused with the structural
2330
+ // form. Decode at the leaves recovers the original characters.
1854
2331
  function splitLabelTags(label) {
1855
2332
  const brIx = label.indexOf('<br>');
1856
2333
  if (brIx < 0) {
1857
- return { name: label, tags: [] };
2334
+ return { name: unescapeMermaidLabel(label), tags: [] };
1858
2335
  }
1859
- const name = label.slice(0, brIx);
2336
+ const name = unescapeMermaidLabel(label.slice(0, brIx));
1860
2337
  const tagsStr = label.slice(brIx + '<br>'.length);
1861
- const tags = tagsStr.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
2338
+ const tags = tagsStr
2339
+ .split(',')
2340
+ .map((t) => unescapeMermaidLabel(t.trim()))
2341
+ .filter((t) => t.length > 0);
1862
2342
  return { name, tags };
1863
2343
  }
1864
2344
  function fromMermaid(text) {
@@ -2018,7 +2498,12 @@ function fromMermaid(text) {
2018
2498
  const tm = line.match(labeledTransitionRegex);
2019
2499
  if (tm) {
2020
2500
  const fromId = parseMermaidId(tm[1]);
2021
- const label = tm[2];
2501
+ // Decode the WHOLE captured label up front (#194). Structural
2502
+ // separators (`[`, `]`, `,`, `|`, `/`, ` → `) are all safe ASCII
2503
+ // outside the escape set and pass through encode unchanged, so it's
2504
+ // safe to decode before structural parsing; only embedded alphabet
2505
+ // symbols inside `'...'` get reconstituted.
2506
+ const label = unescapeMermaidLabel(tm[2]);
2022
2507
  const toId = parseMermaidId(tm[3]);
2023
2508
  const arrowIx = label.indexOf(' → ');
2024
2509
  if (arrowIx === -1) {