@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 +42 -0
- package/README.md +151 -61
- package/dist/classes/DebugSession.d.ts +119 -0
- package/dist/classes/State.d.ts +50 -4
- package/dist/classes/TuringMachine.d.ts +86 -71
- package/dist/index.cjs +721 -307
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +720 -308
- package/package.json +2 -2
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
|
|
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
|
|
311
|
-
|
|
312
|
+
// Run to halt — `run()` is synchronous, returns void:
|
|
313
|
+
machine.run({ initialState, stepsLimit: 1e5 });
|
|
312
314
|
|
|
313
|
-
// Or step-by-step (useful for
|
|
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()
|
|
338
|
+
#### Choosing between `run()`, `runStepByStep()`, and `DebugSession`
|
|
335
339
|
|
|
336
|
-
|
|
340
|
+
Three non-overlapping entry points, picked by the consumer's actual need:
|
|
337
341
|
|
|
338
|
-
| | `run()` | `runStepByStep()` |
|
|
339
|
-
|
|
340
|
-
| Shape |
|
|
341
|
-
|
|
|
342
|
-
|
|
|
343
|
-
|
|
|
344
|
-
|
|
|
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.**
|
|
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
|
-
**
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
483
|
+
## Debugging
|
|
482
484
|
|
|
483
|
-
|
|
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.
|
|
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
|
-
> ⚠️ **
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
545
|
+
### DebugSession API
|
|
534
546
|
|
|
535
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
667
|
+
For sync per-iter tracing without breakpoint-driven flow, iterate the `runStepByStep` generator directly — `onStep` no longer exists:
|
|
584
668
|
|
|
585
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
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;
|
|
@@ -102,7 +101,7 @@ export default class State {
|
|
|
102
101
|
matchedSymbol: symbol;
|
|
103
102
|
ix: number;
|
|
104
103
|
};
|
|
105
|
-
withOverriddenHaltState(overriddenHaltState: 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 {};
|