@turing-machine-js/machine 6.0.0 → 6.0.1
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/README.md +208 -5
- package/dist/index.cjs +30 -19
- package/dist/index.mjs +30 -19
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -46,12 +46,42 @@ await machine.run({
|
|
|
46
46
|
[ifOtherSymbol]: {
|
|
47
47
|
command: [{ movement: movements.right }],
|
|
48
48
|
},
|
|
49
|
-
}),
|
|
49
|
+
}, 'replaceB'),
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
console.log(tape.symbols.join('').trim()); // a*c*a
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
The state graph for the example above:
|
|
56
|
+
|
|
57
|
+
```mermaid
|
|
58
|
+
flowchart LR
|
|
59
|
+
S(("**replaceB**"))
|
|
60
|
+
H((("**halt**")))
|
|
61
|
+
S -- "b → *, R" --> S
|
|
62
|
+
S -- "_ → keep, L" --> H
|
|
63
|
+
S -- "any other → keep, R" --> S
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
*Reading the labels: `read → write, move`. `_` is the blank symbol.*
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary>📊 Same diagram, generated by <code>toMermaid()</code> (the engine's actual output)</summary>
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart TD
|
|
73
|
+
%% alphabets: [[" ","a","b","c","*"]]
|
|
74
|
+
s0(((halt)))
|
|
75
|
+
s1(("replaceB"))
|
|
76
|
+
s1 -- "b → */R" --> s1
|
|
77
|
+
s1 -- "- → ·/L" --> s0
|
|
78
|
+
s1 -- "* → ·/R" --> s1
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Engine notation: `read → write/move`; `·` = keep, `⌫` = erase, `*` = `ifOtherSymbol` catch-all, `-` = the blank symbol. `(((double-paren)))` = halt; `(("round"))` = the initial state passed to `toGraph`; `["square"]` = a state reachable from the initial state.
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
|
|
55
85
|
A `State` is keyed by JS `Symbol`s returned from `tapeBlock.symbol(pattern)` — the pattern lists the expected symbol under each tape's head. `ifOtherSymbol` is the fallback key when nothing else matches; transitioning into `haltState` stops the run.
|
|
56
86
|
|
|
57
87
|
For multi-tape machines, pass one element per tape: `tapeBlock.symbol(['0', 'a'])` matches only when tape 1 is at `'0'` and tape 2 is at `'a'`.
|
|
@@ -143,6 +173,16 @@ tape.right(); // move head right; auto-extends with blanks at the edge
|
|
|
143
173
|
tape.symbol = 'X'; // write the cell under head
|
|
144
174
|
```
|
|
145
175
|
|
|
176
|
+
For visualization-friendly UIs, `Tape` exposes a fixed-width viewport centered on the head:
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
const tape = new Tape({ alphabet, symbols: ['a', 'b', 'c'], viewportWidth: 7 });
|
|
180
|
+
tape.viewport; // 7-cell snapshot centered on the head, padded with blanks
|
|
181
|
+
tape.viewportWidth; // 7 (the constructor bumps even values to the next odd)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`viewportWidth` defaults to `1` and must be ≥ 1; `tape.viewport` always has exactly `viewportWidth` cells regardless of how many symbols the tape actually holds. Useful for rendering a sliding window in a UI; ignore if you only need `tape.symbols` / `tape.position`.
|
|
185
|
+
|
|
146
186
|
### TapeBlock
|
|
147
187
|
|
|
148
188
|
A bundle of one or more `Tape`s that the machine reads/writes together in lock-step. Construct via either factory:
|
|
@@ -186,8 +226,7 @@ A node in the transition graph. Construct with a definition object whose keys ar
|
|
|
186
226
|
```javascript
|
|
187
227
|
const s = new State({
|
|
188
228
|
[tapeBlock.symbol(['1'])]: { command: { symbol: '0', movement: movements.right } },
|
|
189
|
-
[tapeBlock.symbol(['$'])]: { nextState: haltState },
|
|
190
|
-
[ifOtherSymbol]: { command: { movement: movements.right } },
|
|
229
|
+
[tapeBlock.symbol(['$'])]: { command: { movement: movements.left }, nextState: haltState },
|
|
191
230
|
}, 'name');
|
|
192
231
|
```
|
|
193
232
|
|
|
@@ -198,6 +237,32 @@ Notable members and statics:
|
|
|
198
237
|
- **`State.toGraph(state, tapeBlock)`** — walks the reachable graph from `state` and returns a serializable `Graph` (states, transitions, alphabets).
|
|
199
238
|
- **`State.fromGraph(graph)`** — inverse of `toGraph`: rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips together with `toMermaid` / `fromMermaid`.
|
|
200
239
|
|
|
240
|
+
For visualization, pair `State.toGraph` with `toMermaid` to render the graph in any Mermaid-aware viewer (GitHub, VS Code, mermaid.live):
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
import { State, toMermaid } from '@turing-machine-js/machine';
|
|
244
|
+
|
|
245
|
+
const graph = State.toGraph(s, tapeBlock);
|
|
246
|
+
console.log(toMermaid(graph));
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The string `toMermaid` produces is a real Mermaid flowchart that renders in-place anywhere Mermaid is supported:
|
|
250
|
+
|
|
251
|
+
```mermaid
|
|
252
|
+
flowchart TD
|
|
253
|
+
%% alphabets: [[" ","0","1","$"]]
|
|
254
|
+
s0(((halt)))
|
|
255
|
+
s1(("name"))
|
|
256
|
+
s1 -- "1 → 0/R" --> s1
|
|
257
|
+
s1 -- "$ → ·/L" --> s0
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
*Edge labels are `read → write/move`. `·` is the keep-current-symbol marker (no write); `L` / `R` / `S` are head moves.*
|
|
261
|
+
|
|
262
|
+
> 💡 **Mermaid renders at most one edge per source/target pair.** If a state has two distinct transitions back to itself (or two parallel transitions to the same target), only one shows in the diagram. The string output is correct — this is a viewer-side limitation. For graphs with multiple parallel edges, paste the `toMermaid` output into [mermaid.live](https://mermaid.live) and switch to the `stateDiagram-v2` renderer, or post-process the output to your preferred format.
|
|
263
|
+
|
|
264
|
+
`fromMermaid` parses the same format back into a `Graph`. The round-trip is **behaviorally** lossless — the rebuilt graph runs to the same outputs on the same inputs (tested in `test/round-trip.spec.ts` for the binary-numbers libraries). It is *not* bytewise lossless: state IDs auto-reassign on each rebuild, and for `withOverrodeHaltState` wrappers the composite name gains a `>${override.name}` suffix on each pass (e.g., `scanToX>eraseHere` becomes `scanToX>eraseHere>eraseHere` on a second round-trip — tracked in [#138](https://github.com/mellonis/turing-machine-js/issues/138)).
|
|
265
|
+
|
|
201
266
|
### Reference
|
|
202
267
|
|
|
203
268
|
A forward-declaration handle, used when a `State` needs to point at another `State` that doesn't exist yet (cyclic graphs). Construct unbound, pass as `nextState`, call `.bind(actualState)` once that state has been built.
|
|
@@ -209,7 +274,33 @@ const b = new State({ [symbol(['y'])]: { nextState: a } }, 'b');
|
|
|
209
274
|
ref.bind(b); // a's transition now resolves to b at run time
|
|
210
275
|
```
|
|
211
276
|
|
|
212
|
-
|
|
277
|
+
The resulting cycle:
|
|
278
|
+
|
|
279
|
+
```mermaid
|
|
280
|
+
flowchart LR
|
|
281
|
+
a(("**a**"))
|
|
282
|
+
b(("**b**"))
|
|
283
|
+
a -- "x" --> b
|
|
284
|
+
b -- "y" --> a
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
<details>
|
|
288
|
+
<summary>📊 Same diagram, generated by <code>toMermaid()</code></summary>
|
|
289
|
+
|
|
290
|
+
```mermaid
|
|
291
|
+
flowchart TD
|
|
292
|
+
%% alphabets: [[" ","x","y"]]
|
|
293
|
+
s1(("a"))
|
|
294
|
+
s2["b"]
|
|
295
|
+
s1 -- "x → ·/S" --> s2
|
|
296
|
+
s2 -- "y → ·/S" --> s1
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`a` is round (the initial state passed to `toGraph`); `b` is square (reachable from `a`).
|
|
300
|
+
|
|
301
|
+
</details>
|
|
302
|
+
|
|
303
|
+
`reference.ref` returns the bound state and throws if the reference is still unbound when the machine runs. `bind()` is sticky — the first call wins; subsequent calls are silent no-ops that return the existing binding.
|
|
213
304
|
|
|
214
305
|
### TuringMachine
|
|
215
306
|
|
|
@@ -227,8 +318,118 @@ for (const step of machine.runStepByStep({ initialState })) {
|
|
|
227
318
|
}
|
|
228
319
|
```
|
|
229
320
|
|
|
321
|
+
Each yielded `step` (`MachineState`) has these fields:
|
|
322
|
+
|
|
323
|
+
| Field | Type | Meaning |
|
|
324
|
+
|---|---|---|
|
|
325
|
+
| `step` | `number` | 1-indexed iteration number |
|
|
326
|
+
| `state` | `State` | the state about to execute |
|
|
327
|
+
| `currentSymbols` | `string[]` | per-tape head symbols, before the command applies |
|
|
328
|
+
| `nextSymbols` | `string[]` | per-tape symbols that will be written |
|
|
329
|
+
| `movements` | `symbol[]` | per-tape head moves (`movements.left/right/stay`) |
|
|
330
|
+
| `nextState` | `State` | the state that will execute next |
|
|
331
|
+
| `debugBreak?` | `{ before?: true, after?: true }` | only set when `state.debug` matched on this iter — see *Debugging breakpoints* below |
|
|
332
|
+
|
|
230
333
|
`stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
|
|
231
334
|
|
|
335
|
+
## Subroutine composition with `withOverrodeHaltState`
|
|
336
|
+
|
|
337
|
+
`state.withOverrodeHaltState(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.
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
import { Alphabet, State, TapeBlock, TuringMachine, Tape, haltState, ifOtherSymbol, movements, symbolCommands } from '@turing-machine-js/machine';
|
|
341
|
+
|
|
342
|
+
const alphabet = new Alphabet([' ', 'a', 'b', 'X']);
|
|
343
|
+
const tapeBlock = TapeBlock.fromAlphabets([alphabet]);
|
|
344
|
+
const { symbol } = tapeBlock;
|
|
345
|
+
|
|
346
|
+
// Reusable subroutine 1: walk right until 'X', halt on it.
|
|
347
|
+
const scanToX = new State({
|
|
348
|
+
[symbol(['X'])]: { nextState: haltState },
|
|
349
|
+
[ifOtherSymbol]: { command: { movement: movements.right } },
|
|
350
|
+
}, 'scanToX');
|
|
351
|
+
|
|
352
|
+
// Reusable subroutine 2: erase the head cell, halt.
|
|
353
|
+
const eraseHere = new State({
|
|
354
|
+
[ifOtherSymbol]: { command: { symbol: symbolCommands.erase }, nextState: haltState },
|
|
355
|
+
}, 'eraseHere');
|
|
356
|
+
|
|
357
|
+
// Compose: scan to X, then ERASE it. scanToX is unmodified.
|
|
358
|
+
const scanThenErase = scanToX.withOverrodeHaltState(eraseHere);
|
|
359
|
+
|
|
360
|
+
const tape = new Tape({ alphabet, symbols: ['a', 'b', 'X', 'b', 'a'] });
|
|
361
|
+
tapeBlock.replaceTape(tape);
|
|
362
|
+
await new TuringMachine({ tapeBlock }).run({ initialState: scanThenErase });
|
|
363
|
+
|
|
364
|
+
console.log(tape.symbols.join('')); // "ab ba" — the X at index 2 is gone, head landed there.
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
What changes between *running `scanToX` standalone* and *running the composed wrapper*:
|
|
368
|
+
|
|
369
|
+
```mermaid
|
|
370
|
+
flowchart LR
|
|
371
|
+
subgraph standalone["scanToX (standalone) — halts at X"]
|
|
372
|
+
direction LR
|
|
373
|
+
a1(("scanToX"))
|
|
374
|
+
h1(((halt)))
|
|
375
|
+
a1 -- "X → keep, S" --> h1
|
|
376
|
+
a1 -- "any other → keep, R" --> a1
|
|
377
|
+
end
|
|
378
|
+
subgraph composed["scanToX.withOverrodeHaltState(eraseHere) — halt is intercepted"]
|
|
379
|
+
direction LR
|
|
380
|
+
a2(("scanToX"))
|
|
381
|
+
b2(("eraseHere"))
|
|
382
|
+
h2(((halt)))
|
|
383
|
+
a2 -. "X → keep, S<br/>intercepted" .-> b2
|
|
384
|
+
a2 -- "any other → keep, R" --> a2
|
|
385
|
+
b2 -- "any → erase, S" --> h2
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
<details>
|
|
390
|
+
<summary>📊 Both diagrams, generated by <code>toMermaid()</code></summary>
|
|
391
|
+
|
|
392
|
+
`toMermaid(toGraph(scanToX, tapeBlock))` — the standalone subroutine:
|
|
393
|
+
|
|
394
|
+
```mermaid
|
|
395
|
+
flowchart TD
|
|
396
|
+
%% alphabets: [[" ","a","b","X"]]
|
|
397
|
+
s0(((halt)))
|
|
398
|
+
s1(("scanToX"))
|
|
399
|
+
s1 -- "X → ·/S" --> s0
|
|
400
|
+
s1 -- "* → ·/R" --> s1
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`toMermaid(toGraph(scanThenErase, tapeBlock))` — the wrapped composition:
|
|
404
|
+
|
|
405
|
+
```mermaid
|
|
406
|
+
flowchart TD
|
|
407
|
+
%% alphabets: [[" ","a","b","X"]]
|
|
408
|
+
s0(((halt)))
|
|
409
|
+
s1["scanToX"]
|
|
410
|
+
s2["eraseHere"]
|
|
411
|
+
s3(("scanToX>eraseHere"))
|
|
412
|
+
s1 -- "X → ·/S" --> s0
|
|
413
|
+
s1 -- "* → ·/R" --> s1
|
|
414
|
+
s2 -- "* → ⌫/S" --> s0
|
|
415
|
+
s3 -- "X → ·/S" --> s0
|
|
416
|
+
s3 -- "* → ·/R" --> s1
|
|
417
|
+
s3 -. onHalt .-> s2
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Reading guide** — the wrapped diagram is denser than the simplified hand-drawn version above. To parse it:
|
|
421
|
+
|
|
422
|
+
1. **Three non-halt nodes:** the wrapper `scanToX>eraseHere` (round, the initial state); `scanToX` (square, the original subroutine — unmodified); `eraseHere` (square, the override target). The wrapper appears as a *separate* state from `scanToX`-the-original because `withOverrodeHaltState` returns a new `State` instance — even though it shares the transition map.
|
|
423
|
+
2. **Solid edges from the wrapper duplicate `scanToX`'s edges.** That's because the wrapper inherits the same `symbolToDataMap`. Importantly, the wrapper's `* → ·/R` edge points at *`scanToX`-the-original*, not at the wrapper itself — so after the first iteration, control transfers to `scanToX` and stays there.
|
|
424
|
+
3. **The dotted `onHalt` edge is attached to the wrapper** but it doesn't fire on a single edge at runtime. Instead, the runtime pushes `eraseHere` onto an internal stack at startup, and *any* halt-bound transition reachable during the run (whether on the wrapper itself or on `scanToX`) gets redirected to `eraseHere` via stack-pop. The dotted edge is the engine's static fingerprint of "this graph was wrapped by `withOverrodeHaltState`."
|
|
425
|
+
4. **What actually fires at runtime, on tape `['a','b','X','b','a']`:** the wrapper runs once, transferring to `scanToX`; `scanToX` self-loops on `* → ·/R` until it sees `X`; the `X → ·/S` edge tries to go to halt; the runtime pops `eraseHere` off the stack and substitutes it; `eraseHere` erases the cell and halts. The wrapper's own `X → ·/S → halt` edge in the diagram is *never traversed* because control left the wrapper after iteration 1.
|
|
426
|
+
|
|
427
|
+
> 💡 **The engine's emit could be more user-friendly here.** Tracked in [#138](https://github.com/mellonis/turing-machine-js/issues/138) — the wrapper's duplicated edges and the misleading single-edge `onHalt` placement are candidates for a cleaner `toMermaid` output.
|
|
428
|
+
|
|
429
|
+
</details>
|
|
430
|
+
|
|
431
|
+
Wrappers nest: `inner.withOverrodeHaltState(middle).withOverrodeHaltState(outer)` chains halt-redirects through `middle → outer → halt`. `library-binary-numbers/src/index.ts`'s `minusOne` (the `~(~x + 1)` composition) uses a 4-deep nest of wrappers.
|
|
432
|
+
|
|
232
433
|
## Debugging breakpoints (v4+)
|
|
233
434
|
|
|
234
435
|
Any `State` can carry a runtime-mutable `debug` config that pauses execution at chosen points.
|
|
@@ -254,7 +455,9 @@ haltState.debug = { before: true };
|
|
|
254
455
|
myState.debug = null;
|
|
255
456
|
```
|
|
256
457
|
|
|
257
|
-
|
|
458
|
+
> ⚠️ **`haltState.debug.after` throws.** Halt is terminal — there is no iteration-after-halt for an after-fire to anchor on. Assigning a truthy `.after` to `haltState.debug` (including `{ before: true, after: true }`) throws at write time. Symbol-list filters on `haltState.debug.before` are silent no-ops, since halt has no head symbol; only the wildcard `true` activates.
|
|
459
|
+
|
|
460
|
+
The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. The internal cell is shared with `state.withOverrodeHaltState(...)` wrappers, so an assignment on the original is visible from every wrapper. Plain-object input (`state.debug = { before: true }`) is wrapped in a `DebugConfig` instance automatically; the wrapper's per-property setters validate and freeze the stored array, so `state.debug.before.push(...)` throws `TypeError`.
|
|
258
461
|
|
|
259
462
|
`run()` is async and accepts an `onPause` hook:
|
|
260
463
|
|
package/dist/index.cjs
CHANGED
|
@@ -790,10 +790,21 @@ class State {
|
|
|
790
790
|
}
|
|
791
791
|
/** @internal — invoked by DebugConfig setters via module-private symbol. */
|
|
792
792
|
[(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), validateDebugFilter)](fieldName, filter) {
|
|
793
|
-
if (filter === undefined
|
|
793
|
+
if (filter === undefined)
|
|
794
794
|
return;
|
|
795
|
-
// haltState has no
|
|
796
|
-
//
|
|
795
|
+
// #108 part 2: `.after` on haltState has no semantic anchor — halt is
|
|
796
|
+
// terminal, so there is no iteration-after-halt for an after-fire to
|
|
797
|
+
// attach to. Reject any truthy assignment (true OR list) at write time
|
|
798
|
+
// so misuse surfaces immediately rather than silently no-op'ing.
|
|
799
|
+
if (this.isHalt && fieldName === 'after') {
|
|
800
|
+
throw new Error('haltState.debug.after is not supported: halt is terminal, so there is '
|
|
801
|
+
+ 'no iteration-after-halt for an after-fire to anchor on. Use '
|
|
802
|
+
+ '{ before: true } to pause on halt entry.');
|
|
803
|
+
}
|
|
804
|
+
if (filter === true)
|
|
805
|
+
return;
|
|
806
|
+
// haltState has no own transitions; symbol-list filters on `before` are
|
|
807
|
+
// silent no-ops at the engine level (spec §8.6), so accept any list shape.
|
|
797
808
|
if (this.isHalt)
|
|
798
809
|
return;
|
|
799
810
|
for (const sym of filter) {
|
|
@@ -1035,22 +1046,21 @@ class TuringMachine {
|
|
|
1035
1046
|
get tapeBlock() {
|
|
1036
1047
|
return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
|
|
1037
1048
|
}
|
|
1038
|
-
async run({ initialState, stepsLimit = 1e5, onStep,
|
|
1049
|
+
async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
|
|
1039
1050
|
const generator = this.runStepByStep({ initialState, stepsLimit });
|
|
1040
|
-
let prevYield = null;
|
|
1041
1051
|
for (const machineState of generator) {
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (machineState.debugBreak?.before && onDebugBreak) {
|
|
1048
|
-
await onDebugBreak({ ...machineState, debugBreak: { before: true } });
|
|
1052
|
+
// Per-iter lifecycle: before → step → after. All three operate on the
|
|
1053
|
+
// same yielded MachineState, so the consumer sees a coherent ordering
|
|
1054
|
+
// within each iteration without cross-tick coordination.
|
|
1055
|
+
if (debug && machineState.debugBreak?.before && onPause) {
|
|
1056
|
+
await onPause({ ...machineState, debugBreak: { before: true } });
|
|
1049
1057
|
}
|
|
1050
1058
|
if (onStep instanceof Function) {
|
|
1051
1059
|
onStep(machineState);
|
|
1052
1060
|
}
|
|
1053
|
-
|
|
1061
|
+
if (debug && machineState.debugBreak?.after && onPause) {
|
|
1062
|
+
await onPause({ ...machineState, debugBreak: { after: true } });
|
|
1063
|
+
}
|
|
1054
1064
|
}
|
|
1055
1065
|
}
|
|
1056
1066
|
*runStepByStep({ initialState, stepsLimit = 1e5 }) {
|
|
@@ -1064,7 +1074,6 @@ class TuringMachine {
|
|
|
1064
1074
|
stack.push(state.overrodeHaltState);
|
|
1065
1075
|
}
|
|
1066
1076
|
let i = 0;
|
|
1067
|
-
let pendingAfterFromPrev = false;
|
|
1068
1077
|
while (!state.isHalt) {
|
|
1069
1078
|
if (i === stepsLimit) {
|
|
1070
1079
|
throw new Error('Long execution');
|
|
@@ -1074,8 +1083,12 @@ class TuringMachine {
|
|
|
1074
1083
|
const command = state.getCommand(symbol);
|
|
1075
1084
|
let nextState = state.getNextState(symbol).ref;
|
|
1076
1085
|
try {
|
|
1086
|
+
// Both before and after refer to THIS iter (#119 / v6.0.0).
|
|
1087
|
+
// The halting iter's after-fire just rides along on the iter's
|
|
1088
|
+
// own yield — no post-loop drain needed.
|
|
1077
1089
|
const beforeMatch = matchFilter(state.debug?.before, symbol)
|
|
1078
1090
|
|| (nextState.isHalt && nextState.debug?.before === true);
|
|
1091
|
+
const afterMatch = matchFilter(state.debug?.after, symbol);
|
|
1079
1092
|
const nextStateForYield = nextState.isHalt && stack.length
|
|
1080
1093
|
? stack.slice(-1)[0]
|
|
1081
1094
|
: nextState;
|
|
@@ -1099,17 +1112,15 @@ class TuringMachine {
|
|
|
1099
1112
|
movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
|
|
1100
1113
|
nextState: nextStateForYield,
|
|
1101
1114
|
};
|
|
1102
|
-
if (
|
|
1115
|
+
if (beforeMatch || afterMatch) {
|
|
1103
1116
|
const dbg = {};
|
|
1104
|
-
if (pendingAfterFromPrev)
|
|
1105
|
-
dbg.after = true;
|
|
1106
1117
|
if (beforeMatch)
|
|
1107
1118
|
dbg.before = true;
|
|
1119
|
+
if (afterMatch)
|
|
1120
|
+
dbg.after = true;
|
|
1108
1121
|
yielded.debugBreak = dbg;
|
|
1109
1122
|
}
|
|
1110
1123
|
yield yielded;
|
|
1111
|
-
// Re-evaluate 'after' for THIS visit, to fire on the NEXT yield.
|
|
1112
|
-
pendingAfterFromPrev = matchFilter(state.debug?.after, symbol);
|
|
1113
1124
|
__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
|
|
1114
1125
|
if (nextState.isHalt && stack.length) {
|
|
1115
1126
|
nextState = stack.pop();
|
package/dist/index.mjs
CHANGED
|
@@ -788,10 +788,21 @@ class State {
|
|
|
788
788
|
}
|
|
789
789
|
/** @internal — invoked by DebugConfig setters via module-private symbol. */
|
|
790
790
|
[(_State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap(), _State_debugRef = new WeakMap(), validateDebugFilter)](fieldName, filter) {
|
|
791
|
-
if (filter === undefined
|
|
791
|
+
if (filter === undefined)
|
|
792
792
|
return;
|
|
793
|
-
// haltState has no
|
|
794
|
-
//
|
|
793
|
+
// #108 part 2: `.after` on haltState has no semantic anchor — halt is
|
|
794
|
+
// terminal, so there is no iteration-after-halt for an after-fire to
|
|
795
|
+
// attach to. Reject any truthy assignment (true OR list) at write time
|
|
796
|
+
// so misuse surfaces immediately rather than silently no-op'ing.
|
|
797
|
+
if (this.isHalt && fieldName === 'after') {
|
|
798
|
+
throw new Error('haltState.debug.after is not supported: halt is terminal, so there is '
|
|
799
|
+
+ 'no iteration-after-halt for an after-fire to anchor on. Use '
|
|
800
|
+
+ '{ before: true } to pause on halt entry.');
|
|
801
|
+
}
|
|
802
|
+
if (filter === true)
|
|
803
|
+
return;
|
|
804
|
+
// haltState has no own transitions; symbol-list filters on `before` are
|
|
805
|
+
// silent no-ops at the engine level (spec §8.6), so accept any list shape.
|
|
795
806
|
if (this.isHalt)
|
|
796
807
|
return;
|
|
797
808
|
for (const sym of filter) {
|
|
@@ -1033,22 +1044,21 @@ class TuringMachine {
|
|
|
1033
1044
|
get tapeBlock() {
|
|
1034
1045
|
return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
|
|
1035
1046
|
}
|
|
1036
|
-
async run({ initialState, stepsLimit = 1e5, onStep,
|
|
1047
|
+
async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
|
|
1037
1048
|
const generator = this.runStepByStep({ initialState, stepsLimit });
|
|
1038
|
-
let prevYield = null;
|
|
1039
1049
|
for (const machineState of generator) {
|
|
1040
|
-
//
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if (machineState.debugBreak?.before && onDebugBreak) {
|
|
1046
|
-
await onDebugBreak({ ...machineState, debugBreak: { before: true } });
|
|
1050
|
+
// Per-iter lifecycle: before → step → after. All three operate on the
|
|
1051
|
+
// same yielded MachineState, so the consumer sees a coherent ordering
|
|
1052
|
+
// within each iteration without cross-tick coordination.
|
|
1053
|
+
if (debug && machineState.debugBreak?.before && onPause) {
|
|
1054
|
+
await onPause({ ...machineState, debugBreak: { before: true } });
|
|
1047
1055
|
}
|
|
1048
1056
|
if (onStep instanceof Function) {
|
|
1049
1057
|
onStep(machineState);
|
|
1050
1058
|
}
|
|
1051
|
-
|
|
1059
|
+
if (debug && machineState.debugBreak?.after && onPause) {
|
|
1060
|
+
await onPause({ ...machineState, debugBreak: { after: true } });
|
|
1061
|
+
}
|
|
1052
1062
|
}
|
|
1053
1063
|
}
|
|
1054
1064
|
*runStepByStep({ initialState, stepsLimit = 1e5 }) {
|
|
@@ -1062,7 +1072,6 @@ class TuringMachine {
|
|
|
1062
1072
|
stack.push(state.overrodeHaltState);
|
|
1063
1073
|
}
|
|
1064
1074
|
let i = 0;
|
|
1065
|
-
let pendingAfterFromPrev = false;
|
|
1066
1075
|
while (!state.isHalt) {
|
|
1067
1076
|
if (i === stepsLimit) {
|
|
1068
1077
|
throw new Error('Long execution');
|
|
@@ -1072,8 +1081,12 @@ class TuringMachine {
|
|
|
1072
1081
|
const command = state.getCommand(symbol);
|
|
1073
1082
|
let nextState = state.getNextState(symbol).ref;
|
|
1074
1083
|
try {
|
|
1084
|
+
// Both before and after refer to THIS iter (#119 / v6.0.0).
|
|
1085
|
+
// The halting iter's after-fire just rides along on the iter's
|
|
1086
|
+
// own yield — no post-loop drain needed.
|
|
1075
1087
|
const beforeMatch = matchFilter(state.debug?.before, symbol)
|
|
1076
1088
|
|| (nextState.isHalt && nextState.debug?.before === true);
|
|
1089
|
+
const afterMatch = matchFilter(state.debug?.after, symbol);
|
|
1077
1090
|
const nextStateForYield = nextState.isHalt && stack.length
|
|
1078
1091
|
? stack.slice(-1)[0]
|
|
1079
1092
|
: nextState;
|
|
@@ -1097,17 +1110,15 @@ class TuringMachine {
|
|
|
1097
1110
|
movements: command.tapesCommands.map((tapeCommand) => tapeCommand.movement),
|
|
1098
1111
|
nextState: nextStateForYield,
|
|
1099
1112
|
};
|
|
1100
|
-
if (
|
|
1113
|
+
if (beforeMatch || afterMatch) {
|
|
1101
1114
|
const dbg = {};
|
|
1102
|
-
if (pendingAfterFromPrev)
|
|
1103
|
-
dbg.after = true;
|
|
1104
1115
|
if (beforeMatch)
|
|
1105
1116
|
dbg.before = true;
|
|
1117
|
+
if (afterMatch)
|
|
1118
|
+
dbg.after = true;
|
|
1106
1119
|
yielded.debugBreak = dbg;
|
|
1107
1120
|
}
|
|
1108
1121
|
yield yielded;
|
|
1109
|
-
// Re-evaluate 'after' for THIS visit, to fire on the NEXT yield.
|
|
1110
|
-
pendingAfterFromPrev = matchFilter(state.debug?.after, symbol);
|
|
1111
1122
|
__classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f").applyCommand(command, executionSymbol);
|
|
1112
1123
|
if (nextState.isHalt && stack.length) {
|
|
1113
1124
|
nextState = stack.pop();
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turing-machine-js/machine",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "A convenient Turing machine",
|
|
5
5
|
"engines": {
|
|
6
6
|
"npm": ">=7.0.0"
|
|
7
7
|
},
|
|
8
|
-
"author": "Ruslan Gilmullin <
|
|
8
|
+
"author": "Ruslan Gilmullin <mellonis@yandex.ru>",
|
|
9
9
|
"homepage": "https://github.com/mellonis/turing-machine-js#readme",
|
|
10
10
|
"license": "GPL-3.0-or-later",
|
|
11
11
|
"publishConfig": {
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"default": "./dist/index.mjs"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "d7ebca4e8379a548e3ca97eb83e96387de2cda32"
|
|
38
38
|
}
|