@turing-machine-js/machine 6.0.0 → 6.1.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/README.md CHANGED
@@ -3,7 +3,25 @@
3
3
  [![build](https://github.com/mellonis/turing-machine-js/actions/workflows/main.yml/badge.svg)](https://github.com/mellonis/turing-machine-js/actions/workflows/main.yml)
4
4
  ![npm (tag)](https://img.shields.io/npm/v/@turing-machine-js/machine)
5
5
 
6
- Some basic objects to build your own turing machine
6
+ A composable Turing-machine engine for JavaScript: multi-tape, subroutine composition via `withOverrodeHaltState`, Mermaid round-trip, and runtime breakpoints.
7
+
8
+ <details>
9
+ <summary>Table of contents</summary>
10
+
11
+ - [Install](#install)
12
+ - [Quick start](#quick-start)
13
+ - [Building from a state table](#building-from-a-state-table)
14
+ - [Classes](#classes) — [`Alphabet`](#alphabet) · [`Tape`](#tape) · [`TapeBlock`](#tapeblock) · [`TapeCommand`](#tapecommand) · [`Command`](#command) · [`State`](#state) · [`Reference`](#reference) · [`TuringMachine`](#turingmachine)
15
+ - [Subroutine composition with `withOverrodeHaltState`](#subroutine-composition-with-withoverrodehaltstate)
16
+ - [Debugging breakpoints](#debugging-breakpoints)
17
+ - [Special objects](#special-objects) — [`haltState`](#haltstate) · [`ifOtherSymbol`](#ifothersymbol) · [`movements`](#movements) · [`symbolCommands`](#symbolcommands)
18
+ - [Introspection and testing](#introspection-and-testing)
19
+ - [Versioning notes](#versioning-notes)
20
+ - [Libraries](#libraries)
21
+ - [Links](#links)
22
+
23
+ </details>
24
+
7
25
 
8
26
  ## Install
9
27
 
@@ -46,13 +64,43 @@ await machine.run({
46
64
  [ifOtherSymbol]: {
47
65
  command: [{ movement: movements.right }],
48
66
  },
49
- }),
67
+ }, 'replaceB'),
50
68
  });
51
69
 
52
70
  console.log(tape.symbols.join('').trim()); // a*c*a
53
71
  ```
54
72
 
55
- A `State` is keyed by JS `Symbol`s returned from `tapeBlock.symbol(pattern)` — the pattern lists the expected symbol under each tape's head. `ifOtherSymbol` is the fallback key when nothing else matches; transitioning into `haltState` stops the run.
73
+ The state graph for the example above:
74
+
75
+ ```mermaid
76
+ flowchart LR
77
+ S(("**replaceB**"))
78
+ H((("**halt**")))
79
+ S -- "b → *, R" --> S
80
+ S -- "_ → keep, L" --> H
81
+ S -- "any other → keep, R" --> S
82
+ ```
83
+
84
+ *Reading the labels: `read → write, move`. `_` is the blank symbol.*
85
+
86
+ <details>
87
+ <summary>📊 Same diagram, generated by <code>toMermaid()</code> (the engine's actual output)</summary>
88
+
89
+ ```mermaid
90
+ flowchart TD
91
+ %% alphabets: [[" ","a","b","c","*"]]
92
+ s0(((halt)))
93
+ s1(("replaceB"))
94
+ s1 -- "b → */R" --> s1
95
+ s1 -- "- → ·/L" --> s0
96
+ s1 -- "* → ·/R" --> s1
97
+ ```
98
+
99
+ Engine notation: `read → write/move`; `·` = keep, `⌫` = erase, `*` = `ifOtherSymbol` catch-all, `-` = the blank symbol. `(((double-paren)))` = halt; `(("round"))` = the initial state passed to `toGraph`; `["square"]` = a state reachable from the initial state.
100
+
101
+ </details>
102
+
103
+ A `State` is keyed by JS `Symbol`s returned from `tapeBlock.symbol(pattern)` — the pattern lists the expected symbol under each tape's head. Sentinels and constants used throughout: [`ifOtherSymbol`](#ifothersymbol) is the fallback key when nothing else matches; transitioning into [`haltState`](#haltstate) stops the run; [`movements`](#movements)`.{left,right,stay}` direct head moves; [`symbolCommands`](#symbolcommands)`.{keep,erase}` are write shortcuts. Full definitions in [§Special objects](#special-objects).
56
104
 
57
105
  For multi-tape machines, pass one element per tape: `tapeBlock.symbol(['0', 'a'])` matches only when tape 1 is at `'0'` and tape 2 is at `'a'`.
58
106
 
@@ -143,6 +191,16 @@ tape.right(); // move head right; auto-extends with blanks at the edge
143
191
  tape.symbol = 'X'; // write the cell under head
144
192
  ```
145
193
 
194
+ For visualization-friendly UIs, `Tape` exposes a fixed-width viewport centered on the head:
195
+
196
+ ```javascript
197
+ const tape = new Tape({ alphabet, symbols: ['a', 'b', 'c'], viewportWidth: 7 });
198
+ tape.viewport; // 7-cell snapshot centered on the head, padded with blanks
199
+ tape.viewportWidth; // 7 (the constructor bumps even values to the next odd)
200
+ ```
201
+
202
+ `viewportWidth` defaults to `1` and must be ≥ 1; `tape.viewport` always has exactly `viewportWidth` cells regardless of how many symbols the tape actually holds. Useful for rendering a sliding window in a UI; ignore if you only need `tape.symbols` / `tape.position`.
203
+
146
204
  ### TapeBlock
147
205
 
148
206
  A bundle of one or more `Tape`s that the machine reads/writes together in lock-step. Construct via either factory:
@@ -186,8 +244,7 @@ A node in the transition graph. Construct with a definition object whose keys ar
186
244
  ```javascript
187
245
  const s = new State({
188
246
  [tapeBlock.symbol(['1'])]: { command: { symbol: '0', movement: movements.right } },
189
- [tapeBlock.symbol(['$'])]: { nextState: haltState },
190
- [ifOtherSymbol]: { command: { movement: movements.right } },
247
+ [tapeBlock.symbol(['$'])]: { command: { movement: movements.left }, nextState: haltState },
191
248
  }, 'name');
192
249
  ```
193
250
 
@@ -198,6 +255,32 @@ Notable members and statics:
198
255
  - **`State.toGraph(state, tapeBlock)`** — walks the reachable graph from `state` and returns a serializable `Graph` (states, transitions, alphabets).
199
256
  - **`State.fromGraph(graph)`** — inverse of `toGraph`: rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips together with `toMermaid` / `fromMermaid`.
200
257
 
258
+ For visualization, pair `State.toGraph` with `toMermaid` to render the graph in any Mermaid-aware viewer (GitHub, VS Code, mermaid.live):
259
+
260
+ ```javascript
261
+ import { State, toMermaid } from '@turing-machine-js/machine';
262
+
263
+ const graph = State.toGraph(s, tapeBlock);
264
+ console.log(toMermaid(graph));
265
+ ```
266
+
267
+ The string `toMermaid` produces is a real Mermaid flowchart that renders in-place anywhere Mermaid is supported:
268
+
269
+ ```mermaid
270
+ flowchart TD
271
+ %% alphabets: [[" ","0","1","$"]]
272
+ s0(((halt)))
273
+ s1(("name"))
274
+ s1 -- "1 → 0/R" --> s1
275
+ s1 -- "$ → ·/L" --> s0
276
+ ```
277
+
278
+ *Edge labels are `read → write/move`. `·` is the keep-current-symbol marker (no write); `L` / `R` / `S` are head moves.*
279
+
280
+ > 💡 **Mermaid renders at most one edge per source/target pair.** If a state has two distinct transitions back to itself (or two parallel transitions to the same target), only one shows in the diagram. The string output is correct — this is a viewer-side limitation. For graphs with multiple parallel edges, paste the `toMermaid` output into [mermaid.live](https://mermaid.live) and switch to the `stateDiagram-v2` renderer, or post-process the output to your preferred format.
281
+
282
+ `fromMermaid` parses the same format back into a `Graph`. The round-trip is **behaviorally** lossless — the rebuilt graph runs to the same outputs on the same inputs (tested in `test/round-trip.spec.ts` for the binary-numbers libraries). It is *not* bytewise lossless: state IDs auto-reassign on each rebuild, and for `withOverrodeHaltState` wrappers the composite name gains a `>${override.name}` suffix on each pass (e.g., `scanToX>eraseHere` becomes `scanToX>eraseHere>eraseHere` on a second round-trip — tracked in [#138](https://github.com/mellonis/turing-machine-js/issues/138)).
283
+
201
284
  ### Reference
202
285
 
203
286
  A forward-declaration handle, used when a `State` needs to point at another `State` that doesn't exist yet (cyclic graphs). Construct unbound, pass as `nextState`, call `.bind(actualState)` once that state has been built.
@@ -209,7 +292,33 @@ const b = new State({ [symbol(['y'])]: { nextState: a } }, 'b');
209
292
  ref.bind(b); // a's transition now resolves to b at run time
210
293
  ```
211
294
 
212
- `reference.ref` returns the bound state and throws if the reference is still unbound when the machine runs.
295
+ The resulting cycle:
296
+
297
+ ```mermaid
298
+ flowchart LR
299
+ a(("**a**"))
300
+ b(("**b**"))
301
+ a -- "x" --> b
302
+ b -- "y" --> a
303
+ ```
304
+
305
+ <details>
306
+ <summary>📊 Same diagram, generated by <code>toMermaid()</code></summary>
307
+
308
+ ```mermaid
309
+ flowchart TD
310
+ %% alphabets: [[" ","x","y"]]
311
+ s1(("a"))
312
+ s2["b"]
313
+ s1 -- "x → ·/S" --> s2
314
+ s2 -- "y → ·/S" --> s1
315
+ ```
316
+
317
+ `a` is round (the initial state passed to `toGraph`); `b` is square (reachable from `a`).
318
+
319
+ </details>
320
+
321
+ `reference.ref` returns the bound state and throws if the reference is still unbound when the machine runs. `bind()` is sticky — the first call wins; subsequent calls are silent no-ops that return the existing binding.
213
322
 
214
323
  ### TuringMachine
215
324
 
@@ -218,7 +327,7 @@ The runtime. Owns one `TapeBlock` and drives a state graph until it reaches `hal
218
327
  ```javascript
219
328
  const machine = new TuringMachine({ tapeBlock });
220
329
 
221
- // Run to halt — `run()` is async (v4+), it returns a Promise<void>:
330
+ // Run to halt — `run()` returns a Promise<void>:
222
331
  await machine.run({ initialState, stepsLimit: 1e5 });
223
332
 
224
333
  // Or step-by-step (useful for visualization / debugging):
@@ -227,34 +336,163 @@ for (const step of machine.runStepByStep({ initialState })) {
227
336
  }
228
337
  ```
229
338
 
339
+ Each yielded `step` (`MachineState`) has these fields:
340
+
341
+ | Field | Type | Meaning |
342
+ |---|---|---|
343
+ | `step` | `number` | 1-indexed iteration number |
344
+ | `state` | `State` | the state about to execute |
345
+ | `currentSymbols` | `string[]` | per-tape head symbols, before the command applies |
346
+ | `nextSymbols` | `string[]` | per-tape symbols that will be written |
347
+ | `movements` | `symbol[]` | per-tape head moves (`movements.left/right/stay`) |
348
+ | `nextState` | `State` | the state that will execute next |
349
+ | `debugBreak?` | `{ before?: true, after?: true }` | only set when `state.debug` matched on this iter — see *Debugging breakpoints* below |
350
+
230
351
  `stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
231
352
 
232
- ## Debugging breakpoints (v4+)
353
+ #### Choosing between `run()` and `runStepByStep()`
354
+
355
+ Both APIs are first-class — `run()` is built on top of `runStepByStep()` (see [TuringMachine.ts](src/classes/TuringMachine.ts)), and both stay supported. They model different consumer needs:
356
+
357
+ | | `run()` | `runStepByStep()` |
358
+ |---|---|---|
359
+ | Shape | async, returns `Promise<void>` | synchronous generator |
360
+ | Iteration timing | owned by the engine | owned by the consumer (`.next()` per step) |
361
+ | Lifecycle hooks | dispatches `onStep`, `onPause` (gated by the `debug` master switch) | none — yields raw `MachineState` |
362
+ | How `state.debug` reaches the consumer | the `onPause` callback (when `debug: true`) | the optional `debugBreak` field on each yield (always populated; consumer decides what to do) |
363
+ | Best for | run-to-halt with optional breakpoint UI; anything wanting the v6 per-iter `before → step → after` callbacks | synchronous test harnesses, visualizers that need tight control over step timing, custom batching |
364
+
365
+ **Rule of thumb.** If your consumer reads `state.debug` and expects the engine to act on it (pause, fire callbacks), use `run()`. If you want pull-based iteration with full control over timing, use `runStepByStep()` — the `debugBreak` field is still on every yield, so you can inspect breakpoint metadata yourself.
366
+
367
+ **Don't split one logical flow across both APIs.** A consumer that wants stepwise UI *and* hook-driven breakpoints should use `run({ onStep, onPause, debug })` exclusively, implementing "wait between steps" by awaiting a resolvable Promise inside `onStep`. Routing some operations through `runStepByStep()` and others through `run()` means `state.debug` only flows through one of the two paths — a subtle footgun where breakpoints silently disappear on whichever code path uses the generator directly.
368
+
369
+ ## Subroutine composition with `withOverrodeHaltState`
370
+
371
+ `state.withOverrodeHaltState(other)` returns a copy of `state` whose would-be halt transitions fall through to `other` at run time. The original is left untouched. This is the engine's only composition primitive — bigger machines are built by stacking smaller halt-on-completion subroutines.
372
+
373
+ ```javascript
374
+ import { Alphabet, State, TapeBlock, TuringMachine, Tape, haltState, ifOtherSymbol, movements, symbolCommands } from '@turing-machine-js/machine';
375
+
376
+ const alphabet = new Alphabet([' ', 'a', 'b', 'X']);
377
+ const tapeBlock = TapeBlock.fromAlphabets([alphabet]);
378
+ const { symbol } = tapeBlock;
379
+
380
+ // Reusable subroutine 1: walk right until 'X', halt on it.
381
+ const scanToX = new State({
382
+ [symbol(['X'])]: { nextState: haltState },
383
+ [ifOtherSymbol]: { command: { movement: movements.right } },
384
+ }, 'scanToX');
385
+
386
+ // Reusable subroutine 2: erase the head cell, halt.
387
+ const eraseHere = new State({
388
+ [ifOtherSymbol]: { command: { symbol: symbolCommands.erase }, nextState: haltState },
389
+ }, 'eraseHere');
390
+
391
+ // Compose: scan to X, then ERASE it. scanToX is unmodified.
392
+ const scanThenErase = scanToX.withOverrodeHaltState(eraseHere);
393
+
394
+ const tape = new Tape({ alphabet, symbols: ['a', 'b', 'X', 'b', 'a'] });
395
+ tapeBlock.replaceTape(tape);
396
+ await new TuringMachine({ tapeBlock }).run({ initialState: scanThenErase });
397
+
398
+ console.log(tape.symbols.join('')); // "ab ba" — the X at index 2 is gone, head landed there.
399
+ ```
400
+
401
+ What changes between *running `scanToX` standalone* and *running the composed wrapper*:
402
+
403
+ ```mermaid
404
+ flowchart LR
405
+ subgraph standalone["scanToX (standalone) — halts at X"]
406
+ direction LR
407
+ a1(("scanToX"))
408
+ h1(((halt)))
409
+ a1 -- "X → keep, S" --> h1
410
+ a1 -- "any other → keep, R" --> a1
411
+ end
412
+ subgraph composed["scanToX.withOverrodeHaltState(eraseHere) — halt is intercepted"]
413
+ direction LR
414
+ a2(("scanToX"))
415
+ b2(("eraseHere"))
416
+ h2(((halt)))
417
+ a2 -. "X → keep, S<br/>intercepted" .-> b2
418
+ a2 -- "any other → keep, R" --> a2
419
+ b2 -- "any → erase, S" --> h2
420
+ end
421
+ ```
422
+
423
+ <details>
424
+ <summary>📊 Both diagrams, generated by <code>toMermaid()</code></summary>
425
+
426
+ `toMermaid(toGraph(scanToX, tapeBlock))` — the standalone subroutine:
427
+
428
+ ```mermaid
429
+ flowchart TD
430
+ %% alphabets: [[" ","a","b","X"]]
431
+ s0(((halt)))
432
+ s1(("scanToX"))
433
+ s1 -- "X → ·/S" --> s0
434
+ s1 -- "* → ·/R" --> s1
435
+ ```
436
+
437
+ `toMermaid(toGraph(scanThenErase, tapeBlock))` — the wrapped composition:
438
+
439
+ ```mermaid
440
+ flowchart TD
441
+ %% alphabets: [[" ","a","b","X"]]
442
+ s0(((halt)))
443
+ s1["scanToX"]
444
+ s2["eraseHere"]
445
+ s3(("scanToX>eraseHere"))
446
+ s1 -- "X → ·/S" --> s0
447
+ s1 -- "* → ·/R" --> s1
448
+ s2 -- "* → ⌫/S" --> s0
449
+ s3 -- "X → ·/S" --> s0
450
+ s3 -- "* → ·/R" --> s1
451
+ s3 -. onHalt .-> s2
452
+ ```
453
+
454
+ **Reading guide** — the wrapped diagram is denser than the simplified hand-drawn version above. To parse it:
455
+
456
+ 1. **Three non-halt nodes:** the wrapper `scanToX>eraseHere` (round, the initial state); `scanToX` (square, the original subroutine — unmodified); `eraseHere` (square, the override target). The wrapper appears as a *separate* state from `scanToX`-the-original because `withOverrodeHaltState` returns a new `State` instance — even though it shares the transition map.
457
+ 2. **Solid edges from the wrapper duplicate `scanToX`'s edges.** That's because the wrapper inherits the same `symbolToDataMap`. Importantly, the wrapper's `* → ·/R` edge points at *`scanToX`-the-original*, not at the wrapper itself — so after the first iteration, control transfers to `scanToX` and stays there.
458
+ 3. **The dotted `onHalt` edge is attached to the wrapper** but it doesn't fire on a single edge at runtime. Instead, the runtime pushes `eraseHere` onto an internal stack at startup, and *any* halt-bound transition reachable during the run (whether on the wrapper itself or on `scanToX`) gets redirected to `eraseHere` via stack-pop. The dotted edge is the engine's static fingerprint of "this graph was wrapped by `withOverrodeHaltState`."
459
+ 4. **What actually fires at runtime, on tape `['a','b','X','b','a']`:** the wrapper runs once, transferring to `scanToX`; `scanToX` self-loops on `* → ·/R` until it sees `X`; the `X → ·/S` edge tries to go to halt; the runtime pops `eraseHere` off the stack and substitutes it; `eraseHere` erases the cell and halts. The wrapper's own `X → ·/S → halt` edge in the diagram is *never traversed* because control left the wrapper after iteration 1.
460
+
461
+ > 💡 **The engine's emit could be more user-friendly here.** Tracked in [#138](https://github.com/mellonis/turing-machine-js/issues/138) — the wrapper's duplicated edges and the misleading single-edge `onHalt` placement are candidates for a cleaner `toMermaid` output.
462
+
463
+ </details>
464
+
465
+ Wrappers nest: `inner.withOverrodeHaltState(middle).withOverrodeHaltState(outer)` chains halt-redirects through `middle → outer → halt`. `library-binary-numbers/src/index.ts`'s `minusOne` (the `~(~x + 1)` composition) uses a 4-deep nest of wrappers.
466
+
467
+ ## Debugging breakpoints
233
468
 
234
469
  Any `State` can carry a runtime-mutable `debug` config that pauses execution at chosen points.
235
470
 
236
471
  ```ts
237
- import { State, haltState, ifOtherSymbol, type DebugConfig } from '@turing-machine-js/machine';
472
+ import { State, haltState, ifOtherSymbol } from '@turing-machine-js/machine';
238
473
 
239
474
  const myState = new State({...});
240
475
 
241
- // Pause before applying any of myState's commands:
242
- myState.debug = { before: true };
476
+ // state.debug is always a DebugConfig instance — chained writes work
477
+ // without prior whole-object assignment:
478
+ myState.debug.before = true;
479
+ myState.debug.after = [symA];
243
480
 
244
- // Pause only when the head shows symA:
481
+ // Whole-object assignment also works for one-shot setup:
482
+ myState.debug = { before: true };
245
483
  myState.debug = { before: [symA] };
246
-
247
- // Pause both before and after for the same symbol — two pauses per visit:
248
484
  myState.debug = { before: [symA], after: [symA] };
249
485
 
250
486
  // Pause when the engine is about to enter halt (program exit OR subroutine pop):
251
487
  haltState.debug = { before: true };
252
488
 
253
- // Disable later:
489
+ // Reset filters later — next read returns a fresh empty DebugConfig:
254
490
  myState.debug = null;
255
491
  ```
256
492
 
257
- The `debug` field is mutabletoggle 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.
493
+ > ⚠️ **`haltState.debug.after` throws.** Halt is terminalthere is no iteration-after-halt for an after-fire to anchor on. Assigning a truthy `.after` to `haltState.debug` (including `{ before: true, after: true }`) throws at write time. Symbol-list filters on `haltState.debug.before` are silent no-ops, since halt has no head symbol; only the wildcard `true` activates.
494
+
495
+ 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. `state.debug` is always a `DebugConfig` instance (lazy-initialized on first read); plain-object input (`state.debug = { before: true }`) is wrapped in a fresh `DebugConfig` automatically. The instance itself is `Object.seal`-ed — typos like `state.debug.bofore = true` throw `TypeError` instead of silently creating a useless property. Per-property setters validate and freeze the stored array, so `state.debug.before.push(...)` also throws `TypeError`.
258
496
 
259
497
  `run()` is async and accepts an `onPause` hook:
260
498
 
@@ -344,6 +582,17 @@ Together: use `summarize` to ask "is this machine the right shape?" (size, compo
344
582
 
345
583
  For visualization and round-tripping, see `State.toGraph` / `State.fromGraph` and `toMermaid` / `fromMermaid`.
346
584
 
585
+ ## Versioning notes
586
+
587
+ API surface changes since v3, in past tense so the timing of each piece is explicit:
588
+
589
+ - **v4** — `run()` became async (`Promise<void>`). Per-state runtime breakpoints landed (`state.debug.before` / `state.debug.after`); `run()` accepted an `onDebugBreak` hook. `MachineState` exposed on each yield.
590
+ - **v5** — `onDebugBreak` renamed to `onPause`. New `run({ debug: boolean })` master switch suppresses all `onPause` dispatches without unsetting `state.debug` assignments. Assigning a truthy `.after` to `haltState.debug` now throws at write time (halt is terminal — no iteration-after-halt to anchor on).
591
+ - **v6** — Per-iter lifecycle reordered to `before → step → after`, all firing on the same yield. Previously `after` fired on iter K+1's tick with a `prevYield` substitution dance; that substitution is gone. The `MachineState.debugBreak` field shape is unchanged across all three versions.
592
+ - **v6.1** — `state.debug` ergonomics: the field is now always a non-null `DebugConfig` instance (lazy-initialized on first read), so chained field writes like `state.debug.before = true` work on a fresh state without a prior whole-object assignment. The `DebugConfig` instance is `Object.seal`-ed, so typos like `state.debug.bofore = true` throw `TypeError` at write time instead of silently creating a useless property. `state.debug = null` continues to work but semantically means "reset filters" — the next read returns a fresh empty `DebugConfig` (#150).
593
+
594
+ For the full release history, see the [GitHub releases page](https://github.com/mellonis/turing-machine-js/releases).
595
+
347
596
  ## Libraries
348
597
 
349
598
  - [@turing-machine-js/library-binary-numbers](https://github.com/mellonis/turing-machine-js/tree/master/packages/library-binary-numbers) — binary arithmetic with `^…$` markers, multi-number-per-tape support
@@ -351,4 +600,4 @@ For visualization and round-tripping, see `State.toGraph` / `State.fromGraph` an
351
600
 
352
601
  ## Links
353
602
 
354
- - [Turing Machine](https://en.wikipedia.org/wiki/Turing_machine) on the Wikipedia
603
+ - [Turing Machine](https://en.wikipedia.org/wiki/Turing_machine) on Wikipedia
@@ -27,7 +27,7 @@ export default class State {
27
27
  get isHalt(): boolean;
28
28
  get overrodeHaltState(): State | null;
29
29
  get ref(): this;
30
- get debug(): DebugConfig | null;
30
+ get debug(): DebugConfig;
31
31
  set debug(value: DebugConfig | {
32
32
  before?: symbol[] | readonly symbol[] | true;
33
33
  after?: symbol[] | readonly symbol[] | true;
package/dist/index.cjs CHANGED
@@ -688,6 +688,12 @@ class DebugConfig {
688
688
  this.after = initial.after;
689
689
  }
690
690
  }
691
+ // Seal the instance so typos like `cfg.bofore = true` throw at write
692
+ // time (in strict mode, which TS-emitted modules use) instead of
693
+ // silently creating a useless own property. The class's `before`/`after`
694
+ // setters still work — they resolve through the prototype chain and
695
+ // write to private fields, neither of which Object.seal restricts.
696
+ Object.seal(this);
691
697
  }
692
698
  get before() {
693
699
  return __classPrivateFieldGet$1(this, _DebugConfig_before, "f");
@@ -775,6 +781,14 @@ class State {
775
781
  return this;
776
782
  }
777
783
  get debug() {
784
+ // Lazy-init: `state.debug` is never null at read time, so chained writes
785
+ // like `state.debug.before = true` work on a fresh state without a prior
786
+ // whole-object assignment. The setter still accepts `null` to reset the
787
+ // filters; the next read recreates a fresh empty `DebugConfig` on demand.
788
+ // See #150.
789
+ if (__classPrivateFieldGet$1(this, _State_debugRef, "f").current === null) {
790
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this);
791
+ }
778
792
  return __classPrivateFieldGet$1(this, _State_debugRef, "f").current;
779
793
  }
780
794
  set debug(value) {
@@ -790,10 +804,21 @@ class State {
790
804
  }
791
805
  /** @internal — invoked by DebugConfig setters via module-private symbol. */
792
806
  [(_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)
807
+ if (filter === undefined)
808
+ return;
809
+ // #108 part 2: `.after` on haltState has no semantic anchor — halt is
810
+ // terminal, so there is no iteration-after-halt for an after-fire to
811
+ // attach to. Reject any truthy assignment (true OR list) at write time
812
+ // so misuse surfaces immediately rather than silently no-op'ing.
813
+ if (this.isHalt && fieldName === 'after') {
814
+ throw new Error('haltState.debug.after is not supported: halt is terminal, so there is '
815
+ + 'no iteration-after-halt for an after-fire to anchor on. Use '
816
+ + '{ before: true } to pause on halt entry.');
817
+ }
818
+ if (filter === true)
794
819
  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.
820
+ // haltState has no own transitions; symbol-list filters on `before` are
821
+ // silent no-ops at the engine level (spec §8.6), so accept any list shape.
797
822
  if (this.isHalt)
798
823
  return;
799
824
  for (const sym of filter) {
@@ -1035,22 +1060,21 @@ class TuringMachine {
1035
1060
  get tapeBlock() {
1036
1061
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
1037
1062
  }
1038
- async run({ initialState, stepsLimit = 1e5, onStep, onDebugBreak, }) {
1063
+ async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
1039
1064
  const generator = this.runStepByStep({ initialState, stepsLimit });
1040
- let prevYield = null;
1041
1065
  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 } });
1066
+ // Per-iter lifecycle: before step after. All three operate on the
1067
+ // same yielded MachineState, so the consumer sees a coherent ordering
1068
+ // within each iteration without cross-tick coordination.
1069
+ if (debug && machineState.debugBreak?.before && onPause) {
1070
+ await onPause({ ...machineState, debugBreak: { before: true } });
1049
1071
  }
1050
1072
  if (onStep instanceof Function) {
1051
1073
  onStep(machineState);
1052
1074
  }
1053
- prevYield = machineState;
1075
+ if (debug && machineState.debugBreak?.after && onPause) {
1076
+ await onPause({ ...machineState, debugBreak: { after: true } });
1077
+ }
1054
1078
  }
1055
1079
  }
1056
1080
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
@@ -1064,7 +1088,6 @@ class TuringMachine {
1064
1088
  stack.push(state.overrodeHaltState);
1065
1089
  }
1066
1090
  let i = 0;
1067
- let pendingAfterFromPrev = false;
1068
1091
  while (!state.isHalt) {
1069
1092
  if (i === stepsLimit) {
1070
1093
  throw new Error('Long execution');
@@ -1074,8 +1097,12 @@ class TuringMachine {
1074
1097
  const command = state.getCommand(symbol);
1075
1098
  let nextState = state.getNextState(symbol).ref;
1076
1099
  try {
1100
+ // Both before and after refer to THIS iter (#119 / v6.0.0).
1101
+ // The halting iter's after-fire just rides along on the iter's
1102
+ // own yield — no post-loop drain needed.
1077
1103
  const beforeMatch = matchFilter(state.debug?.before, symbol)
1078
1104
  || (nextState.isHalt && nextState.debug?.before === true);
1105
+ const afterMatch = matchFilter(state.debug?.after, symbol);
1079
1106
  const nextStateForYield = nextState.isHalt && stack.length
1080
1107
  ? stack.slice(-1)[0]
1081
1108
  : nextState;
@@ -1099,17 +1126,15 @@ class TuringMachine {
1099
1126
  movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
1100
1127
  nextState: nextStateForYield,
1101
1128
  };
1102
- if (pendingAfterFromPrev || beforeMatch) {
1129
+ if (beforeMatch || afterMatch) {
1103
1130
  const dbg = {};
1104
- if (pendingAfterFromPrev)
1105
- dbg.after = true;
1106
1131
  if (beforeMatch)
1107
1132
  dbg.before = true;
1133
+ if (afterMatch)
1134
+ dbg.after = true;
1108
1135
  yielded.debugBreak = dbg;
1109
1136
  }
1110
1137
  yield yielded;
1111
- // Re-evaluate 'after' for THIS visit, to fire on the NEXT yield.
1112
- pendingAfterFromPrev = matchFilter(state.debug?.after, symbol);
1113
1138
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
1114
1139
  if (nextState.isHalt && stack.length) {
1115
1140
  nextState = stack.pop();
package/dist/index.mjs CHANGED
@@ -686,6 +686,12 @@ class DebugConfig {
686
686
  this.after = initial.after;
687
687
  }
688
688
  }
689
+ // Seal the instance so typos like `cfg.bofore = true` throw at write
690
+ // time (in strict mode, which TS-emitted modules use) instead of
691
+ // silently creating a useless own property. The class's `before`/`after`
692
+ // setters still work — they resolve through the prototype chain and
693
+ // write to private fields, neither of which Object.seal restricts.
694
+ Object.seal(this);
689
695
  }
690
696
  get before() {
691
697
  return __classPrivateFieldGet$1(this, _DebugConfig_before, "f");
@@ -773,6 +779,14 @@ class State {
773
779
  return this;
774
780
  }
775
781
  get debug() {
782
+ // Lazy-init: `state.debug` is never null at read time, so chained writes
783
+ // like `state.debug.before = true` work on a fresh state without a prior
784
+ // whole-object assignment. The setter still accepts `null` to reset the
785
+ // filters; the next read recreates a fresh empty `DebugConfig` on demand.
786
+ // See #150.
787
+ if (__classPrivateFieldGet$1(this, _State_debugRef, "f").current === null) {
788
+ __classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this);
789
+ }
776
790
  return __classPrivateFieldGet$1(this, _State_debugRef, "f").current;
777
791
  }
778
792
  set debug(value) {
@@ -788,10 +802,21 @@ class State {
788
802
  }
789
803
  /** @internal — invoked by DebugConfig setters via module-private symbol. */
790
804
  [(_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)
805
+ if (filter === undefined)
806
+ return;
807
+ // #108 part 2: `.after` on haltState has no semantic anchor — halt is
808
+ // terminal, so there is no iteration-after-halt for an after-fire to
809
+ // attach to. Reject any truthy assignment (true OR list) at write time
810
+ // so misuse surfaces immediately rather than silently no-op'ing.
811
+ if (this.isHalt && fieldName === 'after') {
812
+ throw new Error('haltState.debug.after is not supported: halt is terminal, so there is '
813
+ + 'no iteration-after-halt for an after-fire to anchor on. Use '
814
+ + '{ before: true } to pause on halt entry.');
815
+ }
816
+ if (filter === true)
792
817
  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.
818
+ // haltState has no own transitions; symbol-list filters on `before` are
819
+ // silent no-ops at the engine level (spec §8.6), so accept any list shape.
795
820
  if (this.isHalt)
796
821
  return;
797
822
  for (const sym of filter) {
@@ -1033,22 +1058,21 @@ class TuringMachine {
1033
1058
  get tapeBlock() {
1034
1059
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
1035
1060
  }
1036
- async run({ initialState, stepsLimit = 1e5, onStep, onDebugBreak, }) {
1061
+ async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
1037
1062
  const generator = this.runStepByStep({ initialState, stepsLimit });
1038
- let prevYield = null;
1039
1063
  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 } });
1064
+ // Per-iter lifecycle: before step after. All three operate on the
1065
+ // same yielded MachineState, so the consumer sees a coherent ordering
1066
+ // within each iteration without cross-tick coordination.
1067
+ if (debug && machineState.debugBreak?.before && onPause) {
1068
+ await onPause({ ...machineState, debugBreak: { before: true } });
1047
1069
  }
1048
1070
  if (onStep instanceof Function) {
1049
1071
  onStep(machineState);
1050
1072
  }
1051
- prevYield = machineState;
1073
+ if (debug && machineState.debugBreak?.after && onPause) {
1074
+ await onPause({ ...machineState, debugBreak: { after: true } });
1075
+ }
1052
1076
  }
1053
1077
  }
1054
1078
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
@@ -1062,7 +1086,6 @@ class TuringMachine {
1062
1086
  stack.push(state.overrodeHaltState);
1063
1087
  }
1064
1088
  let i = 0;
1065
- let pendingAfterFromPrev = false;
1066
1089
  while (!state.isHalt) {
1067
1090
  if (i === stepsLimit) {
1068
1091
  throw new Error('Long execution');
@@ -1072,8 +1095,12 @@ class TuringMachine {
1072
1095
  const command = state.getCommand(symbol);
1073
1096
  let nextState = state.getNextState(symbol).ref;
1074
1097
  try {
1098
+ // Both before and after refer to THIS iter (#119 / v6.0.0).
1099
+ // The halting iter's after-fire just rides along on the iter's
1100
+ // own yield — no post-loop drain needed.
1075
1101
  const beforeMatch = matchFilter(state.debug?.before, symbol)
1076
1102
  || (nextState.isHalt && nextState.debug?.before === true);
1103
+ const afterMatch = matchFilter(state.debug?.after, symbol);
1077
1104
  const nextStateForYield = nextState.isHalt && stack.length
1078
1105
  ? stack.slice(-1)[0]
1079
1106
  : nextState;
@@ -1097,17 +1124,15 @@ class TuringMachine {
1097
1124
  movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
1098
1125
  nextState: nextStateForYield,
1099
1126
  };
1100
- if (pendingAfterFromPrev || beforeMatch) {
1127
+ if (beforeMatch || afterMatch) {
1101
1128
  const dbg = {};
1102
- if (pendingAfterFromPrev)
1103
- dbg.after = true;
1104
1129
  if (beforeMatch)
1105
1130
  dbg.before = true;
1131
+ if (afterMatch)
1132
+ dbg.after = true;
1106
1133
  yielded.debugBreak = dbg;
1107
1134
  }
1108
1135
  yield yielded;
1109
- // Re-evaluate 'after' for THIS visit, to fire on the NEXT yield.
1110
- pendingAfterFromPrev = matchFilter(state.debug?.after, symbol);
1111
1136
  __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
1112
1137
  if (nextState.isHalt && stack.length) {
1113
1138
  nextState = stack.pop();
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@turing-machine-js/machine",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "A convenient Turing machine",
5
5
  "engines": {
6
6
  "npm": ">=7.0.0"
7
7
  },
8
- "author": "Ruslan Gilmullin <mellonis14@gmain.com>",
8
+ "author": "Ruslan Gilmullin <mellonis@yandex.ru>",
9
9
  "homepage": "https://github.com/mellonis/turing-machine-js#readme",
10
10
  "license": "GPL-3.0-or-later",
11
11
  "publishConfig": {
@@ -34,5 +34,5 @@
34
34
  "default": "./dist/index.mjs"
35
35
  }
36
36
  },
37
- "gitHead": "6c7b2ac3055470b56c6882471767c746f5a9d7fb"
37
+ "gitHead": "5a3e3ced5bacd541fefdf087f5b4301267be01d2"
38
38
  }