@turing-machine-js/machine 3.0.2 → 5.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,50 @@ 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
+ ## [5.0.0] - 2026-05-09
8
+
9
+ ### Changed (BREAKING)
10
+
11
+ - **Hook renamed: `onDebugBreak` → `onPause`** on `run()` ([#110](https://github.com/mellonis/turing-machine-js/issues/110)). Hard rename, no deprecation alias. The hook signature, payload shape, and dispatch semantics are unchanged — only the option key on `run()` differs. Resolution of the [#109 RFC](https://github.com/mellonis/turing-machine-js/issues/109): the hook now describes the consumer's relationship (this is where you can pause) rather than the engine's event (a debug break fired). The `m.debugBreak` payload field is unchanged.
12
+ - **`haltState.debug.after` now throws on assignment** ([#108](https://github.com/mellonis/turing-machine-js/issues/108) part 2). Halt is terminal — there is no iteration-after-halt for an after-fire to anchor on. Symmetric `{ before: true, after: true }` writes also throw; use `{ before: true }`. Symbol-list filters on `haltState.debug.before` remain silent no-ops as in v4.
13
+
14
+ ### Fixed
15
+
16
+ - **Halting iter's `state.debug.after` now fires** ([#108](https://github.com/mellonis/turing-machine-js/issues/108) part 1). Previously `pendingAfterFromPrev` set after the halting iter's yield was discarded when the `while (!state.isHalt)` loop exited — losing that after-fire. `run()` now drains a final after-fire after the loop when the prior yield armed one. Observable: a debugger UI sees a "we just halted because of state X's after-filter" event that was silent before.
17
+
18
+ ### Added
19
+
20
+ - **`run({ debug: boolean })` flag** ([#106](https://github.com/mellonis/turing-machine-js/issues/106)). Master switch for `onPause` dispatch. When `false`, suppresses all pause-fires (before, after, and the post-loop after-drain) regardless of `state.debug` assignments. `onStep` is unaffected. Defaults to `true`. The `m.debugBreak` field is still populated on yields by the underlying generator (it's a property of the iteration); only `run()`'s hook dispatch is gated. Useful for toggling "debug mode" without editing `state.debug = ...` across the state graph.
21
+
22
+ ### Migration
23
+
24
+ ```diff
25
+ await machine.run({
26
+ initialState,
27
+ - onDebugBreak: (m) => { ... },
28
+ + onPause: (m) => { ... },
29
+ });
30
+ ```
31
+
32
+ ```diff
33
+ - haltState.debug = { before: true, after: true }; // throws in v5
34
+ + haltState.debug = { before: true };
35
+ ```
36
+
37
+ ## [4.0.0] - 2026-05-07
38
+
39
+ ### Added
40
+
41
+ - **`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.
42
+ - **`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.
43
+ - **`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.
44
+ - **`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.
45
+ - **`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.
46
+
47
+ ### Changed (BREAKING)
48
+
49
+ - **`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.
50
+
7
51
  ## [3.0.2] - 2026-05-04
8
52
 
9
53
  ### 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 `onPause` hook:
260
+
261
+ ```ts
262
+ await machine.run({
263
+ initialState,
264
+ onStep: (m) => { /* logger sees every step */ },
265
+ onPause: 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 `onPause` 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,24 @@ 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, onPause, }: RunParameter & {
35
+ /**
36
+ * Sync, ~free hook fired on every iteration. Use for logging/tracing —
37
+ * the hot loop runs this without a microtask boundary, so it must not
38
+ * be async.
39
+ */
22
40
  onStep?: (machineState: MachineState) => void;
23
- }): void;
24
- runStepByStep({ initialState, stepsLimit }: RunParameter): Generator<MachineState>;
41
+ /**
42
+ * Async hook fired only when `state.debug[when]` matches at the current
43
+ * iteration. The promise is awaited inline, so the consumer can suspend
44
+ * execution by deferring its resolution. Use for pause-capable inspection
45
+ * (debugger UIs, conditional breakpoints in tests).
46
+ *
47
+ * Renamed from `onDebugBreak` in v5.0.0. The `m.debugBreak` payload field
48
+ * keeps its name (it describes the engine's reason for pausing).
49
+ */
50
+ onPause?: (machineState: MachineState) => void | Promise<void>;
51
+ }): Promise<void>;
52
+ runStepByStep({ initialState, stepsLimit }: RunParameter): Generator<MachineState, MachineState | null>;
25
53
  }
26
54
  export {};
package/dist/index.cjs CHANGED
@@ -186,13 +186,18 @@ 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');
@@ -200,11 +205,10 @@ class Tape {
200
205
  __classPrivateFieldSet$4(this, _Tape_alphabet, new Alphabet(alphabet), "f");
201
206
  __classPrivateFieldSet$4(this, _Tape_position, position, "f");
202
207
  __classPrivateFieldSet$4(this, _Tape_viewportWidth, 1, "f");
203
- const symbolsCopy = Array.from(symbols);
204
- if (symbolsCopy.length === 0) {
205
- symbolsCopy.push(__classPrivateFieldGet$4(this, _Tape_alphabet, "f").blankSymbol);
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");
208
212
  this.viewportWidth = viewportWidth;
209
213
  }
210
214
  get alphabet() {
@@ -214,27 +218,46 @@ class Tape {
214
218
  return (__classPrivateFieldGet$4(this, _Tape_viewportWidth, "f") - 1) / 2;
215
219
  }
216
220
  get position() {
217
- 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;
218
225
  }
219
226
  get symbol() {
220
- 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")));
221
228
  }
222
229
  set symbol(symbol) {
223
230
  if (!__classPrivateFieldGet$4(this, _Tape_alphabet, "f").has(symbol)) {
224
231
  throw new Error('Invalid symbol');
225
232
  }
226
- __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");
227
241
  }
228
242
  get symbols() {
229
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
230
- .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;
231
251
  }
232
252
  get viewport() {
233
- const startIx = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
234
- const endIx = __classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount + 1;
235
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
236
- .slice(startIx, endIx)
237
- .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")];
238
261
  }
239
262
  get viewportWidth() {
240
263
  return __classPrivateFieldGet$4(this, _Tape_viewportWidth, "f");
@@ -248,27 +271,38 @@ class Tape {
248
271
  finalWidth += 1;
249
272
  }
250
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");
251
276
  this.normalise();
252
277
  }
253
278
  left() {
254
279
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") - 1, "f");
255
280
  this.normalise();
281
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
256
282
  }
257
283
  normalise() {
258
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount < 0) {
259
- __classPrivateFieldGet$4(this, _Tape_symbols, "f").unshift(0);
260
- __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);
261
288
  }
262
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount >= __classPrivateFieldGet$4(this, _Tape_symbols, "f").length) {
263
- __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);
264
291
  }
265
292
  }
266
293
  right() {
267
294
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") + 1, "f");
268
295
  this.normalise();
296
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
269
297
  }
270
298
  }
271
- _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
+ };
272
306
 
273
307
  var __classPrivateFieldGet$3 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
274
308
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
@@ -624,25 +658,64 @@ function decodeWriteSymbol(symbol) {
624
658
  }
625
659
  // Format converters (toMermaid / fromMermaid) live in ./graphFormats.
626
660
 
627
- var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
628
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
629
- 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");
630
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
631
- };
632
661
  var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
633
662
  if (kind === "m") throw new TypeError("Private method is not writable");
634
663
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
635
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");
636
665
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
637
666
  };
638
- 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;
639
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();
640
708
  class State {
641
709
  constructor(stateDefinition = null, name) {
642
710
  _State_id.set(this, id(this));
643
711
  _State_name.set(this, void 0);
644
712
  _State_overrodeHaltState.set(this, null);
645
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 });
646
719
  if (stateDefinition) {
647
720
  const keys = Object.getOwnPropertyNames(stateDefinition);
648
721
  if (keys.length) {
@@ -701,6 +774,36 @@ class State {
701
774
  get ref() {
702
775
  return this;
703
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
+ }
704
807
  getSymbol(tapeBlock) {
705
808
  const symbol = [...__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").keys()].find((currentSymbol) => tapeBlock.isMatched({
706
809
  symbol: currentSymbol,
@@ -726,6 +829,7 @@ class State {
726
829
  const state = new State(null, `${this.name}>${overrodeHaltState.name}`);
727
830
  __classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f"), "f");
728
831
  __classPrivateFieldSet$1(state, _State_overrodeHaltState, overrodeHaltState, "f");
832
+ __classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(this, _State_debugRef, "f"), "f");
729
833
  return state;
730
834
  }
731
835
  // Single-state introspection — no traversal, no tapeBlock required.
@@ -896,7 +1000,6 @@ class State {
896
1000
  };
897
1001
  }
898
1002
  }
899
- _State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap();
900
1003
  const haltState = new State(null);
901
1004
 
902
1005
  var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
@@ -911,6 +1014,15 @@ var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) ||
911
1014
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
912
1015
  };
913
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
+ }
914
1026
  class TuringMachine {
915
1027
  constructor({ tapeBlock, } = {}) {
916
1028
  _TuringMachine_tapeBlock.set(this, void 0);
@@ -923,12 +1035,22 @@ class TuringMachine {
923
1035
  get tapeBlock() {
924
1036
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
925
1037
  }
926
- run({ initialState, stepsLimit = 1e5, onStep }) {
1038
+ async run({ initialState, stepsLimit = 1e5, onStep, onDebugBreak, }) {
927
1039
  const generator = this.runStepByStep({ initialState, stepsLimit });
1040
+ let prevYield = null;
928
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
+ }
929
1050
  if (onStep instanceof Function) {
930
1051
  onStep(machineState);
931
1052
  }
1053
+ prevYield = machineState;
932
1054
  }
933
1055
  }
934
1056
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
@@ -942,6 +1064,7 @@ class TuringMachine {
942
1064
  stack.push(state.overrodeHaltState);
943
1065
  }
944
1066
  let i = 0;
1067
+ let pendingAfterFromPrev = false;
945
1068
  while (!state.isHalt) {
946
1069
  if (i === stepsLimit) {
947
1070
  throw new Error('Long execution');
@@ -951,10 +1074,12 @@ class TuringMachine {
951
1074
  const command = state.getCommand(symbol);
952
1075
  let nextState = state.getNextState(symbol).ref;
953
1076
  try {
1077
+ const beforeMatch = matchFilter(state.debug?.before, symbol)
1078
+ || (nextState.isHalt && nextState.debug?.before === true);
954
1079
  const nextStateForYield = nextState.isHalt && stack.length
955
1080
  ? stack.slice(-1)[0]
956
1081
  : nextState;
957
- yield {
1082
+ const yielded = {
958
1083
  step: i,
959
1084
  state,
960
1085
  currentSymbols: __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").currentSymbols,
@@ -974,6 +1099,17 @@ class TuringMachine {
974
1099
  movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
975
1100
  nextState: nextStateForYield,
976
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);
977
1113
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
978
1114
  if (nextState.isHalt && stack.length) {
979
1115
  nextState = stack.pop();
@@ -1304,6 +1440,7 @@ function runOnce(runnable, input, stepsLimit) {
1304
1440
 
1305
1441
  exports.Alphabet = Alphabet;
1306
1442
  exports.Command = Command;
1443
+ exports.DebugConfig = DebugConfig;
1307
1444
  exports.Reference = Reference;
1308
1445
  exports.State = State;
1309
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,13 +184,18 @@ 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');
@@ -198,11 +203,10 @@ class Tape {
198
203
  __classPrivateFieldSet$4(this, _Tape_alphabet, new Alphabet(alphabet), "f");
199
204
  __classPrivateFieldSet$4(this, _Tape_position, position, "f");
200
205
  __classPrivateFieldSet$4(this, _Tape_viewportWidth, 1, "f");
201
- const symbolsCopy = Array.from(symbols);
202
- if (symbolsCopy.length === 0) {
203
- symbolsCopy.push(__classPrivateFieldGet$4(this, _Tape_alphabet, "f").blankSymbol);
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");
206
210
  this.viewportWidth = viewportWidth;
207
211
  }
208
212
  get alphabet() {
@@ -212,27 +216,46 @@ class Tape {
212
216
  return (__classPrivateFieldGet$4(this, _Tape_viewportWidth, "f") - 1) / 2;
213
217
  }
214
218
  get position() {
215
- 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;
216
223
  }
217
224
  get symbol() {
218
- 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")));
219
226
  }
220
227
  set symbol(symbol) {
221
228
  if (!__classPrivateFieldGet$4(this, _Tape_alphabet, "f").has(symbol)) {
222
229
  throw new Error('Invalid symbol');
223
230
  }
224
- __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");
225
239
  }
226
240
  get symbols() {
227
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
228
- .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;
229
249
  }
230
250
  get viewport() {
231
- const startIx = __classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount;
232
- const endIx = __classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount + 1;
233
- return __classPrivateFieldGet$4(this, _Tape_symbols, "f")
234
- .slice(startIx, endIx)
235
- .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")];
236
259
  }
237
260
  get viewportWidth() {
238
261
  return __classPrivateFieldGet$4(this, _Tape_viewportWidth, "f");
@@ -246,27 +269,38 @@ class Tape {
246
269
  finalWidth += 1;
247
270
  }
248
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");
249
274
  this.normalise();
250
275
  }
251
276
  left() {
252
277
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") - 1, "f");
253
278
  this.normalise();
279
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
254
280
  }
255
281
  normalise() {
256
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") - this.extraCellsCount < 0) {
257
- __classPrivateFieldGet$4(this, _Tape_symbols, "f").unshift(0);
258
- __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);
259
286
  }
260
- while (__classPrivateFieldGet$4(this, _Tape_position, "f") + this.extraCellsCount >= __classPrivateFieldGet$4(this, _Tape_symbols, "f").length) {
261
- __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);
262
289
  }
263
290
  }
264
291
  right() {
265
292
  __classPrivateFieldSet$4(this, _Tape_position, __classPrivateFieldGet$4(this, _Tape_position, "f") + 1, "f");
266
293
  this.normalise();
294
+ __classPrivateFieldSet$4(this, _Tape_viewportDirty, true, "f");
267
295
  }
268
296
  }
269
- _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
+ };
270
304
 
271
305
  var __classPrivateFieldGet$3 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
272
306
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
@@ -622,25 +656,64 @@ function decodeWriteSymbol(symbol) {
622
656
  }
623
657
  // Format converters (toMermaid / fromMermaid) live in ./graphFormats.
624
658
 
625
- var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
626
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
627
- 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");
628
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
629
- };
630
659
  var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
631
660
  if (kind === "m") throw new TypeError("Private method is not writable");
632
661
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
633
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");
634
663
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
635
664
  };
636
- 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;
637
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();
638
706
  class State {
639
707
  constructor(stateDefinition = null, name) {
640
708
  _State_id.set(this, id(this));
641
709
  _State_name.set(this, void 0);
642
710
  _State_overrodeHaltState.set(this, null);
643
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 });
644
717
  if (stateDefinition) {
645
718
  const keys = Object.getOwnPropertyNames(stateDefinition);
646
719
  if (keys.length) {
@@ -699,6 +772,36 @@ class State {
699
772
  get ref() {
700
773
  return this;
701
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
+ }
702
805
  getSymbol(tapeBlock) {
703
806
  const symbol = [...__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").keys()].find((currentSymbol) => tapeBlock.isMatched({
704
807
  symbol: currentSymbol,
@@ -724,6 +827,7 @@ class State {
724
827
  const state = new State(null, `${this.name}>${overrodeHaltState.name}`);
725
828
  __classPrivateFieldSet$1(state, _State_symbolToDataMap, __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f"), "f");
726
829
  __classPrivateFieldSet$1(state, _State_overrodeHaltState, overrodeHaltState, "f");
830
+ __classPrivateFieldSet$1(state, _State_debugRef, __classPrivateFieldGet$1(this, _State_debugRef, "f"), "f");
727
831
  return state;
728
832
  }
729
833
  // Single-state introspection — no traversal, no tapeBlock required.
@@ -894,7 +998,6 @@ class State {
894
998
  };
895
999
  }
896
1000
  }
897
- _State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap();
898
1001
  const haltState = new State(null);
899
1002
 
900
1003
  var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
@@ -909,6 +1012,15 @@ var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) ||
909
1012
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
910
1013
  };
911
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
+ }
912
1024
  class TuringMachine {
913
1025
  constructor({ tapeBlock, } = {}) {
914
1026
  _TuringMachine_tapeBlock.set(this, void 0);
@@ -921,12 +1033,22 @@ class TuringMachine {
921
1033
  get tapeBlock() {
922
1034
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
923
1035
  }
924
- run({ initialState, stepsLimit = 1e5, onStep }) {
1036
+ async run({ initialState, stepsLimit = 1e5, onStep, onDebugBreak, }) {
925
1037
  const generator = this.runStepByStep({ initialState, stepsLimit });
1038
+ let prevYield = null;
926
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
+ }
927
1048
  if (onStep instanceof Function) {
928
1049
  onStep(machineState);
929
1050
  }
1051
+ prevYield = machineState;
930
1052
  }
931
1053
  }
932
1054
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
@@ -940,6 +1062,7 @@ class TuringMachine {
940
1062
  stack.push(state.overrodeHaltState);
941
1063
  }
942
1064
  let i = 0;
1065
+ let pendingAfterFromPrev = false;
943
1066
  while (!state.isHalt) {
944
1067
  if (i === stepsLimit) {
945
1068
  throw new Error('Long execution');
@@ -949,10 +1072,12 @@ class TuringMachine {
949
1072
  const command = state.getCommand(symbol);
950
1073
  let nextState = state.getNextState(symbol).ref;
951
1074
  try {
1075
+ const beforeMatch = matchFilter(state.debug?.before, symbol)
1076
+ || (nextState.isHalt && nextState.debug?.before === true);
952
1077
  const nextStateForYield = nextState.isHalt && stack.length
953
1078
  ? stack.slice(-1)[0]
954
1079
  : nextState;
955
- yield {
1080
+ const yielded = {
956
1081
  step: i,
957
1082
  state,
958
1083
  currentSymbols: __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").currentSymbols,
@@ -972,6 +1097,17 @@ class TuringMachine {
972
1097
  movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
973
1098
  nextState: nextStateForYield,
974
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);
975
1111
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
976
1112
  if (nextState.isHalt && stack.length) {
977
1113
  nextState = stack.pop();
@@ -1300,4 +1436,4 @@ function runOnce(runnable, input, stepsLimit) {
1300
1436
  };
1301
1437
  }
1302
1438
 
1303
- 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.2",
3
+ "version": "5.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": "0a78b4d0d40bbdbf79e3c53694adf930f0567b0a"
37
+ "gitHead": "ccf9a4743965c2a4ea6340ce1de8f6596e094251"
38
38
  }