@turing-machine-js/machine 6.0.1 → 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
 
@@ -82,7 +100,7 @@ Engine notation: `read → write/move`; `·` = keep, `⌫` = erase, `*` = `ifOth
82
100
 
83
101
  </details>
84
102
 
85
- 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.
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).
86
104
 
87
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'`.
88
106
 
@@ -309,7 +327,7 @@ The runtime. Owns one `TapeBlock` and drives a state graph until it reaches `hal
309
327
  ```javascript
310
328
  const machine = new TuringMachine({ tapeBlock });
311
329
 
312
- // Run to halt — `run()` is async (v4+), it returns a Promise<void>:
330
+ // Run to halt — `run()` returns a Promise<void>:
313
331
  await machine.run({ initialState, stepsLimit: 1e5 });
314
332
 
315
333
  // Or step-by-step (useful for visualization / debugging):
@@ -332,6 +350,22 @@ Each yielded `step` (`MachineState`) has these fields:
332
350
 
333
351
  `stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
334
352
 
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
+
335
369
  ## Subroutine composition with `withOverrodeHaltState`
336
370
 
337
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.
@@ -430,34 +464,35 @@ flowchart TD
430
464
 
431
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.
432
466
 
433
- ## Debugging breakpoints (v4+)
467
+ ## Debugging breakpoints
434
468
 
435
469
  Any `State` can carry a runtime-mutable `debug` config that pauses execution at chosen points.
436
470
 
437
471
  ```ts
438
- import { State, haltState, ifOtherSymbol, type DebugConfig } from '@turing-machine-js/machine';
472
+ import { State, haltState, ifOtherSymbol } from '@turing-machine-js/machine';
439
473
 
440
474
  const myState = new State({...});
441
475
 
442
- // Pause before applying any of myState's commands:
443
- 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];
444
480
 
445
- // Pause only when the head shows symA:
481
+ // Whole-object assignment also works for one-shot setup:
482
+ myState.debug = { before: true };
446
483
  myState.debug = { before: [symA] };
447
-
448
- // Pause both before and after for the same symbol — two pauses per visit:
449
484
  myState.debug = { before: [symA], after: [symA] };
450
485
 
451
486
  // Pause when the engine is about to enter halt (program exit OR subroutine pop):
452
487
  haltState.debug = { before: true };
453
488
 
454
- // Disable later:
489
+ // Reset filters later — next read returns a fresh empty DebugConfig:
455
490
  myState.debug = null;
456
491
  ```
457
492
 
458
493
  > ⚠️ **`haltState.debug.after` throws.** Halt is terminal — there 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.
459
494
 
460
- 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. Plain-object input (`state.debug = { before: true }`) is wrapped in a `DebugConfig` instance automatically; the wrapper's per-property setters validate and freeze the stored array, so `state.debug.before.push(...)` throws `TypeError`.
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`.
461
496
 
462
497
  `run()` is async and accepts an `onPause` hook:
463
498
 
@@ -547,6 +582,17 @@ Together: use `summarize` to ask "is this machine the right shape?" (size, compo
547
582
 
548
583
  For visualization and round-tripping, see `State.toGraph` / `State.fromGraph` and `toMermaid` / `fromMermaid`.
549
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
+
550
596
  ## Libraries
551
597
 
552
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
@@ -554,4 +600,4 @@ For visualization and round-tripping, see `State.toGraph` / `State.fromGraph` an
554
600
 
555
601
  ## Links
556
602
 
557
- - [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) {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turing-machine-js/machine",
3
- "version": "6.0.1",
3
+ "version": "6.1.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": "d7ebca4e8379a548e3ca97eb83e96387de2cda32"
37
+ "gitHead": "5a3e3ced5bacd541fefdf087f5b4301267be01d2"
38
38
  }