@turing-machine-js/machine 6.3.0 → 7.0.0-alpha.1
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 +160 -0
- package/README.md +140 -111
- package/dist/classes/State.d.ts +3 -3
- package/dist/classes/TuringMachine.d.ts +21 -10
- package/dist/index.cjs +495 -108
- package/dist/index.mjs +495 -108
- package/dist/utilities/graph.d.ts +4 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,166 @@ 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.1] - 2026-05-21
|
|
8
|
+
|
|
9
|
+
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`.
|
|
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.1`. Migration walkthrough at the bottom.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`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:
|
|
16
|
+
- public method `State.prototype.withOverrodeHaltState` → `withOverriddenHaltState`
|
|
17
|
+
- getter `state.overrodeHaltState` → `state.overriddenHaltState`
|
|
18
|
+
- private field `#overrodeHaltState` → `#overriddenHaltState`
|
|
19
|
+
- serialized `Graph` data field `node.overrodeHaltStateId` → `node.overriddenHaltStateId`
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
23
|
+
- **`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).
|
|
24
|
+
|
|
25
|
+
- **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).
|
|
26
|
+
|
|
27
|
+
- **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."
|
|
28
|
+
|
|
29
|
+
- **`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).
|
|
30
|
+
|
|
31
|
+
- **`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`.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **#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.
|
|
36
|
+
|
|
37
|
+
- **Multi-tape example** in the engine README illustrating the bracketed format with two tapes.
|
|
38
|
+
|
|
39
|
+
- **§Diagram conventions section** in the engine README — full reference: node shapes, edge styles, groupings, edge label format + cell vocabulary, alternation rule, multi-tape example.
|
|
40
|
+
|
|
41
|
+
- **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.
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- 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.
|
|
46
|
+
|
|
47
|
+
### Migration
|
|
48
|
+
|
|
49
|
+
For consumers updating from v6.x:
|
|
50
|
+
|
|
51
|
+
**1. Identifier rename** (mechanical):
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
git grep -l 'OverrodeHaltState\|overrodeHaltState' | xargs sed -i '' \
|
|
55
|
+
-e 's/OverrodeHaltState/OverriddenHaltState/g' \
|
|
56
|
+
-e 's/overrodeHaltState/overriddenHaltState/g'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Persisted `State.toGraph` JSON dumps need the same `overrodeHaltStateId` → `overriddenHaltStateId` field rename.
|
|
60
|
+
|
|
61
|
+
**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)'`.
|
|
62
|
+
|
|
63
|
+
**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.
|
|
64
|
+
|
|
65
|
+
**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.
|
|
66
|
+
|
|
67
|
+
**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).
|
|
68
|
+
|
|
69
|
+
### Out of v7-alpha.1 (still pending for stable v7.0.0)
|
|
70
|
+
|
|
71
|
+
- **[#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`.
|
|
72
|
+
|
|
73
|
+
### Compatibility
|
|
74
|
+
|
|
75
|
+
- 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`.
|
|
76
|
+
|
|
77
|
+
## [6.4.0] - 2026-05-19
|
|
78
|
+
|
|
79
|
+
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).
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`onIter` callback** on `run()`: `onIter?: (m: MachineState) => void | Promise<void>`. Fires once per iteration at end-of-iter — *after* both `onPause(before, K)` and `onPause(after, K)` dispatches on the same yield. Awaited inline, so consumers returning a Promise suspend the run loop between iters. Unaffected by the `debug` master switch — `onIter` fires on every iter regardless of whether any `state.debug` breakpoints are armed.
|
|
84
|
+
|
|
85
|
+
Use cases the hook serves:
|
|
86
|
+
- **Per-iter throttle** (interactive debugger / animation UIs) — `await new Promise(r => setTimeout(r, intervalMs))` inside the callback gives a "wait between iters" pattern with no `state.debug` mutation.
|
|
87
|
+
- **Iter-correct bookkeeping** — for downstream libraries that maintain per-iter prev state (e.g. PostMachine's `arrivalPath` tracking), `onIter` is the natural "iter K is done" boundary, after all `onPause` hooks have read their own snapshots. Avoids the prev-advance-during-onStep ordering hazard.
|
|
88
|
+
- **Yield-to-other-work in batched runs** — a sync `await new Promise(r => queueMicrotask(r))` per iter lets long runs interleave with other event-loop work without owning the run loop.
|
|
89
|
+
|
|
90
|
+
### Three-hook contract recap
|
|
91
|
+
|
|
92
|
+
| Hook | Sync/Async | When | Use |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| `onStep` | Sync, not awaited | Mid-iter (between before/after) | Cheap tracer/logger, microtask-free hot loop |
|
|
95
|
+
| `onPause` | Awaited | Conditional on `state.debug[when]` match | User-authored breakpoints / debugger UI |
|
|
96
|
+
| `onIter` | Awaited | End of every iter | Per-iter coordination — throttle, prev-tracking, animation, yield-to-other-work |
|
|
97
|
+
|
|
98
|
+
### Changed (docs)
|
|
99
|
+
|
|
100
|
+
- README's **Throttle pattern** section rewritten to recommend `onIter` as the canonical hook. The v6.3.0 `onPause`-rearm workaround is superseded — that pattern still works (it's just the engine-native primitives), but `onIter` is the cleaner shape.
|
|
101
|
+
|
|
102
|
+
### Compatibility
|
|
103
|
+
|
|
104
|
+
- Default `onIter` is `undefined` → no behavior change for any existing consumer that doesn't opt in.
|
|
105
|
+
- Sync consumers using only `onStep` pay zero microtask overhead — `onIter` is purely opt-in.
|
|
106
|
+
- Peer-deps unchanged (`^6.0.0` includes 6.4.0 for the dependents).
|
|
107
|
+
|
|
108
|
+
## [6.3.0] - 2026-05-19
|
|
109
|
+
|
|
110
|
+
Corrective minor release. **Reverts v6.2.0's `await onStep` change** and restores the original sync `onStep` contract that held since the hook was introduced. See [GitHub release](https://github.com/mellonis/turing-machine-js/releases/tag/v6.3.0) for the full post-mortem.
|
|
111
|
+
|
|
112
|
+
### Changed
|
|
113
|
+
|
|
114
|
+
- **`TuringMachine.run`** — `await onStep(m)` reverted to `onStep(m)`; type narrowed back from `(m: MachineState) => void | Promise<void>` to `(m: MachineState) => void`. The original "Sync, ~free hook ... must not be async" contract is restored, with the docstring augmented to point readers at the new Throttle pattern section.
|
|
115
|
+
|
|
116
|
+
### Removed
|
|
117
|
+
|
|
118
|
+
- The v6.2.0 test `async onStep is awaited between iters (#158)` (no longer the contract).
|
|
119
|
+
|
|
120
|
+
### Added (docs)
|
|
121
|
+
|
|
122
|
+
- README: new **Throttle pattern** section under *Debugging breakpoints* showing the engine-native shape for per-iter throttle / "wait between iters" UIs — arm `state.debug.after = true` on the initial state, throttle inside `onPause`, rearm `m.nextState.debug.after = true` in the callback. Replaces the v6.2.0 throttle-in-`onStep` pattern.
|
|
123
|
+
|
|
124
|
+
### Kept from v6.2.0 (independent improvements)
|
|
125
|
+
|
|
126
|
+
- CI `typecheck` step + dependent packages' `tsconfig` `paths`/`rootDir` fix.
|
|
127
|
+
|
|
128
|
+
### Migration
|
|
129
|
+
|
|
130
|
+
- From v6.2.0: drop any `await` you wrote inside `onStep`. The engine ignores the returned Promise now. If you used the throttle-in-`onStep` pattern, migrate to the Throttle pattern section in the README.
|
|
131
|
+
- From v6.1.0 or earlier: no change for you.
|
|
132
|
+
|
|
133
|
+
## [6.2.0] - 2026-05-19 [SUPERSEDED by 6.3.0]
|
|
134
|
+
|
|
135
|
+
> ⚠️ **This release was a mistake.** It overturned the sync `onStep` contract that had held since the hook was introduced, motivated by a downstream throttle use case that didn't actually need an engine API change. Restored to sync in [6.3.0](#630---2026-05-19). The `await onStep` semantic shipped briefly on npm; consumers should upgrade to 6.3.0 and use the README's Throttle pattern instead.
|
|
136
|
+
|
|
137
|
+
### Changed
|
|
138
|
+
|
|
139
|
+
- **`TuringMachine.run`** — `onStep` type widened from `(m) => void` to `(m) => void | Promise<void>`; the run loop now awaits each `onStep(m)` call.
|
|
140
|
+
|
|
141
|
+
### Internal
|
|
142
|
+
|
|
143
|
+
- CI `typecheck` step added (mirrors the spec-including tsconfig to catch TS errors in `*.spec.ts`).
|
|
144
|
+
- Dependent packages' `tsconfig` `paths` and `rootDir` corrected — they now resolve `@turing-machine-js/machine` from source in CI (previously required a pre-built `dist/`).
|
|
145
|
+
|
|
146
|
+
## [6.1.0] - 2026-05-16
|
|
147
|
+
|
|
148
|
+
Minor release. One additive ergonomic improvement to `state.debug`, no breaking changes.
|
|
149
|
+
|
|
150
|
+
### Added
|
|
151
|
+
|
|
152
|
+
- **Lazy-init `DebugConfig` + `Object.seal` on instance ([#151](https://github.com/mellonis/turing-machine-js/pull/151), closes [#150](https://github.com/mellonis/turing-machine-js/issues/150)).** `state.debug` is now always a non-null `DebugConfig` instance, lazy-initialized on first read. Chained writes like `state.debug.before = true` work on a fresh state without a prior `state.debug = {}` setup step. The `DebugConfig` instance is `Object.seal`-ed — typos like `state.debug.bofore = true` throw `TypeError` (in strict mode — default for TS-emitted modules) instead of silently creating a useless own property.
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- **`state.debug` getter type narrowed** — `DebugConfig | null` → `DebugConfig`. Setter still accepts `null`; the semantic shifts to "reset filters" (next read returns a fresh empty config rather than literal `null`).
|
|
157
|
+
- **Wrapper sharing preserved** — `withOverrodeHaltState`'s `#debugRef` cell continues to share a single `DebugConfig` instance across wrapper and original; first read on either lazy-creates, both subsequent reads return the same instance.
|
|
158
|
+
|
|
159
|
+
### Migration
|
|
160
|
+
|
|
161
|
+
No code changes required:
|
|
162
|
+
- `state.debug?.X` patterns keep working (the `?.` is now redundant but correct).
|
|
163
|
+
- `state.debug = { ... }` whole-object assignment works as before.
|
|
164
|
+
- `state.debug = null` works as before, with the new "reset filters" semantic.
|
|
165
|
+
- Consumer code checking `if (state.debug === null)` becomes dead code but doesn't crash; the narrowed getter type may surface TS warnings on those branches, safe to delete.
|
|
166
|
+
|
|
7
167
|
## [6.0.0] - 2026-05-09
|
|
8
168
|
|
|
9
169
|
### Changed (BREAKING)
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/mellonis/turing-machine-js/actions/workflows/main.yml)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
A composable Turing-machine engine for JavaScript: multi-tape, subroutine composition via `
|
|
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 `
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
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
|
-
|
|
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.
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
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`.
|
|
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 `
|
|
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). It is *not* bytewise lossless: state IDs auto-reassign on each rebuild, and for `withOverriddenHaltState` wrappers the composite name gains an extra `(${override.name})` wrapping 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
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
|
|
289
|
+
s1["a"]
|
|
312
290
|
s2["b"]
|
|
313
|
-
|
|
314
|
-
|
|
291
|
+
idle([idle])
|
|
292
|
+
idle -. enter .-> s1
|
|
293
|
+
s1 -- "['x'] → [K]/[S]" --> s2
|
|
294
|
+
s2 -- "['y'] → [K]/[S]" --> s1
|
|
315
295
|
```
|
|
316
296
|
|
|
317
|
-
`
|
|
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 `
|
|
347
|
+
## Subroutine composition with `withOverriddenHaltState`
|
|
370
348
|
|
|
371
|
-
`state.
|
|
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.
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
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,32 @@ flowchart TD
|
|
|
440
397
|
flowchart TD
|
|
441
398
|
%% alphabets: [[" ","a","b","X"]]
|
|
442
399
|
s0(((halt)))
|
|
443
|
-
s1["scanToX"]
|
|
444
400
|
s2["eraseHere"]
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
401
|
+
idle([idle])
|
|
402
|
+
subgraph w_3["halt frame"]
|
|
403
|
+
s3[["scanToX"]]
|
|
404
|
+
c3(((halt)))
|
|
405
|
+
end
|
|
406
|
+
idle -. enter .-> s3
|
|
407
|
+
s2 -- "[*] → [E]/[S]" --> s0
|
|
408
|
+
s3 -- "['X'] → [K]/[S]" --> c3
|
|
409
|
+
s3 -- "[*] → [K]/[R]" --> s3
|
|
451
410
|
s3 -. onHalt .-> s2
|
|
452
411
|
```
|
|
453
412
|
|
|
454
|
-
**Reading guide** — the
|
|
413
|
+
**Reading guide** — the v7 emit (introduced in [#138](https://github.com/mellonis/turing-machine-js/issues/138)) makes the wrapper's runtime stack-frame semantics visible:
|
|
455
414
|
|
|
456
|
-
1. **
|
|
457
|
-
2. **
|
|
458
|
-
3. **The
|
|
459
|
-
4. **
|
|
415
|
+
1. **The subgraph rectangle labeled `"halt frame"`** is the wrapper's runtime scope — while execution is "inside" this rectangle, the override target (`eraseHere`) sits on the runtime stack waiting to catch a halt. Visual-only; it does not mutate any edges.
|
|
416
|
+
2. **`[[scanToX]]` (Mermaid subroutine / double-walled-rectangle shape)** is the wrapper node. It's both the runtime entry point (execution starts here when entering the wrapper) AND the source of the dotted `onHalt` redirect. The wrapper's composite name (`scanToX(eraseHere)`) is computed at runtime via `state.name` but does not appear as a graph node label — only the bare's name is in the graph.
|
|
417
|
+
3. **The halt-marker `(((halt)))` inside the subgraph** (`c3` here) is where the bare's halt-bound transitions land *inside* the wrapper's scope. `haltState` is a runtime singleton; the halt marker is a teaching aid showing "halt is caught here, not at the real terminus." Solid arrows from the bare to the halt marker all stay inside the rectangle.
|
|
418
|
+
4. **The dotted `onHalt` arrow from `[[scanToX]]` to `eraseHere`** is the wrapper's catch-and-redirect. Originates from the wrapper-node since the wrapper *is* the catcher. Solid arrows from `[[scanToX]]` to other states can also cross the subgraph border — those are just regular runtime transitions whose target happens to be drawn outside this rectangle (only the dotted `onHalt` carries wrapper-machinery meaning). In larger compositions (`library-binary-numbers`'s `minusOne`), solid transitions whose target is *itself* a wrapped state render as a **thick `==>` arrow** instead of `-->` — that's the visual signal for "this transition enters a halt frame, pushing the override onto the runtime stack." Stack-growth structure is then scannable from the diagram: count thick arrows along an execution path to see how deep the stack gets.
|
|
419
|
+
5. **Real `(((halt)))` outside any subgraph** (`s0`) is the actual run terminus. Reached only by states that are *not* inside a wrapper's halt-frame — here, by `eraseHere` after it erases the cell.
|
|
460
420
|
|
|
461
|
-
|
|
421
|
+
**Reading runtime sequence on tape `['a','b','X','b','a']`:** enter the `halt frame` at `[[scanToX]]` (with `eraseHere` on the stack); `[*] → [K]/[R]` self-loops until the head sees `X`; the `['X'] → [K]/[S]` solid edge would normally halt — it lands on the halt marker `c3`, the wrapper's catch-and-redirect kicks in, pop the stack → `eraseHere`; `eraseHere` runs `[*] → [E]/[S]` and halts at real `s0`. Run terminates.
|
|
462
422
|
|
|
463
|
-
|
|
423
|
+
> 💡 **Round-trip caveat.** `toMermaid → fromMermaid → toGraph → toMermaid` is bytewise stable for simple wrappers like this one ([#139](https://github.com/mellonis/turing-machine-js/issues/139) regression). For shared-bare cases (same `State` instance used as the bare in multiple wrappers — e.g., `library-binary-numbers`'s `minusOne`), per-context duplication produces wrapper-id-dependent ordering that doesn't byte-match across rebuilds — equivalent runtime behavior, different emit-line order.
|
|
464
424
|
|
|
465
|
-
Wrappers nest: `inner.
|
|
425
|
+
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
426
|
|
|
467
427
|
## Debugging breakpoints
|
|
468
428
|
|
|
@@ -492,7 +452,7 @@ myState.debug = null;
|
|
|
492
452
|
|
|
493
453
|
> ⚠️ **`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
454
|
|
|
495
|
-
The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. The internal cell is shared with `state.
|
|
455
|
+
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
456
|
|
|
497
457
|
`run()` is async and accepts an `onPause` hook:
|
|
498
458
|
|
|
@@ -518,34 +478,27 @@ If `onPause` is not provided, breaks fire-and-resume invisibly — the trajector
|
|
|
518
478
|
|
|
519
479
|
### Throttle pattern
|
|
520
480
|
|
|
521
|
-
For per-iter throttle / animation / "wait between steps" UIs,
|
|
481
|
+
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
482
|
|
|
523
483
|
```ts
|
|
524
|
-
initialState.debug.after = true; // first pause-point
|
|
525
|
-
|
|
526
484
|
await machine.run({
|
|
527
485
|
initialState,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
await new Promise((r) => setTimeout(r, intervalMs)); // throttle
|
|
532
|
-
|
|
533
|
-
// Rearm for the next iter (engine processes m.nextState next).
|
|
534
|
-
// `state.debug` is shared across `withOverrodeHaltState` wrappers — a
|
|
535
|
-
// single arm reaches all of them.
|
|
536
|
-
if (!m.nextState.isHalt) m.nextState.debug.after = true;
|
|
486
|
+
onIter: async (m) => {
|
|
487
|
+
// Fires after before(m.state) / step / after(m.state) on iter m.step.
|
|
488
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
537
489
|
},
|
|
538
490
|
});
|
|
539
491
|
```
|
|
540
492
|
|
|
493
|
+
`onIter` is unaffected by the `debug` master switch and unrelated to `state.debug` — it fires on every iter regardless of whether any breakpoints are armed. It coexists cleanly with user-authored `state.debug` breakpoints: on an iter with both `.before` and `.after` armed, the consumer sees `onPause(before)` → `onStep` → `onPause(after)` → `onIter`, in that order, on the same yield.
|
|
494
|
+
|
|
541
495
|
A few details:
|
|
542
496
|
|
|
543
|
-
- **Halting iter**: `
|
|
544
|
-
-
|
|
545
|
-
- **
|
|
546
|
-
- **Click-pause** during throttling: keep a flag set from the outside; check it inside `onPause` before the `setTimeout` and surface a "real" pause (await a resolvable Promise the UI controls) instead of the throttle one. The engine doesn't need to know — it just sees a longer awaited `onPause`.
|
|
497
|
+
- **Halting iter**: `onIter` still fires on the iter whose `m.nextState === haltState`, after any halt-time `onPause` dispatches. Engine returns cleanly after that. Use this to land "halted" UI state in interactive consumers.
|
|
498
|
+
- **Click-pause / external interruption**: keep a flag set from the outside; check it inside `onIter` and `await` a resolvable Promise the UI controls (instead of the bare `setTimeout`). The engine just sees a longer awaited `onIter` — no engine surface needed for the pause.
|
|
499
|
+
- **Sync consumers should keep using `onStep`**: it's microtask-free; `onIter` adds one awaited boundary per iter. Use the right hook for the right verb (logging/tracing → `onStep`, throttle/coordination → `onIter`, user breakpoints → `onPause`).
|
|
547
500
|
|
|
548
|
-
(v6.2.0 briefly widened `onStep` to
|
|
501
|
+
(History: v6.2.0 briefly widened `onStep` to `void | Promise<void>` and added an inline `await`, motivated by this same throttle use case. That was a mistake — restored to sync in v6.3.0. v6.3.0 documented a workaround using `onPause` self-rearm on `state.debug.after = true`; that workaround is superseded by `onIter` in v6.4.0+.)
|
|
549
502
|
|
|
550
503
|
## Special objects
|
|
551
504
|
|
|
@@ -613,6 +566,77 @@ Together: use `summarize` to ask "is this machine the right shape?" (size, compo
|
|
|
613
566
|
|
|
614
567
|
For visualization and round-tripping, see `State.toGraph` / `State.fromGraph` and `toMermaid` / `fromMermaid`.
|
|
615
568
|
|
|
569
|
+
## Diagram conventions
|
|
570
|
+
|
|
571
|
+
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.
|
|
572
|
+
|
|
573
|
+
### Node shapes
|
|
574
|
+
|
|
575
|
+
| Shape | Meaning |
|
|
576
|
+
|---|---|
|
|
577
|
+
| `s0(((halt)))` | the halt state |
|
|
578
|
+
| `sN["name"]` | a regular state |
|
|
579
|
+
| `sN[["name"]]` | a `withOverriddenHaltState` wrapper-bare (subroutine shape) — see [§Subroutine composition](#subroutine-composition-with-withoverriddenhaltstate) |
|
|
580
|
+
| `cN(((halt)))` inside a subgraph | halt marker (visualization aid; maps back to the singleton `haltState` at runtime) |
|
|
581
|
+
| `idle([idle])` | pre-execution sentinel (not a real state) |
|
|
582
|
+
|
|
583
|
+
### Edge styles
|
|
584
|
+
|
|
585
|
+
| Style | Where | Meaning |
|
|
586
|
+
|---|---|---|
|
|
587
|
+
| `-->` regular solid | between states | plain transition |
|
|
588
|
+
| `==>` thick solid | between states | transition INTO a wrapped state — stack-push happens at runtime |
|
|
589
|
+
| `-. onHalt .->` dotted | from `[[bare]]` to override | wrapper's catch-and-redirect |
|
|
590
|
+
| `-. enter .->` dotted | from `idle` to initial state | execution-start marker |
|
|
591
|
+
|
|
592
|
+
### Groupings
|
|
593
|
+
|
|
594
|
+
`subgraph w_N["halt frame"] … end` wraps a `[[bare]]` + its halt marker — visual grouping of the wrapper's runtime halt-handling scope.
|
|
595
|
+
|
|
596
|
+
### Edge label format
|
|
597
|
+
|
|
598
|
+
`[reads] → [writes]/[moves]`. Each bracketed list is a tape-block reading — one entry per tape; brackets always present, even single-tape.
|
|
599
|
+
|
|
600
|
+
| Glyph | Where | Meaning |
|
|
601
|
+
|---|---|---|
|
|
602
|
+
| `'X'` | read, write | literal alphabet symbol (single-quoted) |
|
|
603
|
+
| `*` | read only | `ifOtherSymbol` catch-all (ASCII `*`; a literal `*` in the alphabet renders as the quoted `'*'`, so the marker stays unambiguous) |
|
|
604
|
+
| `B` | read only | the tape's blank symbol (a literal `B` in the alphabet appears as `'B'`, so the marker stays unambiguous) |
|
|
605
|
+
| `K` | write only | keep (no write) |
|
|
606
|
+
| `E` | write only | erase (write the tape's blank) |
|
|
607
|
+
| `L` / `R` / `S` | move only | left / right / stay |
|
|
608
|
+
|
|
609
|
+
### Alternation rule
|
|
610
|
+
|
|
611
|
+
Alternative read patterns are always per-pattern-bracket:
|
|
612
|
+
|
|
613
|
+
- Single-tape: `['^']|['1']|['0']`
|
|
614
|
+
- Multi-tape: `['0','a']|['1','b']` — "(tape 1=`'0'` AND tape 2=`'a'`) OR (tape 1=`'1'` AND tape 2=`'b'`)"
|
|
615
|
+
|
|
616
|
+
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.
|
|
617
|
+
|
|
618
|
+
### Multi-tape example
|
|
619
|
+
|
|
620
|
+
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:
|
|
621
|
+
|
|
622
|
+
```mermaid
|
|
623
|
+
flowchart TD
|
|
624
|
+
%% alphabets: [[" ","0","1"],[" ","0","1"]]
|
|
625
|
+
s0(((halt)))
|
|
626
|
+
s1["copy"]
|
|
627
|
+
idle([idle])
|
|
628
|
+
idle -. enter .-> s1
|
|
629
|
+
s1 -- "['0',*] → [K,'0']/[R,R]" --> s1
|
|
630
|
+
s1 -- "['1',*] → [K,'1']/[R,R]" --> s1
|
|
631
|
+
s1 -- "[B,*] → [K]/[S]" --> s0
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Reading `['0',*] → [K,'0']/[R,R]`:
|
|
635
|
+
|
|
636
|
+
- **Read** `['0',*]` — tape 1 must be literal `'0'`; tape 2 is `ifOtherSymbol` (any).
|
|
637
|
+
- **Write** `[K,'0']` — tape 1: keep; tape 2: write literal `'0'`.
|
|
638
|
+
- **Move** `[R,R]` — both tapes step right.
|
|
639
|
+
|
|
616
640
|
## Versioning notes
|
|
617
641
|
|
|
618
642
|
API surface changes since v3, in past tense so the timing of each piece is explicit:
|
|
@@ -623,6 +647,11 @@ API surface changes since v3, in past tense so the timing of each piece is expli
|
|
|
623
647
|
- **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).
|
|
624
648
|
- **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.
|
|
625
649
|
- **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.
|
|
650
|
+
- **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.
|
|
651
|
+
- **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:
|
|
652
|
+
- **`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.
|
|
653
|
+
- **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.
|
|
654
|
+
- **`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)). The wrapper-and-its-bare pair collapses into a single graph node (`isWrapped: true`); the wrapper's composite name no longer appears as a node label (only the bare's name does). Each wrapper gets a Mermaid `subgraph w_${bareId}["halt frame"] … end` block containing the `[[bare]]` (subroutine shape) plus a halt-marker `(((halt)))` (visualization aid showing where halt-bound transitions land inside the scope). The dotted `onHalt` edge originates from the `[[bare]]` and crosses the subgraph border to the override target — exactly one per wrapper. `Graph` data shape gains `isWrapped` and `isHaltMarker` flags on `GraphNode` and a stable `id` on `GraphTransition` (deterministic per-edge identifier — supports downstream tooling like the `machines-demo` interactive viewer at [machines-demo#10](https://github.com/mellonis/machines-demo/issues/10)). Halt-marker graph nodes use negative ids and round-trip back to the singleton `haltState` via `fromGraph`. Bytewise round-trip stability falls out for simple wrappers (no composite name in the graph means `fromGraph(toGraph(state))` recomputes names fresh — no accumulation). Shared-bare cases (e.g. `minusOne`'s repeated `invertNumber`) use per-context duplication in the graph emit.
|
|
626
655
|
|
|
627
656
|
For the full release history, see the [GitHub releases page](https://github.com/mellonis/turing-machine-js/releases).
|
|
628
657
|
|
package/dist/classes/State.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
40
|
+
withOverriddenHaltState(overriddenHaltState: State): State;
|
|
41
41
|
static inspect(state: State): {
|
|
42
42
|
id: number;
|
|
43
43
|
name: string;
|
|
44
44
|
isHalt: boolean;
|
|
45
|
-
|
|
45
|
+
overriddenHaltState: {
|
|
46
46
|
id: number;
|
|
47
47
|
name: string;
|
|
48
48
|
} | null;
|