@turing-machine-js/machine 6.4.0 → 7.0.0-alpha.2

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,126 @@ 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
+ ## [7.0.0-alpha.2] - 2026-05-21
8
+
9
+ Second v7 pre-release. Refines alpha.1's `toMermaid` wrapped-state emit into the function-call model ([#174](https://github.com/mellonis/turing-machine-js/issues/174)) and adds two construction-time improvements to `withOverriddenHaltState` ([#175](https://github.com/mellonis/turing-machine-js/issues/175), [#176](https://github.com/mellonis/turing-machine-js/issues/176)). Published to npm under the `next` dist-tag: `npm install @turing-machine-js/machine@next`.
10
+
11
+ **Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@turing-machine-js/machine@7.0.0-alpha.2`. Migration walkthrough at the bottom.
12
+
13
+ ### Changed
14
+
15
+ - **`toMermaid` callable-subtree emit** ([#174](https://github.com/mellonis/turing-machine-js/issues/174)) — supersedes alpha.1's collapsed-bare emit (#138/#139) with a function-call model:
16
+ - **Wrapper and bare are separate graph nodes.** The wrapper sits OUTSIDE any subgraph as `[[composite-name]]` (call site). The bare lives INSIDE its callable subtree subgraph as a regular `[name]` node.
17
+ - **Subgraph label** changed from `"halt frame"` to `"callable subtree of NAME"` (single bare) or `"callable scope: A ∪ B"` (multi-bare union frame).
18
+ - **Halt marker is per-frame**, not per-wrapper. When union-find merges two bares' subtrees (shared body state), they share one halt marker.
19
+ - **Arrow vocabulary rewritten**:
20
+ - Solid `-->` for regular transitions AND for the wrapper's post-return `--> override` (just an ordinary transition under the call/return model).
21
+ - Bold `==> "call"` is RESERVED for the wrapper-to-bare call. `&` ribbon syntax (`s_W1 & s_W2 == "call" ==> s_A`) collapses multiple wrappers that share a bare.
22
+ - Dotted `-.->` is reserved for frame-level dispatch: `w_N -. "return" .-> wrapper` (demand-emit), `w_N -. "halt" .-> s0` (demand-emit on non-wrapper entry), `idle -. enter .-> sN` (unchanged).
23
+ - The `-. onHalt .->` keyword from alpha.1 is **retired** — replaced by a solid `--> override` arrow.
24
+ - **No per-context duplication.** Shared bares (e.g. `library-binary-numbers/minusOne`'s `invertNumber`, called by two wrappers) appear as a single subtree with `& `-joined call arrows from each wrapper.
25
+ - **`GraphNode` field changes**: `isWrapped` removed; `isWrapper: boolean`, `bareStateId: number | null`, `frameId: number | null` added. `overriddenHaltStateId` now lives on wrapper nodes only.
26
+ - Bytewise round-trip stability now holds for **all** wrapped states, including shared-bare cases (alpha.1 was only stable for simple wrappers).
27
+
28
+ - **Nested `.wohs()` chain collapse** ([#176](https://github.com/mellonis/turing-machine-js/issues/176)) — `A.withOverriddenHaltState(t1).withOverriddenHaltState(t2)` is now equivalent to `A.withOverriddenHaltState(t2)`. The chain's inner override (`t1`) is dead at runtime (only the outermost wrapper's override is pushed onto the halt stack on entry — verified empirically). Composite name now reflects runtime behavior: `A(t2)`, not the misleading `A(t1)(t2)`. `withOverriddenHaltState` unwraps `this` to its bare before constructing the new wrapper.
29
+
30
+ ### Added
31
+
32
+ - **`withOverriddenHaltState` memoization** ([#175](https://github.com/mellonis/turing-machine-js/issues/175)) — calls with the same `(bare, override)` pair return the literally-same `State` instance. Backed by a two-level `WeakMap` keyed by the bare with `WeakRef`-valued entries, so cached wrappers can be GC'd when nothing else holds them. Composes with #176's chain-collapse: `A.wohs(t1).wohs(t2)` and `A.wohs(t2)` both resolve to the same instance.
33
+
34
+ - **Spec doc** at `docs/superpowers/specs/2026-05-21-halt-frame-transitive-closure.md` capturing the callable-subtree model design (worked examples for simple wrappers, PostMachine subroutines, shared-bare wrappers, self-wrapping, nested chains, Reference cycles, shared body states).
35
+
36
+ ### Migration from alpha.1
37
+
38
+ Three breaking changes vs alpha.1, all in the visualization layer:
39
+
40
+ **1. Mermaid format** — alpha.1 Mermaid strings (with `-. onHalt .->` edges or `[[bare]]` inside subgraphs) will NOT parse with the new `fromMermaid`. The format is one-way: re-emit via `toMermaid(toGraph(state, tapeBlock))` on alpha.2 to regenerate.
41
+
42
+ **2. `Graph` data shape** — `GraphNode.isWrapped` removed; replaced by `isWrapper: boolean`, `bareStateId: number | null`, `frameId: number | null`. If you store serialized `Graph` JSON from alpha.1, re-emit on alpha.2.
43
+
44
+ **3. Wrapper composite name in nested chains** — `A.wohs(t1).wohs(t2)` was `A(t1)(t2)` in alpha.1; under #176 it's now `A(t2)`. Code that parsed the composite name to extract intermediate overrides will need to adapt (those overrides were never actually pushed at runtime — alpha.1's name was misleading).
45
+
46
+ `withOverriddenHaltState` memoization (#175) is fully additive — same arguments now return the same instance, but no observable behavior changes for code that doesn't rely on instance identity.
47
+
48
+ ### Out of v7-alpha.2 (still pending for stable v7.0.0)
49
+
50
+ - **[#102](https://github.com/mellonis/turing-machine-js/issues/102)** — debugger step-in / step-out / step-over primitives. Additive — won't change any existing API. Will land in `v7.0.0-alpha.3` or stable `v7.0.0`.
51
+ - **[#180](https://github.com/mellonis/turing-machine-js/issues/180)** — extract `State.toGraph`/`fromGraph` to its own module. Internal refactor; no API change.
52
+
53
+ ### Compatibility
54
+
55
+ - Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.1` → `^7.0.0-alpha.2` on `@turing-machine-js/builder`, `@turing-machine-js/library-binary-numbers`, `@turing-machine-js/library-binary-numbers-bare`.
56
+
57
+ ## [7.0.0-alpha.1] - 2026-05-21
58
+
59
+ First v7 pre-release. Consolidates the composition-representation overhaul landed across [#149](https://github.com/mellonis/turing-machine-js/issues/149), [#148](https://github.com/mellonis/turing-machine-js/issues/148), [#138](https://github.com/mellonis/turing-machine-js/issues/138), and [#139](https://github.com/mellonis/turing-machine-js/issues/139). Published to npm under the `next` dist-tag: `npm install @turing-machine-js/machine@next`.
60
+
61
+ **Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@turing-machine-js/machine@7.0.0-alpha.1`. Migration walkthrough at the bottom.
62
+
63
+ ### Changed
64
+
65
+ - **`State.prototype.withOverrodeHaltState` renamed to `withOverriddenHaltState`** ([#149](https://github.com/mellonis/turing-machine-js/issues/149)). Grammar fix on a name introduced in 2019: `overridden` (past participle) fits the "with a halt-state that has been ___" naming idiom; `overrode` (simple past) didn't. Hard cutover — no deprecated alias. Renames in lockstep:
66
+ - public method `State.prototype.withOverrodeHaltState` → `withOverriddenHaltState`
67
+ - getter `state.overrodeHaltState` → `state.overriddenHaltState`
68
+ - private field `#overrodeHaltState` → `#overriddenHaltState`
69
+ - serialized `Graph` data field `node.overrodeHaltStateId` → `node.overriddenHaltStateId`
70
+
71
+ - **Wrapped-state composite name format flipped from `bare>override` to `bare(override)`** ([#148](https://github.com/mellonis/turing-machine-js/issues/148)). The old `>`-flat notation collided structurally-distinct wrap-trees into the same string: `A.with(B.with(A))` and `A.with(B).with(A)` both rendered as `A>B>A` despite being different runtime shapes. Paren-nested keeps them distinct: `A(B(A))` vs `A(B)(A)`. As a consequence, **user-provided state names must not contain `(` or `)`** — `State` constructor now throws on names with these characters. `>` is no longer reserved and is valid in user-provided names again.
72
+
73
+ - **`toMermaid` wrapped-state emit overhaul** ([#138](https://github.com/mellonis/turing-machine-js/issues/138), [#139](https://github.com/mellonis/turing-machine-js/issues/139)). Each `withOverriddenHaltState` wrapper collapses onto its bare's representation — `GraphNode.isWrapped: true`, no separate wrapper node in graph data. `toMermaid` wraps each `[[bare]]` (Mermaid subroutine shape) + a synthesized `(((halt)))` halt-marker (`GraphNode.isHaltMarker: true`) inside a `subgraph w_${bareId}["halt frame"] … end` block. Dotted `onHalt` from `[[bare]]` crosses the subgraph border to the override target. An always-emitted `idle([idle])` stadium sentinel + `idle -. enter .-> sN` arrow marks the initial state (replaces the v6 `((round))` shape convention).
74
+
75
+ - **Edge-label vocabulary rewritten** for readability — `[reads] → [writes]/[moves]` with each role wrapped in `[…]` (the tape-block indicator; always present, even single-tape). Read cells: `'X'` literal-quoted, `*` (ASCII; ifOtherSymbol catch-all — literal `*` in the alphabet renders as `'*'`), `B` (tape's blank shorthand). Write cells: literal-quoted, `K` (keep), `E` (erase = write blank). Move cells: `L` / `R` / `S`. Alternation per-pattern bracket: `['^']|['1']` for single-tape, `['0','a']|['1','b']` for multi-tape. Compact in-bracket `['^'|'1']` form is rejected by `fromMermaid` (would read as cross-product semantics in multi-tape).
76
+
77
+ - **Thick `==>` arrows for stack-pushing transitions.** When a transition's target is a wrapped state AND ≠ source (i.e. would push the wrapper's override onto the runtime stack per `TuringMachine.run`'s line ~220 transition-time push), the engine emits a thick `==>` arrow — visual signal for "this transition fires a stack push."
78
+
79
+ - **`GraphTransition.id`** — stable, deterministic per-edge identifier (`${fromNodeId}-${patternIx}`). Supports downstream tooling for edge-targeting in rendered Mermaid SVG (e.g. highlight-the-next-transition in interactive viewers).
80
+
81
+ - **`summarize().stateCount` filters `isHaltMarker` nodes** — halt markers are visualization sentinels (all map back to singleton `haltState` at runtime), not distinct runtime states. Matches the per-algorithm header count in `library-binary-numbers/states.md`.
82
+
83
+ ### Added
84
+
85
+ - **#139 regression test** — `toMermaid → fromMermaid → toGraph → toMermaid` is bytewise stable for simple wrappers. The wrapper's composite name (e.g. `scanToX(eraseHere)`) no longer appears as any graph-node label, so `fromGraph` reconstructs and recomputes the composite fresh on each pass — no round-trip name accumulation.
86
+
87
+ - **Multi-tape example** in the engine README illustrating the bracketed format with two tapes.
88
+
89
+ - **§Diagram conventions section** in the engine README — full reference: node shapes, edge styles, groupings, edge label format + cell vocabulary, alternation rule, multi-tape example.
90
+
91
+ - **Cross-package introspection consistency** — `library-binary-numbers/states.md` per-algorithm header line now uses `summarizeGraph(graph)` for its `N states; N transitions; N wrappers (max nesting depth N); has cycles` summary, dogfooding the same API any consumer would use.
92
+
93
+ ### Removed
94
+
95
+ - Hand-drawn pedagogical Mermaid blocks in engine + root READMEs. The v7 `toMermaid` output is now the primary illustration — no more vocabulary mismatch between hand-drawn and engine-rendered diagrams.
96
+
97
+ ### Migration
98
+
99
+ For consumers updating from v6.x:
100
+
101
+ **1. Identifier rename** (mechanical):
102
+
103
+ ```sh
104
+ git grep -l 'OverrodeHaltState\|overrodeHaltState' | xargs sed -i '' \
105
+ -e 's/OverrodeHaltState/OverriddenHaltState/g' \
106
+ -e 's/overrodeHaltState/overriddenHaltState/g'
107
+ ```
108
+
109
+ Persisted `State.toGraph` JSON dumps need the same `overrodeHaltStateId` → `overriddenHaltStateId` field rename.
110
+
111
+ **2. Wrapper composite name format** — code that pattern-matches `state.name` for wrapper composites needs to switch from `>`-split to paren-parse: `'A>B'` is now `'A(B)'`.
112
+
113
+ **3. State name validation** — any code that constructs `new State(null, 'foo(bar)')` now throws. Real-world identifier-style state names (`scanToX`, `goHome`, `incT1`) are unaffected.
114
+
115
+ **4. `toMermaid` output format** — if you render `toMermaid` output programmatically, the edge-label vocabulary changed completely (bracketed-tape-block format with `K`/`E`/`B`/`*`/`L`/`R`/`S`). See the engine README's §Diagram conventions for the full vocabulary.
116
+
117
+ **5. `Graph` data shape** — `GraphNode` gained `isWrapped: boolean` + `isHaltMarker: boolean`; `GraphTransition` gained `id: string`. Most consumers don't need to change. If you call `summarize().stateCount`, the value now excludes halt markers (typically matches what you actually wanted — runtime state count, not visualization-node count).
118
+
119
+ ### Out of v7-alpha.1 (still pending for stable v7.0.0)
120
+
121
+ - **[#102](https://github.com/mellonis/turing-machine-js/issues/102)** — debugger step-in / step-out / step-over primitives. Additive — won't change any existing API. Will land in `v7.0.0-alpha.2` or stable `v7.0.0`.
122
+
123
+ ### Compatibility
124
+
125
+ - Peer dep `@turing-machine-js/machine` widened `^6.0.0` → `^7.0.0-alpha.1` on `@turing-machine-js/builder`, `@turing-machine-js/library-binary-numbers`, `@turing-machine-js/library-binary-numbers-bare`.
126
+
7
127
  ## [6.4.0] - 2026-05-19
8
128
 
9
129
  Adds a new awaited hook on `TuringMachine.run`. Minor release, additive — no breaking changes. Closes [#163](https://github.com/mellonis/turing-machine-js/issues/163).
package/README.md CHANGED
@@ -3,7 +3,7 @@
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
- A composable Turing-machine engine for JavaScript: multi-tape, subroutine composition via `withOverrodeHaltState`, Mermaid round-trip, and runtime breakpoints.
6
+ A composable Turing-machine engine for JavaScript: multi-tape, subroutine composition via `withOverriddenHaltState`, Mermaid round-trip, and runtime breakpoints.
7
7
 
8
8
  <details>
9
9
  <summary>Table of contents</summary>
@@ -12,10 +12,11 @@ A composable Turing-machine engine for JavaScript: multi-tape, subroutine compos
12
12
  - [Quick start](#quick-start)
13
13
  - [Building from a state table](#building-from-a-state-table)
14
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)
15
+ - [Subroutine composition with `withOverriddenHaltState`](#subroutine-composition-with-withoverriddenhaltstate)
16
16
  - [Debugging breakpoints](#debugging-breakpoints)
17
17
  - [Special objects](#special-objects) — [`haltState`](#haltstate) · [`ifOtherSymbol`](#ifothersymbol) · [`movements`](#movements) · [`symbolCommands`](#symbolcommands)
18
18
  - [Introspection and testing](#introspection-and-testing)
19
+ - [Diagram conventions](#diagram-conventions)
19
20
  - [Versioning notes](#versioning-notes)
20
21
  - [Libraries](#libraries)
21
22
  - [Links](#links)
@@ -70,39 +71,25 @@ await machine.run({
70
71
  console.log(tape.symbols.join('').trim()); // a*c*a
71
72
  ```
72
73
 
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>
74
+ The state graph for the example above (`toMermaid(toGraph(replaceB, tapeBlock))`):
88
75
 
89
76
  ```mermaid
90
77
  flowchart TD
91
78
  %% alphabets: [[" ","a","b","c","*"]]
92
79
  s0(((halt)))
93
- s1(("replaceB"))
94
- s1 -- "b → */R" --> s1
95
- s1 -- "- ·/L" --> s0
96
- s1 -- "*·/R" --> s1
80
+ s1["replaceB"]
81
+ idle([idle])
82
+ idle -. enter .-> s1
83
+ s1 -- "['b']['*']/[R]" --> s1
84
+ s1 -- "[B] → [K]/[L]" --> s0
85
+ s1 -- "[*] → [K]/[R]" --> s1
97
86
  ```
98
87
 
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>
88
+ Reading this specific diagram: `replaceB` (the rectangle) is the start state, marked by the dotted `enter` arrow from the `idle` sentinel. Three self-or-halt transitions: read `'b'` write `'*'` and step right; read anything else (`*`) keep, step right; read blank (`B`) keep, step left, halt. Full notation reference — shapes, edge styles, label vocabulary — in [§Diagram conventions](#diagram-conventions).
102
89
 
103
90
  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).
104
91
 
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'`.
92
+ 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'`. See the multi-tape example in [§Diagram conventions](#diagram-conventions) for what the rendered graph looks like.
106
93
 
107
94
  ## Building from a state table
108
95
 
@@ -251,7 +238,7 @@ const s = new State({
251
238
  Notable members and statics:
252
239
 
253
240
  - **`state.id`**, **`state.name`** — identity (`isHalt` is `id === 0`).
254
- - **`state.withOverrodeHaltState(other)`** — returns a copy whose would-be halt transitions fall through to `other`. The subroutine-call composition mechanism (see `library-binary-numbers/src/index.ts` for examples).
241
+ - **`state.withOverriddenHaltState(other)`** — returns a copy whose would-be halt transitions fall through to `other`. The subroutine-call composition mechanism (see `library-binary-numbers/src/index.ts` for examples).
255
242
  - **`State.toGraph(state, tapeBlock)`** — walks the reachable graph from `state` and returns a serializable `Graph` (states, transitions, alphabets).
256
243
  - **`State.fromGraph(graph)`** — inverse of `toGraph`: rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips together with `toMermaid` / `fromMermaid`.
257
244
 
@@ -270,16 +257,18 @@ The string `toMermaid` produces is a real Mermaid flowchart that renders in-plac
270
257
  flowchart TD
271
258
  %% alphabets: [[" ","0","1","$"]]
272
259
  s0(((halt)))
273
- s1(("name"))
274
- s1 -- "1 → 0/R" --> s1
275
- s1 -- "$ ·/L" --> s0
260
+ s1["name"]
261
+ idle([idle])
262
+ idle -. enter .-> s1
263
+ s1 -- "['1'] → ['0']/[R]" --> s1
264
+ s1 -- "['$'] → [K]/[L]" --> s0
276
265
  ```
277
266
 
278
- *Edge labels are `read → write/move`. `·` is the keep-current-symbol marker (no write); `L` / `R` / `S` are head moves.*
267
+ *Edge labels are `read → write/move`. Write commands: `K` = keep (no write), `E` = erase (write the blank). Literal alphabet symbols are quoted (`'1'`, `'$'`). Movements: `L` (left), `R` (right), `S` (stay).*
279
268
 
280
269
  > 💡 **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
270
 
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)).
271
+ `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). Under the v7 callable-subtree emit (#174), bytewise stability holds across rebuilds even for shared-bare cases (modulo state-id renumbering, which the test normalizes). The composite name is not stored as any graph node's label `fromGraph` recomputes it fresh on reconstruction so the accumulation problem from #138 cannot reoccur.
283
272
 
284
273
  ### Reference
285
274
 
@@ -292,31 +281,20 @@ const b = new State({ [symbol(['y'])]: { nextState: a } }, 'b');
292
281
  ref.bind(b); // a's transition now resolves to b at run time
293
282
  ```
294
283
 
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>
284
+ The resulting cycle (`toMermaid(toGraph(a, tapeBlock))`):
307
285
 
308
286
  ```mermaid
309
287
  flowchart TD
310
288
  %% alphabets: [[" ","x","y"]]
311
- s1(("a"))
289
+ s1["a"]
312
290
  s2["b"]
313
- s1 -- "x → ·/S" --> s2
314
- s2 -- "y ·/S" --> s1
291
+ idle([idle])
292
+ idle -. enter .-> s1
293
+ s1 -- "['x'] → [K]/[S]" --> s2
294
+ s2 -- "['y'] → [K]/[S]" --> s1
315
295
  ```
316
296
 
317
- `a` is round (the initial state passed to `toGraph`); `b` is square (reachable from `a`).
318
-
319
- </details>
297
+ `idle -. enter .->` points at the initial state passed to `toGraph` (`a` here); `b` is reachable from `a` via the bound `Reference`.
320
298
 
321
299
  `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.
322
300
 
@@ -366,9 +344,9 @@ Both APIs are first-class — `run()` is built on top of `runStepByStep()` (see
366
344
 
367
345
  **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. 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. For per-iter throttle / "wait between steps" UIs, see [Throttle pattern](#throttle-pattern).
368
346
 
369
- ## Subroutine composition with `withOverrodeHaltState`
347
+ ## Subroutine composition with `withOverriddenHaltState`
370
348
 
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.
349
+ `state.withOverriddenHaltState(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
350
 
373
351
  ```javascript
374
352
  import { Alphabet, State, TapeBlock, TuringMachine, Tape, haltState, ifOtherSymbol, movements, symbolCommands } from '@turing-machine-js/machine';
@@ -389,7 +367,7 @@ const eraseHere = new State({
389
367
  }, 'eraseHere');
390
368
 
391
369
  // Compose: scan to X, then ERASE it. scanToX is unmodified.
392
- const scanThenErase = scanToX.withOverrodeHaltState(eraseHere);
370
+ const scanThenErase = scanToX.withOverriddenHaltState(eraseHere);
393
371
 
394
372
  const tape = new Tape({ alphabet, symbols: ['a', 'b', 'X', 'b', 'a'] });
395
373
  tapeBlock.replaceTape(tape);
@@ -400,38 +378,17 @@ console.log(tape.symbols.join('')); // "ab ba" — the X at index 2 is gone, hea
400
378
 
401
379
  What changes between *running `scanToX` standalone* and *running the composed wrapper*:
402
380
 
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
381
  `toMermaid(toGraph(scanToX, tapeBlock))` — the standalone subroutine:
427
382
 
428
383
  ```mermaid
429
384
  flowchart TD
430
385
  %% alphabets: [[" ","a","b","X"]]
431
386
  s0(((halt)))
432
- s1(("scanToX"))
433
- s1 -- "X → ·/S" --> s0
434
- s1 -- "* ·/R" --> s1
387
+ s1["scanToX"]
388
+ idle([idle])
389
+ idle -. enter .-> s1
390
+ s1 -- "['X'] → [K]/[S]" --> s0
391
+ s1 -- "[*] → [K]/[R]" --> s1
435
392
  ```
436
393
 
437
394
  `toMermaid(toGraph(scanThenErase, tapeBlock))` — the wrapped composition:
@@ -440,29 +397,35 @@ flowchart TD
440
397
  flowchart TD
441
398
  %% alphabets: [[" ","a","b","X"]]
442
399
  s0(((halt)))
443
- s1["scanToX"]
444
400
  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
401
+ s3[["scanToX(eraseHere)"]]
402
+ idle([idle])
403
+ subgraph w_1["callable subtree of scanToX"]
404
+ s1["scanToX"]
405
+ c1(((halt)))
406
+ end
407
+ idle -. enter .-> s3
408
+ s3 == "call" ==> s1
409
+ w_1 -. "return" .-> s3
410
+ s3 --> s2
411
+ s1 -- "['X'] → [K]/[S]" --> c1
412
+ s1 -- "[*] → [K]/[R]" --> s1
413
+ s2 -- "[*] → [E]/[S]" --> s0
452
414
  ```
453
415
 
454
- **Reading guide** — the wrapped diagram is denser than the simplified hand-drawn version above. To parse it:
416
+ **Reading guide** — the v7 callable-subtree emit (introduced in [#174](https://github.com/mellonis/turing-machine-js/issues/174)) models `withOverriddenHaltState` as a function call: the wrapper is the call site, the bare's subtree is the callable body.
455
417
 
456
- 1. **Three non-halt nodes:** the wrapper `scanToX>eraseHere` (round, the initial state); `scanToX` (square, the original subroutineunmodified); `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.
418
+ 1. **`[[scanToX(eraseHere)]]` (Mermaid subroutine / double-walled-rectangle shape)** is the wrapper node, drawn OUTSIDE any subgraph. It's the runtime entry point — `idle -. enter .->` arrives here and shows the composite name (`bare(override)`). Wrappers have no transitions of their own; they delegate to the bare via the `call` arrow.
419
+ 2. **`subgraph w_1["callable subtree of scanToX"]`** is the bare's callable subtree the scope of code that runs when the wrapper is "called." It contains the bare `s1["scanToX"]`, any body states reachable from the bare, and a local halt marker `c1(((halt)))` where the bare's halt-bound transitions land.
420
+ 3. **The bold `==> call`** from wrapper to bare is the call arrow visual signature of "wrapper invokes this callable subtree, pushing its override onto the runtime stack." Bold arrows are reserved for wrapper-to-bare calls; counting them in a diagram counts the wrappers in play.
421
+ 4. **The dotted `-. return .->`** from the subtree back to the wrapper is the return arrow fires when the bare halts (lands on `c1`) and the stack pops. The wrapper's solid `--> s2` (to `eraseHere`) is the post-return continuation; ordinary transition under the function-call mental model.
422
+ 5. **Real `(((halt)))` outside any subgraph** (`s0`) is the actual run terminus. Reached only by states OUTSIDE any callable subtree — here, by `eraseHere` after it erases the cell.
460
423
 
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.
424
+ **Reading runtime sequence on tape `['a','b','X','b','a']`:** enter at wrapper `[[scanToX(eraseHere)]]` (with `eraseHere` queued as the override); `call` into the subtree of `scanToX`; `[*] → [K]/[R]` self-loops on `s1` until the head sees `X`; the `['X'] → [K]/[S]` edge lands on `c1`; `return` to the wrapper; solid `--> s2` to `eraseHere`; `eraseHere` runs `[*] → [E]/[S]` and halts at real `s0`. Run terminates.
462
425
 
463
- </details>
426
+ > 💡 **Round-trip stability.** `toMermaid → fromMermaid → toGraph → toMermaid` is bytewise stable for wrapped states ([#139](https://github.com/mellonis/turing-machine-js/issues/139) regression). The callable-subtree emit (#174) eliminates per-context duplication: shared bares like `library-binary-numbers`'s `invertNumber` (used by two wrappers in `minusOne`) render as a single subtree with two `& `-joined call arrows — so even shared-bare cases now produce stable, dedup'd round-trips.
464
427
 
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.
428
+ Wrappers nest: `inner.withOverriddenHaltState(middle).withOverriddenHaltState(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
429
 
467
430
  ## Debugging breakpoints
468
431
 
@@ -492,7 +455,7 @@ myState.debug = null;
492
455
 
493
456
  > ⚠️ **`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.
494
457
 
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`.
458
+ The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. The internal cell is shared with `state.withOverriddenHaltState(...)` 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`.
496
459
 
497
460
  `run()` is async and accepts an `onPause` hook:
498
461
 
@@ -516,7 +479,7 @@ If `onPause` is not provided, breaks fire-and-resume invisibly — the trajector
516
479
 
517
480
  **Caveat:** `haltState` is a module-level singleton. Setting `haltState.debug` affects every machine in the process; clear in `afterEach` / `finally` for test isolation.
518
481
 
519
- ### Throttle pattern (v6.4.0+)
482
+ ### Throttle pattern
520
483
 
521
484
  For per-iter throttle / animation / "wait between steps" UIs, use the **`onIter`** hook — an awaited callback that fires once at the end of every iter, after both `onPause` dispatches on the same yield. It's the engine-native shape for per-iter coordination:
522
485
 
@@ -606,6 +569,80 @@ Together: use `summarize` to ask "is this machine the right shape?" (size, compo
606
569
 
607
570
  For visualization and round-tripping, see `State.toGraph` / `State.fromGraph` and `toMermaid` / `fromMermaid`.
608
571
 
572
+ ## Diagram conventions
573
+
574
+ The full reference for reading `toMermaid` output — shapes, edge styles, and the bracketed edge-label vocabulary. All shapes and arrows are standard [Mermaid flowchart syntax](https://mermaid.js.org/syntax/flowchart.html); any Mermaid renderer (GitHub preview, IDE plugins, [mermaid-js](https://github.com/mermaid-js/mermaid) client-side) paints these diagrams the same way.
575
+
576
+ ### Node shapes
577
+
578
+ | Shape | Meaning |
579
+ |---|---|
580
+ | `s0(((halt)))` | the halt state |
581
+ | `sN["name"]` | a regular state (or a bare, when inside a subgraph) |
582
+ | `sN[["composite-name"]]` | a `withOverriddenHaltState` wrapper (call site, outside any subgraph) — see [§Subroutine composition](#subroutine-composition-with-withoverriddenhaltstate) |
583
+ | `cN(((halt)))` inside a subgraph | halt marker (visualization aid; maps back to the singleton `haltState` at runtime) |
584
+ | `idle([idle])` | pre-execution sentinel (not a real state) |
585
+
586
+ ### Edge styles
587
+
588
+ | Style | Where | Meaning |
589
+ |---|---|---|
590
+ | `-->` regular solid | between states; wrapper → override | plain transition / wrapper's post-return continuation |
591
+ | `==> "call"` thick solid | wrapper → bare | the wrapper's call into its callable subtree; reserved for wrapper-to-bare |
592
+ | `w_N -. "return" .->` dotted | subtree → wrapper | the subtree's halt-marker has incoming → control returns to the calling wrapper |
593
+ | `w_N -. "halt" .->` dotted | subtree → `s0` | the subtree has a non-wrapper entry path → halt-marker can fire with empty stack (real halt) |
594
+ | `idle -. enter .->` dotted | from `idle` to initial state | execution-start marker |
595
+
596
+ The `&` ribbon syntax (`s_W1 & s_W2 == "call" ==> s_A`) collapses multiple wrappers that share a bare into one arrow. Bold `==>` is reserved exclusively for the wrapper-to-bare `call` arrow.
597
+
598
+ ### Groupings
599
+
600
+ `subgraph w_N["callable subtree of NAME"] … end` wraps a bare + its body + a halt marker — the callable scope of code that runs when a wrapper "calls" the bare. Multi-bare frames (union-find merged from shared body states) use the label `"callable scope: A ∪ B"`.
601
+
602
+ ### Edge label format
603
+
604
+ `[reads] → [writes]/[moves]`. Each bracketed list is a tape-block reading — one entry per tape; brackets always present, even single-tape.
605
+
606
+ | Glyph | Where | Meaning |
607
+ |---|---|---|
608
+ | `'X'` | read, write | literal alphabet symbol (single-quoted) |
609
+ | `*` | read only | `ifOtherSymbol` catch-all (ASCII `*`; a literal `*` in the alphabet renders as the quoted `'*'`, so the marker stays unambiguous) |
610
+ | `B` | read only | the tape's blank symbol (a literal `B` in the alphabet appears as `'B'`, so the marker stays unambiguous) |
611
+ | `K` | write only | keep (no write) |
612
+ | `E` | write only | erase (write the tape's blank) |
613
+ | `L` / `R` / `S` | move only | left / right / stay |
614
+
615
+ ### Alternation rule
616
+
617
+ Alternative read patterns are always per-pattern-bracket:
618
+
619
+ - Single-tape: `['^']|['1']|['0']`
620
+ - Multi-tape: `['0','a']|['1','b']` — "(tape 1=`'0'` AND tape 2=`'a'`) OR (tape 1=`'1'` AND tape 2=`'b'`)"
621
+
622
+ The compact in-bracket form `['^'|'1']` is **rejected** by `fromMermaid` — and never emitted by `toMermaid`. The reason is pedagogical: each alternative is its own drawn transition, and the compact form would read as cross-product semantics in multi-tape (`['0'|'1','a'|'b']` could mean 4 combinations rather than 2 paired alternatives). One consistent rule across tape counts: each alternative is a full bracketed pattern.
623
+
624
+ ### Multi-tape example
625
+
626
+ A 2-tape "copier" machine — as long as tape 1 reads a non-blank, write the same symbol to tape 2 and step both right; halt when tape 1 reads blank:
627
+
628
+ ```mermaid
629
+ flowchart TD
630
+ %% alphabets: [[" ","0","1"],[" ","0","1"]]
631
+ s0(((halt)))
632
+ s1["copy"]
633
+ idle([idle])
634
+ idle -. enter .-> s1
635
+ s1 -- "['0',*] → [K,'0']/[R,R]" --> s1
636
+ s1 -- "['1',*] → [K,'1']/[R,R]" --> s1
637
+ s1 -- "[B,*] → [K]/[S]" --> s0
638
+ ```
639
+
640
+ Reading `['0',*] → [K,'0']/[R,R]`:
641
+
642
+ - **Read** `['0',*]` — tape 1 must be literal `'0'`; tape 2 is `ifOtherSymbol` (any).
643
+ - **Write** `[K,'0']` — tape 1: keep; tape 2: write literal `'0'`.
644
+ - **Move** `[R,R]` — both tapes step right.
645
+
609
646
  ## Versioning notes
610
647
 
611
648
  API surface changes since v3, in past tense so the timing of each piece is explicit:
@@ -617,6 +654,10 @@ API surface changes since v3, in past tense so the timing of each piece is expli
617
654
  - **v6.2** *(superseded by v6.3.0)* — widened `onStep`'s signature to `(m) => void | Promise<void>` and added an inline `await onStep(...)` in the run loop, enabling throttle-in-`onStep` patterns. This overturned the docstring-stated contract that `onStep` is sync (microtask-free); the right place for per-iter throttling is `onPause` with self-rearm (see [Throttle pattern](#throttle-pattern)). Restored in v6.3.0.
618
655
  - **v6.3** — `onStep` reverted to its v6.0–v6.1 sync contract — `(m) => void`, called synchronously inside the run loop. The Throttle pattern section documents the engine-native shape for per-iter throttle / "wait between iters" UIs. No other API changes.
619
656
  - **v6.4** — New **`onIter`** hook on `run()`: awaited, fires once at the end of every iter (after both `onPause` dispatches on the same yield), unaffected by the `debug` master switch. Use for per-iter throttle / animation / coordination needing a suspend point; complements the existing sync `onStep` (tracing) and conditional `onPause` (user breakpoints). Three-hook contract is now `onStep` (sync, mid-iter) / `onPause` (awaited, on `state.debug` match) / `onIter` (awaited, end-of-iter). Additive — peer-deps unchanged. The v6.3.0 README's `onPause`-rearm throttle workaround is superseded.
657
+ - **v7** *(alpha 1, 2026-05-21)* — Composition-representation overhaul. **First pre-release on the `next` dist-tag:** `npm install @turing-machine-js/machine@next` (or pin `@7.0.0-alpha.1`). Stable v7.0.0 still pending [#102](https://github.com/mellonis/turing-machine-js/issues/102) (debugger step-in/over/out primitives). Landed in alpha.1:
658
+ - **`withOverrodeHaltState` → `withOverriddenHaltState`** ([#149](https://github.com/mellonis/turing-machine-js/issues/149)). Grammar fix on a name introduced in 2019: the past-participle `overridden` fits the "with a halt-state that has been ___" naming idiom; `overrode` (simple past) didn't. Hard cutover — no deprecated alias. The getter (`state.overrodeHaltState` → `state.overriddenHaltState`) and the serialized `Graph` data field (`node.overrodeHaltStateId` → `node.overriddenHaltStateId`) rename in lockstep. Consumer migration: global find/replace `OverrodeHaltState` → `OverriddenHaltState` and `overrodeHaltState` → `overriddenHaltState`. Persisted `State.toGraph` JSON dumps would need the same field-rename treatment, but persistence isn't a known consumer pattern.
659
+ - **Paren-based wrapped-state naming** ([#148](https://github.com/mellonis/turing-machine-js/issues/148)). `withOverriddenHaltState`'s composite name format changed from flat `bare>override` to nested `bare(override)`. Same nesting depth reads as `A(B(A))` (bare = `A`, override = `B(A)`) versus `A(B)(A)` (bare = `A(B)`, override = `A`) — two structurally-different wrap-trees that the old `>`-flat notation collided into the single string `A>B>A`. As a consequence, **user-provided state names must not contain `(` or `)`** — `State` now throws at construction time if a user passes such a name. The `>` character stays valid in user names (no longer reserved). The `inspect()` / `toGraph` / `toMermaid` outputs carry the new format. `states.md` files in `library-binary-numbers` regenerate accordingly.
660
+ - **`toMermaid` callable-subtree emit** ([#174](https://github.com/mellonis/turing-machine-js/issues/174), supersedes the alpha.1 collapsed-bare shape from #138/#139). `withOverriddenHaltState` is modeled as a function call: the wrapper is a `[[composite-name]]` call site OUTSIDE any subgraph, the bare's reachable subtree becomes a `subgraph w_${frameId}["callable subtree of NAME"] … end` block containing the bare + body states + a per-frame halt marker `c${frameId}(((halt)))`. Frames are computed via union-find on bare-reachability — overlapping reach sets merge into a single union frame, so shared bares (`library-binary-numbers/minusOne`'s `invertNumber`) appear ONCE with `& `-joined call arrows from each wrapper. Bold `==> "call"` arrows are reserved for the wrapper-to-bare call; dotted `-.->` is reserved for frame-level dispatch (`return` / `halt` / `enter`). The retired `-. onHalt .->` keyword is replaced by a solid `--> override` arrow (just an ordinary transition under the call/return mental model). `GraphNode` gains `isWrapper`, `bareStateId`, `frameId` fields (and drops `isWrapped`). Bytewise round-trip stability now holds for all wrapped states including shared-bare cases (no per-context duplication).
620
661
 
621
662
  For the full release history, see the [GitHub releases page](https://github.com/mellonis/turing-machine-js/releases).
622
663
 
@@ -25,7 +25,7 @@ export default class State {
25
25
  get id(): number;
26
26
  get name(): string;
27
27
  get isHalt(): boolean;
28
- get overrodeHaltState(): State | null;
28
+ get overriddenHaltState(): State | null;
29
29
  get ref(): this;
30
30
  get debug(): DebugConfig;
31
31
  set debug(value: DebugConfig | {
@@ -37,12 +37,12 @@ export default class State {
37
37
  getSymbol(tapeBlock: TapeBlock): symbol;
38
38
  getCommand(symbol: symbol): Command;
39
39
  getNextState(symbol: symbol): State | Reference;
40
- withOverrodeHaltState(overrodeHaltState: State): State;
40
+ withOverriddenHaltState(overriddenHaltState: State): State;
41
41
  static inspect(state: State): {
42
42
  id: number;
43
43
  name: string;
44
44
  isHalt: boolean;
45
- overrodeHaltState: {
45
+ overriddenHaltState: {
46
46
  id: number;
47
47
  name: string;
48
48
  } | null;