@turing-machine-js/machine 3.0.1 → 4.0.0

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 CHANGED
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [4.0.0] - 2026-05-07
8
+
9
+ ### Added
10
+
11
+ - **`State.debug` field** — runtime-mutable per-state breakpoints with `{ before, after }` symbol filtering ([#98](https://github.com/mellonis/turing-machine-js/issues/98)). Backed by a shared `Ref` cell so `withOverrodeHaltState` wrappers see assignments propagated from the original state. Validator rejects symbols that aren't transition keys of the owning state (catches cross-tape-block / cross-state mistakes); for `haltState`, list filters are silent no-ops because halt has no resolved head symbol.
12
+ - **`DebugConfig` class** — exported from the package. Per-property setters validate and freeze the stored array, so push / index-write throw `TypeError`. Plain-object input to `state.debug = { ... }` is wrapped in a `DebugConfig` instance automatically.
13
+ - **`onDebugBreak` hook** on `run()` — awaited at every break (one or two times per yield: `after` from the previous step, then `before` for the current step). For `after` calls, `run()` substitutes the previous yield's snapshot so `m.state` corresponds to the state whose `after` fired. `onStep` always sees the original (un-substituted) yield.
14
+ - **`haltState.debug.before = true`** — pauses on every halt entry (program exit AND subroutine return via halt-stack pop). Symbol-list filters on `haltState` are silent no-ops; only the `true` wildcard activates. `haltState` is a module-level singleton — clear in `afterEach` / `finally` for test isolation.
15
+ - **`MachineState.debugBreak`** field — optional metadata on every yield from `runStepByStep`; consumers can opt in to reading it and mirror `run()`'s pause behavior if desired.
16
+
17
+ ### Changed (BREAKING)
18
+
19
+ - **`run()` is now `async`** and returns `Promise<void>`. Existing callers must update to `await machine.run({ ... })` (or `void machine.run({ ... })` if intentionally discarding the result). Without an `onDebugBreak` hook, debug breaks fire-and-resume invisibly — the trajectory observed by `onStep` is identical to running with all `debug` fields cleared.
20
+
21
+ ## [3.0.2] - 2026-05-04
22
+
23
+ ### Fixed
24
+
25
+ - **`Tape` constructor now normalises and validates `viewportWidth`** ([#95](https://github.com/mellonis/turing-machine-js/issues/95)). The constructor stored `viewportWidth` raw — the setter padded `#symbols` via `normalise()` and validated `>= 1` / bumped even values to odd, but the constructor did neither. As a result, `new Tape({ alphabet, symbols: ['a','b','a','b'], viewportWidth: 23 }).viewport.length` was `4` instead of `23`, and `viewportWidth: 0` or `viewportWidth: 4` silently produced an invalid tape. The constructor now defers to the setter, so a single source of truth handles validation + normalisation.
26
+
27
+ ### Changed (observable)
28
+
29
+ - A constructor call with an out-of-range `position` (e.g. `position: 5` with `symbols: ['x']`) now normalises at construction — the symbol array is padded with blanks so `position` lands inside the viewport. Previously the tape was left in a malformed state until the first head movement triggered `normalise()`. Strictly a fix, but observable if user code inspected `.symbols.length` immediately after construction.
30
+
7
31
  ## [3.0.1] - 2026-04-30
8
32
 
9
33
  ### Fixed
package/README.md CHANGED
@@ -34,7 +34,7 @@ const tape = new Tape({ alphabet, symbols: ['a', 'b', 'c', 'b', 'a'] });
34
34
  const tapeBlock = TapeBlock.fromTapes([tape]);
35
35
  const machine = new TuringMachine({ tapeBlock });
36
36
 
37
- machine.run({
37
+ await machine.run({
38
38
  initialState: new State({
39
39
  [tapeBlock.symbol(['b'])]: {
40
40
  command: [{ symbol: '*', movement: movements.right }],
@@ -218,8 +218,8 @@ The runtime. Owns one `TapeBlock` and drives a state graph until it reaches `hal
218
218
  ```javascript
219
219
  const machine = new TuringMachine({ tapeBlock });
220
220
 
221
- // Run to halt:
222
- machine.run({ initialState, stepsLimit: 1e5 });
221
+ // Run to halt — `run()` is async (v4+), it returns a Promise<void>:
222
+ await machine.run({ initialState, stepsLimit: 1e5 });
223
223
 
224
224
  // Or step-by-step (useful for visualization / debugging):
225
225
  for (const step of machine.runStepByStep({ initialState })) {
@@ -229,6 +229,55 @@ for (const step of machine.runStepByStep({ initialState })) {
229
229
 
230
230
  `stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
231
231
 
232
+ ## Debugging breakpoints (v4+)
233
+
234
+ Any `State` can carry a runtime-mutable `debug` config that pauses execution at chosen points.
235
+
236
+ ```ts
237
+ import { State, haltState, ifOtherSymbol, type DebugConfig } from '@turing-machine-js/machine';
238
+
239
+ const myState = new State({...});
240
+
241
+ // Pause before applying any of myState's commands:
242
+ myState.debug = { before: true };
243
+
244
+ // Pause only when the head shows symA:
245
+ myState.debug = { before: [symA] };
246
+
247
+ // Pause both before and after for the same symbol — two pauses per visit:
248
+ myState.debug = { before: [symA], after: [symA] };
249
+
250
+ // Pause when the engine is about to enter halt (program exit OR subroutine pop):
251
+ haltState.debug = { before: true };
252
+
253
+ // Disable later:
254
+ myState.debug = null;
255
+ ```
256
+
257
+ The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. The internal cell is shared with `state.withOverrodeHaltState(...)` wrappers, so an assignment on the original is visible from every wrapper.
258
+
259
+ `run()` is async and accepts an `onDebugBreak` hook:
260
+
261
+ ```ts
262
+ await machine.run({
263
+ initialState,
264
+ onStep: (m) => { /* logger sees every step */ },
265
+ onDebugBreak: async (m) => {
266
+ // Awaited at every break — hold execution until you resolve.
267
+ if (m.debugBreak?.before) console.log('before:', m.state.name);
268
+ if (m.debugBreak?.after) console.log('after:', m.state.name);
269
+ },
270
+ });
271
+ ```
272
+
273
+ For `after` calls, `m` is the previous yield's snapshot — `m.state` is the state whose `after` filter fired. For `before` calls, `m` is the current iteration. `onStep` always sees the original (un-substituted) yield.
274
+
275
+ If `onDebugBreak` is not provided, breaks fire-and-resume invisibly — the trajectory is identical to running without `debug` set.
276
+
277
+ **Filter semantics:** `true` is a wildcard (match any symbol). `[ifOtherSymbol]` is NOT a wildcard — it matches only the catch-all resolution case (same meaning as in transition keys).
278
+
279
+ **Caveat:** `haltState` is a module-level singleton. Setting `haltState.debug` affects every machine in the process; clear in `afterEach` / `finally` for test isolation.
280
+
232
281
  ## Special objects
233
282
 
234
283
  ### haltState
@@ -4,6 +4,18 @@ import TapeBlock from './TapeBlock';
4
4
  import TapeCommand from './TapeCommand';
5
5
  import { type Graph } from '../utilities/graph';
6
6
  export declare const ifOtherSymbol: unique symbol;
7
+ declare const validateDebugFilter: unique symbol;
8
+ export declare class DebugConfig {
9
+ #private;
10
+ constructor(ownerState: State, initial?: {
11
+ before?: symbol[] | readonly symbol[] | true;
12
+ after?: symbol[] | readonly symbol[] | true;
13
+ });
14
+ get before(): readonly symbol[] | true | undefined;
15
+ set before(v: symbol[] | readonly symbol[] | true | undefined);
16
+ get after(): readonly symbol[] | true | undefined;
17
+ set after(v: symbol[] | readonly symbol[] | true | undefined);
18
+ }
7
19
  export default class State {
8
20
  #private;
9
21
  constructor(stateDefinition?: Record<string | symbol, {
@@ -15,6 +27,13 @@ export default class State {
15
27
  get isHalt(): boolean;
16
28
  get overrodeHaltState(): State | null;
17
29
  get ref(): this;
30
+ get debug(): DebugConfig | null;
31
+ set debug(value: DebugConfig | {
32
+ before?: symbol[] | readonly symbol[] | true;
33
+ after?: symbol[] | readonly symbol[] | true;
34
+ } | null);
35
+ /** @internal — invoked by DebugConfig setters via module-private symbol. */
36
+ [validateDebugFilter](fieldName: 'before' | 'after', filter: readonly symbol[] | true | undefined): void;
18
37
  getSymbol(tapeBlock: TapeBlock): symbol;
19
38
  getCommand(symbol: symbol): Command;
20
39
  getNextState(symbol: symbol): State | Reference;
@@ -47,3 +66,4 @@ export default class State {
47
66
  };
48
67
  }
49
68
  export declare const haltState: State;
69
+ export {};
@@ -11,6 +11,19 @@ export type MachineState = {
11
11
  nextSymbols: string[];
12
12
  movements: symbol[];
13
13
  nextState: State;
14
+ /**
15
+ * Set only when this iteration boundary is a debug break.
16
+ * Field is OMITTED entirely when no break fires (no `debugBreak: undefined`).
17
+ * At least one of `before` / `after` is `true` when the field is present.
18
+ *
19
+ * For consumers of the `runStepByStep` generator the `state` field reflects
20
+ * the current iteration regardless of timing; `run()` substitutes the prior
21
+ * yield's snapshot for `after` calls so consumers see the source state.
22
+ */
23
+ debugBreak?: {
24
+ before?: true;
25
+ after?: true;
26
+ };
14
27
  };
15
28
  export default class TuringMachine {
16
29
  #private;
@@ -18,9 +31,10 @@ export default class TuringMachine {
18
31
  tapeBlock?: TapeBlock;
19
32
  });
20
33
  get tapeBlock(): TapeBlock;
21
- run({ initialState, stepsLimit, onStep }: RunParameter & {
34
+ run({ initialState, stepsLimit, onStep, onDebugBreak, }: RunParameter & {
22
35
  onStep?: (machineState: MachineState) => void;
23
- }): void;
36
+ onDebugBreak?: (machineState: MachineState) => void | Promise<void>;
37
+ }): Promise<void>;
24
38
  runStepByStep({ initialState, stepsLimit }: RunParameter): Generator<MachineState>;
25
39
  }
26
40
  export {};
package/dist/index.cjs CHANGED
@@ -186,25 +186,30 @@ var __classPrivateFieldGet$4 = (undefined && undefined.__classPrivateFieldGet) |
186
186
  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");
187
187
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
188
188
  };
189
- var _Tape_alphabet, _Tape_symbols, _Tape_position, _Tape_viewportWidth;
189
+ var _Tape_instances, _Tape_alphabet, _Tape_right, _Tape_left, _Tape_position, _Tape_viewportWidth, _Tape_viewportBuffer, _Tape_viewportDirty, _Tape_cellAt;
190
+ const BLANK_INDEX = 0;
190
191
  class Tape {
191
192
  constructor({ alphabet, symbols = [], position = 0, viewportWidth = 1, }) {
193
+ _Tape_instances.add(this);
192
194
  _Tape_alphabet.set(this, void 0);
193
- _Tape_symbols.set(this, void 0);
195
+ _Tape_right.set(this, []);
196
+ _Tape_left.set(this, []);
194
197
  _Tape_position.set(this, void 0);
195
198
  _Tape_viewportWidth.set(this, void 0);
199
+ _Tape_viewportBuffer.set(this, []);
200
+ _Tape_viewportDirty.set(this, true);
196
201
  const isSymbolsValid = symbols.every((symbol) => alphabet.has(symbol));
197
202
  if (!isSymbolsValid) {
198
203
  throw new Error('symbolList contains invalid symbol');
199
204
  }
200
205
  __classPrivateFieldSet$4(this, _Tape_alphabet, new Alphabet(alphabet), "f");
201
206
  __classPrivateFieldSet$4(this, _Tape_position, position, "f");
202
- __classPrivateFieldSet$4(this, _Tape_viewportWidth, viewportWidth, "f");
203
- const symbolsCopy = Array.from(symbols);
204
- if (symbolsCopy.length === 0) {
205
- symbolsCopy.push(__classPrivateFieldGet$4(this, _Tape_alphabet, "f").blankSymbol);
207
+ __classPrivateFieldSet$4(this, _Tape_viewportWidth, 1, "f");
208
+ const initialSymbols = symbols.length === 0 ? [__classPrivateFieldGet$4(this, _Tape_alphabet, "f").blankSymbol] : symbols;
209
+ for (const symbol of initialSymbols) {
210
+ __classPrivateFieldGet$4(this, _Tape_right, "f").push(__classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol));
206
211
  }
207
- __classPrivateFieldSet$4(this, _Tape_symbols, symbolsCopy.map((symbol) => __classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol)), "f");
212
+ this.viewportWidth = viewportWidth;
208
213
  }
209
214
  get alphabet() {
210
215
  return __classPrivateFieldGet$4(this, _Tape_alphabet, "f");
@@ -213,27 +218,46 @@ class Tape {
213
218
  return (__classPrivateFieldGet$4(this, _Tape_viewportWidth, "f") - 1) / 2;
214
219
  }
215
220
  get position() {
216
- return __classPrivateFieldGet$4(this, _Tape_position, "f");
221
+ // Public contract: index of the head in the `symbols` array. With the
222
+ // two-array deque, `#position` is the head's logical position; adding
223
+ // `#left.length` shifts it back into "index from the leftmost backed cell".
224
+ return __classPrivateFieldGet$4(this, _Tape_position, "f") + __classPrivateFieldGet$4(this, _Tape_left, "f").length;
217
225
  }
218
226
  get symbol() {
219
- return __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_symbols, "f")[__classPrivateFieldGet$4(this, _Tape_position, "f")]);
227
+ return __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_instances, "m", _Tape_cellAt).call(this, __classPrivateFieldGet$4(this, _Tape_position, "f")));
220
228
  }
221
229
  set symbol(symbol) {
222
230
  if (!__classPrivateFieldGet$4(this, _Tape_alphabet, "f").has(symbol)) {
223
231
  throw new Error('Invalid symbol');
224
232
  }
225
- __classPrivateFieldGet$4(this, _Tape_symbols, "f")[__classPrivateFieldGet$4(this, _Tape_position, "f")] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol);
233
+ const index = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol);
234
+ if (__classPrivateFieldGet$4(this, _Tape_position, "f") >= 0) {
235
+ __classPrivateFieldGet$4(this, _Tape_right, "f")[__classPrivateFieldGet$4(this, _Tape_position, "f")] = index;
236
+ }
237
+ else {
238
+ __classPrivateFieldGet$4(this, _Tape_left, "f")[-__classPrivateFieldGet$4(this, _Tape_position, "f") - 1] = index;
239
+ }
240
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
226
241
  }
227
242
  get symbols() {
228
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
229
- .map((index) => __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(index));
243
+ const result = new Array(__classPrivateFieldGet$4(this, _Tape_left, "f").length + __classPrivateFieldGet$4(this, _Tape_right, "f").length);
244
+ for (let i = 0; i < __classPrivateFieldGet$4(this, _Tape_left, "f").length; i += 1) {
245
+ result[i] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_left, "f")[__classPrivateFieldGet$4(this, _Tape_left, "f").length - 1 - i]);
246
+ }
247
+ for (let i = 0; i < __classPrivateFieldGet$4(this, _Tape_right, "f").length; i += 1) {
248
+ result[__classPrivateFieldGet$4(this, _Tape_left, "f").length + i] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_right, "f")[i]);
249
+ }
250
+ return result;
230
251
  }
231
252
  get viewport() {
232
- const startIx = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
233
- const endIx = __classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount + 1;
234
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
235
- .slice(startIx, endIx)
236
- .map((index) => __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(index));
253
+ if (__classPrivateFieldGet$4(this, _Tape_viewportDirty, "f")) {
254
+ const start = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
255
+ for (let i = 0; i < __classPrivateFieldGet$4(this, _Tape_viewportWidth, "f"); i += 1) {
256
+ __classPrivateFieldGet$4(this, _Tape_viewportBuffer, "f")[i] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_instances, "m", _Tape_cellAt).call(this, start + i));
257
+ }
258
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, false, "f");
259
+ }
260
+ return [...__classPrivateFieldGet$4(this, _Tape_viewportBuffer, "f")];
237
261
  }
238
262
  get viewportWidth() {
239
263
  return __classPrivateFieldGet$4(this, _Tape_viewportWidth, "f");
@@ -247,27 +271,38 @@ class Tape {
247
271
  finalWidth += 1;
248
272
  }
249
273
  __classPrivateFieldSet$4(this, _Tape_viewportWidth, finalWidth, "f");
274
+ __classPrivateFieldGet$4(this, _Tape_viewportBuffer, "f").length = finalWidth;
275
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
250
276
  this.normalise();
251
277
  }
252
278
  left() {
253
279
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") - 1, "f");
254
280
  this.normalise();
281
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
255
282
  }
256
283
  normalise() {
257
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount < 0) {
258
- __classPrivateFieldGet$4(this, _Tape_symbols, "f").unshift(0);
259
- __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") + 1, "f");
284
+ const minLogical = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
285
+ const maxLogical = __classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount;
286
+ while (-__classPrivateFieldGet$4(this, _Tape_left, "f").length > minLogical) {
287
+ __classPrivateFieldGet$4(this, _Tape_left, "f").push(BLANK_INDEX);
260
288
  }
261
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount >= __classPrivateFieldGet$4(this, _Tape_symbols, "f").length) {
262
- __classPrivateFieldGet$4(this, _Tape_symbols, "f").push(0);
289
+ while (__classPrivateFieldGet$4(this, _Tape_right, "f").length - 1 < maxLogical) {
290
+ __classPrivateFieldGet$4(this, _Tape_right, "f").push(BLANK_INDEX);
263
291
  }
264
292
  }
265
293
  right() {
266
294
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") + 1, "f");
267
295
  this.normalise();
296
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
268
297
  }
269
298
  }
270
- _Tape_alphabet = new WeakMap(), _Tape_symbols = new WeakMap(), _Tape_position = new WeakMap(), _Tape_viewportWidth = new WeakMap();
299
+ _Tape_alphabet = new WeakMap(), _Tape_right = new WeakMap(), _Tape_left = new WeakMap(), _Tape_position = new WeakMap(), _Tape_viewportWidth = new WeakMap(), _Tape_viewportBuffer = new WeakMap(), _Tape_viewportDirty = new WeakMap(), _Tape_instances = new WeakSet(), _Tape_cellAt = function _Tape_cellAt(logical) {
300
+ if (logical >= 0) {
301
+ return logical < __classPrivateFieldGet$4(this, _Tape_right, "f").length ? __classPrivateFieldGet$4(this, _Tape_right, "f")[logical] : BLANK_INDEX;
302
+ }
303
+ const ix = -logical - 1;
304
+ return ix < __classPrivateFieldGet$4(this, _Tape_left, "f").length ? __classPrivateFieldGet$4(this, _Tape_left, "f")[ix] : BLANK_INDEX;
305
+ };
271
306
 
272
307
  var __classPrivateFieldGet$3 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
273
308
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
@@ -623,25 +658,64 @@ function decodeWriteSymbol(symbol) {
623
658
  }
624
659
  // Format converters (toMermaid / fromMermaid) live in ./graphFormats.
625
660
 
626
- var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
627
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
628
- 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");
629
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
630
- };
631
661
  var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
632
662
  if (kind === "m") throw new TypeError("Private method is not writable");
633
663
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
634
664
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
635
665
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
636
666
  };
637
- var _State_id, _State_name, _State_overrodeHaltState, _State_symbolToDataMap;
667
+ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
668
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
669
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
670
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
671
+ };
672
+ var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_id, _State_name, _State_overrodeHaltState, _State_symbolToDataMap, _State_debugRef;
638
673
  const ifOtherSymbol = Symbol('other symbol');
674
+ // Module-private symbol used by DebugConfig setters to call State's validator
675
+ // without exposing the validator on the public surface.
676
+ const validateDebugFilter = Symbol('validateDebugFilter');
677
+ class DebugConfig {
678
+ constructor(ownerState, initial) {
679
+ _DebugConfig_ownerState.set(this, void 0);
680
+ _DebugConfig_before.set(this, void 0);
681
+ _DebugConfig_after.set(this, void 0);
682
+ __classPrivateFieldSet$1(this, _DebugConfig_ownerState, ownerState, "f");
683
+ if (initial) {
684
+ if (initial.before !== undefined) {
685
+ this.before = initial.before;
686
+ }
687
+ if (initial.after !== undefined) {
688
+ this.after = initial.after;
689
+ }
690
+ }
691
+ }
692
+ get before() {
693
+ return __classPrivateFieldGet$1(this, _DebugConfig_before, "f");
694
+ }
695
+ set before(v) {
696
+ __classPrivateFieldGet$1(this, _DebugConfig_ownerState, "f")[validateDebugFilter]('before', v);
697
+ __classPrivateFieldSet$1(this, _DebugConfig_before, Array.isArray(v) ? Object.freeze([...v]) : v, "f");
698
+ }
699
+ get after() {
700
+ return __classPrivateFieldGet$1(this, _DebugConfig_after, "f");
701
+ }
702
+ set after(v) {
703
+ __classPrivateFieldGet$1(this, _DebugConfig_ownerState, "f")[validateDebugFilter]('after', v);
704
+ __classPrivateFieldSet$1(this, _DebugConfig_after, Array.isArray(v) ? Object.freeze([...v]) : v, "f");
705
+ }
706
+ }
707
+ _DebugConfig_ownerState = new WeakMap(), _DebugConfig_before = new WeakMap(), _DebugConfig_after = new WeakMap();
639
708
  class State {
640
709
  constructor(stateDefinition = null, name) {
641
710
  _State_id.set(this, id(this));
642
711
  _State_name.set(this, void 0);
643
712
  _State_overrodeHaltState.set(this, null);
644
713
  _State_symbolToDataMap.set(this, new Map());
714
+ // Shared mutable cell — withOverrodeHaltState wrappers reference the same
715
+ // object so that `state.debug = ...` (and nullings) propagate across them.
716
+ // Note: toGraph / fromGraph deliberately do not serialize debug — debug is
717
+ // a runtime concern, not part of the structural graph.
718
+ _State_debugRef.set(this, { current: null });
645
719
  if (stateDefinition) {
646
720
  const keys = Object.getOwnPropertyNames(stateDefinition);
647
721
  if (keys.length) {
@@ -700,6 +774,36 @@ class State {
700
774
  get ref() {
701
775
  return this;
702
776
  }
777
+ get debug() {
778
+ return __classPrivateFieldGet$1(this, _State_debugRef, "f").current;
779
+ }
780
+ set debug(value) {
781
+ if (value === null) {
782
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = null;
783
+ return;
784
+ }
785
+ if (value instanceof DebugConfig) {
786
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = value;
787
+ return;
788
+ }
789
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, value);
790
+ }
791
+ /** @internal — invoked by DebugConfig setters via module-private symbol. */
792
+ [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), validateDebugFilter)](fieldName, filter) {
793
+ if (filter === undefined || filter === true)
794
+ return;
795
+ // haltState has no own transitions; symbol-list filters on it are silent
796
+ // no-ops at the engine level (spec §8.6), so accept any list shape here.
797
+ if (this.isHalt)
798
+ return;
799
+ for (const sym of filter) {
800
+ if (sym !== ifOtherSymbol && !__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").has(sym)) {
801
+ throw new Error(`State.debug.${fieldName}: symbol is not a transition key of this state `
802
+ + `(state name: ${__classPrivateFieldGet$1(this, _State_name, "f")}). Common cause: symbol comes from a `
803
+ + 'different tape block, or doesn\'t match any of this state\'s transitions.');
804
+ }
805
+ }
806
+ }
703
807
  getSymbol(tapeBlock) {
704
808
  const symbol = [...__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").keys()].find((currentSymbol) => tapeBlock.isMatched({
705
809
  symbol: currentSymbol,
@@ -725,6 +829,7 @@ class State {
725
829
  const state = new State(null, `${this.name}>${overrodeHaltState.name}`);
726
830
  __classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f"), "f");
727
831
  __classPrivateFieldSet$1(state, _State_overrodeHaltState, overrodeHaltState, "f");
832
+ __classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(this, _State_debugRef, "f"), "f");
728
833
  return state;
729
834
  }
730
835
  // Single-state introspection — no traversal, no tapeBlock required.
@@ -895,7 +1000,6 @@ class State {
895
1000
  };
896
1001
  }
897
1002
  }
898
- _State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap();
899
1003
  const haltState = new State(null);
900
1004
 
901
1005
  var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
@@ -910,6 +1014,15 @@ var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) ||
910
1014
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
911
1015
  };
912
1016
  var _TuringMachine_tapeBlock, _TuringMachine_stack;
1017
+ // True iff `filter` matches `symbol` per the DebugConfig semantics.
1018
+ // undefined / [] -> never; true -> always; symbol[] -> exact membership.
1019
+ function matchFilter(filter, symbol) {
1020
+ if (filter === undefined)
1021
+ return false;
1022
+ if (filter === true)
1023
+ return true;
1024
+ return filter.includes(symbol);
1025
+ }
913
1026
  class TuringMachine {
914
1027
  constructor({ tapeBlock, } = {}) {
915
1028
  _TuringMachine_tapeBlock.set(this, void 0);
@@ -922,12 +1035,22 @@ class TuringMachine {
922
1035
  get tapeBlock() {
923
1036
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
924
1037
  }
925
- run({ initialState, stepsLimit = 1e5, onStep }) {
1038
+ async run({ initialState, stepsLimit = 1e5, onStep, onDebugBreak, }) {
926
1039
  const generator = this.runStepByStep({ initialState, stepsLimit });
1040
+ let prevYield = null;
927
1041
  for (const machineState of generator) {
1042
+ // 'after' (from prev step) — fire FIRST, with prev yield substituted as the source view.
1043
+ if (machineState.debugBreak?.after && onDebugBreak && prevYield) {
1044
+ await onDebugBreak({ ...prevYield, debugBreak: { after: true } });
1045
+ }
1046
+ // 'before' (current step) — pass current machineState with only the before flag.
1047
+ if (machineState.debugBreak?.before && onDebugBreak) {
1048
+ await onDebugBreak({ ...machineState, debugBreak: { before: true } });
1049
+ }
928
1050
  if (onStep instanceof Function) {
929
1051
  onStep(machineState);
930
1052
  }
1053
+ prevYield = machineState;
931
1054
  }
932
1055
  }
933
1056
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
@@ -941,6 +1064,7 @@ class TuringMachine {
941
1064
  stack.push(state.overrodeHaltState);
942
1065
  }
943
1066
  let i = 0;
1067
+ let pendingAfterFromPrev = false;
944
1068
  while (!state.isHalt) {
945
1069
  if (i === stepsLimit) {
946
1070
  throw new Error('Long execution');
@@ -950,10 +1074,12 @@ class TuringMachine {
950
1074
  const command = state.getCommand(symbol);
951
1075
  let nextState = state.getNextState(symbol).ref;
952
1076
  try {
1077
+ const beforeMatch = matchFilter(state.debug?.before, symbol)
1078
+ || (nextState.isHalt && nextState.debug?.before === true);
953
1079
  const nextStateForYield = nextState.isHalt && stack.length
954
1080
  ? stack.slice(-1)[0]
955
1081
  : nextState;
956
- yield {
1082
+ const yielded = {
957
1083
  step: i,
958
1084
  state,
959
1085
  currentSymbols: __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").currentSymbols,
@@ -973,6 +1099,17 @@ class TuringMachine {
973
1099
  movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
974
1100
  nextState: nextStateForYield,
975
1101
  };
1102
+ if (pendingAfterFromPrev || beforeMatch) {
1103
+ const dbg = {};
1104
+ if (pendingAfterFromPrev)
1105
+ dbg.after = true;
1106
+ if (beforeMatch)
1107
+ dbg.before = true;
1108
+ yielded.debugBreak = dbg;
1109
+ }
1110
+ yield yielded;
1111
+ // Re-evaluate 'after' for THIS visit, to fire on the NEXT yield.
1112
+ pendingAfterFromPrev = matchFilter(state.debug?.after, symbol);
976
1113
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
977
1114
  if (nextState.isHalt && stack.length) {
978
1115
  nextState = stack.pop();
@@ -1303,6 +1440,7 @@ function runOnce(runnable, input, stepsLimit) {
1303
1440
 
1304
1441
  exports.Alphabet = Alphabet;
1305
1442
  exports.Command = Command;
1443
+ exports.DebugConfig = DebugConfig;
1306
1444
  exports.Reference = Reference;
1307
1445
  exports.State = State;
1308
1446
  exports.Tape = Tape;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { default as Alphabet } from './classes/Alphabet';
2
2
  export { default as Command } from './classes/Command';
3
3
  export { default as Reference } from './classes/Reference';
4
- export { default as State, haltState, ifOtherSymbol } from './classes/State';
4
+ export { default as State, DebugConfig, haltState, ifOtherSymbol } from './classes/State';
5
5
  export { default as Tape } from './classes/Tape';
6
6
  export { default as TapeBlock } from './classes/TapeBlock';
7
7
  export { default as TapeCommand, movements, symbolCommands } from './classes/TapeCommand';
package/dist/index.mjs CHANGED
@@ -184,25 +184,30 @@ var __classPrivateFieldGet$4 = (undefined && undefined.__classPrivateFieldGet) |
184
184
  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");
185
185
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
186
186
  };
187
- var _Tape_alphabet, _Tape_symbols, _Tape_position, _Tape_viewportWidth;
187
+ var _Tape_instances, _Tape_alphabet, _Tape_right, _Tape_left, _Tape_position, _Tape_viewportWidth, _Tape_viewportBuffer, _Tape_viewportDirty, _Tape_cellAt;
188
+ const BLANK_INDEX = 0;
188
189
  class Tape {
189
190
  constructor({ alphabet, symbols = [], position = 0, viewportWidth = 1, }) {
191
+ _Tape_instances.add(this);
190
192
  _Tape_alphabet.set(this, void 0);
191
- _Tape_symbols.set(this, void 0);
193
+ _Tape_right.set(this, []);
194
+ _Tape_left.set(this, []);
192
195
  _Tape_position.set(this, void 0);
193
196
  _Tape_viewportWidth.set(this, void 0);
197
+ _Tape_viewportBuffer.set(this, []);
198
+ _Tape_viewportDirty.set(this, true);
194
199
  const isSymbolsValid = symbols.every((symbol) => alphabet.has(symbol));
195
200
  if (!isSymbolsValid) {
196
201
  throw new Error('symbolList contains invalid symbol');
197
202
  }
198
203
  __classPrivateFieldSet$4(this, _Tape_alphabet, new Alphabet(alphabet), "f");
199
204
  __classPrivateFieldSet$4(this, _Tape_position, position, "f");
200
- __classPrivateFieldSet$4(this, _Tape_viewportWidth, viewportWidth, "f");
201
- const symbolsCopy = Array.from(symbols);
202
- if (symbolsCopy.length === 0) {
203
- symbolsCopy.push(__classPrivateFieldGet$4(this, _Tape_alphabet, "f").blankSymbol);
205
+ __classPrivateFieldSet$4(this, _Tape_viewportWidth, 1, "f");
206
+ const initialSymbols = symbols.length === 0 ? [__classPrivateFieldGet$4(this, _Tape_alphabet, "f").blankSymbol] : symbols;
207
+ for (const symbol of initialSymbols) {
208
+ __classPrivateFieldGet$4(this, _Tape_right, "f").push(__classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol));
204
209
  }
205
- __classPrivateFieldSet$4(this, _Tape_symbols, symbolsCopy.map((symbol) => __classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol)), "f");
210
+ this.viewportWidth = viewportWidth;
206
211
  }
207
212
  get alphabet() {
208
213
  return __classPrivateFieldGet$4(this, _Tape_alphabet, "f");
@@ -211,27 +216,46 @@ class Tape {
211
216
  return (__classPrivateFieldGet$4(this, _Tape_viewportWidth, "f") - 1) / 2;
212
217
  }
213
218
  get position() {
214
- return __classPrivateFieldGet$4(this, _Tape_position, "f");
219
+ // Public contract: index of the head in the `symbols` array. With the
220
+ // two-array deque, `#position` is the head's logical position; adding
221
+ // `#left.length` shifts it back into "index from the leftmost backed cell".
222
+ return __classPrivateFieldGet$4(this, _Tape_position, "f") + __classPrivateFieldGet$4(this, _Tape_left, "f").length;
215
223
  }
216
224
  get symbol() {
217
- return __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_symbols, "f")[__classPrivateFieldGet$4(this, _Tape_position, "f")]);
225
+ return __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_instances, "m", _Tape_cellAt).call(this, __classPrivateFieldGet$4(this, _Tape_position, "f")));
218
226
  }
219
227
  set symbol(symbol) {
220
228
  if (!__classPrivateFieldGet$4(this, _Tape_alphabet, "f").has(symbol)) {
221
229
  throw new Error('Invalid symbol');
222
230
  }
223
- __classPrivateFieldGet$4(this, _Tape_symbols, "f")[__classPrivateFieldGet$4(this, _Tape_position, "f")] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol);
231
+ const index = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").index(symbol);
232
+ if (__classPrivateFieldGet$4(this, _Tape_position, "f") >= 0) {
233
+ __classPrivateFieldGet$4(this, _Tape_right, "f")[__classPrivateFieldGet$4(this, _Tape_position, "f")] = index;
234
+ }
235
+ else {
236
+ __classPrivateFieldGet$4(this, _Tape_left, "f")[-__classPrivateFieldGet$4(this, _Tape_position, "f") - 1] = index;
237
+ }
238
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
224
239
  }
225
240
  get symbols() {
226
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
227
- .map((index) => __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(index));
241
+ const result = new Array(__classPrivateFieldGet$4(this, _Tape_left, "f").length + __classPrivateFieldGet$4(this, _Tape_right, "f").length);
242
+ for (let i = 0; i < __classPrivateFieldGet$4(this, _Tape_left, "f").length; i += 1) {
243
+ result[i] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_left, "f")[__classPrivateFieldGet$4(this, _Tape_left, "f").length - 1 - i]);
244
+ }
245
+ for (let i = 0; i < __classPrivateFieldGet$4(this, _Tape_right, "f").length; i += 1) {
246
+ result[__classPrivateFieldGet$4(this, _Tape_left, "f").length + i] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_right, "f")[i]);
247
+ }
248
+ return result;
228
249
  }
229
250
  get viewport() {
230
- const startIx = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
231
- const endIx = __classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount + 1;
232
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
233
- .slice(startIx, endIx)
234
- .map((index) => __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(index));
251
+ if (__classPrivateFieldGet$4(this, _Tape_viewportDirty, "f")) {
252
+ const start = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
253
+ for (let i = 0; i < __classPrivateFieldGet$4(this, _Tape_viewportWidth, "f"); i += 1) {
254
+ __classPrivateFieldGet$4(this, _Tape_viewportBuffer, "f")[i] = __classPrivateFieldGet$4(this, _Tape_alphabet, "f").get(__classPrivateFieldGet$4(this, _Tape_instances, "m", _Tape_cellAt).call(this, start + i));
255
+ }
256
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, false, "f");
257
+ }
258
+ return [...__classPrivateFieldGet$4(this, _Tape_viewportBuffer, "f")];
235
259
  }
236
260
  get viewportWidth() {
237
261
  return __classPrivateFieldGet$4(this, _Tape_viewportWidth, "f");
@@ -245,27 +269,38 @@ class Tape {
245
269
  finalWidth += 1;
246
270
  }
247
271
  __classPrivateFieldSet$4(this, _Tape_viewportWidth, finalWidth, "f");
272
+ __classPrivateFieldGet$4(this, _Tape_viewportBuffer, "f").length = finalWidth;
273
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
248
274
  this.normalise();
249
275
  }
250
276
  left() {
251
277
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") - 1, "f");
252
278
  this.normalise();
279
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
253
280
  }
254
281
  normalise() {
255
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount < 0) {
256
- __classPrivateFieldGet$4(this, _Tape_symbols, "f").unshift(0);
257
- __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") + 1, "f");
282
+ const minLogical = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
283
+ const maxLogical = __classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount;
284
+ while (-__classPrivateFieldGet$4(this, _Tape_left, "f").length > minLogical) {
285
+ __classPrivateFieldGet$4(this, _Tape_left, "f").push(BLANK_INDEX);
258
286
  }
259
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount >= __classPrivateFieldGet$4(this, _Tape_symbols, "f").length) {
260
- __classPrivateFieldGet$4(this, _Tape_symbols, "f").push(0);
287
+ while (__classPrivateFieldGet$4(this, _Tape_right, "f").length - 1 < maxLogical) {
288
+ __classPrivateFieldGet$4(this, _Tape_right, "f").push(BLANK_INDEX);
261
289
  }
262
290
  }
263
291
  right() {
264
292
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") + 1, "f");
265
293
  this.normalise();
294
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
266
295
  }
267
296
  }
268
- _Tape_alphabet = new WeakMap(), _Tape_symbols = new WeakMap(), _Tape_position = new WeakMap(), _Tape_viewportWidth = new WeakMap();
297
+ _Tape_alphabet = new WeakMap(), _Tape_right = new WeakMap(), _Tape_left = new WeakMap(), _Tape_position = new WeakMap(), _Tape_viewportWidth = new WeakMap(), _Tape_viewportBuffer = new WeakMap(), _Tape_viewportDirty = new WeakMap(), _Tape_instances = new WeakSet(), _Tape_cellAt = function _Tape_cellAt(logical) {
298
+ if (logical >= 0) {
299
+ return logical < __classPrivateFieldGet$4(this, _Tape_right, "f").length ? __classPrivateFieldGet$4(this, _Tape_right, "f")[logical] : BLANK_INDEX;
300
+ }
301
+ const ix = -logical - 1;
302
+ return ix < __classPrivateFieldGet$4(this, _Tape_left, "f").length ? __classPrivateFieldGet$4(this, _Tape_left, "f")[ix] : BLANK_INDEX;
303
+ };
269
304
 
270
305
  var __classPrivateFieldGet$3 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
271
306
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
@@ -621,25 +656,64 @@ function decodeWriteSymbol(symbol) {
621
656
  }
622
657
  // Format converters (toMermaid / fromMermaid) live in ./graphFormats.
623
658
 
624
- var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
625
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
626
- 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");
627
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
628
- };
629
659
  var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
630
660
  if (kind === "m") throw new TypeError("Private method is not writable");
631
661
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
632
662
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
633
663
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
634
664
  };
635
- var _State_id, _State_name, _State_overrodeHaltState, _State_symbolToDataMap;
665
+ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
666
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
667
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
668
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
669
+ };
670
+ var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_id, _State_name, _State_overrodeHaltState, _State_symbolToDataMap, _State_debugRef;
636
671
  const ifOtherSymbol = Symbol('other symbol');
672
+ // Module-private symbol used by DebugConfig setters to call State's validator
673
+ // without exposing the validator on the public surface.
674
+ const validateDebugFilter = Symbol('validateDebugFilter');
675
+ class DebugConfig {
676
+ constructor(ownerState, initial) {
677
+ _DebugConfig_ownerState.set(this, void 0);
678
+ _DebugConfig_before.set(this, void 0);
679
+ _DebugConfig_after.set(this, void 0);
680
+ __classPrivateFieldSet$1(this, _DebugConfig_ownerState, ownerState, "f");
681
+ if (initial) {
682
+ if (initial.before !== undefined) {
683
+ this.before = initial.before;
684
+ }
685
+ if (initial.after !== undefined) {
686
+ this.after = initial.after;
687
+ }
688
+ }
689
+ }
690
+ get before() {
691
+ return __classPrivateFieldGet$1(this, _DebugConfig_before, "f");
692
+ }
693
+ set before(v) {
694
+ __classPrivateFieldGet$1(this, _DebugConfig_ownerState, "f")[validateDebugFilter]('before', v);
695
+ __classPrivateFieldSet$1(this, _DebugConfig_before, Array.isArray(v) ? Object.freeze([...v]) : v, "f");
696
+ }
697
+ get after() {
698
+ return __classPrivateFieldGet$1(this, _DebugConfig_after, "f");
699
+ }
700
+ set after(v) {
701
+ __classPrivateFieldGet$1(this, _DebugConfig_ownerState, "f")[validateDebugFilter]('after', v);
702
+ __classPrivateFieldSet$1(this, _DebugConfig_after, Array.isArray(v) ? Object.freeze([...v]) : v, "f");
703
+ }
704
+ }
705
+ _DebugConfig_ownerState = new WeakMap(), _DebugConfig_before = new WeakMap(), _DebugConfig_after = new WeakMap();
637
706
  class State {
638
707
  constructor(stateDefinition = null, name) {
639
708
  _State_id.set(this, id(this));
640
709
  _State_name.set(this, void 0);
641
710
  _State_overrodeHaltState.set(this, null);
642
711
  _State_symbolToDataMap.set(this, new Map());
712
+ // Shared mutable cell — withOverrodeHaltState wrappers reference the same
713
+ // object so that `state.debug = ...` (and nullings) propagate across them.
714
+ // Note: toGraph / fromGraph deliberately do not serialize debug — debug is
715
+ // a runtime concern, not part of the structural graph.
716
+ _State_debugRef.set(this, { current: null });
643
717
  if (stateDefinition) {
644
718
  const keys = Object.getOwnPropertyNames(stateDefinition);
645
719
  if (keys.length) {
@@ -698,6 +772,36 @@ class State {
698
772
  get ref() {
699
773
  return this;
700
774
  }
775
+ get debug() {
776
+ return __classPrivateFieldGet$1(this, _State_debugRef, "f").current;
777
+ }
778
+ set debug(value) {
779
+ if (value === null) {
780
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = null;
781
+ return;
782
+ }
783
+ if (value instanceof DebugConfig) {
784
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = value;
785
+ return;
786
+ }
787
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, value);
788
+ }
789
+ /** @internal — invoked by DebugConfig setters via module-private symbol. */
790
+ [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), validateDebugFilter)](fieldName, filter) {
791
+ if (filter === undefined || filter === true)
792
+ return;
793
+ // haltState has no own transitions; symbol-list filters on it are silent
794
+ // no-ops at the engine level (spec §8.6), so accept any list shape here.
795
+ if (this.isHalt)
796
+ return;
797
+ for (const sym of filter) {
798
+ if (sym !== ifOtherSymbol && !__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").has(sym)) {
799
+ throw new Error(`State.debug.${fieldName}: symbol is not a transition key of this state `
800
+ + `(state name: ${__classPrivateFieldGet$1(this, _State_name, "f")}). Common cause: symbol comes from a `
801
+ + 'different tape block, or doesn\'t match any of this state\'s transitions.');
802
+ }
803
+ }
804
+ }
701
805
  getSymbol(tapeBlock) {
702
806
  const symbol = [...__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").keys()].find((currentSymbol) => tapeBlock.isMatched({
703
807
  symbol: currentSymbol,
@@ -723,6 +827,7 @@ class State {
723
827
  const state = new State(null, `${this.name}>${overrodeHaltState.name}`);
724
828
  __classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f"), "f");
725
829
  __classPrivateFieldSet$1(state, _State_overrodeHaltState, overrodeHaltState, "f");
830
+ __classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(this, _State_debugRef, "f"), "f");
726
831
  return state;
727
832
  }
728
833
  // Single-state introspection — no traversal, no tapeBlock required.
@@ -893,7 +998,6 @@ class State {
893
998
  };
894
999
  }
895
1000
  }
896
- _State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap();
897
1001
  const haltState = new State(null);
898
1002
 
899
1003
  var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
@@ -908,6 +1012,15 @@ var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) ||
908
1012
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
909
1013
  };
910
1014
  var _TuringMachine_tapeBlock, _TuringMachine_stack;
1015
+ // True iff `filter` matches `symbol` per the DebugConfig semantics.
1016
+ // undefined / [] -> never; true -> always; symbol[] -> exact membership.
1017
+ function matchFilter(filter, symbol) {
1018
+ if (filter === undefined)
1019
+ return false;
1020
+ if (filter === true)
1021
+ return true;
1022
+ return filter.includes(symbol);
1023
+ }
911
1024
  class TuringMachine {
912
1025
  constructor({ tapeBlock, } = {}) {
913
1026
  _TuringMachine_tapeBlock.set(this, void 0);
@@ -920,12 +1033,22 @@ class TuringMachine {
920
1033
  get tapeBlock() {
921
1034
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
922
1035
  }
923
- run({ initialState, stepsLimit = 1e5, onStep }) {
1036
+ async run({ initialState, stepsLimit = 1e5, onStep, onDebugBreak, }) {
924
1037
  const generator = this.runStepByStep({ initialState, stepsLimit });
1038
+ let prevYield = null;
925
1039
  for (const machineState of generator) {
1040
+ // 'after' (from prev step) — fire FIRST, with prev yield substituted as the source view.
1041
+ if (machineState.debugBreak?.after && onDebugBreak && prevYield) {
1042
+ await onDebugBreak({ ...prevYield, debugBreak: { after: true } });
1043
+ }
1044
+ // 'before' (current step) — pass current machineState with only the before flag.
1045
+ if (machineState.debugBreak?.before && onDebugBreak) {
1046
+ await onDebugBreak({ ...machineState, debugBreak: { before: true } });
1047
+ }
926
1048
  if (onStep instanceof Function) {
927
1049
  onStep(machineState);
928
1050
  }
1051
+ prevYield = machineState;
929
1052
  }
930
1053
  }
931
1054
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
@@ -939,6 +1062,7 @@ class TuringMachine {
939
1062
  stack.push(state.overrodeHaltState);
940
1063
  }
941
1064
  let i = 0;
1065
+ let pendingAfterFromPrev = false;
942
1066
  while (!state.isHalt) {
943
1067
  if (i === stepsLimit) {
944
1068
  throw new Error('Long execution');
@@ -948,10 +1072,12 @@ class TuringMachine {
948
1072
  const command = state.getCommand(symbol);
949
1073
  let nextState = state.getNextState(symbol).ref;
950
1074
  try {
1075
+ const beforeMatch = matchFilter(state.debug?.before, symbol)
1076
+ || (nextState.isHalt && nextState.debug?.before === true);
951
1077
  const nextStateForYield = nextState.isHalt && stack.length
952
1078
  ? stack.slice(-1)[0]
953
1079
  : nextState;
954
- yield {
1080
+ const yielded = {
955
1081
  step: i,
956
1082
  state,
957
1083
  currentSymbols: __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").currentSymbols,
@@ -971,6 +1097,17 @@ class TuringMachine {
971
1097
  movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
972
1098
  nextState: nextStateForYield,
973
1099
  };
1100
+ if (pendingAfterFromPrev || beforeMatch) {
1101
+ const dbg = {};
1102
+ if (pendingAfterFromPrev)
1103
+ dbg.after = true;
1104
+ if (beforeMatch)
1105
+ dbg.before = true;
1106
+ yielded.debugBreak = dbg;
1107
+ }
1108
+ yield yielded;
1109
+ // Re-evaluate 'after' for THIS visit, to fire on the NEXT yield.
1110
+ pendingAfterFromPrev = matchFilter(state.debug?.after, symbol);
974
1111
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
975
1112
  if (nextState.isHalt && stack.length) {
976
1113
  nextState = stack.pop();
@@ -1299,4 +1436,4 @@ function runOnce(runnable, input, stepsLimit) {
1299
1436
  };
1300
1437
  }
1301
1438
 
1302
- export { Alphabet, Command, Reference, State, Tape, TapeBlock, TapeCommand, TuringMachine, equivalentOn, fromMermaid, haltState, ifOtherSymbol, movements, summarize, summarizeGraph, symbolCommands, toMermaid };
1439
+ export { Alphabet, Command, DebugConfig, Reference, State, Tape, TapeBlock, TapeCommand, TuringMachine, equivalentOn, fromMermaid, haltState, ifOtherSymbol, movements, summarize, summarizeGraph, symbolCommands, toMermaid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turing-machine-js/machine",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "description": "A convenient Turing machine",
5
5
  "engines": {
6
6
  "npm": ">=7.0.0"
@@ -34,5 +34,5 @@
34
34
  "default": "./dist/index.mjs"
35
35
  }
36
36
  },
37
- "gitHead": "0a8d7ff87a3a73ea7ee1844f7f4602cbb23d20fe"
37
+ "gitHead": "6641f9e8fd9073771ffd1768a7a5931ff93349a6"
38
38
  }