@turing-machine-js/machine 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +266 -17
- package/dist/classes/State.d.ts +1 -1
- package/dist/index.cjs +44 -19
- package/dist/index.mjs +44 -19
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,25 @@
|
|
|
3
3
|
[](https://github.com/mellonis/turing-machine-js/actions/workflows/main.yml)
|
|
4
4
|

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