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