@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/CHANGELOG.md +82 -0
- package/README.md +75 -6
- package/dist/classes/State.d.ts +128 -2
- package/dist/classes/TapeBlock.d.ts +25 -0
- package/dist/classes/TuringMachine.d.ts +23 -0
- package/dist/index.cjs +1031 -546
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +1031 -546
- package/dist/utilities/stateGraph.d.ts +92 -0
- package/package.json +2 -2
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
987
|
+
return { initialId: initialState[STATE_INTERNAL]().id, alphabets, nodes };
|
|
600
988
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
1229
|
+
return result;
|
|
678
1230
|
}
|
|
679
|
-
//
|
|
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
|
-
|
|
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 (
|
|
839
|
-
__classPrivateFieldGet$1(this, _State_debugRef, "f").current =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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()
|
|
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 `<br>`)
|
|
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 '&';
|
|
1964
|
+
case '"': return '"';
|
|
1965
|
+
case '<': return '<';
|
|
1966
|
+
case '>': return '>';
|
|
1967
|
+
case '\n': return ' ';
|
|
1968
|
+
case '\r': return ' ';
|
|
1969
|
+
default: return `&#${ch.charCodeAt(0)};`;
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
// Inverse of escapeMermaidLabel. Decodes the four named entities the
|
|
1974
|
+
// encoder emits (`&`, `"`, `<`, `>`) 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
|
+
// `&quot;` (literal `"` as user text) decode to `"` 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 (`😀`) 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
|
|
1596
|
-
|
|
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, ','));
|
|
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
|
-
|
|
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 (`<`, `"`, 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
|
|
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
|
-
|
|
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) {
|