@turing-machine-js/machine 7.0.0-alpha.6 → 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,20 @@ 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
+
7
21
  ## [7.0.0-alpha.6] - 2026-05-28
8
22
 
9
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`.
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).
@@ -378,7 +380,7 @@ for (const m of machine.runStepByStep({initialState})) {
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,7 +474,7 @@ 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
 
@@ -530,9 +532,11 @@ myState.debug = null;
530
532
 
531
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'`.
532
534
 
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.
536
+
533
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`.**
534
538
 
535
- The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. The internal cell is shared with `state.withOverriddenHaltState(...)` wrappers, so an assignment on the original is visible from every wrapper. `state.debug` is always a `DebugConfig` instance (lazy-initialized on first read); plain-object input is wrapped automatically. The instance is `Object.seal`-ed — typos like `state.debug.bofore = true` throw `TypeError` instead of silently creating a useless property.
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.
536
540
 
537
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).
538
542
 
@@ -752,7 +756,7 @@ The full reference for reading `toMermaid` output — shapes, edge styles, and t
752
756
  |---|---|
753
757
  | `s0(((halt)))` | the halt state |
754
758
  | `sN["name"]` | a regular state (or a bare, when inside a subgraph) |
755
- | `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)) |
756
760
  | `cN(((halt)))` inside a subgraph | halt marker (visualization aid; maps back to the singleton `haltState` at runtime) |
757
761
  | `idle([idle])` | pre-execution sentinel (not a real state) |
758
762
 
@@ -101,7 +101,7 @@ export default class State {
101
101
  matchedSymbol: symbol;
102
102
  ix: number;
103
103
  };
104
- withOverriddenHaltState(overriddenHaltState: State): State;
104
+ withOverriddenHaltState(overriddenHaltState: State): CallFrame;
105
105
  /**
106
106
  * @internal
107
107
  *
@@ -209,4 +209,51 @@ export type HaltState = State & {
209
209
  set debug(value: boolean | null);
210
210
  };
211
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
+ }
212
259
  export {};
package/dist/index.cjs CHANGED
@@ -853,33 +853,70 @@ function toGraph(initialState, tapeBlock) {
853
853
  };
854
854
  }
855
855
  // Pass 2: For each bare, compute its forward-reachable set (following
856
- // transitions; stopping at halt and at wrappers both are frame
857
- // boundaries).
856
+ // transitions; stopping at halt; including wrappers AND tunneling through
857
+ // them to their `overriddenHaltStateId` continuation).
858
+ //
859
+ // Wrappers are call-site markers — semantically owned by the CALLER (the
860
+ // bare whose body invokes the sub-call). Both the wrapper itself and its
861
+ // continuation (its `--> override` arrow in the rendered diagram, sourced
862
+ // from `overriddenHaltStateId`) belong to the caller's frame: the wrapper
863
+ // visually anchors the call site inside the caller's subgraph; the
864
+ // continuation is the state the caller's body resumes at AFTER the inner
865
+ // sub-call returns. So when reach traversal hits a wrapper, we PUSH the
866
+ // wrapper (joining the caller's reach-set) AND tunnel through to its
867
+ // continuation (also joining). Wrappers carry no `transitions` of their
868
+ // own (Pass 1 emits `transitions: []` on wrapper nodes), so the main loop
869
+ // pops them and adds them to `reach` without further traversal — but the
870
+ // continuation already entered via the wrapper-tunnel chain in
871
+ // `resolveAndPush`.
872
+ //
873
+ // Halt-bound retargeting + union-find then "just work": continuation
874
+ // states' halt-bound transitions get retargeted to the caller's halt
875
+ // marker (so an in-subroutine halt returns to the caller, not the
876
+ // program's terminal halt); when two bares both reach the same
877
+ // continuation through different wrapper chains, union-find merges their
878
+ // frames as it already does for non-wrapper overlap.
879
+ //
880
+ // Wrapper chains (continuation IS another wrapper, e.g., nested
881
+ // compositions) are walked transitively by the inner while-loop in
882
+ // `resolveAndPush` — each tunnel hop pushes the intermediate wrapper.
858
883
  const computeReach = (startId) => {
859
884
  const reach = new Set();
860
- const stack = [startId];
885
+ const stack = [];
886
+ const resolveAndPush = (id) => {
887
+ let current = id;
888
+ while (true) {
889
+ const target = nodes[current];
890
+ if (!target || target.isHalt) {
891
+ return;
892
+ }
893
+ if (!target.isWrapper) {
894
+ stack.push(current);
895
+ return;
896
+ }
897
+ // Wrapper: push it (so it joins the caller's frame) AND tunnel to
898
+ // its continuation. Both belong to the caller's frame.
899
+ stack.push(current);
900
+ /* c8 ignore next 3 — every wrapper emitted by Pass 1 has a
901
+ non-null overriddenHaltStateId (lines 76-101); this branch
902
+ only guards against future wrapper variants that might not. */
903
+ if (target.overriddenHaltStateId === null) {
904
+ return;
905
+ }
906
+ current = target.overriddenHaltStateId;
907
+ }
908
+ };
909
+ resolveAndPush(startId);
861
910
  while (stack.length > 0) {
862
911
  const id = stack.pop();
863
912
  if (reach.has(id)) {
864
913
  continue;
865
914
  }
866
- const node = nodes[id];
867
- // `nodes[id]` is always populated for `id` that the BFS reached, so
868
- // a defensive `!node` check would be dead. `isHalt` / `isWrapper`
869
- // are real boundaries — both stop reach-set expansion.
870
- /* c8 ignore next 3 — defensive: the push site below already filters
871
- halt/wrapper targets, and the initial push is always a bare, so
872
- this branch is unreachable in practice. */
873
- if (node.isHalt || node.isWrapper) {
874
- continue;
875
- }
876
915
  reach.add(id);
877
- for (const t of node.transitions) {
878
- const target = nodes[t.nextStateId];
879
- if (!target || target.isHalt || target.isWrapper) {
880
- continue;
881
- }
882
- stack.push(t.nextStateId);
916
+ // Wrappers have empty transitions arrays — the for-loop runs zero
917
+ // iterations and we proceed to the next stack entry.
918
+ for (const t of nodes[id].transitions) {
919
+ resolveAndPush(t.nextStateId);
883
920
  }
884
921
  }
885
922
  return reach;
@@ -1240,7 +1277,7 @@ var __classPrivateFieldGet$2 = (undefined && undefined.__classPrivateFieldGet) |
1240
1277
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
1241
1278
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
1242
1279
  };
1243
- var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_instances, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef, _State_haltDebug, _State_tags, _State_getEntry;
1280
+ var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_instances, _a, _State_wrapperCache, _State_id, _State_name, _State_symbolToDataMap, _State_debugRef, _State_haltDebug, _State_tags, _State_getEntry, _CallFrame_bare, _CallFrame_override;
1244
1281
  const ifOtherSymbol = Symbol('other symbol');
1245
1282
  // Module-private symbol used by DebugConfig setters to call State's validator
1246
1283
  // without exposing the validator on the public surface.
@@ -1312,12 +1349,6 @@ class State {
1312
1349
  // composed name on a no-arg `new State()` to bypass the constructor's
1313
1350
  // user-facing name validation (composite names contain `(` and `)`).
1314
1351
  _State_name.set(this, void 0);
1315
- _State_overriddenHaltState.set(this, null);
1316
- // For wrapper states (produced by `withOverriddenHaltState`), points at the
1317
- // State whose transition map was wrapped. `null` on bare/atomic states.
1318
- // `toGraph` reads this to render wrapper and bare as separate nodes linked
1319
- // by a `==> call` arrow.
1320
- _State_bareState.set(this, null);
1321
1352
  _State_symbolToDataMap.set(this, new Map());
1322
1353
  // Shared mutable cell — withOverriddenHaltState wrappers reference the same
1323
1354
  // object so that `state.debug = ...` (and nullings) propagate across them.
@@ -1395,8 +1426,10 @@ class State {
1395
1426
  get isHalt() {
1396
1427
  return __classPrivateFieldGet$2(this, _State_id, "f") === 0;
1397
1428
  }
1429
+ // Plain States never override the halt state — only a `CallFrame` (produced
1430
+ // by `withOverriddenHaltState`) carries an override, via its own getter.
1398
1431
  get overriddenHaltState() {
1399
- return __classPrivateFieldGet$2(this, _State_overriddenHaltState, "f");
1432
+ return null;
1400
1433
  }
1401
1434
  get ref() {
1402
1435
  return this;
@@ -1498,7 +1531,7 @@ class State {
1498
1531
  /** @internal — invoked by DebugConfig setters via module-private symbol.
1499
1532
  * haltState's `debug` setter rejects object writes before reaching
1500
1533
  * DebugConfig, so this validator only sees non-halt states. */
1501
- [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), _State_haltDebug = new WeakMap(), _State_tags = new WeakMap(), _State_instances = new WeakSet(), validateDebugFilter)](fieldName, filter) {
1534
+ [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), _State_haltDebug = new WeakMap(), _State_tags = new WeakMap(), _State_instances = new WeakSet(), validateDebugFilter)](fieldName, filter) {
1502
1535
  if (filter === undefined)
1503
1536
  return;
1504
1537
  if (filter === true)
@@ -1557,13 +1590,13 @@ class State {
1557
1590
  return { nextState: entry.nextState, matchedSymbol: symbol, ix };
1558
1591
  }
1559
1592
  withOverriddenHaltState(overriddenHaltState) {
1560
- // Unwrap `this` if it's itself a wrapper — the chain's inner overrides
1593
+ // Unwrap `this` if it's itself a CallFrame — the chain's inner overrides
1561
1594
  // are dead at runtime anyway (only the outermost `.wohs()`'s override is
1562
1595
  // pushed onto the halt-stack on entry; verified empirically). Composite
1563
1596
  // name reflects runtime behavior, not construction history. See #176.
1564
- const bare = __classPrivateFieldGet$2(this, _State_bareState, "f") ?? this;
1597
+ const bare = this instanceof CallFrame ? this.bare : this;
1565
1598
  // Memoize by (bare, override) so identical args return the same instance
1566
- // (#175). The cache uses WeakMaps + WeakRefs so cached wrappers can be
1599
+ // (#175). The cache uses WeakMaps + WeakRefs so cached frames can be
1567
1600
  // GC'd when nothing else holds them. Compounds with the chain-collapse
1568
1601
  // above: `A.wohs(t1).wohs(t2)` keys as (A, t2) after the unwrap, hitting
1569
1602
  // the same cache slot as a direct `A.wohs(t2)`.
@@ -1581,17 +1614,9 @@ class State {
1581
1614
  innerCache = new WeakMap();
1582
1615
  __classPrivateFieldGet$2(_a, _a, "f", _State_wrapperCache).set(bare, innerCache);
1583
1616
  }
1584
- // Cache miss construct with no name, then overwrite #name directly
1585
- // (composed names contain `(` and `)` which the constructor's user-facing
1586
- // validation would reject; private-field access bypasses that).
1587
- const state = new _a();
1588
- __classPrivateFieldSet$2(state, _State_name, `${bare.name}(${overriddenHaltState.name})`, "f");
1589
- __classPrivateFieldSet$2(state, _State_symbolToDataMap, __classPrivateFieldGet$2(bare, _State_symbolToDataMap, "f"), "f");
1590
- __classPrivateFieldSet$2(state, _State_overriddenHaltState, overriddenHaltState, "f");
1591
- __classPrivateFieldSet$2(state, _State_debugRef, __classPrivateFieldGet$2(bare, _State_debugRef, "f"), "f");
1592
- __classPrivateFieldSet$2(state, _State_bareState, bare, "f");
1593
- innerCache.set(overriddenHaltState, new WeakRef(state));
1594
- return state;
1617
+ const frame = new CallFrame(bare, overriddenHaltState);
1618
+ innerCache.set(overriddenHaltState, new WeakRef(frame));
1619
+ return frame;
1595
1620
  }
1596
1621
  /**
1597
1622
  * @internal
@@ -1634,8 +1659,8 @@ class State {
1634
1659
  get id() { return __classPrivateFieldGet$2(self, _State_id, "f"); },
1635
1660
  get name() { return __classPrivateFieldGet$2(self, _State_name, "f"); },
1636
1661
  set name(v) { __classPrivateFieldSet$2(self, _State_name, v, "f"); },
1637
- get bareState() { return __classPrivateFieldGet$2(self, _State_bareState, "f"); },
1638
- get overriddenHaltState() { return __classPrivateFieldGet$2(self, _State_overriddenHaltState, "f"); },
1662
+ get bareState() { return null; },
1663
+ get overriddenHaltState() { return null; },
1639
1664
  get symbolToDataMap() { return __classPrivateFieldGet$2(self, _State_symbolToDataMap, "f"); },
1640
1665
  get tags() { return __classPrivateFieldGet$2(self, _State_tags, "f"); },
1641
1666
  };
@@ -1646,8 +1671,13 @@ class State {
1646
1671
  // Symbol patterns are returned as the raw description string from the
1647
1672
  // interned JS Symbol (decode via decodePatternDescription if needed).
1648
1673
  static inspect(state) {
1674
+ // Route through the STATE_INTERNAL view so a CallFrame reports its bare's
1675
+ // transitions and its own override — the view delegates those to the bare
1676
+ // / the frame's #override, whereas the raw private fields on a CallFrame
1677
+ // are empty/null.
1678
+ const internal = state[STATE_INTERNAL]();
1649
1679
  const transitions = [];
1650
- for (const [sym, { command, nextState }] of __classPrivateFieldGet$2(state, _State_symbolToDataMap, "f")) {
1680
+ for (const [sym, { command, nextState }] of internal.symbolToDataMap) {
1651
1681
  let target = null;
1652
1682
  try {
1653
1683
  target = nextState instanceof _a ? nextState : nextState.ref;
@@ -1664,12 +1694,13 @@ class State {
1664
1694
  nextState: target ? { id: target.id, name: target.name } : null,
1665
1695
  });
1666
1696
  }
1697
+ const override = internal.overriddenHaltState;
1667
1698
  return {
1668
- id: __classPrivateFieldGet$2(state, _State_id, "f"),
1669
- name: __classPrivateFieldGet$2(state, _State_name, "f"),
1699
+ id: internal.id,
1700
+ name: internal.name,
1670
1701
  isHalt: state.isHalt,
1671
- overriddenHaltState: __classPrivateFieldGet$2(state, _State_overriddenHaltState, "f")
1672
- ? { id: __classPrivateFieldGet$2(state, _State_overriddenHaltState, "f").id, name: __classPrivateFieldGet$2(state, _State_overriddenHaltState, "f").name }
1702
+ overriddenHaltState: override
1703
+ ? { id: override.id, name: override.name }
1673
1704
  : null,
1674
1705
  transitions,
1675
1706
  };
@@ -1714,6 +1745,79 @@ _a = State;
1714
1745
  // them, with cache misses simply reconstructing fresh wrappers.
1715
1746
  _State_wrapperCache = { value: new WeakMap() };
1716
1747
  const haltState = new State(null);
1748
+ /**
1749
+ * A first-class call frame produced by `State.withOverriddenHaltState`
1750
+ * (#213). A `CallFrame` is a `State` — `instanceof State` holds, so it flows
1751
+ * anywhere a `State` does (as a `nextState`, through `toGraph`/`fromGraph`,
1752
+ * etc.) — but it carries its own `bare` (the wrapped State) and `override`
1753
+ * (the continuation pushed onto the run-stack on entry). `instanceof
1754
+ * CallFrame` is the explicit wrapper discriminator.
1755
+ *
1756
+ * It owns no transitions of its own: lookups (`getSymbol`/`getCommand`/
1757
+ * `getNextState`/`getMatchedTransition`) and `debug` DELEGATE to the bare,
1758
+ * replacing the v6 field-aliasing (where a wrapper was a plain `State` whose
1759
+ * private `#symbolToDataMap`/`#debugRef` were physically shared with the
1760
+ * bare). `id`, `name` (composite `bare(override)`), and `tags` are its own
1761
+ * (inherited State fields) — so memoized frames sharing a bare keep
1762
+ * independent tags (#186), and the frame is never the halt singleton
1763
+ * (fresh nonzero `#id` → `isHalt === false`).
1764
+ */
1765
+ class CallFrame extends State {
1766
+ constructor(bare, override) {
1767
+ super(null);
1768
+ _CallFrame_bare.set(this, void 0);
1769
+ _CallFrame_override.set(this, void 0);
1770
+ __classPrivateFieldSet$2(this, _CallFrame_bare, bare, "f");
1771
+ __classPrivateFieldSet$2(this, _CallFrame_override, override, "f");
1772
+ // Composite name contains `(` / `)`, which the constructor's user-facing
1773
+ // name validator rejects; the STATE_INTERNAL name setter bypasses it
1774
+ // (writes the inherited #name). `super[...]` reaches State's own view so
1775
+ // we don't recurse through this subclass's override below.
1776
+ super[STATE_INTERNAL]().name = `${bare.name}(${override.name})`;
1777
+ }
1778
+ get bare() {
1779
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f");
1780
+ }
1781
+ get overriddenHaltState() {
1782
+ return __classPrivateFieldGet$2(this, _CallFrame_override, "f");
1783
+ }
1784
+ getSymbol(tapeBlock) {
1785
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getSymbol(tapeBlock);
1786
+ }
1787
+ getCommand(symbol) {
1788
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getCommand(symbol);
1789
+ }
1790
+ getNextState(symbol) {
1791
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getNextState(symbol);
1792
+ }
1793
+ getMatchedTransition(symbol) {
1794
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getMatchedTransition(symbol);
1795
+ }
1796
+ get debug() {
1797
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").debug;
1798
+ }
1799
+ set debug(value) {
1800
+ __classPrivateFieldGet$2(this, _CallFrame_bare, "f").debug = value;
1801
+ }
1802
+ [(_CallFrame_bare = new WeakMap(), _CallFrame_override = new WeakMap(), STATE_INTERNAL)]() {
1803
+ // Own id / name / tags come from the inherited State fields (via super's
1804
+ // view); bareState / overriddenHaltState / the transition map delegate to
1805
+ // #bare / #override so sibling modules (stateGraph, inspect) see the
1806
+ // frame's true shape.
1807
+ const own = super[STATE_INTERNAL]();
1808
+ const bare = __classPrivateFieldGet$2(this, _CallFrame_bare, "f");
1809
+ const override = __classPrivateFieldGet$2(this, _CallFrame_override, "f");
1810
+ return {
1811
+ get id() { return own.id; },
1812
+ get name() { return own.name; },
1813
+ set name(v) { own.name = v; },
1814
+ get bareState() { return bare; },
1815
+ get overriddenHaltState() { return override; },
1816
+ get symbolToDataMap() { return bare[STATE_INTERNAL]().symbolToDataMap; },
1817
+ get tags() { return own.tags; },
1818
+ };
1819
+ }
1820
+ }
1717
1821
 
1718
1822
  var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
1719
1823
  if (kind === "m") throw new TypeError("Private method is not writable");
@@ -1810,16 +1914,10 @@ class TuringMachine {
1810
1914
  const command = state.getCommand(symbol);
1811
1915
  const matched = state.getMatchedTransition(symbol);
1812
1916
  let nextState = matched.nextState.ref;
1813
- // For wrapper-entry iters, the wrapper's transitions in `toGraph`
1814
- // are empty (wrappers delegate to the bare via shared
1815
- // `#symbolToDataMap`); the resolvable transition id lives under
1816
- // the bare's stateId. `bareState` is non-null only when `state`
1817
- // is a wrapper produced by `withOverriddenHaltState`. Accessed
1818
- // via the STATE_INTERNAL package-private view (same pattern
1819
- // `utilities/stateGraph.ts` uses) to avoid widening the public
1820
- // State API for this internal need.
1821
- const stateInternal = state[STATE_INTERNAL]();
1822
- const resolvableStateId = stateInternal.bareState?.id ?? state.id;
1917
+ // For wrapper-entry iters, a CallFrame's own transitions in `toGraph`
1918
+ // are empty (it delegates lookups to its bare); the resolvable
1919
+ // transition id lives under the bare's stateId.
1920
+ const resolvableStateId = state instanceof CallFrame ? state.bare.id : state.id;
1823
1921
  const matchedTransition = {
1824
1922
  id: `${resolvableStateId}.${matched.ix}`,
1825
1923
  matchKinds: __classPrivateFieldGet$1(this, _TuringMachine_tapeBlock, "f").patternKinds(matched.matchedSymbol),
@@ -2321,14 +2419,20 @@ function toMermaid(graph) {
2321
2419
  const nodes = Object.values(graph.nodes).slice().sort((a, b) => a.id - b.id);
2322
2420
  // Bucket nodes for emit order.
2323
2421
  const topLevelNodes = nodes.filter((n) => n.frameId === null && !n.isWrapper);
2422
+ // All wrappers — needed by the call / return / wrapper-to-override
2423
+ // arrow emit passes below regardless of where the wrapper renders.
2324
2424
  const wrapperNodes = nodes.filter((n) => n.isWrapper);
2325
- // Bares-and-bodies inside frames, grouped by frameId.
2425
+ // Top-level wrappers: wrappers whose caller has no frame (the caller is
2426
+ // the top-level program). Framed wrappers (whose caller IS a subroutine
2427
+ // with its own frame) render INSIDE that frame's subgraph below.
2428
+ const topLevelWrapperNodes = wrapperNodes.filter((w) => w.frameId === null);
2429
+ // Bares-bodies-and-framed-wrappers inside frames, grouped by frameId.
2326
2430
  const nodesByFrame = new Map();
2327
2431
  // Halt-marker per frame (kept separate so it always emits LAST inside the
2328
2432
  // subgraph for deterministic shape).
2329
2433
  const haltMarkerByFrame = new Map();
2330
2434
  for (const node of nodes) {
2331
- if (node.frameId === null || node.isWrapper)
2435
+ if (node.frameId === null)
2332
2436
  continue;
2333
2437
  if (node.isHaltMarker) {
2334
2438
  haltMarkerByFrame.set(node.frameId, node);
@@ -2371,8 +2475,9 @@ function toMermaid(graph) {
2371
2475
  lines.push(` ${mid}["${labelOf(node)}"]`);
2372
2476
  }
2373
2477
  }
2374
- // 2. Emit wrappers at top level.
2375
- for (const wrapper of wrapperNodes) {
2478
+ // 2. Emit top-level wrappers (wrappers owned by the top-level program;
2479
+ // wrappers owned by a subroutine emit inside that subroutine's subgraph).
2480
+ for (const wrapper of topLevelWrapperNodes) {
2376
2481
  lines.push(` ${mermaidIdFor(wrapper.id)}[["${labelOf(wrapper)}"]]`);
2377
2482
  }
2378
2483
  // 3. `idle` sentinel.
@@ -2389,9 +2494,16 @@ function toMermaid(graph) {
2389
2494
  ? `callable scope: ${frameBareNames.join(' ∪ ')}`
2390
2495
  : `callable subtree of ${frameBareNames[0] ?? frameId}`;
2391
2496
  lines.push(` subgraph ${frameSubgraphId(frameId)}["${label}"]`);
2392
- // Inner nodes — sort by id for determinism.
2497
+ // Inner nodes — sort by id for determinism. Framed wrappers (call sites
2498
+ // owned by this frame) render with the `[[name]]` wrapper shape; bares
2499
+ // and body states render with the regular `["name"]` shape.
2393
2500
  for (const node of (nodesByFrame.get(frameId) ?? []).slice().sort((a, b) => a.id - b.id)) {
2394
- lines.push(` ${mermaidIdFor(node.id)}["${labelOf(node)}"]`);
2501
+ if (node.isWrapper) {
2502
+ lines.push(` ${mermaidIdFor(node.id)}[["${labelOf(node)}"]]`);
2503
+ }
2504
+ else {
2505
+ lines.push(` ${mermaidIdFor(node.id)}["${labelOf(node)}"]`);
2506
+ }
2395
2507
  }
2396
2508
  // Every frame has a halt marker — `State.toGraph`'s frame-emit pass
2397
2509
  // creates one for each frame. Non-null assertion is safe; a defensive
@@ -3029,6 +3141,7 @@ function runOnce(runnable, input, stepsLimit) {
3029
3141
  }
3030
3142
 
3031
3143
  exports.Alphabet = Alphabet;
3144
+ exports.CallFrame = CallFrame;
3032
3145
  exports.Command = Command;
3033
3146
  exports.DebugConfig = DebugConfig;
3034
3147
  exports.DebugSession = DebugSession;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { default as Alphabet } from './classes/Alphabet';
2
2
  export { default as Command } from './classes/Command';
3
3
  export { default as Reference } from './classes/Reference';
4
- export { default as State, DebugConfig, haltState, ifOtherSymbol } from './classes/State';
4
+ export { default as State, CallFrame, DebugConfig, haltState, ifOtherSymbol } from './classes/State';
5
5
  export { default as Tape } from './classes/Tape';
6
6
  export { default as TapeBlock } from './classes/TapeBlock';
7
7
  export { default as TapeCommand, movements, symbolCommands } from './classes/TapeCommand';
package/dist/index.mjs CHANGED
@@ -851,33 +851,70 @@ function toGraph(initialState, tapeBlock) {
851
851
  };
852
852
  }
853
853
  // Pass 2: For each bare, compute its forward-reachable set (following
854
- // transitions; stopping at halt and at wrappers both are frame
855
- // boundaries).
854
+ // transitions; stopping at halt; including wrappers AND tunneling through
855
+ // them to their `overriddenHaltStateId` continuation).
856
+ //
857
+ // Wrappers are call-site markers — semantically owned by the CALLER (the
858
+ // bare whose body invokes the sub-call). Both the wrapper itself and its
859
+ // continuation (its `--> override` arrow in the rendered diagram, sourced
860
+ // from `overriddenHaltStateId`) belong to the caller's frame: the wrapper
861
+ // visually anchors the call site inside the caller's subgraph; the
862
+ // continuation is the state the caller's body resumes at AFTER the inner
863
+ // sub-call returns. So when reach traversal hits a wrapper, we PUSH the
864
+ // wrapper (joining the caller's reach-set) AND tunnel through to its
865
+ // continuation (also joining). Wrappers carry no `transitions` of their
866
+ // own (Pass 1 emits `transitions: []` on wrapper nodes), so the main loop
867
+ // pops them and adds them to `reach` without further traversal — but the
868
+ // continuation already entered via the wrapper-tunnel chain in
869
+ // `resolveAndPush`.
870
+ //
871
+ // Halt-bound retargeting + union-find then "just work": continuation
872
+ // states' halt-bound transitions get retargeted to the caller's halt
873
+ // marker (so an in-subroutine halt returns to the caller, not the
874
+ // program's terminal halt); when two bares both reach the same
875
+ // continuation through different wrapper chains, union-find merges their
876
+ // frames as it already does for non-wrapper overlap.
877
+ //
878
+ // Wrapper chains (continuation IS another wrapper, e.g., nested
879
+ // compositions) are walked transitively by the inner while-loop in
880
+ // `resolveAndPush` — each tunnel hop pushes the intermediate wrapper.
856
881
  const computeReach = (startId) => {
857
882
  const reach = new Set();
858
- const stack = [startId];
883
+ const stack = [];
884
+ const resolveAndPush = (id) => {
885
+ let current = id;
886
+ while (true) {
887
+ const target = nodes[current];
888
+ if (!target || target.isHalt) {
889
+ return;
890
+ }
891
+ if (!target.isWrapper) {
892
+ stack.push(current);
893
+ return;
894
+ }
895
+ // Wrapper: push it (so it joins the caller's frame) AND tunnel to
896
+ // its continuation. Both belong to the caller's frame.
897
+ stack.push(current);
898
+ /* c8 ignore next 3 — every wrapper emitted by Pass 1 has a
899
+ non-null overriddenHaltStateId (lines 76-101); this branch
900
+ only guards against future wrapper variants that might not. */
901
+ if (target.overriddenHaltStateId === null) {
902
+ return;
903
+ }
904
+ current = target.overriddenHaltStateId;
905
+ }
906
+ };
907
+ resolveAndPush(startId);
859
908
  while (stack.length > 0) {
860
909
  const id = stack.pop();
861
910
  if (reach.has(id)) {
862
911
  continue;
863
912
  }
864
- const node = nodes[id];
865
- // `nodes[id]` is always populated for `id` that the BFS reached, so
866
- // a defensive `!node` check would be dead. `isHalt` / `isWrapper`
867
- // are real boundaries — both stop reach-set expansion.
868
- /* c8 ignore next 3 — defensive: the push site below already filters
869
- halt/wrapper targets, and the initial push is always a bare, so
870
- this branch is unreachable in practice. */
871
- if (node.isHalt || node.isWrapper) {
872
- continue;
873
- }
874
913
  reach.add(id);
875
- for (const t of node.transitions) {
876
- const target = nodes[t.nextStateId];
877
- if (!target || target.isHalt || target.isWrapper) {
878
- continue;
879
- }
880
- stack.push(t.nextStateId);
914
+ // Wrappers have empty transitions arrays — the for-loop runs zero
915
+ // iterations and we proceed to the next stack entry.
916
+ for (const t of nodes[id].transitions) {
917
+ resolveAndPush(t.nextStateId);
881
918
  }
882
919
  }
883
920
  return reach;
@@ -1238,7 +1275,7 @@ var __classPrivateFieldGet$2 = (undefined && undefined.__classPrivateFieldGet) |
1238
1275
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
1239
1276
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
1240
1277
  };
1241
- var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_instances, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef, _State_haltDebug, _State_tags, _State_getEntry;
1278
+ var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _State_instances, _a, _State_wrapperCache, _State_id, _State_name, _State_symbolToDataMap, _State_debugRef, _State_haltDebug, _State_tags, _State_getEntry, _CallFrame_bare, _CallFrame_override;
1242
1279
  const ifOtherSymbol = Symbol('other symbol');
1243
1280
  // Module-private symbol used by DebugConfig setters to call State's validator
1244
1281
  // without exposing the validator on the public surface.
@@ -1310,12 +1347,6 @@ class State {
1310
1347
  // composed name on a no-arg `new State()` to bypass the constructor's
1311
1348
  // user-facing name validation (composite names contain `(` and `)`).
1312
1349
  _State_name.set(this, void 0);
1313
- _State_overriddenHaltState.set(this, null);
1314
- // For wrapper states (produced by `withOverriddenHaltState`), points at the
1315
- // State whose transition map was wrapped. `null` on bare/atomic states.
1316
- // `toGraph` reads this to render wrapper and bare as separate nodes linked
1317
- // by a `==> call` arrow.
1318
- _State_bareState.set(this, null);
1319
1350
  _State_symbolToDataMap.set(this, new Map());
1320
1351
  // Shared mutable cell — withOverriddenHaltState wrappers reference the same
1321
1352
  // object so that `state.debug = ...` (and nullings) propagate across them.
@@ -1393,8 +1424,10 @@ class State {
1393
1424
  get isHalt() {
1394
1425
  return __classPrivateFieldGet$2(this, _State_id, "f") === 0;
1395
1426
  }
1427
+ // Plain States never override the halt state — only a `CallFrame` (produced
1428
+ // by `withOverriddenHaltState`) carries an override, via its own getter.
1396
1429
  get overriddenHaltState() {
1397
- return __classPrivateFieldGet$2(this, _State_overriddenHaltState, "f");
1430
+ return null;
1398
1431
  }
1399
1432
  get ref() {
1400
1433
  return this;
@@ -1496,7 +1529,7 @@ class State {
1496
1529
  /** @internal — invoked by DebugConfig setters via module-private symbol.
1497
1530
  * haltState's `debug` setter rejects object writes before reaching
1498
1531
  * DebugConfig, so this validator only sees non-halt states. */
1499
- [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overriddenHaltState = new WeakMap(), _State_bareState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), _State_haltDebug = new WeakMap(), _State_tags = new WeakMap(), _State_instances = new WeakSet(), validateDebugFilter)](fieldName, filter) {
1532
+ [(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), _State_haltDebug = new WeakMap(), _State_tags = new WeakMap(), _State_instances = new WeakSet(), validateDebugFilter)](fieldName, filter) {
1500
1533
  if (filter === undefined)
1501
1534
  return;
1502
1535
  if (filter === true)
@@ -1555,13 +1588,13 @@ class State {
1555
1588
  return { nextState: entry.nextState, matchedSymbol: symbol, ix };
1556
1589
  }
1557
1590
  withOverriddenHaltState(overriddenHaltState) {
1558
- // Unwrap `this` if it's itself a wrapper — the chain's inner overrides
1591
+ // Unwrap `this` if it's itself a CallFrame — the chain's inner overrides
1559
1592
  // are dead at runtime anyway (only the outermost `.wohs()`'s override is
1560
1593
  // pushed onto the halt-stack on entry; verified empirically). Composite
1561
1594
  // name reflects runtime behavior, not construction history. See #176.
1562
- const bare = __classPrivateFieldGet$2(this, _State_bareState, "f") ?? this;
1595
+ const bare = this instanceof CallFrame ? this.bare : this;
1563
1596
  // Memoize by (bare, override) so identical args return the same instance
1564
- // (#175). The cache uses WeakMaps + WeakRefs so cached wrappers can be
1597
+ // (#175). The cache uses WeakMaps + WeakRefs so cached frames can be
1565
1598
  // GC'd when nothing else holds them. Compounds with the chain-collapse
1566
1599
  // above: `A.wohs(t1).wohs(t2)` keys as (A, t2) after the unwrap, hitting
1567
1600
  // the same cache slot as a direct `A.wohs(t2)`.
@@ -1579,17 +1612,9 @@ class State {
1579
1612
  innerCache = new WeakMap();
1580
1613
  __classPrivateFieldGet$2(_a, _a, "f", _State_wrapperCache).set(bare, innerCache);
1581
1614
  }
1582
- // Cache miss construct with no name, then overwrite #name directly
1583
- // (composed names contain `(` and `)` which the constructor's user-facing
1584
- // validation would reject; private-field access bypasses that).
1585
- const state = new _a();
1586
- __classPrivateFieldSet$2(state, _State_name, `${bare.name}(${overriddenHaltState.name})`, "f");
1587
- __classPrivateFieldSet$2(state, _State_symbolToDataMap, __classPrivateFieldGet$2(bare, _State_symbolToDataMap, "f"), "f");
1588
- __classPrivateFieldSet$2(state, _State_overriddenHaltState, overriddenHaltState, "f");
1589
- __classPrivateFieldSet$2(state, _State_debugRef, __classPrivateFieldGet$2(bare, _State_debugRef, "f"), "f");
1590
- __classPrivateFieldSet$2(state, _State_bareState, bare, "f");
1591
- innerCache.set(overriddenHaltState, new WeakRef(state));
1592
- return state;
1615
+ const frame = new CallFrame(bare, overriddenHaltState);
1616
+ innerCache.set(overriddenHaltState, new WeakRef(frame));
1617
+ return frame;
1593
1618
  }
1594
1619
  /**
1595
1620
  * @internal
@@ -1632,8 +1657,8 @@ class State {
1632
1657
  get id() { return __classPrivateFieldGet$2(self, _State_id, "f"); },
1633
1658
  get name() { return __classPrivateFieldGet$2(self, _State_name, "f"); },
1634
1659
  set name(v) { __classPrivateFieldSet$2(self, _State_name, v, "f"); },
1635
- get bareState() { return __classPrivateFieldGet$2(self, _State_bareState, "f"); },
1636
- get overriddenHaltState() { return __classPrivateFieldGet$2(self, _State_overriddenHaltState, "f"); },
1660
+ get bareState() { return null; },
1661
+ get overriddenHaltState() { return null; },
1637
1662
  get symbolToDataMap() { return __classPrivateFieldGet$2(self, _State_symbolToDataMap, "f"); },
1638
1663
  get tags() { return __classPrivateFieldGet$2(self, _State_tags, "f"); },
1639
1664
  };
@@ -1644,8 +1669,13 @@ class State {
1644
1669
  // Symbol patterns are returned as the raw description string from the
1645
1670
  // interned JS Symbol (decode via decodePatternDescription if needed).
1646
1671
  static inspect(state) {
1672
+ // Route through the STATE_INTERNAL view so a CallFrame reports its bare's
1673
+ // transitions and its own override — the view delegates those to the bare
1674
+ // / the frame's #override, whereas the raw private fields on a CallFrame
1675
+ // are empty/null.
1676
+ const internal = state[STATE_INTERNAL]();
1647
1677
  const transitions = [];
1648
- for (const [sym, { command, nextState }] of __classPrivateFieldGet$2(state, _State_symbolToDataMap, "f")) {
1678
+ for (const [sym, { command, nextState }] of internal.symbolToDataMap) {
1649
1679
  let target = null;
1650
1680
  try {
1651
1681
  target = nextState instanceof _a ? nextState : nextState.ref;
@@ -1662,12 +1692,13 @@ class State {
1662
1692
  nextState: target ? { id: target.id, name: target.name } : null,
1663
1693
  });
1664
1694
  }
1695
+ const override = internal.overriddenHaltState;
1665
1696
  return {
1666
- id: __classPrivateFieldGet$2(state, _State_id, "f"),
1667
- name: __classPrivateFieldGet$2(state, _State_name, "f"),
1697
+ id: internal.id,
1698
+ name: internal.name,
1668
1699
  isHalt: state.isHalt,
1669
- overriddenHaltState: __classPrivateFieldGet$2(state, _State_overriddenHaltState, "f")
1670
- ? { id: __classPrivateFieldGet$2(state, _State_overriddenHaltState, "f").id, name: __classPrivateFieldGet$2(state, _State_overriddenHaltState, "f").name }
1700
+ overriddenHaltState: override
1701
+ ? { id: override.id, name: override.name }
1671
1702
  : null,
1672
1703
  transitions,
1673
1704
  };
@@ -1712,6 +1743,79 @@ _a = State;
1712
1743
  // them, with cache misses simply reconstructing fresh wrappers.
1713
1744
  _State_wrapperCache = { value: new WeakMap() };
1714
1745
  const haltState = new State(null);
1746
+ /**
1747
+ * A first-class call frame produced by `State.withOverriddenHaltState`
1748
+ * (#213). A `CallFrame` is a `State` — `instanceof State` holds, so it flows
1749
+ * anywhere a `State` does (as a `nextState`, through `toGraph`/`fromGraph`,
1750
+ * etc.) — but it carries its own `bare` (the wrapped State) and `override`
1751
+ * (the continuation pushed onto the run-stack on entry). `instanceof
1752
+ * CallFrame` is the explicit wrapper discriminator.
1753
+ *
1754
+ * It owns no transitions of its own: lookups (`getSymbol`/`getCommand`/
1755
+ * `getNextState`/`getMatchedTransition`) and `debug` DELEGATE to the bare,
1756
+ * replacing the v6 field-aliasing (where a wrapper was a plain `State` whose
1757
+ * private `#symbolToDataMap`/`#debugRef` were physically shared with the
1758
+ * bare). `id`, `name` (composite `bare(override)`), and `tags` are its own
1759
+ * (inherited State fields) — so memoized frames sharing a bare keep
1760
+ * independent tags (#186), and the frame is never the halt singleton
1761
+ * (fresh nonzero `#id` → `isHalt === false`).
1762
+ */
1763
+ class CallFrame extends State {
1764
+ constructor(bare, override) {
1765
+ super(null);
1766
+ _CallFrame_bare.set(this, void 0);
1767
+ _CallFrame_override.set(this, void 0);
1768
+ __classPrivateFieldSet$2(this, _CallFrame_bare, bare, "f");
1769
+ __classPrivateFieldSet$2(this, _CallFrame_override, override, "f");
1770
+ // Composite name contains `(` / `)`, which the constructor's user-facing
1771
+ // name validator rejects; the STATE_INTERNAL name setter bypasses it
1772
+ // (writes the inherited #name). `super[...]` reaches State's own view so
1773
+ // we don't recurse through this subclass's override below.
1774
+ super[STATE_INTERNAL]().name = `${bare.name}(${override.name})`;
1775
+ }
1776
+ get bare() {
1777
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f");
1778
+ }
1779
+ get overriddenHaltState() {
1780
+ return __classPrivateFieldGet$2(this, _CallFrame_override, "f");
1781
+ }
1782
+ getSymbol(tapeBlock) {
1783
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getSymbol(tapeBlock);
1784
+ }
1785
+ getCommand(symbol) {
1786
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getCommand(symbol);
1787
+ }
1788
+ getNextState(symbol) {
1789
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getNextState(symbol);
1790
+ }
1791
+ getMatchedTransition(symbol) {
1792
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").getMatchedTransition(symbol);
1793
+ }
1794
+ get debug() {
1795
+ return __classPrivateFieldGet$2(this, _CallFrame_bare, "f").debug;
1796
+ }
1797
+ set debug(value) {
1798
+ __classPrivateFieldGet$2(this, _CallFrame_bare, "f").debug = value;
1799
+ }
1800
+ [(_CallFrame_bare = new WeakMap(), _CallFrame_override = new WeakMap(), STATE_INTERNAL)]() {
1801
+ // Own id / name / tags come from the inherited State fields (via super's
1802
+ // view); bareState / overriddenHaltState / the transition map delegate to
1803
+ // #bare / #override so sibling modules (stateGraph, inspect) see the
1804
+ // frame's true shape.
1805
+ const own = super[STATE_INTERNAL]();
1806
+ const bare = __classPrivateFieldGet$2(this, _CallFrame_bare, "f");
1807
+ const override = __classPrivateFieldGet$2(this, _CallFrame_override, "f");
1808
+ return {
1809
+ get id() { return own.id; },
1810
+ get name() { return own.name; },
1811
+ set name(v) { own.name = v; },
1812
+ get bareState() { return bare; },
1813
+ get overriddenHaltState() { return override; },
1814
+ get symbolToDataMap() { return bare[STATE_INTERNAL]().symbolToDataMap; },
1815
+ get tags() { return own.tags; },
1816
+ };
1817
+ }
1818
+ }
1715
1819
 
1716
1820
  var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
1717
1821
  if (kind === "m") throw new TypeError("Private method is not writable");
@@ -1808,16 +1912,10 @@ class TuringMachine {
1808
1912
  const command = state.getCommand(symbol);
1809
1913
  const matched = state.getMatchedTransition(symbol);
1810
1914
  let nextState = matched.nextState.ref;
1811
- // For wrapper-entry iters, the wrapper's transitions in `toGraph`
1812
- // are empty (wrappers delegate to the bare via shared
1813
- // `#symbolToDataMap`); the resolvable transition id lives under
1814
- // the bare's stateId. `bareState` is non-null only when `state`
1815
- // is a wrapper produced by `withOverriddenHaltState`. Accessed
1816
- // via the STATE_INTERNAL package-private view (same pattern
1817
- // `utilities/stateGraph.ts` uses) to avoid widening the public
1818
- // State API for this internal need.
1819
- const stateInternal = state[STATE_INTERNAL]();
1820
- const resolvableStateId = stateInternal.bareState?.id ?? state.id;
1915
+ // For wrapper-entry iters, a CallFrame's own transitions in `toGraph`
1916
+ // are empty (it delegates lookups to its bare); the resolvable
1917
+ // transition id lives under the bare's stateId.
1918
+ const resolvableStateId = state instanceof CallFrame ? state.bare.id : state.id;
1821
1919
  const matchedTransition = {
1822
1920
  id: `${resolvableStateId}.${matched.ix}`,
1823
1921
  matchKinds: __classPrivateFieldGet$1(this, _TuringMachine_tapeBlock, "f").patternKinds(matched.matchedSymbol),
@@ -2319,14 +2417,20 @@ function toMermaid(graph) {
2319
2417
  const nodes = Object.values(graph.nodes).slice().sort((a, b) => a.id - b.id);
2320
2418
  // Bucket nodes for emit order.
2321
2419
  const topLevelNodes = nodes.filter((n) => n.frameId === null && !n.isWrapper);
2420
+ // All wrappers — needed by the call / return / wrapper-to-override
2421
+ // arrow emit passes below regardless of where the wrapper renders.
2322
2422
  const wrapperNodes = nodes.filter((n) => n.isWrapper);
2323
- // Bares-and-bodies inside frames, grouped by frameId.
2423
+ // Top-level wrappers: wrappers whose caller has no frame (the caller is
2424
+ // the top-level program). Framed wrappers (whose caller IS a subroutine
2425
+ // with its own frame) render INSIDE that frame's subgraph below.
2426
+ const topLevelWrapperNodes = wrapperNodes.filter((w) => w.frameId === null);
2427
+ // Bares-bodies-and-framed-wrappers inside frames, grouped by frameId.
2324
2428
  const nodesByFrame = new Map();
2325
2429
  // Halt-marker per frame (kept separate so it always emits LAST inside the
2326
2430
  // subgraph for deterministic shape).
2327
2431
  const haltMarkerByFrame = new Map();
2328
2432
  for (const node of nodes) {
2329
- if (node.frameId === null || node.isWrapper)
2433
+ if (node.frameId === null)
2330
2434
  continue;
2331
2435
  if (node.isHaltMarker) {
2332
2436
  haltMarkerByFrame.set(node.frameId, node);
@@ -2369,8 +2473,9 @@ function toMermaid(graph) {
2369
2473
  lines.push(` ${mid}["${labelOf(node)}"]`);
2370
2474
  }
2371
2475
  }
2372
- // 2. Emit wrappers at top level.
2373
- for (const wrapper of wrapperNodes) {
2476
+ // 2. Emit top-level wrappers (wrappers owned by the top-level program;
2477
+ // wrappers owned by a subroutine emit inside that subroutine's subgraph).
2478
+ for (const wrapper of topLevelWrapperNodes) {
2374
2479
  lines.push(` ${mermaidIdFor(wrapper.id)}[["${labelOf(wrapper)}"]]`);
2375
2480
  }
2376
2481
  // 3. `idle` sentinel.
@@ -2387,9 +2492,16 @@ function toMermaid(graph) {
2387
2492
  ? `callable scope: ${frameBareNames.join(' ∪ ')}`
2388
2493
  : `callable subtree of ${frameBareNames[0] ?? frameId}`;
2389
2494
  lines.push(` subgraph ${frameSubgraphId(frameId)}["${label}"]`);
2390
- // Inner nodes — sort by id for determinism.
2495
+ // Inner nodes — sort by id for determinism. Framed wrappers (call sites
2496
+ // owned by this frame) render with the `[[name]]` wrapper shape; bares
2497
+ // and body states render with the regular `["name"]` shape.
2391
2498
  for (const node of (nodesByFrame.get(frameId) ?? []).slice().sort((a, b) => a.id - b.id)) {
2392
- lines.push(` ${mermaidIdFor(node.id)}["${labelOf(node)}"]`);
2499
+ if (node.isWrapper) {
2500
+ lines.push(` ${mermaidIdFor(node.id)}[["${labelOf(node)}"]]`);
2501
+ }
2502
+ else {
2503
+ lines.push(` ${mermaidIdFor(node.id)}["${labelOf(node)}"]`);
2504
+ }
2393
2505
  }
2394
2506
  // Every frame has a halt marker — `State.toGraph`'s frame-emit pass
2395
2507
  // creates one for each frame. Non-null assertion is safe; a defensive
@@ -3026,4 +3138,4 @@ function runOnce(runnable, input, stepsLimit) {
3026
3138
  };
3027
3139
  }
3028
3140
 
3029
- export { Alphabet, Command, DebugConfig, DebugSession, Reference, State, Tape, TapeBlock, TapeCommand, TuringMachine, equivalentOn, fromMermaid, haltState, ifOtherSymbol, movements, summarize, summarizeGraph, symbolCommands, toMermaid };
3141
+ export { Alphabet, CallFrame, Command, DebugConfig, DebugSession, Reference, State, Tape, TapeBlock, TapeCommand, TuringMachine, equivalentOn, fromMermaid, haltState, ifOtherSymbol, movements, summarize, summarizeGraph, symbolCommands, toMermaid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turing-machine-js/machine",
3
- "version": "7.0.0-alpha.6",
3
+ "version": "7.0.0-alpha.7",
4
4
  "description": "A convenient Turing machine",
5
5
  "engines": {
6
6
  "npm": ">=7.0.0"
@@ -38,5 +38,5 @@
38
38
  "default": "./dist/index.mjs"
39
39
  }
40
40
  },
41
- "gitHead": "0b44e44db9da4a2f50bb46e5f723382345fdc687"
41
+ "gitHead": "130d4fd8b964da21a408af2986295e8000828f78"
42
42
  }