@turing-machine-js/machine 7.0.0-alpha.5 → 7.0.0-alpha.6
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 +28 -0
- package/README.md +142 -56
- package/dist/classes/DebugSession.d.ts +119 -0
- package/dist/classes/State.d.ts +2 -3
- package/dist/classes/TuringMachine.d.ts +86 -71
- package/dist/index.cjs +558 -257
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +558 -258
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ 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.6] - 2026-05-28
|
|
8
|
+
|
|
9
|
+
Sixth v7 pre-release. Lands the debugger step controls ([#102](https://github.com/mellonis/turing-machine-js/issues/102), [PR #214](https://github.com/mellonis/turing-machine-js/pull/214)) and, in doing so, **reshapes the entire debug surface** into three clearly-separated entry points: `run()` is now pure synchronous execution (no callbacks), `runStepByStep` is the minimal pure-iteration primitive (no breakpoint detection), and a new **`DebugSession`** class owns all interactive debugging — events, step controls, throttle, and pause coordination. With #102 the **v7 milestone is feature-complete**; the stable v7.0.0 cut is the only remaining step. Published 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.6`.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`DebugSession`** ([#102](https://github.com/mellonis/turing-machine-js/issues/102)) — `new DebugSession(machine, { initialState })`. The sole interactive-debugging surface (there is no `debugRun()` factory on `TuringMachine`). Emits `pause` / `step` / `iter` / `halt` events via `session.on(event, listener)`; `iter` listeners are **awaited** (the per-iter throttle / coordination point), `pause` / `step` / `halt` are fire-and-forget. Drive controls: `continue()`, `stepIn()`, `stepOver()`, `stepOut()`, external `pause()`, `stop()`, and `setRunInterval(ms)` for a per-iter throttle. Breakpoint detection (`state.debug` filters, `haltState.debug`) lives entirely here — `runStepByStep` no longer evaluates it. Concurrent sessions on one machine are rejected with a clear error (the underlying `TapeBlock` lock is single-active-run).
|
|
16
|
+
|
|
17
|
+
- **`PauseInfo` / `PausedMachineState`** ([#102](https://github.com/mellonis/turing-machine-js/issues/102)) — the `pause` event payload is a `MachineState & { pause: PauseInfo }` where `PauseInfo = { side: 'before' | 'after'; cause: 'breakpoint' | 'step' | 'manual' }`. Exactly one side per descriptor — `DebugSession` dispatches the two timings as separate `pause` events, so the v6 "both timings on one yield" shape no longer exists. `cause` precedence when an iter satisfies more than one trigger: `breakpoint > step > manual`; `'step'` / `'manual'` only ever fire on the `'before'` side.
|
|
18
|
+
|
|
19
|
+
- **Depth-based step controls mirroring Chrome DevTools** ([#102](https://github.com/mellonis/turing-machine-js/issues/102)) — `stepIn` = next iter at any depth; `stepOver` = next iter at `depth ≤ click-time depth` (nested `.withOverriddenHaltState(...)` subroutines run to completion, pause back at the starting level); `stepOut` = next iter at `depth < click-time depth` (current subroutine frame popped; throws at depth 0, mirroring DevTools' top-frame Step Out).
|
|
20
|
+
|
|
21
|
+
- **`matchFilter` export** ([#102](https://github.com/mellonis/turing-machine-js/issues/102), `@internal`) — predicate evaluating a `DebugConfig` side-filter against a matched symbol. Exported for sibling-module use in `DebugSession`; NOT re-exported from the package root.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **`run()` is synchronous and callback-free** ([#102](https://github.com/mellonis/turing-machine-js/issues/102)) — signature is now `run({ initialState, stepsLimit? }): void` (was `async … : Promise<void>` with `onPause` / `onStep` / `onIter` callbacks). Pure execution, no debug overhead. Reverses v4's `run` → `async run` change now that all callback machinery has moved to `DebugSession`. **Breaking** for callers awaiting `run()` or passing callbacks — construct a `DebugSession` instead.
|
|
26
|
+
|
|
27
|
+
- **`runStepByStep` is the minimal pure-iteration primitive** ([#102](https://github.com/mellonis/turing-machine-js/issues/102)) — it advances the machine and yields a plain `MachineState`; it evaluates no `state.debug` filters and stamps no pause field. All breakpoint detection moved to `DebugSession`. **Breaking** for consumers that read a pause / breakpoint field off raw yields.
|
|
28
|
+
|
|
29
|
+
- **`haltState.debug` pause is delivered via `DebugSession`** ([#102](https://github.com/mellonis/turing-machine-js/issues/102)) — still fires on the AFTER side of the halt-triggering iter (alpha.5 semantics: `m.state` is the triggering state, not `haltState`), but now arrives as a `pause` event with `{ side: 'after', cause: 'breakpoint' }` instead of a `debugBreak` field on the yield.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
- **`MachineState.debugBreak`** ([#102](https://github.com/mellonis/turing-machine-js/issues/102)) — the per-yield `{ before?, after?, cause }` descriptor from the v4–alpha.5 debugger is **removed**. Its replacement is the one-sided `m.pause: { side, cause }` carried ONLY on a `DebugSession` `pause` event (`PausedMachineState`); raw `runStepByStep` yields carry no pause field. **Breaking from alpha.5** — consumers reading `m.debugBreak.before` / `.after` / `.cause` must move to a `DebugSession` and read `m.pause.side` / `m.pause.cause`.
|
|
34
|
+
|
|
7
35
|
## [7.0.0-alpha.5] - 2026-05-25
|
|
8
36
|
|
|
9
37
|
Fifth v7 pre-release. Adds per-iter `matchedTransition` to `MachineState` so consumers can resolve the firing transition by graph id without re-deriving from `(source, nextState)` ambiguous pairs ([#205](https://github.com/mellonis/turing-machine-js/issues/205)), collapses `haltState.debug` to a `boolean` and moves the halt-imminent pause to the AFTER side of the halt-triggering iter so the wording + diagram cursor agree with the just-fired transition ([#207](https://github.com/mellonis/turing-machine-js/issues/207)), and renames the `GraphTransition.id` separator from `-` to `.` for parser-friendly splitting. Published under the `next` dist-tag: `npm install @turing-machine-js/machine@next`.
|
package/README.md
CHANGED
|
@@ -307,13 +307,16 @@ The runtime. Owns one `TapeBlock` and drives a state graph until it reaches `hal
|
|
|
307
307
|
```javascript
|
|
308
308
|
const machine = new TuringMachine({ tapeBlock });
|
|
309
309
|
|
|
310
|
-
// Run to halt — `run()` returns
|
|
311
|
-
|
|
310
|
+
// Run to halt — `run()` is synchronous, returns void:
|
|
311
|
+
machine.run({ initialState, stepsLimit: 1e5 });
|
|
312
312
|
|
|
313
|
-
// Or step-by-step (useful for
|
|
313
|
+
// Or step-by-step (useful for tracing / observation):
|
|
314
314
|
for (const step of machine.runStepByStep({ initialState })) {
|
|
315
315
|
console.log(step.state.name, step.currentSymbols, '→', step.nextSymbols, step.movements);
|
|
316
316
|
}
|
|
317
|
+
|
|
318
|
+
// For interactive debugging (breakpoints, step-in / step-over / step-out,
|
|
319
|
+
// throttle, click-pause), construct a DebugSession — see "Debugging" below.
|
|
317
320
|
```
|
|
318
321
|
|
|
319
322
|
Each yielded `step` (`MachineState`) has these fields:
|
|
@@ -326,26 +329,26 @@ Each yielded `step` (`MachineState`) has these fields:
|
|
|
326
329
|
| `nextSymbols` | `string[]` | per-tape symbols that will be written |
|
|
327
330
|
| `movements` | `symbol[]` | per-tape head moves (`movements.left/right/stay`) |
|
|
328
331
|
| `nextState` | `State` | the state that will execute next |
|
|
329
|
-
| `debugBreak?` | `{ before?: true, after?: true }` | only set when `state.debug` matched on this iter — see *Debugging breakpoints* below |
|
|
330
332
|
| `matchedTransition` | `{ id: string, matchKinds: ('wildcard'\|'literal')[] }` | the transition the engine picked for this iter — see *Matched transition* below |
|
|
331
333
|
|
|
332
334
|
`stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
|
|
333
335
|
|
|
334
|
-
#### Choosing between `run()
|
|
336
|
+
#### Choosing between `run()`, `runStepByStep()`, and `DebugSession`
|
|
335
337
|
|
|
336
|
-
|
|
338
|
+
Three non-overlapping entry points, picked by the consumer's actual need:
|
|
337
339
|
|
|
338
|
-
| | `run()` | `runStepByStep()` |
|
|
339
|
-
|
|
340
|
-
| Shape |
|
|
341
|
-
|
|
|
342
|
-
|
|
|
343
|
-
|
|
|
344
|
-
|
|
|
340
|
+
| | `run()` | `runStepByStep()` | `new DebugSession(machine, ...)` |
|
|
341
|
+
|---|---|---|---|
|
|
342
|
+
| Shape | sync, returns `void` | sync generator | async session with events |
|
|
343
|
+
| Observation | none | per-iter via `.next()` | event-based (`pause`, `step`, `iter`, `halt`) |
|
|
344
|
+
| Step controls | — | — | `continue / stepIn / stepOver / stepOut / pause / stop` |
|
|
345
|
+
| Throttle | — | — | `setRunInterval(ms)` |
|
|
346
|
+
| Breakpoint handling | not consulted — runs straight to halt | not consulted — yields every iter; `state.debug` is ignored | the only mode that *reacts* — fires a `pause` event |
|
|
347
|
+
| Best for | run-to-halt with no observation overhead | tracing, snapshots, test harnesses, custom batching | UI debuggers, IDE extensions, educational demos |
|
|
345
348
|
|
|
346
|
-
**Rule of thumb.**
|
|
349
|
+
**Rule of thumb.** No observation → `run()`. Sync per-iter observation → iterate `runStepByStep()`. Anything interactive (breakpoints, step controls, throttle, click-pause) → construct a `DebugSession`.
|
|
347
350
|
|
|
348
|
-
**
|
|
351
|
+
**Breakpoint detection lives only in `DebugSession`.** `runStepByStep` is the pure-iteration primitive — it advances the machine and reports a minimal `MachineState` with no pause/debug field, ignoring `state.debug` entirely. `DebugSession` is what evaluates the filters and turns a match into a `pause` event. (A consumer that wants its own breakpoint behavior on the raw generator can read `state.debug` itself.)
|
|
349
352
|
|
|
350
353
|
### Matched transition
|
|
351
354
|
|
|
@@ -365,15 +368,12 @@ matchedTransition: {
|
|
|
365
368
|
Example use:
|
|
366
369
|
|
|
367
370
|
```javascript
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
console.log(`step ${m.step}: fired transition ${m.matchedTransition.id} (wildcards at tapes: ${wildcardPositions.join(',') || 'none'})`);
|
|
375
|
-
},
|
|
376
|
-
});
|
|
371
|
+
for (const m of machine.runStepByStep({initialState})) {
|
|
372
|
+
const wildcardPositions = m.matchedTransition.matchKinds // per-tape, e.g. ['wildcard', 'literal']
|
|
373
|
+
.map((k, i) => k === 'wildcard' ? i : -1)
|
|
374
|
+
.filter((i) => i >= 0);
|
|
375
|
+
console.log(`step ${m.step}: fired transition ${m.matchedTransition.id} (wildcards at tapes: ${wildcardPositions.join(',') || 'none'})`);
|
|
376
|
+
}
|
|
377
377
|
```
|
|
378
378
|
|
|
379
379
|
## Subroutine composition with `withOverriddenHaltState`
|
|
@@ -478,9 +478,29 @@ s.tags; // readonly ['subroutine-entry']
|
|
|
478
478
|
|
|
479
479
|
See [§Diagram conventions § Tags](#tags) for the full emit shape.
|
|
480
480
|
|
|
481
|
-
## Debugging
|
|
481
|
+
## Debugging
|
|
482
|
+
|
|
483
|
+
The library exposes interactive debugging through the `DebugSession` class, constructed directly with a `TuringMachine`. Breakpoints are configured on the engine (`state.debug` / `haltState.debug`); the session dispatches them as events plus offers step-in / step-over / step-out controls, an external `pause()`, and a per-iter throttle.
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
import { DebugSession } from '@turing-machine-js/machine';
|
|
487
|
+
|
|
488
|
+
const session = new DebugSession(machine, { initialState });
|
|
489
|
+
|
|
490
|
+
session.on('pause', (m) => {
|
|
491
|
+
console.log(`paused at ${m.state.name}, ${m.pause.side} side, cause: ${m.pause.cause}`);
|
|
492
|
+
session.stepIn(); // or stepOver(), stepOut(), continue(), stop()
|
|
493
|
+
});
|
|
494
|
+
session.on('step', (m) => { /* fires once per iter, mid-iter */ });
|
|
495
|
+
session.on('iter', (m) => { /* fires once per iter, end-of-iter */ });
|
|
496
|
+
session.on('halt', () => { /* fires on natural halt (not on stop()) */ });
|
|
497
|
+
|
|
498
|
+
await session.start(); // resolves on halt or stop()
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Configuring breakpoints
|
|
482
502
|
|
|
483
|
-
|
|
503
|
+
Breakpoints live on the engine, not on the session. The session reads `state.debug` / `haltState.debug` and fires a `pause` event when a filter matches.
|
|
484
504
|
|
|
485
505
|
```ts
|
|
486
506
|
import { State, haltState, ifOtherSymbol } from '@turing-machine-js/machine';
|
|
@@ -508,33 +528,62 @@ haltState.debug = null; // alias of false (reset)
|
|
|
508
528
|
myState.debug = null;
|
|
509
529
|
```
|
|
510
530
|
|
|
511
|
-
> ⚠️ **`haltState.debug` is `boolean`-only.** Any object-shaped write (`{ before: true }`, `{ after: true }`, `{ before: true, after: true }`) throws at write time. The pause fires on the AFTER side of the iter whose transition leads to halt — `m.state` is the triggering state (not haltState), `m.
|
|
531
|
+
> ⚠️ **`haltState.debug` is `boolean`-only.** Any object-shaped write (`{ before: true }`, `{ after: true }`, `{ before: true, after: true }`) throws at write time. The pause fires on the AFTER side of the iter whose transition leads to halt — `m.state` is the triggering state (not haltState), `m.pause.side === 'after'`.
|
|
512
532
|
|
|
513
|
-
> ⚠️ **Chained-form `haltState.debug.before = true` doesn't throw in non-strict mode** — this is a JavaScript primitive quirk, not engine behavior. The getter returns the boolean `false`; assigning `.before` to that boolean is a no-op in non-strict mode (silent), a `TypeError` in strict mode. The engine setter only sees whole-object writes (`haltState.debug = X`)
|
|
533
|
+
> ⚠️ **Chained-form `haltState.debug.before = true` doesn't throw in non-strict mode** — this is a JavaScript primitive quirk, not engine behavior. The getter returns the boolean `false`; assigning `.before` to that boolean is a no-op in non-strict mode (silent), a `TypeError` in strict mode. The engine setter only sees whole-object writes (`haltState.debug = X`). **Always use the whole-object form: `haltState.debug = true` / `= false` / `= null`.**
|
|
514
534
|
|
|
515
|
-
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
|
|
535
|
+
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 is wrapped automatically. The instance is `Object.seal`-ed — typos like `state.debug.bofore = true` throw `TypeError` instead of silently creating a useless property.
|
|
516
536
|
|
|
517
|
-
`
|
|
537
|
+
**Filter semantics:** `true` is a wildcard (match any symbol). `[ifOtherSymbol]` is NOT a wildcard — it matches only the catch-all resolution case (same meaning as in transition keys).
|
|
518
538
|
|
|
519
|
-
|
|
520
|
-
await machine.run({
|
|
521
|
-
initialState,
|
|
522
|
-
onStep: (m) => { /* logger sees every step */ },
|
|
523
|
-
onPause: async (m) => {
|
|
524
|
-
// Awaited at every break — hold execution until you resolve.
|
|
525
|
-
if (m.debugBreak?.before) console.log('before:', m.state.name);
|
|
526
|
-
if (m.debugBreak?.after) console.log('after:', m.state.name);
|
|
527
|
-
},
|
|
528
|
-
});
|
|
529
|
-
```
|
|
539
|
+
**Caveat:** `haltState` is a module-level singleton. Setting `haltState.debug` affects every machine in the process; clear in `afterEach` / `finally` for test isolation.
|
|
530
540
|
|
|
531
|
-
|
|
541
|
+
### DebugSession API
|
|
532
542
|
|
|
533
|
-
|
|
543
|
+
| Method | Description |
|
|
544
|
+
|---|---|
|
|
545
|
+
| `start(): Promise<void>` | Begin execution. Resolves on natural halt or after `stop()`. Single-use — a second call throws. |
|
|
546
|
+
| `continue()` | Resume from a pause and run to the next breakpoint or halt. |
|
|
547
|
+
| `stepIn()` | Resume and pause at the very next iter, regardless of depth — descends into any subroutine the current iter enters. Mirrors DevTools **Step Into**. |
|
|
548
|
+
| `stepOver()` | Resume and pause at the next iter back at (or above) the click-time halt-stack depth (`depth ≤ clickTimeDepth`) — subroutines the stepped-over iter enters run to completion without pausing inside. Mirrors DevTools **Step Over**. |
|
|
549
|
+
| `stepOut()` | Resume and pause at the next iter strictly shallower than the click-time depth (`depth < clickTimeDepth`) — i.e. once the current frame has been popped. Throws if the click-time depth is 0 (no enclosing frame to exit). Mirrors DevTools **Step Out**. |
|
|
550
|
+
| `pause()` | Request a pause from outside the loop. Fires on the next iter with `cause: 'manual'`. If a breakpoint matches that same iter, the breakpoint wins (single pause, `cause: 'breakpoint'`). |
|
|
551
|
+
| `stop()` | Terminate immediately. `halt` event does NOT fire. |
|
|
552
|
+
| `setRunInterval(ms)` | Insert an awaited `setTimeout(ms)` at the end of each iter. `0` disables. Useful for visualization UIs. |
|
|
553
|
+
| `on(event, listener) / off(event, listener)` | Register / unregister listeners. Multiple listeners per event are supported. Listener dispatch differs by event — see *Events* below. |
|
|
534
554
|
|
|
535
|
-
|
|
555
|
+
The three step controls are depth-based to mirror DevTools: from a pause at halt-stack depth `D`, `stepIn` pauses at the next iter (any depth), `stepOver` at the next iter with `depth ≤ D`, `stepOut` at the next iter with `depth < D`. For a *plain* iter (no subroutine entry) all of Into/Over coincide — they differ only under genuine nesting (a bare that itself enters a `withOverriddenHaltState` wrapper). One engine-specific nuance: composition is continuation-passing (`A.wohs(B)` = "run A, then B" — sequential, where `wohs` is `withOverriddenHaltState`), so a flat `.wohs()` chain has no real nesting and Over/Out behave the same there; the distinction appears only when a subroutine's body enters another wrapper.
|
|
536
556
|
|
|
537
|
-
|
|
557
|
+
### Events
|
|
558
|
+
|
|
559
|
+
| Event | Argument | Dispatch | Fires |
|
|
560
|
+
|---|---|---|---|
|
|
561
|
+
| `pause` | `PausedMachineState` (`MachineState` + `pause: {side, cause}`) | Implicitly awaited via internal pause-promise — engine blocks until `continue / stepIn / stepOver / stepOut / stop` is called. Listener Promise itself is fire-and-forget. | A breakpoint matched, a step-mode endpoint was reached, or `session.pause()` was requested. |
|
|
562
|
+
| `step` | `MachineState` | Fire-and-forget (sync hot-loop tracing). | Once per iter, between any before-pause and after-pause. |
|
|
563
|
+
| `iter` | `MachineState` | **Awaited** (sequenced, blocks the engine). Use for throttle / per-iter coordination / step-boundary synthesis. | Once per iter, at end. After any after-pause. |
|
|
564
|
+
| `halt` | (none) | Fire-and-forget. | Once, on natural halt. Does NOT fire when `stop()` was called. |
|
|
565
|
+
|
|
566
|
+
### The pause descriptor: `m.pause`
|
|
567
|
+
|
|
568
|
+
`pause` listeners receive a `PausedMachineState` — a plain `MachineState` plus a `pause: { side, cause }` descriptor (raw `runStepByStep` yields have no such field).
|
|
569
|
+
|
|
570
|
+
- **`side`** — `'before'` or `'after'`. Exactly one: DebugSession dispatches the two timings as separate `pause` events, so a descriptor is always one-sided. (`'step'` / `'manual'` causes only ever fire on the `'before'` side.)
|
|
571
|
+
- **`cause`** — distinguishes the origin:
|
|
572
|
+
- `'breakpoint'` — a `state.debug` filter matched, or `haltState.debug === true` triggered.
|
|
573
|
+
- `'step'` — a `stepIn` / `stepOver` / `stepOut` directive's natural endpoint was reached.
|
|
574
|
+
- `'manual'` — `session.pause()` was called from outside.
|
|
575
|
+
|
|
576
|
+
**When an iter satisfies more than one trigger**, exactly **one** `pause` event fires — never two at the same iter — and `cause` is chosen by precedence:
|
|
577
|
+
|
|
578
|
+
```
|
|
579
|
+
breakpoint > step > manual
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
So a step-mode endpoint (or a pending manual pause) that lands exactly on a breakpoint reports `cause: 'breakpoint'`; a manual pause that coincides with a step endpoint reports `cause: 'step'`. Rationale: a breakpoint is the user's explicit, persistent intent ("always stop here"), so it's the most informative attribution; a step directive is a specific computed endpoint, more specific than the vaguer "pause soon" of a manual request. The step-mode is still consumed (one-shot rule below) regardless of which cause is reported — if the iter was the step's endpoint, the step is satisfied. Matches IDE convention (a breakpoint on the line you step onto reports as a breakpoint stop).
|
|
583
|
+
|
|
584
|
+
### One-shot step-mode rule
|
|
585
|
+
|
|
586
|
+
Every active step-mode (`stepIn` / `stepOver` / `stepOut`) is dropped on the next `pause` dispatch — whether that dispatch is the step's own endpoint, an inner breakpoint that fired sooner, or a manual pause. To keep stepping, call `stepIn` / `stepOver` / `stepOut` again from the new pause. Matches IDE convention.
|
|
538
587
|
|
|
539
588
|
### Setting breakpoints by graph id
|
|
540
589
|
|
|
@@ -568,25 +617,62 @@ if (e && sym) {
|
|
|
568
617
|
|
|
569
618
|
### Throttle pattern
|
|
570
619
|
|
|
571
|
-
For per-iter throttle / animation / "wait between steps" UIs, use
|
|
620
|
+
For per-iter throttle / animation / "wait between steps" UIs, use `session.setRunInterval(ms)`:
|
|
572
621
|
|
|
573
622
|
```ts
|
|
623
|
+
const session = new DebugSession(machine, { initialState });
|
|
624
|
+
session.setRunInterval(50); // 50ms between iters
|
|
625
|
+
session.on('iter', (m) => { /* update UI with iter snapshot */ });
|
|
626
|
+
await session.start();
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
The throttle inserts an awaited `setTimeout(ms)` at the end of every iter, after both `pause` dispatches (if any) and the `iter` event. Updates take effect on the next iter. `0` disables.
|
|
630
|
+
|
|
631
|
+
### v7 migration from `run({onPause, onStep, onIter, debug})`
|
|
632
|
+
|
|
633
|
+
v7 splits the v6 mixed-mode `run()` into three non-overlapping entry points. Old shape → new shape:
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
// v6 — async run() with optional callbacks
|
|
574
637
|
await machine.run({
|
|
575
638
|
initialState,
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
639
|
+
onStep: (m) => { ... },
|
|
640
|
+
onPause: async (m) => { ... },
|
|
641
|
+
onIter: async (m) => { ... },
|
|
642
|
+
debug: true,
|
|
580
643
|
});
|
|
644
|
+
|
|
645
|
+
// v7 — DebugSession for interactive use
|
|
646
|
+
const session = new DebugSession(machine, { initialState });
|
|
647
|
+
session.on('step', (m) => { ... });
|
|
648
|
+
session.on('pause', (m) => { ...; session.continue(); });
|
|
649
|
+
session.on('iter', (m) => { ... });
|
|
650
|
+
await session.start();
|
|
581
651
|
```
|
|
582
652
|
|
|
583
|
-
|
|
653
|
+
Or, if you only used `run({ initialState })` with no callbacks, just drop the `await` — `run()` is now synchronous:
|
|
584
654
|
|
|
585
|
-
|
|
655
|
+
```ts
|
|
656
|
+
// v6
|
|
657
|
+
await machine.run({ initialState });
|
|
658
|
+
|
|
659
|
+
// v7
|
|
660
|
+
machine.run({ initialState });
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
For sync per-iter tracing without breakpoint-driven flow, iterate the `runStepByStep` generator directly — `onStep` no longer exists:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
// v6
|
|
667
|
+
await machine.run({ initialState, onStep: (m) => trace(m) });
|
|
668
|
+
|
|
669
|
+
// v7
|
|
670
|
+
for (const m of machine.runStepByStep({ initialState })) {
|
|
671
|
+
trace(m);
|
|
672
|
+
}
|
|
673
|
+
```
|
|
586
674
|
|
|
587
|
-
|
|
588
|
-
- **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.
|
|
589
|
-
- **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`).
|
|
675
|
+
The `debug: false` master switch is gone — in v7 the session is the only consumer of breakpoints; if you don't register a `pause` listener, breakpoints fire-and-resume invisibly (the session's `start()` still resolves cleanly), or you can use `runStepByStep` directly, which ignores `state.debug` altogether (its yields carry no pause field).
|
|
590
676
|
|
|
591
677
|
(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+.)
|
|
592
678
|
|
|
@@ -745,7 +831,7 @@ API surface changes since v3, in past tense so the timing of each piece is expli
|
|
|
745
831
|
|
|
746
832
|
- **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.
|
|
747
833
|
- **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). *Superseded in v7 by #207: `haltState.debug` is now `boolean`, all object-shaped writes throw.*
|
|
748
|
-
- **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
|
|
834
|
+
- **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 held through v6; **v7 removes it** — breakpoint detection moved into `DebugSession`, and the pause descriptor is now `m.pause: {side, cause}` on the `pause` event only.)
|
|
749
835
|
- **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).
|
|
750
836
|
- **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.
|
|
751
837
|
- **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.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import State from './State';
|
|
2
|
+
import TuringMachine, { type MachineState, type PausedMachineState } from './TuringMachine';
|
|
3
|
+
/**
|
|
4
|
+
* Parameters mirror `TuringMachine.runStepByStep` / `run`. DebugSession passes
|
|
5
|
+
* them straight through to the engine generator.
|
|
6
|
+
*/
|
|
7
|
+
export type DebugSessionParameter = {
|
|
8
|
+
initialState: State;
|
|
9
|
+
stepsLimit?: number;
|
|
10
|
+
};
|
|
11
|
+
export type DebugSessionEvent = 'pause' | 'step' | 'iter' | 'halt';
|
|
12
|
+
/**
|
|
13
|
+
* Listener signatures and dispatch contract by event:
|
|
14
|
+
*
|
|
15
|
+
* - `step` — fire-and-forget. Sync hot-loop tracing; listener Promise (if any)
|
|
16
|
+
* is not awaited. Matches the v6 `onStep` contract.
|
|
17
|
+
* - `iter` — **AWAITED** (sequenced, blocks the engine). For per-iter
|
|
18
|
+
* throttle / coordination / step-boundary synthesis where the engine
|
|
19
|
+
* genuinely needs to wait for the listener's work before advancing.
|
|
20
|
+
* Matches the v6 `onIter` contract.
|
|
21
|
+
* - `pause` — implicitly awaited via the session's internal `#pauseResolver`:
|
|
22
|
+
* the engine pauses on the pause-promise, listeners fire (their Promises
|
|
23
|
+
* are NOT awaited individually), and resume is signaled by an explicit
|
|
24
|
+
* call to `session.continue()` / `stepIn` / `stepOver` / `stepOut` /
|
|
25
|
+
* `stop` — fundamentally external to the listener's call site (UI click,
|
|
26
|
+
* postMessage, timer, etc.).
|
|
27
|
+
* - `halt` — fire-and-forget. Terminal notification.
|
|
28
|
+
*
|
|
29
|
+
* `pause` listeners receive a `PausedMachineState` (a `MachineState` plus the
|
|
30
|
+
* one-sided `pause: {side, cause}` descriptor). `step` / `iter` listeners
|
|
31
|
+
* receive a plain `MachineState` — raw yields carry no pause info.
|
|
32
|
+
*/
|
|
33
|
+
export type DebugSessionListener<E extends DebugSessionEvent> = E extends 'halt' ? () => void | Promise<void> : E extends 'pause' ? (machineState: PausedMachineState) => void | Promise<void> : (machineState: MachineState) => void | Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Interactive debugger session for `TuringMachine`. Owns the coordination
|
|
36
|
+
* layer that every UI debugger / IDE extension / educational demo would
|
|
37
|
+
* otherwise reimplement: breakpoint dispatch, step-in / step-over / step-out,
|
|
38
|
+
* click-pause from outside, per-iter throttle, pause/resume promise plumbing.
|
|
39
|
+
*
|
|
40
|
+
* Construction is direct — the engine class stays minimal and doesn't expose a
|
|
41
|
+
* `debugRun()` factory; consumers import both classes and write
|
|
42
|
+
* `new DebugSession(machine, {initialState})`.
|
|
43
|
+
*
|
|
44
|
+
* Lifecycle:
|
|
45
|
+
* const session = new DebugSession(machine, {initialState});
|
|
46
|
+
* session.on('pause', (m) => { ...; session.continue(); });
|
|
47
|
+
* session.on('halt', () => { ... });
|
|
48
|
+
* await session.start(); // resolves on natural halt or stop()
|
|
49
|
+
*
|
|
50
|
+
* Each session is single-use: `start()` may only be called once. Construct a
|
|
51
|
+
* fresh session to re-run.
|
|
52
|
+
*/
|
|
53
|
+
export default class DebugSession {
|
|
54
|
+
#private;
|
|
55
|
+
constructor(machine: TuringMachine, parameter: DebugSessionParameter);
|
|
56
|
+
on<E extends DebugSessionEvent>(event: E, listener: DebugSessionListener<E>): this;
|
|
57
|
+
off<E extends DebugSessionEvent>(event: E, listener: DebugSessionListener<E>): this;
|
|
58
|
+
stop(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Request a pause from outside the run loop. The pause fires on the next
|
|
61
|
+
* iter's before-side with `cause: 'manual'`. If a breakpoint matches that
|
|
62
|
+
* same iter, the breakpoint takes precedence and the request is consumed
|
|
63
|
+
* silently (one pause, cause: 'breakpoint').
|
|
64
|
+
*
|
|
65
|
+
* No-op if the session is already paused — the next `continue` / step call
|
|
66
|
+
* resumes normal execution, then the flag fires on the iter AFTER that.
|
|
67
|
+
* Equivalent to a debouncing one-shot.
|
|
68
|
+
*/
|
|
69
|
+
pause(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Set the per-iter throttle delay in milliseconds. After each iter (including
|
|
72
|
+
* any pause + step + iter listeners on that iter), the loop awaits
|
|
73
|
+
* `setTimeout(ms)` before proceeding to the next iter. `0` disables the
|
|
74
|
+
* throttle.
|
|
75
|
+
*
|
|
76
|
+
* Useful for visualization UIs that want to animate execution at a fixed
|
|
77
|
+
* pace. Updates take effect on the next iter.
|
|
78
|
+
*/
|
|
79
|
+
setRunInterval(ms: number): void;
|
|
80
|
+
/**
|
|
81
|
+
* Resume from the current pause, returning to normal execution until the
|
|
82
|
+
* next breakpoint or natural halt. No-op when called outside of a paused
|
|
83
|
+
* state.
|
|
84
|
+
*/
|
|
85
|
+
continue(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Resume and force a pause on the next iter regardless of whether that
|
|
88
|
+
* iter's `state.debug` filter matches. Step-mode is one-shot: any
|
|
89
|
+
* subsequent pause dispatch (this step-in's endpoint, an inner
|
|
90
|
+
* breakpoint, or a manual pause) drops it. To keep stepping, call
|
|
91
|
+
* stepIn() again from the new pause.
|
|
92
|
+
*/
|
|
93
|
+
stepIn(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Resume and pause at the next iter back at (or above) the click-time depth
|
|
96
|
+
* — i.e. `depth <= clickTimeDepth`. Frames the stepped-over iter pushes are
|
|
97
|
+
* run to completion without pausing inside (the engine's continuation-passing
|
|
98
|
+
* `withOverriddenHaltState` "calls"). Mirrors DevTools Step Over.
|
|
99
|
+
*
|
|
100
|
+
* For a plain iter (no frame push) this coincides with stepIn (next iter is
|
|
101
|
+
* already at the same depth). The Over-vs-In / Over-vs-Out distinction only
|
|
102
|
+
* appears under genuine nesting (a bare that itself enters a wrapper).
|
|
103
|
+
*
|
|
104
|
+
* One-shot: an inner breakpoint or any other pause drops the step-over
|
|
105
|
+
* intent. The endpoint pause carries `cause: 'step'`.
|
|
106
|
+
*/
|
|
107
|
+
stepOver(): void;
|
|
108
|
+
/**
|
|
109
|
+
* Resume and pause at the next iter STRICTLY shallower than the click-time
|
|
110
|
+
* depth — `depth < clickTimeDepth` — i.e. once the current frame itself has
|
|
111
|
+
* been popped. Mirrors DevTools Step Out.
|
|
112
|
+
*
|
|
113
|
+
* Throws when the click-time depth is 0: there's no enclosing frame to exit
|
|
114
|
+
* (IDE convention — "step out of nothing" is a programming error, not a
|
|
115
|
+
* silent no-op).
|
|
116
|
+
*/
|
|
117
|
+
stepOut(): void;
|
|
118
|
+
start(): Promise<void>;
|
|
119
|
+
}
|
package/dist/classes/State.d.ts
CHANGED
|
@@ -74,9 +74,8 @@ export default class State {
|
|
|
74
74
|
*/
|
|
75
75
|
get tags(): readonly string[];
|
|
76
76
|
/** @internal — invoked by DebugConfig setters via module-private symbol.
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* only sees non-halt states here. */
|
|
77
|
+
* haltState's `debug` setter rejects object writes before reaching
|
|
78
|
+
* DebugConfig, so this validator only sees non-halt states. */
|
|
80
79
|
[validateDebugFilter](fieldName: 'before' | 'after', filter: readonly symbol[] | true | undefined): void;
|
|
81
80
|
getSymbol(tapeBlock: TapeBlock): symbol;
|
|
82
81
|
getCommand(symbol: symbol): Command;
|