@turing-machine-js/machine 7.0.0-alpha.4 → 7.0.0-alpha.5
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 +32 -0
- package/README.md +41 -5
- package/dist/classes/State.d.ts +47 -2
- package/dist/classes/TapeBlock.d.ts +25 -0
- package/dist/classes/TuringMachine.d.ts +23 -0
- package/dist/index.cjs +191 -37
- package/dist/index.mjs +191 -37
- package/dist/utilities/stateGraph.d.ts +3 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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.5] - 2026-05-25
|
|
8
|
+
|
|
9
|
+
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`.
|
|
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.5`.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`MachineState.matchedTransition`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)) — every yielded `MachineState` from `run` / `runStepByStep` now carries `{ id: string, matchKinds: ('wildcard' | 'literal')[] }`. `id` is `${stateId}.${transitionIx}` and resolves directly in `toGraph`'s output via `graph.nodes[stateId].transitions.find(t => t.id === ...)`. For wrapper-entry iters, `id` references the BARE's transition (wrappers delegate via `bareState`). `matchKinds` is per-tape, length = tape count; `'wildcard'` iff the winning alternative held `ifOtherSymbol` at that position. Eliminates `(source, nextState)` resolution ambiguity for tools doing exact-edge highlighting, per-transition coverage maps, or wildcard-aware log formatting.
|
|
16
|
+
|
|
17
|
+
- **`State.getMatchedTransition(symbol)`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)) — public method returning `{ nextState, matchedSymbol, ix }` for the symbol's transition. Same lookup `getNextState` does, plus the index — exposes the K used by `MachineState.matchedTransition.id` so callers needing both don't pay for the lookup twice.
|
|
18
|
+
|
|
19
|
+
- **`TapeBlock.patternKinds(symbol, currentSymbols?)`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)) — public method returning per-tape `'wildcard' | 'literal'` for the matched alternative's selector. The engine uses it internally to populate `matchedTransition.matchKinds`; exposed for consumers wiring custom dispatch.
|
|
20
|
+
|
|
21
|
+
- **`HaltState` typed alias** ([#207](https://github.com/mellonis/turing-machine-js/issues/207)) — the exported `haltState` singleton now carries a narrower TypeScript type (`State & { debug: boolean; ... }`) so `haltState.debug = true` is type-safe at the call site. Generic `State` references still see `DebugConfig` — runtime branches on `this.isHalt`.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **`GraphTransition.id` separator: `-` → `.`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)). Was `${stateId}-${patternIx}`, now `${stateId}.${patternIx}`. Aligns with `matchedTransition.id` and makes the id splittable without colliding with user-defined state names that may contain `-`. **Breaking** for any consumer parsing the old separator; alpha-channel only.
|
|
26
|
+
|
|
27
|
+
- **`haltState.debug` is `boolean`** ([#207](https://github.com/mellonis/turing-machine-js/issues/207)). Was `DebugConfig` with per-side `{ before?, after? }` filters; under v7 it's a single `boolean` flag. `haltState.debug = true` enables; `false` / `null` disables. **Object-shaped writes throw** (`{ before: true }`, `{ after: true }`, etc.). The pause fires on the AFTER side of the iter whose transition leads to halt — `m.state` is the triggering state (not haltState), `m.debugBreak.after === true`. Diagram + log narratives read naturally: the halt-bound transition has fired when the pause lands, and halt is the next thing. **Breaking** — pre-alpha.5 consumers using `haltState.debug = { before: true }` must switch to `haltState.debug = true`.
|
|
28
|
+
|
|
29
|
+
⚠️ **Chained-form `haltState.debug.before = true` silently no-ops in non-strict mode.** The getter returns the boolean; assigning `.before` to a primitive is a JS no-op outside strict mode (`TypeError` in strict). The engine setter never sees the write. Use the whole-object form (`haltState.debug = true` / `= false` / `= null`).
|
|
30
|
+
|
|
31
|
+
- **`State` internal: single `#getEntry` helper for `getCommand` / `getNextState` / `getMatchedTransition`** ([#206](https://github.com/mellonis/turing-machine-js/pull/206)). Internal refactor — no public API change. All three accessors now share one `.get()` lookup + unified throw message (`"No transition for symbol at state named ..."` — was per-method messages).
|
|
32
|
+
|
|
33
|
+
- **Internal: dead-code typeof check on `state.debug` in `runStepByStep` removed; `patternKinds` defensive paths now have dedicated test coverage** ([#210](https://github.com/mellonis/turing-machine-js/pull/210)). Engine-internal cleanup that bumps overall coverage to 99.35% statements / 96.58% branches / 100% functions / 99.48% lines.
|
|
34
|
+
|
|
35
|
+
### Docs
|
|
36
|
+
|
|
37
|
+
- ⚠️ **README + CLAUDE.md** ([#209](https://github.com/mellonis/turing-machine-js/pull/209)) — warn that chained-form `haltState.debug.before = true` silently no-ops in non-strict mode (`new Function(...)`, `<script>` without `"use strict"`). Engine can't intercept (the write happens on a primitive after the getter returned); doc steers users to the whole-object form.
|
|
38
|
+
|
|
7
39
|
## [7.0.0-alpha.4] - 2026-05-23
|
|
8
40
|
|
|
9
41
|
Fourth v7 pre-release. Adds an id-keyed lookup helper for the State graph ([#195](https://github.com/mellonis/turing-machine-js/issues/195)), fixes two upstream issues surfaced while wiring the new helper into downstream tooling — a `toMermaid` label-grammar bug ([#194](https://github.com/mellonis/turing-machine-js/issues/194)) and a halt-stack lifetime bug in `runStepByStep` ([#196](https://github.com/mellonis/turing-machine-js/issues/196)) — and extracts graph serialization into its own module without API change ([#180](https://github.com/mellonis/turing-machine-js/issues/180)). Published under the `next` dist-tag: `npm install @turing-machine-js/machine@next`.
|
package/README.md
CHANGED
|
@@ -327,6 +327,7 @@ Each yielded `step` (`MachineState`) has these fields:
|
|
|
327
327
|
| `movements` | `symbol[]` | per-tape head moves (`movements.left/right/stay`) |
|
|
328
328
|
| `nextState` | `State` | the state that will execute next |
|
|
329
329
|
| `debugBreak?` | `{ before?: true, after?: true }` | only set when `state.debug` matched on this iter — see *Debugging breakpoints* below |
|
|
330
|
+
| `matchedTransition` | `{ id: string, matchKinds: ('wildcard'\|'literal')[] }` | the transition the engine picked for this iter — see *Matched transition* below |
|
|
330
331
|
|
|
331
332
|
`stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
|
|
332
333
|
|
|
@@ -346,6 +347,35 @@ Both APIs are first-class — `run()` is built on top of `runStepByStep()` (see
|
|
|
346
347
|
|
|
347
348
|
**Don't split one logical flow across both APIs.** A consumer that wants stepwise UI *and* hook-driven breakpoints should use `run({ onStep, onPause, debug })` exclusively. Routing some operations through `runStepByStep()` and others through `run()` means `state.debug` only flows through one of the two paths — a subtle footgun where breakpoints silently disappear on whichever code path uses the generator directly. For per-iter throttle / "wait between steps" UIs, see [Throttle pattern](#throttle-pattern).
|
|
348
349
|
|
|
350
|
+
### Matched transition
|
|
351
|
+
|
|
352
|
+
Every yielded `MachineState` carries a `matchedTransition` describing which transition the engine picked for that iter. The engine already resolves this via `state.getNextState(symbol)` internally; this field exposes the resolution to consumers so visualizations, log formatters, and coverage maps don't have to re-derive an ambiguous `(source, nextState)` pair (which collides when multiple transitions on the same source share a destination) or parse pattern strings from `toGraph`.
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
matchedTransition: {
|
|
356
|
+
id: string; // resolvable in toGraph
|
|
357
|
+
matchKinds: ('wildcard' | 'literal')[]; // per-tape, length = tape count
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- **`id`** — `${stateId}.${transitionIx}`. Resolvable in `toGraph`'s output: `graph.nodes[stateId].transitions` has an entry with the matching `id`. For wrapper-entry iters (source is a wrapper produced by `withOverriddenHaltState`), `id` references the **bare's** transition — the wrapper's own `transitions` array in `toGraph` is empty because wrappers delegate, and the pattern actually lives on the bare. Detect by comparing `id.split('.')[0]` against `state.id`: different → wrapper delegation.
|
|
362
|
+
|
|
363
|
+
- **`matchKinds`** — per-tape match kind for the matched alternative's selector at each tape position. `'wildcard'` if the position held `ifOtherSymbol` (catch-all) in the winning alternative; `'literal'` if it held a specific symbol or symbol-list. Length always equals tape count.
|
|
364
|
+
|
|
365
|
+
Example use:
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
await machine.run({
|
|
369
|
+
initialState,
|
|
370
|
+
onStep: (m) => {
|
|
371
|
+
const wildcardPositions = m.matchedTransition.matchKinds // per-tape, e.g. ['wildcard', 'literal']
|
|
372
|
+
.map((k, i) => k === 'wildcard' ? i : -1)
|
|
373
|
+
.filter((i) => i >= 0);
|
|
374
|
+
console.log(`step ${m.step}: fired transition ${m.matchedTransition.id} (wildcards at tapes: ${wildcardPositions.join(',') || 'none'})`);
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
349
379
|
## Subroutine composition with `withOverriddenHaltState`
|
|
350
380
|
|
|
351
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.
|
|
@@ -467,14 +497,20 @@ myState.debug = { before: true };
|
|
|
467
497
|
myState.debug = { before: [symA] };
|
|
468
498
|
myState.debug = { before: [symA], after: [symA] };
|
|
469
499
|
|
|
470
|
-
// Pause when the engine is about to enter halt (program exit OR subroutine pop)
|
|
471
|
-
haltState.debug
|
|
500
|
+
// Pause when the engine is about to enter halt (program exit OR subroutine pop).
|
|
501
|
+
// haltState.debug is a `boolean` (#207) — halt is terminal, so there's only
|
|
502
|
+
// one meaningful pause moment (post-triggering-iter, before halt processing).
|
|
503
|
+
haltState.debug = true;
|
|
504
|
+
haltState.debug = false; // turn off
|
|
505
|
+
haltState.debug = null; // alias of false (reset)
|
|
472
506
|
|
|
473
|
-
// Reset filters later — next read returns a fresh empty DebugConfig:
|
|
507
|
+
// Reset filters later on a regular state — next read returns a fresh empty DebugConfig:
|
|
474
508
|
myState.debug = null;
|
|
475
509
|
```
|
|
476
510
|
|
|
477
|
-
> ⚠️ **`haltState.debug
|
|
511
|
+
> ⚠️ **`haltState.debug` is `boolean`-only.** Any object-shaped write (`{ before: true }`, `{ after: true }`, `{ before: true, after: true }`) throws at write time. The pause fires on the AFTER side of the iter whose transition leads to halt — `m.state` is the triggering state (not haltState), `m.debugBreak.after === true`. Diagram + log narratives read naturally: the halt-bound transition has already fired when the pause lands, and halt is the next thing.
|
|
512
|
+
|
|
513
|
+
> ⚠️ **Chained-form `haltState.debug.before = true` doesn't throw in non-strict mode** — this is a JavaScript primitive quirk, not engine behavior. The getter returns the boolean `false`; assigning `.before` to that boolean is a no-op in non-strict mode (silent), a `TypeError` in strict mode. The engine setter only sees whole-object writes (`haltState.debug = X`), so it can't intercept the chained form. **Always use the whole-object form: `haltState.debug = true` / `= false` / `= null`.** Modules built with `tsc` / ESM run in strict mode by default and surface this throw; `new Function(...)` and `<script>` without `"use strict"` run non-strict and silently no-op the chained write.
|
|
478
514
|
|
|
479
515
|
The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. The internal cell is shared with `state.withOverriddenHaltState(...)` wrappers, so an assignment on the original is visible from every wrapper. `state.debug` is always a `DebugConfig` instance (lazy-initialized on first read); plain-object input (`state.debug = { before: true }`) is wrapped in a fresh `DebugConfig` automatically. The instance itself is `Object.seal`-ed — typos like `state.debug.bofore = true` throw `TypeError` instead of silently creating a useless property. Per-property setters validate and freeze the stored array, so `state.debug.before.push(...)` also throws `TypeError`.
|
|
480
516
|
|
|
@@ -708,7 +744,7 @@ Reading `['0',*] → [K,'0']/[R,R]`:
|
|
|
708
744
|
API surface changes since v3, in past tense so the timing of each piece is explicit:
|
|
709
745
|
|
|
710
746
|
- **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.
|
|
711
|
-
- **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).
|
|
747
|
+
- **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.*
|
|
712
748
|
- **v6** — Per-iter lifecycle reordered to `before → step → after`, all firing on the same yield. Previously `after` fired on iter K+1's tick with a `prevYield` substitution dance; that substitution is gone. The `MachineState.debugBreak` field shape is unchanged across all three versions.
|
|
713
749
|
- **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).
|
|
714
750
|
- **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.
|
package/dist/classes/State.d.ts
CHANGED
|
@@ -73,11 +73,35 @@ export default class State {
|
|
|
73
73
|
* code uses). Order matches insertion order of the underlying Set.
|
|
74
74
|
*/
|
|
75
75
|
get tags(): readonly string[];
|
|
76
|
-
/** @internal — invoked by DebugConfig setters via module-private symbol.
|
|
76
|
+
/** @internal — invoked by DebugConfig setters via module-private symbol.
|
|
77
|
+
* Per #207, haltState no longer flows through DebugConfig (its `debug`
|
|
78
|
+
* setter rejects object writes before construction), so the validator
|
|
79
|
+
* only sees non-halt states here. */
|
|
77
80
|
[validateDebugFilter](fieldName: 'before' | 'after', filter: readonly symbol[] | true | undefined): void;
|
|
78
81
|
getSymbol(tapeBlock: TapeBlock): symbol;
|
|
79
82
|
getCommand(symbol: symbol): Command;
|
|
80
83
|
getNextState(symbol: symbol): State | Reference;
|
|
84
|
+
/**
|
|
85
|
+
* Like `getNextState`, but also returns the matched Symbol and its index
|
|
86
|
+
* in this State's transition declaration order (= the `K` in `toGraph`'s
|
|
87
|
+
* `${stateId}.${K}` transition ids). Used by `TuringMachine.runStepByStep`
|
|
88
|
+
* to populate `MachineState.matchedTransition` for #205 — exposes which
|
|
89
|
+
* transition fired so consumers (UIs, log tools, coverage maps) can
|
|
90
|
+
* resolve the firing edge without re-deriving from `(source, nextState)`,
|
|
91
|
+
* which is ambiguous when multiple transitions on the same source go to
|
|
92
|
+
* the same destination.
|
|
93
|
+
*
|
|
94
|
+
* Throws (matching `getNextState` / `getCommand`) when no entry exists for
|
|
95
|
+
* the symbol. For wrappers (states produced by `withOverriddenHaltState`):
|
|
96
|
+
* the symbol-to-data map is shared with the bare via `bareState`, so the
|
|
97
|
+
* returned `ix` is a valid position into BOTH the wrapper's and the
|
|
98
|
+
* bare's transition iteration order — they're the same map.
|
|
99
|
+
*/
|
|
100
|
+
getMatchedTransition(symbol: symbol): {
|
|
101
|
+
nextState: State | Reference;
|
|
102
|
+
matchedSymbol: symbol;
|
|
103
|
+
ix: number;
|
|
104
|
+
};
|
|
81
105
|
withOverriddenHaltState(overriddenHaltState: State): State;
|
|
82
106
|
/**
|
|
83
107
|
* @internal
|
|
@@ -164,5 +188,26 @@ export default class State {
|
|
|
164
188
|
*/
|
|
165
189
|
static collectStates(initialState: State, tapeBlock: TapeBlock): StateMap;
|
|
166
190
|
}
|
|
167
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Typed alias for the haltState singleton (#207). Narrows `debug` from
|
|
193
|
+
* the generic-State `DebugConfig | boolean` union to plain `boolean`,
|
|
194
|
+
* giving compile-time type-safety at the singleton's call sites:
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* haltState.debug = true; // ok
|
|
198
|
+
* haltState.debug = false; // ok
|
|
199
|
+
* haltState.debug = { before: true } // TS error
|
|
200
|
+
* const isOn = haltState.debug; // typed `boolean`
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* Anyone holding a `State` reference that happens to BE the singleton (e.g.
|
|
204
|
+
* via `state.getNextState(sym).ref === haltState`) sees the wider `State`
|
|
205
|
+
* type; runtime throws guide them to the right shape. The singleton export
|
|
206
|
+
* is the canonical access path.
|
|
207
|
+
*/
|
|
208
|
+
export type HaltState = State & {
|
|
209
|
+
get debug(): boolean;
|
|
210
|
+
set debug(value: boolean | null);
|
|
211
|
+
};
|
|
212
|
+
export declare const haltState: HaltState;
|
|
168
213
|
export {};
|
|
@@ -23,6 +23,31 @@ export default class TapeBlock {
|
|
|
23
23
|
currentSymbols?: string[];
|
|
24
24
|
symbol: symbol;
|
|
25
25
|
}): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* For a Symbol returned by `this.symbol([...])` (or the catch-all
|
|
28
|
+
* `ifOtherSymbol`), returns the per-tape match kind for the
|
|
29
|
+
* **alternative that actually matched** given `currentSymbols`:
|
|
30
|
+
* `'wildcard'` if that tape position was `ifOtherSymbol` in the winning
|
|
31
|
+
* alternative, `'literal'` otherwise. Length always equals the tape
|
|
32
|
+
* count.
|
|
33
|
+
*
|
|
34
|
+
* Used by `TuringMachine.runStepByStep` to populate
|
|
35
|
+
* `MachineState.matchedTransition.matchKinds` for #205. The "winning
|
|
36
|
+
* alternative" disambiguation matters for alternations like
|
|
37
|
+
* `[[ifOtherSymbol, 'c'], ['a', 'b']]` — different alternatives can
|
|
38
|
+
* have different per-tape kinds, and only the alternative that matched
|
|
39
|
+
* the current head symbols is meaningful.
|
|
40
|
+
*
|
|
41
|
+
* - `ifOtherSymbol` (the State's catch-all transition fired): all
|
|
42
|
+
* positions are `'wildcard'`.
|
|
43
|
+
* - Symbol with patternList: find the first alternative that matches
|
|
44
|
+
* `currentSymbols` (same predicate as `isMatched`), return its
|
|
45
|
+
* per-position kinds.
|
|
46
|
+
* - Symbol with no winning alternative under the given `currentSymbols`
|
|
47
|
+
* (defensive — shouldn't happen if the caller resolved the Symbol via
|
|
48
|
+
* the State's normal matching): fall back to all `'literal'`.
|
|
49
|
+
*/
|
|
50
|
+
patternKinds(symbol: symbol, currentSymbols?: string[]): ('wildcard' | 'literal')[];
|
|
26
51
|
replaceTape(tape: Tape, tapeIx?: number): void;
|
|
27
52
|
}
|
|
28
53
|
export {};
|
|
@@ -26,6 +26,29 @@ export type MachineState = {
|
|
|
26
26
|
before?: true;
|
|
27
27
|
after?: true;
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* The transition the engine picked for this iter (#205). Always present
|
|
31
|
+
* — `runStepByStep` resolves it at the very start of every iter via
|
|
32
|
+
* `state.getMatchedTransition(symbol)`, well before any callback fires.
|
|
33
|
+
*
|
|
34
|
+
* - `id` — resolvable in `toGraph`'s output: `graph.nodes[…].transitions`
|
|
35
|
+
* contains a `GraphTransition` whose `.id` equals this value. Format is
|
|
36
|
+
* `${stateId}.${transitionIx}`. **For wrapper-entry iters (`state` is
|
|
37
|
+
* produced by `withOverriddenHaltState`): the wrapper's own
|
|
38
|
+
* `transitions` array in `toGraph` is empty because wrappers delegate
|
|
39
|
+
* to the bare; this field carries the BARE's transition id, where the
|
|
40
|
+
* pattern actually lives.** Consumers can detect this case by
|
|
41
|
+
* comparing `id.split('.')[0]` against `state.id` — different = wrapper
|
|
42
|
+
* delegation.
|
|
43
|
+
* - `matchKinds` — per-tape match kind for the picked transition's
|
|
44
|
+
* pattern at each tape position. `'wildcard'` if the matched
|
|
45
|
+
* alternative had `ifOtherSymbol` at that position, `'literal'`
|
|
46
|
+
* otherwise. Length equals tape count.
|
|
47
|
+
*/
|
|
48
|
+
matchedTransition: {
|
|
49
|
+
id: string;
|
|
50
|
+
matchKinds: ('wildcard' | 'literal')[];
|
|
51
|
+
};
|
|
29
52
|
};
|
|
30
53
|
export default class TuringMachine {
|
|
31
54
|
#private;
|
package/dist/index.cjs
CHANGED
|
@@ -655,6 +655,46 @@ class TapeBlock {
|
|
|
655
655
|
.every((everySymbol, ix) => (everySymbol === ifOtherSymbol
|
|
656
656
|
|| everySymbol === currentSymbols[ix])))) ?? false;
|
|
657
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* For a Symbol returned by `this.symbol([...])` (or the catch-all
|
|
660
|
+
* `ifOtherSymbol`), returns the per-tape match kind for the
|
|
661
|
+
* **alternative that actually matched** given `currentSymbols`:
|
|
662
|
+
* `'wildcard'` if that tape position was `ifOtherSymbol` in the winning
|
|
663
|
+
* alternative, `'literal'` otherwise. Length always equals the tape
|
|
664
|
+
* count.
|
|
665
|
+
*
|
|
666
|
+
* Used by `TuringMachine.runStepByStep` to populate
|
|
667
|
+
* `MachineState.matchedTransition.matchKinds` for #205. The "winning
|
|
668
|
+
* alternative" disambiguation matters for alternations like
|
|
669
|
+
* `[[ifOtherSymbol, 'c'], ['a', 'b']]` — different alternatives can
|
|
670
|
+
* have different per-tape kinds, and only the alternative that matched
|
|
671
|
+
* the current head symbols is meaningful.
|
|
672
|
+
*
|
|
673
|
+
* - `ifOtherSymbol` (the State's catch-all transition fired): all
|
|
674
|
+
* positions are `'wildcard'`.
|
|
675
|
+
* - Symbol with patternList: find the first alternative that matches
|
|
676
|
+
* `currentSymbols` (same predicate as `isMatched`), return its
|
|
677
|
+
* per-position kinds.
|
|
678
|
+
* - Symbol with no winning alternative under the given `currentSymbols`
|
|
679
|
+
* (defensive — shouldn't happen if the caller resolved the Symbol via
|
|
680
|
+
* the State's normal matching): fall back to all `'literal'`.
|
|
681
|
+
*/
|
|
682
|
+
patternKinds(symbol, currentSymbols = this.currentSymbols) {
|
|
683
|
+
const tapeCount = __classPrivateFieldGet$2(this, _TapeBlock_tapes, "f").length;
|
|
684
|
+
if (symbol === ifOtherSymbol) {
|
|
685
|
+
return Array.from({ length: tapeCount }, () => 'wildcard');
|
|
686
|
+
}
|
|
687
|
+
const patternList = __classPrivateFieldGet$2(this, _TapeBlock_symbolToPatternListMap, "f").get(symbol);
|
|
688
|
+
if (patternList === undefined) {
|
|
689
|
+
return Array.from({ length: tapeCount }, () => 'literal');
|
|
690
|
+
}
|
|
691
|
+
const winning = patternList.find((pattern) => (pattern.every((everySymbol, ix) => (everySymbol === ifOtherSymbol
|
|
692
|
+
|| everySymbol === currentSymbols[ix]))));
|
|
693
|
+
if (winning === undefined) {
|
|
694
|
+
return Array.from({ length: tapeCount }, () => 'literal');
|
|
695
|
+
}
|
|
696
|
+
return winning.map((everySymbol) => (everySymbol === ifOtherSymbol ? 'wildcard' : 'literal'));
|
|
697
|
+
}
|
|
658
698
|
replaceTape(tape, tapeIx = 0) {
|
|
659
699
|
if (__classPrivateFieldGet$2(this, _TapeBlock_tapes, "f")[tapeIx] == null) {
|
|
660
700
|
throw new Error('invalid tapeIx');
|
|
@@ -789,7 +829,14 @@ function toGraph(initialState, tapeBlock) {
|
|
|
789
829
|
movement: decodeMovement(tc.movement.description),
|
|
790
830
|
})),
|
|
791
831
|
nextStateId: targetInternal.id,
|
|
792
|
-
id: `${
|
|
832
|
+
// Transition id format: `${stateId}.${transitionIx}` (#205).
|
|
833
|
+
// Matches `TuringMachine.runStepByStep`'s `MachineState.
|
|
834
|
+
// matchedTransition.id` so consumers can do
|
|
835
|
+
// `graph.nodes[stateId].transitions.find(t => t.id === id)`.
|
|
836
|
+
// Was `${stateId}-${ix}` pre-#205 — the `.` separator avoids
|
|
837
|
+
// the hyphen reading as a minus sign next to negative halt-
|
|
838
|
+
// marker ids in adjacent contexts.
|
|
839
|
+
id: `${stateInternal.id}.${patternIx}`,
|
|
793
840
|
});
|
|
794
841
|
queue.push(target);
|
|
795
842
|
patternIx += 1;
|
|
@@ -827,6 +874,9 @@ function toGraph(initialState, tapeBlock) {
|
|
|
827
874
|
// `nodes[id]` is always populated for `id` that the BFS reached, so
|
|
828
875
|
// a defensive `!node` check would be dead. `isHalt` / `isWrapper`
|
|
829
876
|
// are real boundaries — both stop reach-set expansion.
|
|
877
|
+
/* c8 ignore next 3 — defensive: the push site below already filters
|
|
878
|
+
halt/wrapper targets, and the initial push is always a bare, so
|
|
879
|
+
this branch is unreachable in practice. */
|
|
830
880
|
if (node.isHalt || node.isWrapper) {
|
|
831
881
|
continue;
|
|
832
882
|
}
|
|
@@ -1071,7 +1121,8 @@ function fromGraph(graph) {
|
|
|
1071
1121
|
* instance + per-pattern Symbol references for breakpoint setup (#195).
|
|
1072
1122
|
*
|
|
1073
1123
|
* **Positional alignment contract.** For any `GraphTransition` whose id
|
|
1074
|
-
* is `${N}
|
|
1124
|
+
* is `${N}.${K}` (#205 changed the separator from `-` to `.`),
|
|
1125
|
+
* `result.get(N)!.transitionSymbols[K]` is the Symbol
|
|
1075
1126
|
* the transition fires on (reference equality, not structural). The K-th
|
|
1076
1127
|
* entry is the K-th key from the source State's `#symbolToDataMap` in
|
|
1077
1128
|
* insertion order, including `ifOtherSymbol` when the user wrote one.
|
|
@@ -1082,7 +1133,7 @@ function fromGraph(graph) {
|
|
|
1082
1133
|
* when a transition's `nextState` is an unresolved `Reference` (it
|
|
1083
1134
|
* `continue`s without pushing the GraphTransition). In that case
|
|
1084
1135
|
* `transitionSymbols[K]` is still set to the K-th Map key, but no
|
|
1085
|
-
* `Graph.nodes[N].transitions` entry exists with id `${N}
|
|
1136
|
+
* `Graph.nodes[N].transitions` entry exists with id `${N}.${K}`. Sparse
|
|
1086
1137
|
* on the Graph side, dense on the `transitionSymbols` side — same
|
|
1087
1138
|
* indexing.
|
|
1088
1139
|
*
|
|
@@ -1170,7 +1221,7 @@ function collectStates(initialState, tapeBlock) {
|
|
|
1170
1221
|
}
|
|
1171
1222
|
// Regular or bare State — enumerate `#symbolToDataMap.keys()` for
|
|
1172
1223
|
// the patternIx alignment. The K-th key is the Symbol that
|
|
1173
|
-
// `${id}
|
|
1224
|
+
// `${id}.${K}` GraphTransition fires on (positional contract).
|
|
1174
1225
|
const state = stateById.get(id);
|
|
1175
1226
|
const transitionSymbols = [...state[STATE_INTERNAL]().symbolToDataMap.keys()];
|
|
1176
1227
|
result.set(id, { state, transitionSymbols });
|
|
@@ -1196,7 +1247,7 @@ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) |
|
|
|
1196
1247
|
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");
|
|
1197
1248
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
1198
1249
|
};
|
|
1199
|
-
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef, _State_tags;
|
|
1250
|
+
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;
|
|
1200
1251
|
const ifOtherSymbol = Symbol('other symbol');
|
|
1201
1252
|
// Module-private symbol used by DebugConfig setters to call State's validator
|
|
1202
1253
|
// without exposing the validator on the public surface.
|
|
@@ -1262,6 +1313,7 @@ class DebugConfig {
|
|
|
1262
1313
|
_DebugConfig_ownerState = new WeakMap(), _DebugConfig_before = new WeakMap(), _DebugConfig_after = new WeakMap();
|
|
1263
1314
|
class State {
|
|
1264
1315
|
constructor(stateDefinition = null, name) {
|
|
1316
|
+
_State_instances.add(this);
|
|
1265
1317
|
_State_id.set(this, id(this));
|
|
1266
1318
|
// Not `readonly` because `withOverriddenHaltState` and `fromGraph` set the
|
|
1267
1319
|
// composed name on a no-arg `new State()` to bypass the constructor's
|
|
@@ -1279,6 +1331,14 @@ class State {
|
|
|
1279
1331
|
// Note: toGraph / fromGraph deliberately do not serialize debug — debug is
|
|
1280
1332
|
// a runtime concern, not part of the structural graph.
|
|
1281
1333
|
_State_debugRef.set(this, { current: null });
|
|
1334
|
+
// Storage for `haltState.debug` (#207). haltState is a singleton terminal
|
|
1335
|
+
// state — it has no iter of its own, so the per-side `{ before, after }`
|
|
1336
|
+
// DebugConfig shape doesn't model anything meaningful for it. Instead the
|
|
1337
|
+
// halt breakpoint is a single boolean ("enabled / disabled"). The pause
|
|
1338
|
+
// anchors on the iter whose transition LEADS to halt, fired at end-of-iter
|
|
1339
|
+
// (after that iter's own after-pause if armed). Only used when `isHalt`;
|
|
1340
|
+
// ignored on every other State (whose `#debugRef` flow is unchanged).
|
|
1341
|
+
_State_haltDebug.set(this, false);
|
|
1282
1342
|
// Out-of-band tags applied to this State (#186). Tags are visualization
|
|
1283
1343
|
// and debugger-tooling metadata — they don't affect runtime transition
|
|
1284
1344
|
// lookup or `equivalentOn` comparisons. Stored as a Set for de-duplication;
|
|
@@ -1349,6 +1409,17 @@ class State {
|
|
|
1349
1409
|
return this;
|
|
1350
1410
|
}
|
|
1351
1411
|
get debug() {
|
|
1412
|
+
// haltState (#207): the canonical access path is the `haltState` singleton
|
|
1413
|
+
// export, which is typed `HaltState` — its `debug` getter is narrowed to
|
|
1414
|
+
// `boolean`. Generic `State` references statically see `DebugConfig` and
|
|
1415
|
+
// (in practice) never refer to haltState — the run loop's `state` is
|
|
1416
|
+
// never haltState because halt is terminal and doesn't iterate. The cast
|
|
1417
|
+
// below makes the runtime boolean return type-compatible with the
|
|
1418
|
+
// declared `DebugConfig` for any rare caller that holds a State
|
|
1419
|
+
// reference happening to be haltState.
|
|
1420
|
+
if (this.isHalt) {
|
|
1421
|
+
return __classPrivateFieldGet$1(this, _State_haltDebug, "f");
|
|
1422
|
+
}
|
|
1352
1423
|
// Lazy-init: `state.debug` is never null at read time, so chained writes
|
|
1353
1424
|
// like `state.debug.before = true` work on a fresh state without a prior
|
|
1354
1425
|
// whole-object assignment. The setter still accepts `null` to reset the
|
|
@@ -1359,16 +1430,47 @@ class State {
|
|
|
1359
1430
|
}
|
|
1360
1431
|
return __classPrivateFieldGet$1(this, _State_debugRef, "f").current;
|
|
1361
1432
|
}
|
|
1433
|
+
// TS signature: non-halt callers (generic `State` reference) get the
|
|
1434
|
+
// `DebugConfig | object | null` surface; boolean is rejected statically.
|
|
1435
|
+
// The `HaltState` typed alias on the singleton export overrides this to
|
|
1436
|
+
// `boolean | null` for the canonical halt access path. Runtime checks
|
|
1437
|
+
// below are defensive against type-bypass / mixed-source callers.
|
|
1362
1438
|
set debug(value) {
|
|
1363
|
-
|
|
1439
|
+
// Defensive runtime cast: TS signature excludes boolean for the generic
|
|
1440
|
+
// State surface, but haltState (via the HaltState alias) DOES accept
|
|
1441
|
+
// boolean, and the runtime needs to handle it for the singleton path.
|
|
1442
|
+
const v = value;
|
|
1443
|
+
// haltState (#207): only `boolean | null` is accepted. `null` aliases
|
|
1444
|
+
// to `false` (reset). Any object-shaped write throws at write-time so
|
|
1445
|
+
// misuse surfaces immediately rather than silently no-op'ing — the
|
|
1446
|
+
// `{before, after}` shape doesn't model anything meaningful for halt
|
|
1447
|
+
// (no own iter to anchor on; halt is terminal).
|
|
1448
|
+
if (this.isHalt) {
|
|
1449
|
+
if (v === null || typeof v === 'boolean') {
|
|
1450
|
+
__classPrivateFieldSet$1(this, _State_haltDebug, v === true, "f");
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
throw new Error('haltState.debug only accepts boolean (or null to reset). Use '
|
|
1454
|
+
+ '`haltState.debug = true` to enable the halt breakpoint, false to '
|
|
1455
|
+
+ 'disable. The pause fires after the iter whose transition leads to '
|
|
1456
|
+
+ 'halt (post-iter, before halt processing).');
|
|
1457
|
+
}
|
|
1458
|
+
// Non-halt states: boolean writes are rejected — the per-side
|
|
1459
|
+
// `{before, after}` granularity is the contract. A boolean shortcut
|
|
1460
|
+
// would hide the asymmetry between before / after.
|
|
1461
|
+
if (typeof v === 'boolean') {
|
|
1462
|
+
throw new Error('state.debug only accepts a DebugConfig or `{ before, after }` object '
|
|
1463
|
+
+ '(or null to reset). Boolean assignment is reserved for `haltState`.');
|
|
1464
|
+
}
|
|
1465
|
+
if (v === null) {
|
|
1364
1466
|
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = null;
|
|
1365
1467
|
return;
|
|
1366
1468
|
}
|
|
1367
|
-
if (
|
|
1368
|
-
__classPrivateFieldGet$1(this, _State_debugRef, "f").current =
|
|
1469
|
+
if (v instanceof DebugConfig) {
|
|
1470
|
+
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = v;
|
|
1369
1471
|
return;
|
|
1370
1472
|
}
|
|
1371
|
-
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this,
|
|
1473
|
+
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, v);
|
|
1372
1474
|
}
|
|
1373
1475
|
/**
|
|
1374
1476
|
* Add one or more tags to this State (#186). Tags are out-of-band metadata
|
|
@@ -1400,25 +1502,15 @@ class State {
|
|
|
1400
1502
|
get tags() {
|
|
1401
1503
|
return Object.freeze([...__classPrivateFieldGet$1(this, _State_tags, "f")]);
|
|
1402
1504
|
}
|
|
1403
|
-
/** @internal — invoked by DebugConfig setters via module-private symbol.
|
|
1404
|
-
|
|
1505
|
+
/** @internal — invoked by DebugConfig setters via module-private symbol.
|
|
1506
|
+
* Per #207, haltState no longer flows through DebugConfig (its `debug`
|
|
1507
|
+
* setter rejects object writes before construction), so the validator
|
|
1508
|
+
* only sees non-halt states here. */
|
|
1509
|
+
[(_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) {
|
|
1405
1510
|
if (filter === undefined)
|
|
1406
1511
|
return;
|
|
1407
|
-
// #108 part 2: `.after` on haltState has no semantic anchor — halt is
|
|
1408
|
-
// terminal, so there is no iteration-after-halt for an after-fire to
|
|
1409
|
-
// attach to. Reject any truthy assignment (true OR list) at write time
|
|
1410
|
-
// so misuse surfaces immediately rather than silently no-op'ing.
|
|
1411
|
-
if (this.isHalt && fieldName === 'after') {
|
|
1412
|
-
throw new Error('haltState.debug.after is not supported: halt is terminal, so there is '
|
|
1413
|
-
+ 'no iteration-after-halt for an after-fire to anchor on. Use '
|
|
1414
|
-
+ '{ before: true } to pause on halt entry.');
|
|
1415
|
-
}
|
|
1416
1512
|
if (filter === true)
|
|
1417
1513
|
return;
|
|
1418
|
-
// haltState has no own transitions; symbol-list filters on `before` are
|
|
1419
|
-
// silent no-ops at the engine level (spec §8.6), so accept any list shape.
|
|
1420
|
-
if (this.isHalt)
|
|
1421
|
-
return;
|
|
1422
1514
|
for (const sym of filter) {
|
|
1423
1515
|
if (sym !== ifOtherSymbol && !__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").has(sym)) {
|
|
1424
1516
|
throw new Error(`State.debug.${fieldName}: symbol is not a transition key of this state `
|
|
@@ -1437,16 +1529,40 @@ class State {
|
|
|
1437
1529
|
return ifOtherSymbol;
|
|
1438
1530
|
}
|
|
1439
1531
|
getCommand(symbol) {
|
|
1440
|
-
|
|
1441
|
-
return __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").get(symbol).command;
|
|
1442
|
-
}
|
|
1443
|
-
throw new Error(`No command for symbol at state named ${__classPrivateFieldGet$1(this, _State_name, "f")}`);
|
|
1532
|
+
return __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol).command;
|
|
1444
1533
|
}
|
|
1445
1534
|
getNextState(symbol) {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1535
|
+
return __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol).nextState;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Like `getNextState`, but also returns the matched Symbol and its index
|
|
1539
|
+
* in this State's transition declaration order (= the `K` in `toGraph`'s
|
|
1540
|
+
* `${stateId}.${K}` transition ids). Used by `TuringMachine.runStepByStep`
|
|
1541
|
+
* to populate `MachineState.matchedTransition` for #205 — exposes which
|
|
1542
|
+
* transition fired so consumers (UIs, log tools, coverage maps) can
|
|
1543
|
+
* resolve the firing edge without re-deriving from `(source, nextState)`,
|
|
1544
|
+
* which is ambiguous when multiple transitions on the same source go to
|
|
1545
|
+
* the same destination.
|
|
1546
|
+
*
|
|
1547
|
+
* Throws (matching `getNextState` / `getCommand`) when no entry exists for
|
|
1548
|
+
* the symbol. For wrappers (states produced by `withOverriddenHaltState`):
|
|
1549
|
+
* the symbol-to-data map is shared with the bare via `bareState`, so the
|
|
1550
|
+
* returned `ix` is a valid position into BOTH the wrapper's and the
|
|
1551
|
+
* bare's transition iteration order — they're the same map.
|
|
1552
|
+
*/
|
|
1553
|
+
getMatchedTransition(symbol) {
|
|
1554
|
+
const entry = __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol);
|
|
1555
|
+
// Iteration order on a Map is insertion order; index lookup is O(N),
|
|
1556
|
+
// acceptable since this fires at most once per iter and N (transitions
|
|
1557
|
+
// per state) is typically tiny. If hot-path measurement ever flags it,
|
|
1558
|
+
// cache as `#symbolToIxMap` mirror.
|
|
1559
|
+
let ix = 0;
|
|
1560
|
+
for (const key of __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").keys()) {
|
|
1561
|
+
if (key === symbol)
|
|
1562
|
+
break;
|
|
1563
|
+
ix += 1;
|
|
1564
|
+
}
|
|
1565
|
+
return { nextState: entry.nextState, matchedSymbol: symbol, ix };
|
|
1450
1566
|
}
|
|
1451
1567
|
withOverriddenHaltState(overriddenHaltState) {
|
|
1452
1568
|
// Unwrap `this` if it's itself a wrapper — the chain's inner overrides
|
|
@@ -1509,7 +1625,13 @@ class State {
|
|
|
1509
1625
|
* read what they need. Adding fields here is a deliberate decision —
|
|
1510
1626
|
* each adds to the implicit contract sibling modules can rely on.
|
|
1511
1627
|
*/
|
|
1512
|
-
[
|
|
1628
|
+
[(_State_getEntry = function _State_getEntry(symbol) {
|
|
1629
|
+
const entry = __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").get(symbol);
|
|
1630
|
+
if (entry === undefined) {
|
|
1631
|
+
throw new Error(`No transition for symbol at state named ${__classPrivateFieldGet$1(this, _State_name, "f")}`);
|
|
1632
|
+
}
|
|
1633
|
+
return entry;
|
|
1634
|
+
}, STATE_INTERNAL)]() {
|
|
1513
1635
|
// Aliasing `this` so the nested object-literal getters/setters below
|
|
1514
1636
|
// can read/write the enclosing State's private fields — getters in an
|
|
1515
1637
|
// object literal can't be arrow functions, so the standard arrow-
|
|
@@ -1678,14 +1800,43 @@ class TuringMachine {
|
|
|
1678
1800
|
i += 1;
|
|
1679
1801
|
const symbol = state.getSymbol(__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f"));
|
|
1680
1802
|
const command = state.getCommand(symbol);
|
|
1681
|
-
|
|
1803
|
+
const matched = state.getMatchedTransition(symbol);
|
|
1804
|
+
let nextState = matched.nextState.ref;
|
|
1805
|
+
// For wrapper-entry iters, the wrapper's transitions in `toGraph`
|
|
1806
|
+
// are empty (wrappers delegate to the bare via shared
|
|
1807
|
+
// `#symbolToDataMap`); the resolvable transition id lives under
|
|
1808
|
+
// the bare's stateId. `bareState` is non-null only when `state`
|
|
1809
|
+
// is a wrapper produced by `withOverriddenHaltState`. Accessed
|
|
1810
|
+
// via the STATE_INTERNAL package-private view (same pattern
|
|
1811
|
+
// `utilities/stateGraph.ts` uses) to avoid widening the public
|
|
1812
|
+
// State API for this internal need.
|
|
1813
|
+
const stateInternal = state[STATE_INTERNAL]();
|
|
1814
|
+
const resolvableStateId = stateInternal.bareState?.id ?? state.id;
|
|
1815
|
+
const matchedTransition = {
|
|
1816
|
+
id: `${resolvableStateId}.${matched.ix}`,
|
|
1817
|
+
matchKinds: __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").patternKinds(matched.matchedSymbol),
|
|
1818
|
+
};
|
|
1682
1819
|
try {
|
|
1683
1820
|
// Both before and after refer to THIS iter (#119 / v6.0.0).
|
|
1684
1821
|
// The halting iter's after-fire just rides along on the iter's
|
|
1685
1822
|
// own yield — no post-loop drain needed.
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1823
|
+
//
|
|
1824
|
+
// #207: `haltState.debug` is now a boolean, and pauses on the
|
|
1825
|
+
// halt-triggering iter's AFTER side (not before). The previous
|
|
1826
|
+
// before-side check (`nextState.debug?.before === true`) was
|
|
1827
|
+
// "early-warning" timing — the user paused before the halt-bound
|
|
1828
|
+
// transition fired, then had to mentally re-derive what would
|
|
1829
|
+
// happen. Now the pause anchors post-step (after the iter's own
|
|
1830
|
+
// after-pause if armed), so consumers see the just-fired halt-
|
|
1831
|
+
// bound transition + diagram cursor still on the triggering state.
|
|
1832
|
+
//
|
|
1833
|
+
// `state` here is always non-halt (halt is terminal — the run
|
|
1834
|
+
// loop never iterates with state === haltState), so `state.debug`
|
|
1835
|
+
// is always `DebugConfig` at runtime. The public getter's return
|
|
1836
|
+
// type matches that.
|
|
1837
|
+
const beforeMatch = matchFilter(state.debug?.before, symbol);
|
|
1838
|
+
const afterMatch = matchFilter(state.debug?.after, symbol)
|
|
1839
|
+
|| (nextState === haltState && haltState.debug);
|
|
1689
1840
|
const nextStateForYield = nextState.isHalt && stack.length
|
|
1690
1841
|
? stack.slice(-1)[0]
|
|
1691
1842
|
: nextState;
|
|
@@ -1708,6 +1859,7 @@ class TuringMachine {
|
|
|
1708
1859
|
}),
|
|
1709
1860
|
movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
|
|
1710
1861
|
nextState: nextStateForYield,
|
|
1862
|
+
matchedTransition,
|
|
1711
1863
|
};
|
|
1712
1864
|
if (beforeMatch || afterMatch) {
|
|
1713
1865
|
const dbg = {};
|
|
@@ -1851,6 +2003,8 @@ function unescapeMermaidLabel(s) {
|
|
|
1851
2003
|
const n = Number.parseInt(hex, 16);
|
|
1852
2004
|
return n <= 0xFFFF ? String.fromCharCode(n) : String.fromCodePoint(n);
|
|
1853
2005
|
}
|
|
2006
|
+
/* c8 ignore next 2 — defensive: the regex shape guarantees one of
|
|
2007
|
+
named / dec / hex is always set, so this fallback is unreachable. */
|
|
1854
2008
|
return match;
|
|
1855
2009
|
}
|
|
1856
2010
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -653,6 +653,46 @@ class TapeBlock {
|
|
|
653
653
|
.every((everySymbol, ix) => (everySymbol === ifOtherSymbol
|
|
654
654
|
|| everySymbol === currentSymbols[ix])))) ?? false;
|
|
655
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* For a Symbol returned by `this.symbol([...])` (or the catch-all
|
|
658
|
+
* `ifOtherSymbol`), returns the per-tape match kind for the
|
|
659
|
+
* **alternative that actually matched** given `currentSymbols`:
|
|
660
|
+
* `'wildcard'` if that tape position was `ifOtherSymbol` in the winning
|
|
661
|
+
* alternative, `'literal'` otherwise. Length always equals the tape
|
|
662
|
+
* count.
|
|
663
|
+
*
|
|
664
|
+
* Used by `TuringMachine.runStepByStep` to populate
|
|
665
|
+
* `MachineState.matchedTransition.matchKinds` for #205. The "winning
|
|
666
|
+
* alternative" disambiguation matters for alternations like
|
|
667
|
+
* `[[ifOtherSymbol, 'c'], ['a', 'b']]` — different alternatives can
|
|
668
|
+
* have different per-tape kinds, and only the alternative that matched
|
|
669
|
+
* the current head symbols is meaningful.
|
|
670
|
+
*
|
|
671
|
+
* - `ifOtherSymbol` (the State's catch-all transition fired): all
|
|
672
|
+
* positions are `'wildcard'`.
|
|
673
|
+
* - Symbol with patternList: find the first alternative that matches
|
|
674
|
+
* `currentSymbols` (same predicate as `isMatched`), return its
|
|
675
|
+
* per-position kinds.
|
|
676
|
+
* - Symbol with no winning alternative under the given `currentSymbols`
|
|
677
|
+
* (defensive — shouldn't happen if the caller resolved the Symbol via
|
|
678
|
+
* the State's normal matching): fall back to all `'literal'`.
|
|
679
|
+
*/
|
|
680
|
+
patternKinds(symbol, currentSymbols = this.currentSymbols) {
|
|
681
|
+
const tapeCount = __classPrivateFieldGet$2(this, _TapeBlock_tapes, "f").length;
|
|
682
|
+
if (symbol === ifOtherSymbol) {
|
|
683
|
+
return Array.from({ length: tapeCount }, () => 'wildcard');
|
|
684
|
+
}
|
|
685
|
+
const patternList = __classPrivateFieldGet$2(this, _TapeBlock_symbolToPatternListMap, "f").get(symbol);
|
|
686
|
+
if (patternList === undefined) {
|
|
687
|
+
return Array.from({ length: tapeCount }, () => 'literal');
|
|
688
|
+
}
|
|
689
|
+
const winning = patternList.find((pattern) => (pattern.every((everySymbol, ix) => (everySymbol === ifOtherSymbol
|
|
690
|
+
|| everySymbol === currentSymbols[ix]))));
|
|
691
|
+
if (winning === undefined) {
|
|
692
|
+
return Array.from({ length: tapeCount }, () => 'literal');
|
|
693
|
+
}
|
|
694
|
+
return winning.map((everySymbol) => (everySymbol === ifOtherSymbol ? 'wildcard' : 'literal'));
|
|
695
|
+
}
|
|
656
696
|
replaceTape(tape, tapeIx = 0) {
|
|
657
697
|
if (__classPrivateFieldGet$2(this, _TapeBlock_tapes, "f")[tapeIx] == null) {
|
|
658
698
|
throw new Error('invalid tapeIx');
|
|
@@ -787,7 +827,14 @@ function toGraph(initialState, tapeBlock) {
|
|
|
787
827
|
movement: decodeMovement(tc.movement.description),
|
|
788
828
|
})),
|
|
789
829
|
nextStateId: targetInternal.id,
|
|
790
|
-
id: `${
|
|
830
|
+
// Transition id format: `${stateId}.${transitionIx}` (#205).
|
|
831
|
+
// Matches `TuringMachine.runStepByStep`'s `MachineState.
|
|
832
|
+
// matchedTransition.id` so consumers can do
|
|
833
|
+
// `graph.nodes[stateId].transitions.find(t => t.id === id)`.
|
|
834
|
+
// Was `${stateId}-${ix}` pre-#205 — the `.` separator avoids
|
|
835
|
+
// the hyphen reading as a minus sign next to negative halt-
|
|
836
|
+
// marker ids in adjacent contexts.
|
|
837
|
+
id: `${stateInternal.id}.${patternIx}`,
|
|
791
838
|
});
|
|
792
839
|
queue.push(target);
|
|
793
840
|
patternIx += 1;
|
|
@@ -825,6 +872,9 @@ function toGraph(initialState, tapeBlock) {
|
|
|
825
872
|
// `nodes[id]` is always populated for `id` that the BFS reached, so
|
|
826
873
|
// a defensive `!node` check would be dead. `isHalt` / `isWrapper`
|
|
827
874
|
// are real boundaries — both stop reach-set expansion.
|
|
875
|
+
/* c8 ignore next 3 — defensive: the push site below already filters
|
|
876
|
+
halt/wrapper targets, and the initial push is always a bare, so
|
|
877
|
+
this branch is unreachable in practice. */
|
|
828
878
|
if (node.isHalt || node.isWrapper) {
|
|
829
879
|
continue;
|
|
830
880
|
}
|
|
@@ -1069,7 +1119,8 @@ function fromGraph(graph) {
|
|
|
1069
1119
|
* instance + per-pattern Symbol references for breakpoint setup (#195).
|
|
1070
1120
|
*
|
|
1071
1121
|
* **Positional alignment contract.** For any `GraphTransition` whose id
|
|
1072
|
-
* is `${N}
|
|
1122
|
+
* is `${N}.${K}` (#205 changed the separator from `-` to `.`),
|
|
1123
|
+
* `result.get(N)!.transitionSymbols[K]` is the Symbol
|
|
1073
1124
|
* the transition fires on (reference equality, not structural). The K-th
|
|
1074
1125
|
* entry is the K-th key from the source State's `#symbolToDataMap` in
|
|
1075
1126
|
* insertion order, including `ifOtherSymbol` when the user wrote one.
|
|
@@ -1080,7 +1131,7 @@ function fromGraph(graph) {
|
|
|
1080
1131
|
* when a transition's `nextState` is an unresolved `Reference` (it
|
|
1081
1132
|
* `continue`s without pushing the GraphTransition). In that case
|
|
1082
1133
|
* `transitionSymbols[K]` is still set to the K-th Map key, but no
|
|
1083
|
-
* `Graph.nodes[N].transitions` entry exists with id `${N}
|
|
1134
|
+
* `Graph.nodes[N].transitions` entry exists with id `${N}.${K}`. Sparse
|
|
1084
1135
|
* on the Graph side, dense on the `transitionSymbols` side — same
|
|
1085
1136
|
* indexing.
|
|
1086
1137
|
*
|
|
@@ -1168,7 +1219,7 @@ function collectStates(initialState, tapeBlock) {
|
|
|
1168
1219
|
}
|
|
1169
1220
|
// Regular or bare State — enumerate `#symbolToDataMap.keys()` for
|
|
1170
1221
|
// the patternIx alignment. The K-th key is the Symbol that
|
|
1171
|
-
// `${id}
|
|
1222
|
+
// `${id}.${K}` GraphTransition fires on (positional contract).
|
|
1172
1223
|
const state = stateById.get(id);
|
|
1173
1224
|
const transitionSymbols = [...state[STATE_INTERNAL]().symbolToDataMap.keys()];
|
|
1174
1225
|
result.set(id, { state, transitionSymbols });
|
|
@@ -1194,7 +1245,7 @@ var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) |
|
|
|
1194
1245
|
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");
|
|
1195
1246
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
1196
1247
|
};
|
|
1197
|
-
var _DebugConfig_ownerState, _DebugConfig_before, _DebugConfig_after, _a, _State_wrapperCache, _State_id, _State_name, _State_overriddenHaltState, _State_bareState, _State_symbolToDataMap, _State_debugRef, _State_tags;
|
|
1248
|
+
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;
|
|
1198
1249
|
const ifOtherSymbol = Symbol('other symbol');
|
|
1199
1250
|
// Module-private symbol used by DebugConfig setters to call State's validator
|
|
1200
1251
|
// without exposing the validator on the public surface.
|
|
@@ -1260,6 +1311,7 @@ class DebugConfig {
|
|
|
1260
1311
|
_DebugConfig_ownerState = new WeakMap(), _DebugConfig_before = new WeakMap(), _DebugConfig_after = new WeakMap();
|
|
1261
1312
|
class State {
|
|
1262
1313
|
constructor(stateDefinition = null, name) {
|
|
1314
|
+
_State_instances.add(this);
|
|
1263
1315
|
_State_id.set(this, id(this));
|
|
1264
1316
|
// Not `readonly` because `withOverriddenHaltState` and `fromGraph` set the
|
|
1265
1317
|
// composed name on a no-arg `new State()` to bypass the constructor's
|
|
@@ -1277,6 +1329,14 @@ class State {
|
|
|
1277
1329
|
// Note: toGraph / fromGraph deliberately do not serialize debug — debug is
|
|
1278
1330
|
// a runtime concern, not part of the structural graph.
|
|
1279
1331
|
_State_debugRef.set(this, { current: null });
|
|
1332
|
+
// Storage for `haltState.debug` (#207). haltState is a singleton terminal
|
|
1333
|
+
// state — it has no iter of its own, so the per-side `{ before, after }`
|
|
1334
|
+
// DebugConfig shape doesn't model anything meaningful for it. Instead the
|
|
1335
|
+
// halt breakpoint is a single boolean ("enabled / disabled"). The pause
|
|
1336
|
+
// anchors on the iter whose transition LEADS to halt, fired at end-of-iter
|
|
1337
|
+
// (after that iter's own after-pause if armed). Only used when `isHalt`;
|
|
1338
|
+
// ignored on every other State (whose `#debugRef` flow is unchanged).
|
|
1339
|
+
_State_haltDebug.set(this, false);
|
|
1280
1340
|
// Out-of-band tags applied to this State (#186). Tags are visualization
|
|
1281
1341
|
// and debugger-tooling metadata — they don't affect runtime transition
|
|
1282
1342
|
// lookup or `equivalentOn` comparisons. Stored as a Set for de-duplication;
|
|
@@ -1347,6 +1407,17 @@ class State {
|
|
|
1347
1407
|
return this;
|
|
1348
1408
|
}
|
|
1349
1409
|
get debug() {
|
|
1410
|
+
// haltState (#207): the canonical access path is the `haltState` singleton
|
|
1411
|
+
// export, which is typed `HaltState` — its `debug` getter is narrowed to
|
|
1412
|
+
// `boolean`. Generic `State` references statically see `DebugConfig` and
|
|
1413
|
+
// (in practice) never refer to haltState — the run loop's `state` is
|
|
1414
|
+
// never haltState because halt is terminal and doesn't iterate. The cast
|
|
1415
|
+
// below makes the runtime boolean return type-compatible with the
|
|
1416
|
+
// declared `DebugConfig` for any rare caller that holds a State
|
|
1417
|
+
// reference happening to be haltState.
|
|
1418
|
+
if (this.isHalt) {
|
|
1419
|
+
return __classPrivateFieldGet$1(this, _State_haltDebug, "f");
|
|
1420
|
+
}
|
|
1350
1421
|
// Lazy-init: `state.debug` is never null at read time, so chained writes
|
|
1351
1422
|
// like `state.debug.before = true` work on a fresh state without a prior
|
|
1352
1423
|
// whole-object assignment. The setter still accepts `null` to reset the
|
|
@@ -1357,16 +1428,47 @@ class State {
|
|
|
1357
1428
|
}
|
|
1358
1429
|
return __classPrivateFieldGet$1(this, _State_debugRef, "f").current;
|
|
1359
1430
|
}
|
|
1431
|
+
// TS signature: non-halt callers (generic `State` reference) get the
|
|
1432
|
+
// `DebugConfig | object | null` surface; boolean is rejected statically.
|
|
1433
|
+
// The `HaltState` typed alias on the singleton export overrides this to
|
|
1434
|
+
// `boolean | null` for the canonical halt access path. Runtime checks
|
|
1435
|
+
// below are defensive against type-bypass / mixed-source callers.
|
|
1360
1436
|
set debug(value) {
|
|
1361
|
-
|
|
1437
|
+
// Defensive runtime cast: TS signature excludes boolean for the generic
|
|
1438
|
+
// State surface, but haltState (via the HaltState alias) DOES accept
|
|
1439
|
+
// boolean, and the runtime needs to handle it for the singleton path.
|
|
1440
|
+
const v = value;
|
|
1441
|
+
// haltState (#207): only `boolean | null` is accepted. `null` aliases
|
|
1442
|
+
// to `false` (reset). Any object-shaped write throws at write-time so
|
|
1443
|
+
// misuse surfaces immediately rather than silently no-op'ing — the
|
|
1444
|
+
// `{before, after}` shape doesn't model anything meaningful for halt
|
|
1445
|
+
// (no own iter to anchor on; halt is terminal).
|
|
1446
|
+
if (this.isHalt) {
|
|
1447
|
+
if (v === null || typeof v === 'boolean') {
|
|
1448
|
+
__classPrivateFieldSet$1(this, _State_haltDebug, v === true, "f");
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
throw new Error('haltState.debug only accepts boolean (or null to reset). Use '
|
|
1452
|
+
+ '`haltState.debug = true` to enable the halt breakpoint, false to '
|
|
1453
|
+
+ 'disable. The pause fires after the iter whose transition leads to '
|
|
1454
|
+
+ 'halt (post-iter, before halt processing).');
|
|
1455
|
+
}
|
|
1456
|
+
// Non-halt states: boolean writes are rejected — the per-side
|
|
1457
|
+
// `{before, after}` granularity is the contract. A boolean shortcut
|
|
1458
|
+
// would hide the asymmetry between before / after.
|
|
1459
|
+
if (typeof v === 'boolean') {
|
|
1460
|
+
throw new Error('state.debug only accepts a DebugConfig or `{ before, after }` object '
|
|
1461
|
+
+ '(or null to reset). Boolean assignment is reserved for `haltState`.');
|
|
1462
|
+
}
|
|
1463
|
+
if (v === null) {
|
|
1362
1464
|
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = null;
|
|
1363
1465
|
return;
|
|
1364
1466
|
}
|
|
1365
|
-
if (
|
|
1366
|
-
__classPrivateFieldGet$1(this, _State_debugRef, "f").current =
|
|
1467
|
+
if (v instanceof DebugConfig) {
|
|
1468
|
+
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = v;
|
|
1367
1469
|
return;
|
|
1368
1470
|
}
|
|
1369
|
-
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this,
|
|
1471
|
+
__classPrivateFieldGet$1(this, _State_debugRef, "f").current = new DebugConfig(this, v);
|
|
1370
1472
|
}
|
|
1371
1473
|
/**
|
|
1372
1474
|
* Add one or more tags to this State (#186). Tags are out-of-band metadata
|
|
@@ -1398,25 +1500,15 @@ class State {
|
|
|
1398
1500
|
get tags() {
|
|
1399
1501
|
return Object.freeze([...__classPrivateFieldGet$1(this, _State_tags, "f")]);
|
|
1400
1502
|
}
|
|
1401
|
-
/** @internal — invoked by DebugConfig setters via module-private symbol.
|
|
1402
|
-
|
|
1503
|
+
/** @internal — invoked by DebugConfig setters via module-private symbol.
|
|
1504
|
+
* Per #207, haltState no longer flows through DebugConfig (its `debug`
|
|
1505
|
+
* setter rejects object writes before construction), so the validator
|
|
1506
|
+
* only sees non-halt states here. */
|
|
1507
|
+
[(_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) {
|
|
1403
1508
|
if (filter === undefined)
|
|
1404
1509
|
return;
|
|
1405
|
-
// #108 part 2: `.after` on haltState has no semantic anchor — halt is
|
|
1406
|
-
// terminal, so there is no iteration-after-halt for an after-fire to
|
|
1407
|
-
// attach to. Reject any truthy assignment (true OR list) at write time
|
|
1408
|
-
// so misuse surfaces immediately rather than silently no-op'ing.
|
|
1409
|
-
if (this.isHalt && fieldName === 'after') {
|
|
1410
|
-
throw new Error('haltState.debug.after is not supported: halt is terminal, so there is '
|
|
1411
|
-
+ 'no iteration-after-halt for an after-fire to anchor on. Use '
|
|
1412
|
-
+ '{ before: true } to pause on halt entry.');
|
|
1413
|
-
}
|
|
1414
1510
|
if (filter === true)
|
|
1415
1511
|
return;
|
|
1416
|
-
// haltState has no own transitions; symbol-list filters on `before` are
|
|
1417
|
-
// silent no-ops at the engine level (spec §8.6), so accept any list shape.
|
|
1418
|
-
if (this.isHalt)
|
|
1419
|
-
return;
|
|
1420
1512
|
for (const sym of filter) {
|
|
1421
1513
|
if (sym !== ifOtherSymbol && !__classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").has(sym)) {
|
|
1422
1514
|
throw new Error(`State.debug.${fieldName}: symbol is not a transition key of this state `
|
|
@@ -1435,16 +1527,40 @@ class State {
|
|
|
1435
1527
|
return ifOtherSymbol;
|
|
1436
1528
|
}
|
|
1437
1529
|
getCommand(symbol) {
|
|
1438
|
-
|
|
1439
|
-
return __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").get(symbol).command;
|
|
1440
|
-
}
|
|
1441
|
-
throw new Error(`No command for symbol at state named ${__classPrivateFieldGet$1(this, _State_name, "f")}`);
|
|
1530
|
+
return __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol).command;
|
|
1442
1531
|
}
|
|
1443
1532
|
getNextState(symbol) {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1533
|
+
return __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol).nextState;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Like `getNextState`, but also returns the matched Symbol and its index
|
|
1537
|
+
* in this State's transition declaration order (= the `K` in `toGraph`'s
|
|
1538
|
+
* `${stateId}.${K}` transition ids). Used by `TuringMachine.runStepByStep`
|
|
1539
|
+
* to populate `MachineState.matchedTransition` for #205 — exposes which
|
|
1540
|
+
* transition fired so consumers (UIs, log tools, coverage maps) can
|
|
1541
|
+
* resolve the firing edge without re-deriving from `(source, nextState)`,
|
|
1542
|
+
* which is ambiguous when multiple transitions on the same source go to
|
|
1543
|
+
* the same destination.
|
|
1544
|
+
*
|
|
1545
|
+
* Throws (matching `getNextState` / `getCommand`) when no entry exists for
|
|
1546
|
+
* the symbol. For wrappers (states produced by `withOverriddenHaltState`):
|
|
1547
|
+
* the symbol-to-data map is shared with the bare via `bareState`, so the
|
|
1548
|
+
* returned `ix` is a valid position into BOTH the wrapper's and the
|
|
1549
|
+
* bare's transition iteration order — they're the same map.
|
|
1550
|
+
*/
|
|
1551
|
+
getMatchedTransition(symbol) {
|
|
1552
|
+
const entry = __classPrivateFieldGet$1(this, _State_instances, "m", _State_getEntry).call(this, symbol);
|
|
1553
|
+
// Iteration order on a Map is insertion order; index lookup is O(N),
|
|
1554
|
+
// acceptable since this fires at most once per iter and N (transitions
|
|
1555
|
+
// per state) is typically tiny. If hot-path measurement ever flags it,
|
|
1556
|
+
// cache as `#symbolToIxMap` mirror.
|
|
1557
|
+
let ix = 0;
|
|
1558
|
+
for (const key of __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").keys()) {
|
|
1559
|
+
if (key === symbol)
|
|
1560
|
+
break;
|
|
1561
|
+
ix += 1;
|
|
1562
|
+
}
|
|
1563
|
+
return { nextState: entry.nextState, matchedSymbol: symbol, ix };
|
|
1448
1564
|
}
|
|
1449
1565
|
withOverriddenHaltState(overriddenHaltState) {
|
|
1450
1566
|
// Unwrap `this` if it's itself a wrapper — the chain's inner overrides
|
|
@@ -1507,7 +1623,13 @@ class State {
|
|
|
1507
1623
|
* read what they need. Adding fields here is a deliberate decision —
|
|
1508
1624
|
* each adds to the implicit contract sibling modules can rely on.
|
|
1509
1625
|
*/
|
|
1510
|
-
[
|
|
1626
|
+
[(_State_getEntry = function _State_getEntry(symbol) {
|
|
1627
|
+
const entry = __classPrivateFieldGet$1(this, _State_symbolToDataMap, "f").get(symbol);
|
|
1628
|
+
if (entry === undefined) {
|
|
1629
|
+
throw new Error(`No transition for symbol at state named ${__classPrivateFieldGet$1(this, _State_name, "f")}`);
|
|
1630
|
+
}
|
|
1631
|
+
return entry;
|
|
1632
|
+
}, STATE_INTERNAL)]() {
|
|
1511
1633
|
// Aliasing `this` so the nested object-literal getters/setters below
|
|
1512
1634
|
// can read/write the enclosing State's private fields — getters in an
|
|
1513
1635
|
// object literal can't be arrow functions, so the standard arrow-
|
|
@@ -1676,14 +1798,43 @@ class TuringMachine {
|
|
|
1676
1798
|
i += 1;
|
|
1677
1799
|
const symbol = state.getSymbol(__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f"));
|
|
1678
1800
|
const command = state.getCommand(symbol);
|
|
1679
|
-
|
|
1801
|
+
const matched = state.getMatchedTransition(symbol);
|
|
1802
|
+
let nextState = matched.nextState.ref;
|
|
1803
|
+
// For wrapper-entry iters, the wrapper's transitions in `toGraph`
|
|
1804
|
+
// are empty (wrappers delegate to the bare via shared
|
|
1805
|
+
// `#symbolToDataMap`); the resolvable transition id lives under
|
|
1806
|
+
// the bare's stateId. `bareState` is non-null only when `state`
|
|
1807
|
+
// is a wrapper produced by `withOverriddenHaltState`. Accessed
|
|
1808
|
+
// via the STATE_INTERNAL package-private view (same pattern
|
|
1809
|
+
// `utilities/stateGraph.ts` uses) to avoid widening the public
|
|
1810
|
+
// State API for this internal need.
|
|
1811
|
+
const stateInternal = state[STATE_INTERNAL]();
|
|
1812
|
+
const resolvableStateId = stateInternal.bareState?.id ?? state.id;
|
|
1813
|
+
const matchedTransition = {
|
|
1814
|
+
id: `${resolvableStateId}.${matched.ix}`,
|
|
1815
|
+
matchKinds: __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").patternKinds(matched.matchedSymbol),
|
|
1816
|
+
};
|
|
1680
1817
|
try {
|
|
1681
1818
|
// Both before and after refer to THIS iter (#119 / v6.0.0).
|
|
1682
1819
|
// The halting iter's after-fire just rides along on the iter's
|
|
1683
1820
|
// own yield — no post-loop drain needed.
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1821
|
+
//
|
|
1822
|
+
// #207: `haltState.debug` is now a boolean, and pauses on the
|
|
1823
|
+
// halt-triggering iter's AFTER side (not before). The previous
|
|
1824
|
+
// before-side check (`nextState.debug?.before === true`) was
|
|
1825
|
+
// "early-warning" timing — the user paused before the halt-bound
|
|
1826
|
+
// transition fired, then had to mentally re-derive what would
|
|
1827
|
+
// happen. Now the pause anchors post-step (after the iter's own
|
|
1828
|
+
// after-pause if armed), so consumers see the just-fired halt-
|
|
1829
|
+
// bound transition + diagram cursor still on the triggering state.
|
|
1830
|
+
//
|
|
1831
|
+
// `state` here is always non-halt (halt is terminal — the run
|
|
1832
|
+
// loop never iterates with state === haltState), so `state.debug`
|
|
1833
|
+
// is always `DebugConfig` at runtime. The public getter's return
|
|
1834
|
+
// type matches that.
|
|
1835
|
+
const beforeMatch = matchFilter(state.debug?.before, symbol);
|
|
1836
|
+
const afterMatch = matchFilter(state.debug?.after, symbol)
|
|
1837
|
+
|| (nextState === haltState && haltState.debug);
|
|
1687
1838
|
const nextStateForYield = nextState.isHalt && stack.length
|
|
1688
1839
|
? stack.slice(-1)[0]
|
|
1689
1840
|
: nextState;
|
|
@@ -1706,6 +1857,7 @@ class TuringMachine {
|
|
|
1706
1857
|
}),
|
|
1707
1858
|
movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
|
|
1708
1859
|
nextState: nextStateForYield,
|
|
1860
|
+
matchedTransition,
|
|
1709
1861
|
};
|
|
1710
1862
|
if (beforeMatch || afterMatch) {
|
|
1711
1863
|
const dbg = {};
|
|
@@ -1849,6 +2001,8 @@ function unescapeMermaidLabel(s) {
|
|
|
1849
2001
|
const n = Number.parseInt(hex, 16);
|
|
1850
2002
|
return n <= 0xFFFF ? String.fromCharCode(n) : String.fromCodePoint(n);
|
|
1851
2003
|
}
|
|
2004
|
+
/* c8 ignore next 2 — defensive: the regex shape guarantees one of
|
|
2005
|
+
named / dec / hex is always set, so this fallback is unreachable. */
|
|
1852
2006
|
return match;
|
|
1853
2007
|
}
|
|
1854
2008
|
}
|
|
@@ -60,7 +60,8 @@ export type StateMap = Map<number, StateMapEntry>;
|
|
|
60
60
|
* instance + per-pattern Symbol references for breakpoint setup (#195).
|
|
61
61
|
*
|
|
62
62
|
* **Positional alignment contract.** For any `GraphTransition` whose id
|
|
63
|
-
* is `${N}
|
|
63
|
+
* is `${N}.${K}` (#205 changed the separator from `-` to `.`),
|
|
64
|
+
* `result.get(N)!.transitionSymbols[K]` is the Symbol
|
|
64
65
|
* the transition fires on (reference equality, not structural). The K-th
|
|
65
66
|
* entry is the K-th key from the source State's `#symbolToDataMap` in
|
|
66
67
|
* insertion order, including `ifOtherSymbol` when the user wrote one.
|
|
@@ -71,7 +72,7 @@ export type StateMap = Map<number, StateMapEntry>;
|
|
|
71
72
|
* when a transition's `nextState` is an unresolved `Reference` (it
|
|
72
73
|
* `continue`s without pushing the GraphTransition). In that case
|
|
73
74
|
* `transitionSymbols[K]` is still set to the K-th Map key, but no
|
|
74
|
-
* `Graph.nodes[N].transitions` entry exists with id `${N}
|
|
75
|
+
* `Graph.nodes[N].transitions` entry exists with id `${N}.${K}`. Sparse
|
|
75
76
|
* on the Graph side, dense on the `transitionSymbols` side — same
|
|
76
77
|
* indexing.
|
|
77
78
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turing-machine-js/machine",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.5",
|
|
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": "
|
|
41
|
+
"gitHead": "4838b823c5b93f74a783d8652746787436b431e4"
|
|
42
42
|
}
|