@turing-machine-js/machine 7.0.0-alpha.5 → 7.0.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,48 @@ 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.7] - 2026-05-30
8
+
9
+ Seventh v7 pre-release. Lands the `CallFrame` extraction ([#213](https://github.com/mellonis/turing-machine-js/issues/213), [PR #218](https://github.com/mellonis/turing-machine-js/pull/218)) and a `toMermaid` framed-wrapper emit fix ([#223](https://github.com/mellonis/turing-machine-js/issues/223), [PR #224](https://github.com/mellonis/turing-machine-js/pull/224)). 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.7`.
12
+
13
+ ### Added
14
+
15
+ - **`CallFrame extends State`** ([#213](https://github.com/mellonis/turing-machine-js/issues/213)) — `withOverriddenHaltState`'s wrapper is now a first-class `State` subclass instead of a `State` instance aliasing the bare's private `#symbolToDataMap` / `#debugRef`. Transition lookups and `debug` access delegate to the bare. `instanceof State` is preserved (no breaking surface change), and `instanceof CallFrame` becomes the wrapper discriminator. `CallFrame` is exported additively from the package root.
16
+
17
+ ### Fixed
18
+
19
+ - **`toMermaid` framed-wrapper emit asymmetry** ([#223](https://github.com/mellonis/turing-machine-js/issues/223)) — `toGraph`'s reach-set previously tunneled through wrappers to their continuation but left the wrappers themselves outside the caller's frame, so framed-wrapper-continuations (e.g. `library-binary-numbers/minusOne`'s `goToNumberStart(invertNumberGoToNumberWithInversion)` inside `invertNumber`'s callable subtree) emitted at the top level, visually disconnected from their owner frame. Fix in `utilities/stateGraph.ts`'s `resolveAndPush` pushes the wrapper itself AND tunnels through `overriddenHaltStateId`, so both join the caller's frame; `utilities/graphFormats.ts` renders framed wrappers inside the owner subgraph with the same `[[…]]` shape. Unframed top-level wrappers still emit outside any subgraph. `library-binary-numbers/states.md` regenerated — only the `minusOne` diagram shape changed.
20
+
21
+ ## [7.0.0-alpha.6] - 2026-05-28
22
+
23
+ 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`.
24
+
25
+ **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`.
26
+
27
+ ### Added
28
+
29
+ - **`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).
30
+
31
+ - **`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.
32
+
33
+ - **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).
34
+
35
+ - **`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.
36
+
37
+ ### Changed
38
+
39
+ - **`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.
40
+
41
+ - **`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.
42
+
43
+ - **`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.
44
+
45
+ ### Removed
46
+
47
+ - **`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`.
48
+
7
49
  ## [7.0.0-alpha.5] - 2026-05-25
8
50
 
9
51
  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
@@ -5,6 +5,8 @@
5
5
 
6
6
  A composable Turing-machine engine for JavaScript: multi-tape, subroutine composition via `withOverriddenHaltState`, Mermaid round-trip, and runtime breakpoints.
7
7
 
8
+ For runtime highlight + breakpoint rendering on top of the engine's `Graph`, plus a byte-identical edge-label formatter and snippet-recording artifacts, see the companion package [`@turing-machine-js/visuals`](../visuals).
9
+
8
10
  <details>
9
11
  <summary>Table of contents</summary>
10
12
 
@@ -239,7 +241,7 @@ const s = new State({
239
241
  Notable members and statics:
240
242
 
241
243
  - **`state.id`**, **`state.name`** — identity (`isHalt` is `id === 0`).
242
- - **`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).
244
+ - **`state.withOverriddenHaltState(other)`** — returns a `CallFrame` (a `State` subclass, so it flows anywhere a `State` does) whose would-be halt transitions fall through to `other`. The subroutine-call composition mechanism (see `library-binary-numbers/src/index.ts` for examples). `instanceof CallFrame` is the wrapper discriminator.
243
245
  - **`State.toGraph(state, tapeBlock)`** — walks the reachable graph from `state` and returns a serializable `Graph` (states, transitions, alphabets).
244
246
  - **`State.fromGraph(graph)`** — inverse of `toGraph`: rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips together with `toMermaid` / `fromMermaid`.
245
247
  - **`State.collectStates(state, tapeBlock)`** — walks the same graph and returns a `Map<number, {state, transitionSymbols}>` keyed by `GraphNode.id`. Use when downstream tooling holds a numeric id (e.g. a clicked node in a rendered graph) and needs the live `State` instance or the per-pattern `Symbol` for breakpoint setup. See [Setting breakpoints by graph id](#setting-breakpoints-by-graph-id).
@@ -307,13 +309,16 @@ The runtime. Owns one `TapeBlock` and drives a state graph until it reaches `hal
307
309
  ```javascript
308
310
  const machine = new TuringMachine({ tapeBlock });
309
311
 
310
- // Run to halt — `run()` returns a Promise<void>:
311
- await machine.run({ initialState, stepsLimit: 1e5 });
312
+ // Run to halt — `run()` is synchronous, returns void:
313
+ machine.run({ initialState, stepsLimit: 1e5 });
312
314
 
313
- // Or step-by-step (useful for visualization / debugging):
315
+ // Or step-by-step (useful for tracing / observation):
314
316
  for (const step of machine.runStepByStep({ initialState })) {
315
317
  console.log(step.state.name, step.currentSymbols, '→', step.nextSymbols, step.movements);
316
318
  }
319
+
320
+ // For interactive debugging (breakpoints, step-in / step-over / step-out,
321
+ // throttle, click-pause), construct a DebugSession — see "Debugging" below.
317
322
  ```
318
323
 
319
324
  Each yielded `step` (`MachineState`) has these fields:
@@ -326,26 +331,26 @@ Each yielded `step` (`MachineState`) has these fields:
326
331
  | `nextSymbols` | `string[]` | per-tape symbols that will be written |
327
332
  | `movements` | `symbol[]` | per-tape head moves (`movements.left/right/stay`) |
328
333
  | `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
334
  | `matchedTransition` | `{ id: string, matchKinds: ('wildcard'\|'literal')[] }` | the transition the engine picked for this iter — see *Matched transition* below |
331
335
 
332
336
  `stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
333
337
 
334
- #### Choosing between `run()` and `runStepByStep()`
338
+ #### Choosing between `run()`, `runStepByStep()`, and `DebugSession`
335
339
 
336
- 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:
340
+ Three non-overlapping entry points, picked by the consumer's actual need:
337
341
 
338
- | | `run()` | `runStepByStep()` |
339
- |---|---|---|
340
- | Shape | async, returns `Promise<void>` | synchronous generator |
341
- | Iteration timing | owned by the engine | owned by the consumer (`.next()` per step) |
342
- | Lifecycle hooks | dispatches `onStep`, `onPause` (gated by the `debug` master switch) | none yields raw `MachineState` |
343
- | 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) |
344
- | 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 |
342
+ | | `run()` | `runStepByStep()` | `new DebugSession(machine, ...)` |
343
+ |---|---|---|---|
344
+ | Shape | sync, returns `void` | sync generator | async session with events |
345
+ | Observation | none | per-iter via `.next()` | event-based (`pause`, `step`, `iter`, `halt`) |
346
+ | Step controls | | | `continue / stepIn / stepOver / stepOut / pause / stop` |
347
+ | Throttle | | | `setRunInterval(ms)` |
348
+ | 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 |
349
+ | Best for | run-to-halt with no observation overhead | tracing, snapshots, test harnesses, custom batching | UI debuggers, IDE extensions, educational demos |
345
350
 
346
- **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.
351
+ **Rule of thumb.** No observation `run()`. Sync per-iter observation iterate `runStepByStep()`. Anything interactive (breakpoints, step controls, throttle, click-pause) construct a `DebugSession`.
347
352
 
348
- **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).
353
+ **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
354
 
350
355
  ### Matched transition
351
356
 
@@ -365,20 +370,17 @@ matchedTransition: {
365
370
  Example use:
366
371
 
367
372
  ```javascript
368
- await machine.run({
369
- initialState,
370
- onStep: (m) => {
371
- const wildcardPositions = m.matchedTransition.matchKinds // per-tape, e.g. ['wildcard', 'literal']
372
- .map((k, i) => k === 'wildcard' ? i : -1)
373
- .filter((i) => i >= 0);
374
- console.log(`step ${m.step}: fired transition ${m.matchedTransition.id} (wildcards at tapes: ${wildcardPositions.join(',') || 'none'})`);
375
- },
376
- });
373
+ for (const m of machine.runStepByStep({initialState})) {
374
+ const wildcardPositions = m.matchedTransition.matchKinds // per-tape, e.g. ['wildcard', 'literal']
375
+ .map((k, i) => k === 'wildcard' ? i : -1)
376
+ .filter((i) => i >= 0);
377
+ console.log(`step ${m.step}: fired transition ${m.matchedTransition.id} (wildcards at tapes: ${wildcardPositions.join(',') || 'none'})`);
378
+ }
377
379
  ```
378
380
 
379
381
  ## Subroutine composition with `withOverriddenHaltState`
380
382
 
381
- `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.
383
+ `state.withOverriddenHaltState(other)` returns a `CallFrame` a `State` subclass that delegates transition lookups and `debug` to `state` (its *bare*) and whose would-be halt transitions fall through to `other` at run time. The bare is left untouched. Because a `CallFrame` is a `State`, it flows anywhere a `State` does (as a `nextState`, through `toGraph`/`fromGraph`); `instanceof CallFrame` distinguishes it from a plain state. This is the engine's only composition primitive — bigger machines are built by stacking smaller halt-on-completion subroutines.
382
384
 
383
385
  ```javascript
384
386
  import { Alphabet, State, TapeBlock, TuringMachine, Tape, haltState, ifOtherSymbol, movements, symbolCommands } from '@turing-machine-js/machine';
@@ -447,7 +449,7 @@ flowchart TD
447
449
 
448
450
  **Reading guide** — the v7 callable-subtree emit (introduced in [#174](https://github.com/mellonis/turing-machine-js/issues/174)) models `withOverriddenHaltState` as a function call: the wrapper is the call site, the bare's subtree is the callable body.
449
451
 
450
- 1. **`[[scanToX(eraseHere)]]` (Mermaid subroutine / double-walled-rectangle shape)** is the wrapper node, drawn OUTSIDE any subgraph. It's the runtime entry point — `idle -. enter .->` arrives here — and shows the composite name (`bare(override)`). Wrappers have no transitions of their own; they delegate to the bare via the `call` arrow.
452
+ 1. **`[[scanToX(eraseHere)]]` (Mermaid subroutine / double-walled-rectangle shape)** is the wrapper node. It's the runtime entry point — `idle -. enter .->` arrives here — and shows the composite name (`bare(override)`). Wrappers have no transitions of their own; they delegate to the bare via the `call` arrow. **Placement**: this top-level wrapper is drawn OUTSIDE any subgraph; a wrapper that participates in a caller's frame (e.g. an inner-call continuation of another wrapper, as in `library-binary-numbers/minusOne`) renders INSIDE its owner frame's subgraph with the same `[[…]]` shape (see [#223](https://github.com/mellonis/turing-machine-js/issues/223)).
451
453
  2. **`subgraph w_1["callable subtree of scanToX"]`** is the bare's callable subtree — the scope of code that runs when the wrapper is "called." It contains the bare `s1["scanToX"]`, any body states reachable from the bare, and a local halt marker `c1(((halt)))` where the bare's halt-bound transitions land.
452
454
  3. **The bold `==> call`** from wrapper to bare is the call arrow — visual signature of "wrapper invokes this callable subtree, pushing its override onto the runtime stack." Bold arrows are reserved for wrapper-to-bare calls; counting them in a diagram counts the wrappers in play.
453
455
  4. **The dotted `-. return .->`** from the subtree back to the wrapper is the return arrow — fires when the bare halts (lands on `c1`) and the stack pops. The wrapper's solid `--> s2` (to `eraseHere`) is the post-return continuation; ordinary transition under the function-call mental model.
@@ -472,15 +474,35 @@ s.untag('hot');
472
474
  s.tags; // readonly ['subroutine-entry']
473
475
  ```
474
476
 
475
- **Scoped to the wrapper instance.** Under [`withOverriddenHaltState` memoization (#175)](https://github.com/mellonis/turing-machine-js/issues/175), `A.wohs(t1)` and `A.wohs(t2)` are distinct wrapper instances even though they share `A`'s `#symbolToDataMap`. Tags live on the instance, so tagging one wrapper doesn't propagate to siblings sharing the same bare. Wrappers from `withOverriddenHaltState` start with an empty tag set (do not inherit from bare); the caller tags explicitly as needed.
477
+ **Scoped to the wrapper instance.** Under [`withOverriddenHaltState` memoization (#175)](https://github.com/mellonis/turing-machine-js/issues/175), `A.wohs(t1)` and `A.wohs(t2)` are distinct `CallFrame` instances even though both delegate to the same bare `A`. Tags live on the frame instance, so tagging one wrapper doesn't propagate to siblings sharing the same bare. Wrappers from `withOverriddenHaltState` start with an empty tag set (do not inherit from bare); the caller tags explicitly as needed.
476
478
 
477
479
  **Round-trip preserved.** `state.toGraph` writes the tag set to `GraphNode.tags`; `state.fromGraph` reads it back and reapplies. `toMermaid` renders tags two ways: inline in the node label (`sN["name<br>tag1, tag2"]`, universal Mermaid line break) and as `classDef tag_<sanitized>` + `class sN tag_<sanitized>` lines for color grouping. `fromMermaid` splits the label on `<br>` as source of truth; the `class` lines are decorative and discarded on parse.
478
480
 
479
481
  See [§Diagram conventions § Tags](#tags) for the full emit shape.
480
482
 
481
- ## Debugging breakpoints
483
+ ## Debugging
482
484
 
483
- Any `State` can carry a runtime-mutable `debug` config that pauses execution at chosen points.
485
+ 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.
486
+
487
+ ```ts
488
+ import { DebugSession } from '@turing-machine-js/machine';
489
+
490
+ const session = new DebugSession(machine, { initialState });
491
+
492
+ session.on('pause', (m) => {
493
+ console.log(`paused at ${m.state.name}, ${m.pause.side} side, cause: ${m.pause.cause}`);
494
+ session.stepIn(); // or stepOver(), stepOut(), continue(), stop()
495
+ });
496
+ session.on('step', (m) => { /* fires once per iter, mid-iter */ });
497
+ session.on('iter', (m) => { /* fires once per iter, end-of-iter */ });
498
+ session.on('halt', () => { /* fires on natural halt (not on stop()) */ });
499
+
500
+ await session.start(); // resolves on halt or stop()
501
+ ```
502
+
503
+ ### Configuring breakpoints
504
+
505
+ 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
506
 
485
507
  ```ts
486
508
  import { State, haltState, ifOtherSymbol } from '@turing-machine-js/machine';
@@ -508,33 +530,64 @@ haltState.debug = null; // alias of false (reset)
508
530
  myState.debug = null;
509
531
  ```
510
532
 
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.debugBreak.after === true`. Diagram + log narratives read naturally: the halt-bound transition has already fired when the pause lands, and halt is the next thing.
533
+ > ⚠️ **`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
534
 
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`), so it can't intercept the chained form. **Always use the whole-object form: `haltState.debug = true` / `= false` / `= null`.** Modules built with `tsc` / ESM run in strict mode by default and surface this throw; `new Function(...)` and `<script>` without `"use strict"` run non-strict and silently no-op the chained write.
535
+ > ⚠️ **An `after`-side breakpoint on the halt-triggering state collapses with `haltState.debug` into a single pause.** Both target the AFTER side of the *same* iter the one whose transition leads to halt and the engine fires at most one pause per iter-side, so you get one `pause` event (`side: 'after'`, `cause: 'breakpoint'`), not two. This is intentional: halt has no iteration of its own, so "after the triggering state" and "before halt" are the **same execution moment**. (Contrast two ordinary states `A B`: `A`'s `after` and `B`'s `before` are *different* iters, so they fire as two pauses.) There is deliberately no flag to emit a second, ephemeral halt pause one event for one real moment.
514
536
 
515
- The `debug` field is mutabletoggle 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`.
537
+ > ⚠️ **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`.**
516
538
 
517
- `run()` is async and accepts an `onPause` hook:
539
+ The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. A `CallFrame` (from `state.withOverriddenHaltState(...)`) delegates its `debug` to the bare, so an assignment on the original is visible from every wrapper and vice versa. `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.
518
540
 
519
- ```ts
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
- ```
541
+ **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).
530
542
 
531
- Both `before` and `after` for the same iteration fire on the iteration's own yield, in the order **before → step → after**. `m.state` is always the iteration's own state; the `m.debugBreak` flag (`{before: true}` or `{after: true}`) tells the consumer which timing fired.
543
+ **Caveat:** `haltState` is a module-level singleton. Setting `haltState.debug` affects every machine in the process; clear in `afterEach` / `finally` for test isolation.
532
544
 
533
- If `onPause` is not provided, breaks fire-and-resume invisibly — the trajectory is identical to running without `debug` set.
545
+ ### DebugSession API
534
546
 
535
- **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).
547
+ | Method | Description |
548
+ |---|---|
549
+ | `start(): Promise<void>` | Begin execution. Resolves on natural halt or after `stop()`. Single-use — a second call throws. |
550
+ | `continue()` | Resume from a pause and run to the next breakpoint or halt. |
551
+ | `stepIn()` | Resume and pause at the very next iter, regardless of depth — descends into any subroutine the current iter enters. Mirrors DevTools **Step Into**. |
552
+ | `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**. |
553
+ | `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**. |
554
+ | `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'`). |
555
+ | `stop()` | Terminate immediately. `halt` event does NOT fire. |
556
+ | `setRunInterval(ms)` | Insert an awaited `setTimeout(ms)` at the end of each iter. `0` disables. Useful for visualization UIs. |
557
+ | `on(event, listener) / off(event, listener)` | Register / unregister listeners. Multiple listeners per event are supported. Listener dispatch differs by event — see *Events* below. |
536
558
 
537
- **Caveat:** `haltState` is a module-level singleton. Setting `haltState.debug` affects every machine in the process; clear in `afterEach` / `finally` for test isolation.
559
+ 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.
560
+
561
+ ### Events
562
+
563
+ | Event | Argument | Dispatch | Fires |
564
+ |---|---|---|---|
565
+ | `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. |
566
+ | `step` | `MachineState` | Fire-and-forget (sync hot-loop tracing). | Once per iter, between any before-pause and after-pause. |
567
+ | `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. |
568
+ | `halt` | (none) | Fire-and-forget. | Once, on natural halt. Does NOT fire when `stop()` was called. |
569
+
570
+ ### The pause descriptor: `m.pause`
571
+
572
+ `pause` listeners receive a `PausedMachineState` — a plain `MachineState` plus a `pause: { side, cause }` descriptor (raw `runStepByStep` yields have no such field).
573
+
574
+ - **`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.)
575
+ - **`cause`** — distinguishes the origin:
576
+ - `'breakpoint'` — a `state.debug` filter matched, or `haltState.debug === true` triggered.
577
+ - `'step'` — a `stepIn` / `stepOver` / `stepOut` directive's natural endpoint was reached.
578
+ - `'manual'` — `session.pause()` was called from outside.
579
+
580
+ **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:
581
+
582
+ ```
583
+ breakpoint > step > manual
584
+ ```
585
+
586
+ 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).
587
+
588
+ ### One-shot step-mode rule
589
+
590
+ 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
591
 
539
592
  ### Setting breakpoints by graph id
540
593
 
@@ -568,25 +621,62 @@ if (e && sym) {
568
621
 
569
622
  ### Throttle pattern
570
623
 
571
- 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:
624
+ For per-iter throttle / animation / "wait between steps" UIs, use `session.setRunInterval(ms)`:
625
+
626
+ ```ts
627
+ const session = new DebugSession(machine, { initialState });
628
+ session.setRunInterval(50); // 50ms between iters
629
+ session.on('iter', (m) => { /* update UI with iter snapshot */ });
630
+ await session.start();
631
+ ```
632
+
633
+ 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.
634
+
635
+ ### v7 migration from `run({onPause, onStep, onIter, debug})`
636
+
637
+ v7 splits the v6 mixed-mode `run()` into three non-overlapping entry points. Old shape → new shape:
572
638
 
573
639
  ```ts
640
+ // v6 — async run() with optional callbacks
574
641
  await machine.run({
575
642
  initialState,
576
- onIter: async (m) => {
577
- // Fires after before(m.state) / step / after(m.state) on iter m.step.
578
- await new Promise((r) => setTimeout(r, intervalMs));
579
- },
643
+ onStep: (m) => { ... },
644
+ onPause: async (m) => { ... },
645
+ onIter: async (m) => { ... },
646
+ debug: true,
580
647
  });
648
+
649
+ // v7 — DebugSession for interactive use
650
+ const session = new DebugSession(machine, { initialState });
651
+ session.on('step', (m) => { ... });
652
+ session.on('pause', (m) => { ...; session.continue(); });
653
+ session.on('iter', (m) => { ... });
654
+ await session.start();
655
+ ```
656
+
657
+ Or, if you only used `run({ initialState })` with no callbacks, just drop the `await` — `run()` is now synchronous:
658
+
659
+ ```ts
660
+ // v6
661
+ await machine.run({ initialState });
662
+
663
+ // v7
664
+ machine.run({ initialState });
581
665
  ```
582
666
 
583
- `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.
667
+ For sync per-iter tracing without breakpoint-driven flow, iterate the `runStepByStep` generator directly `onStep` no longer exists:
584
668
 
585
- A few details:
669
+ ```ts
670
+ // v6
671
+ await machine.run({ initialState, onStep: (m) => trace(m) });
672
+
673
+ // v7
674
+ for (const m of machine.runStepByStep({ initialState })) {
675
+ trace(m);
676
+ }
677
+ ```
586
678
 
587
- - **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.
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`).
679
+ 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
680
 
591
681
  (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
682
 
@@ -666,7 +756,7 @@ The full reference for reading `toMermaid` output — shapes, edge styles, and t
666
756
  |---|---|
667
757
  | `s0(((halt)))` | the halt state |
668
758
  | `sN["name"]` | a regular state (or a bare, when inside a subgraph) |
669
- | `sN[["composite-name"]]` | a `withOverriddenHaltState` wrapper (call site, outside any subgraph) — see [§Subroutine composition](#subroutine-composition-with-withoverriddenhaltstate) |
759
+ | `sN[["composite-name"]]` | a `withOverriddenHaltState` wrapper (call site; outside any subgraph when top-level, INSIDE its owner frame's subgraph when its continuation chain participates in a caller's frame — see [§Subroutine composition](#subroutine-composition-with-withoverriddenhaltstate) and [#223](https://github.com/mellonis/turing-machine-js/issues/223)) |
670
760
  | `cN(((halt)))` inside a subgraph | halt marker (visualization aid; maps back to the singleton `haltState` at runtime) |
671
761
  | `idle([idle])` | pre-execution sentinel (not a real state) |
672
762
 
@@ -745,7 +835,7 @@ API surface changes since v3, in past tense so the timing of each piece is expli
745
835
 
746
836
  - **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
837
  - **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 unchanged across all three versions.
838
+ - **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
839
  - **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
840
  - **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
841
  - **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
+ }
@@ -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
- * Per #207, haltState no longer flows through DebugConfig (its `debug`
78
- * setter rejects object writes before construction), so the validator
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;
@@ -102,7 +101,7 @@ export default class State {
102
101
  matchedSymbol: symbol;
103
102
  ix: number;
104
103
  };
105
- withOverriddenHaltState(overriddenHaltState: State): State;
104
+ withOverriddenHaltState(overriddenHaltState: State): CallFrame;
106
105
  /**
107
106
  * @internal
108
107
  *
@@ -210,4 +209,51 @@ export type HaltState = State & {
210
209
  set debug(value: boolean | null);
211
210
  };
212
211
  export declare const haltState: HaltState;
212
+ /**
213
+ * A first-class call frame produced by `State.withOverriddenHaltState`
214
+ * (#213). A `CallFrame` is a `State` — `instanceof State` holds, so it flows
215
+ * anywhere a `State` does (as a `nextState`, through `toGraph`/`fromGraph`,
216
+ * etc.) — but it carries its own `bare` (the wrapped State) and `override`
217
+ * (the continuation pushed onto the run-stack on entry). `instanceof
218
+ * CallFrame` is the explicit wrapper discriminator.
219
+ *
220
+ * It owns no transitions of its own: lookups (`getSymbol`/`getCommand`/
221
+ * `getNextState`/`getMatchedTransition`) and `debug` DELEGATE to the bare,
222
+ * replacing the v6 field-aliasing (where a wrapper was a plain `State` whose
223
+ * private `#symbolToDataMap`/`#debugRef` were physically shared with the
224
+ * bare). `id`, `name` (composite `bare(override)`), and `tags` are its own
225
+ * (inherited State fields) — so memoized frames sharing a bare keep
226
+ * independent tags (#186), and the frame is never the halt singleton
227
+ * (fresh nonzero `#id` → `isHalt === false`).
228
+ */
229
+ export declare class CallFrame extends State {
230
+ #private;
231
+ constructor(bare: State, override: State);
232
+ get bare(): State;
233
+ get overriddenHaltState(): State;
234
+ getSymbol(tapeBlock: TapeBlock): symbol;
235
+ getCommand(symbol: symbol): Command;
236
+ getNextState(symbol: symbol): State | Reference;
237
+ getMatchedTransition(symbol: symbol): {
238
+ nextState: State | Reference;
239
+ matchedSymbol: symbol;
240
+ ix: number;
241
+ };
242
+ get debug(): DebugConfig;
243
+ set debug(value: DebugConfig | {
244
+ before?: symbol[] | readonly symbol[] | true;
245
+ after?: symbol[] | readonly symbol[] | true;
246
+ } | null);
247
+ [STATE_INTERNAL](): {
248
+ readonly id: number;
249
+ name: string;
250
+ readonly bareState: State | null;
251
+ readonly overriddenHaltState: State | null;
252
+ readonly symbolToDataMap: Map<symbol, {
253
+ command: Command;
254
+ nextState: State | Reference;
255
+ }>;
256
+ readonly tags: ReadonlySet<string>;
257
+ };
258
+ }
213
259
  export {};