@turing-machine-js/machine 2.0.1 → 3.0.0

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 ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
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
+
7
+ ## [3.0.0] - 2026-04-30
8
+
9
+ ### Added
10
+
11
+ - **`State.toGraph(state, tapeBlock)`** static — walks the reachable graph from a state and returns a serializable `Graph` (states, transitions, alphabets).
12
+ - **`State.fromGraph(graph)`** static — inverse of `toGraph`; rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips losslessly via `toMermaid` / `fromMermaid`.
13
+ - **`State.inspect(state)`** static — single-state introspection (id, name, isHalt, override-halt target, transitions) without graph traversal or a tapeBlock.
14
+ - **`toMermaid(graph)`** — renders a `Graph` to Mermaid flowchart syntax.
15
+ - **`fromMermaid(text)`** — parses Mermaid produced by `toMermaid` back into a `Graph`.
16
+ - **`summarize(state, tapeBlock)`** / **`summarizeGraph(graph)`** — quantitative analysis of a state graph (state count, transition count, composition depth, cycles, alphabet sizes). Useful for comparing two implementations of the same algorithm.
17
+ - **`equivalentOn(reference, candidate, cases, options?)`** — behavioral equivalence checking. Runs both machines on test cases and reports agreement, first-divergence step, and per-side step counts. Supports same-alphabet and (with custom comparator) cross-alphabet comparison.
18
+ - New type exports: `Graph`, `GraphNode`, `GraphTransition`, `GraphCommand`, `GraphSummary`, `Runnable`, `EquivalenceCase`, `EquivalenceResult`, `EquivalenceReport`.
19
+
20
+ ### Changed
21
+
22
+ - TypeScript `target` and `module` raised from `ES6` to `ES2020` (consumers see compiled `dist/` only — no observable difference).
23
+
24
+ ### Removed
25
+
26
+ - **BREAKING** — the `./src` subpath in `package.json` `exports` was removed. Consumers using `import { ... } from '@turing-machine-js/machine/src'` must drop the `/src` suffix and use `import { ... } from '@turing-machine-js/machine'`.
27
+
28
+ ### Migration
29
+
30
+ ```diff
31
+ - import { ... } from '@turing-machine-js/machine/src';
32
+ + import { ... } from '@turing-machine-js/machine';
33
+ ```
34
+
35
+ The `/src` subpath was an in-monorepo dev-time shim that never had a real reason to be on the npm tarball.
36
+
37
+ ## [2.0.2] - earlier
38
+
39
+ Initial public 2.x release.
package/README.md CHANGED
@@ -13,48 +13,292 @@ Using npm:
13
13
  npm install @turing-machine-js/machine
14
14
  ```
15
15
 
16
+ ## Quick start
17
+
18
+ Replace every `b` on the tape with `*`:
19
+
20
+ ```javascript
21
+ import {
22
+ Alphabet,
23
+ State,
24
+ Tape,
25
+ TapeBlock,
26
+ TuringMachine,
27
+ haltState,
28
+ ifOtherSymbol,
29
+ movements,
30
+ } from '@turing-machine-js/machine';
31
+
32
+ const alphabet = new Alphabet([' ', 'a', 'b', 'c', '*']);
33
+ const tape = new Tape({ alphabet, symbols: ['a', 'b', 'c', 'b', 'a'] });
34
+ const tapeBlock = TapeBlock.fromTapes([tape]);
35
+ const machine = new TuringMachine({ tapeBlock });
36
+
37
+ machine.run({
38
+ initialState: new State({
39
+ [tapeBlock.symbol(['b'])]: {
40
+ command: [{ symbol: '*', movement: movements.right }],
41
+ },
42
+ [tapeBlock.symbol([alphabet.blankSymbol])]: {
43
+ command: [{ movement: movements.left }],
44
+ nextState: haltState,
45
+ },
46
+ [ifOtherSymbol]: {
47
+ command: [{ movement: movements.right }],
48
+ },
49
+ }),
50
+ });
51
+
52
+ console.log(tape.symbols.join('').trim()); // a*c*a
53
+ ```
54
+
55
+ 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
+
57
+ 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'`.
58
+
59
+ ## Building from a state table
60
+
61
+ If you prefer a textbook-style declarative API where every transition is one row of `(state, currentSymbol) → (nextState, nextSymbol, movement)`, you can build a small helper on top of the raw API. The whole thing fits in ~30 lines:
62
+
63
+ ```javascript
64
+ import {
65
+ Alphabet,
66
+ Reference,
67
+ State,
68
+ TapeBlock,
69
+ TuringMachine,
70
+ haltState,
71
+ ifOtherSymbol,
72
+ movements,
73
+ symbolCommands,
74
+ } from '@turing-machine-js/machine';
75
+
76
+ function buildFromTable({ alphabetString, initialState, finalStates, table }) {
77
+ const alphabet = new Alphabet(alphabetString.split(''));
78
+ const tapeBlock = TapeBlock.fromAlphabets([alphabet]);
79
+ const movementOf = { L: movements.left, R: movements.right, S: movements.stay };
80
+
81
+ // Pre-create a Reference per state name so transitions can point forward.
82
+ const refs = Object.fromEntries(Object.keys(table).map((name) => [name, new Reference()]));
83
+ const states = {};
84
+
85
+ for (const [name, row] of Object.entries(table)) {
86
+ const def = {};
87
+ for (const [read, action] of Object.entries(row)) {
88
+ const key = read === '*' ? ifOtherSymbol : tapeBlock.symbol([read]);
89
+ def[key] = {
90
+ command: {
91
+ symbol: action.write ?? symbolCommands.keep,
92
+ movement: movementOf[action.move ?? 'S'],
93
+ },
94
+ nextState: finalStates.includes(action.goto) ? haltState : refs[action.goto],
95
+ };
96
+ }
97
+ states[name] = new State(def, name);
98
+ refs[name].bind(states[name]);
99
+ }
100
+
101
+ return { tapeBlock, machine: new TuringMachine({ tapeBlock }), initialState: states[initialState] };
102
+ }
103
+
104
+ // Same "replace b with *" example as above, written declaratively:
105
+ const { tapeBlock, machine, initialState } = buildFromTable({
106
+ alphabetString: ' abc*',
107
+ initialState: 'scan',
108
+ finalStates: ['HALT'],
109
+ table: {
110
+ scan: {
111
+ 'b': { write: '*', move: 'R', goto: 'scan' },
112
+ ' ': { move: 'L', goto: 'HALT' },
113
+ '*': { move: 'R', goto: 'scan' }, // '*' = ifOtherSymbol
114
+ },
115
+ },
116
+ });
117
+ ```
118
+
119
+ This is what [`@turing-machine-js/builder`](../builder) provides as a separate package. Inline lets you tweak the format (multi-tape, OR-patterns, custom action shapes) freely; the builder package is more opinionated and limited to single-tape, single-symbol-per-row transitions.
120
+
16
121
  ## Classes
17
122
 
18
123
  ### Alphabet
19
124
 
20
- ### Command
21
-
22
- ### Reference
125
+ The set of single-character symbols a tape can hold. The **first** symbol passed to the constructor is the blank — it fills any tape cell the head reaches before that cell has been written. At least two unique single-character symbols are required.
23
126
 
24
- ### State
127
+ ```javascript
128
+ const alphabet = new Alphabet([' ', '0', '1']);
129
+ alphabet.blankSymbol; // ' '
130
+ alphabet.symbols; // [' ', '0', '1']
131
+ alphabet.has('0'); // true
132
+ alphabet.index('1'); // 2
133
+ ```
25
134
 
26
135
  ### Tape
27
136
 
137
+ An infinite-in-both-directions sequence of cells over an `Alphabet`, plus a head position. Cells the head moves into that haven't been written are blank.
138
+
139
+ ```javascript
140
+ const tape = new Tape({ alphabet, symbols: ['a', 'b', 'c'], position: 0 });
141
+ tape.symbol; // 'a' (cell under head)
142
+ tape.right(); // move head right; auto-extends with blanks at the edge
143
+ tape.symbol = 'X'; // write the cell under head
144
+ ```
145
+
28
146
  ### TapeBlock
29
147
 
148
+ A bundle of one or more `Tape`s that the machine reads/writes together in lock-step. Construct via either factory:
149
+
150
+ ```javascript
151
+ TapeBlock.fromAlphabets([alphabetA, alphabetB]); // creates fresh blank tapes
152
+ TapeBlock.fromTapes([tape1, tape2]); // reuses existing tapes
153
+ ```
154
+
155
+ The key method is **`tapeBlock.symbol(pattern)`**: it returns an interned JS `Symbol` that simultaneously serves as a `State`'s transition key *and* matches the current configuration across all tapes. The pattern is one alphabet character per tape; pass several patterns by concatenating to express alternatives.
156
+
157
+ ```javascript
158
+ tapeBlock.symbol(['^']); // single tape: matches '^'
159
+ tapeBlock.symbol(['^', '0', '1', '$']); // single tape: matches any of '^', '0', '1', '$'
160
+ tapeBlock.symbol(['0', 'a']); // 2 tapes: matches when tape 1 is '0' AND tape 2 is 'a'
161
+ ```
162
+
30
163
  ### TapeCommand
31
164
 
165
+ A single-tape instruction the machine applies in one step: optionally write a symbol, optionally move the head. Defaults to *keep current symbol, do not move*.
166
+
167
+ ```javascript
168
+ const cmds = [
169
+ { symbol: '0', movement: movements.right }, // write '0' and move right
170
+ { movement: movements.left }, // keep current symbol, move left
171
+ { symbol: symbolCommands.erase }, // write the blank, stay
172
+ {}, // no-op
173
+ ];
174
+ ```
175
+
176
+ You'll rarely construct `TapeCommand` instances yourself — pass plain objects in your `State` definitions and they're wrapped automatically.
177
+
178
+ ### Command
179
+
180
+ A bundle of `TapeCommand`s, one per tape in the `TapeBlock`. Like `TapeCommand`, you usually pass a plain array in the `State` definition rather than constructing `Command` directly.
181
+
182
+ ### State
183
+
184
+ A node in the transition graph. Construct with a definition object whose keys are JS `Symbol`s from `tapeBlock.symbol(...)` (or `ifOtherSymbol` for the catch-all). Each value is `{ command, nextState }`.
185
+
186
+ ```javascript
187
+ const s = new State({
188
+ [tapeBlock.symbol(['1'])]: { command: { symbol: '0', movement: movements.right } },
189
+ [tapeBlock.symbol(['$'])]: { nextState: haltState },
190
+ [ifOtherSymbol]: { command: { movement: movements.right } },
191
+ }, 'name');
192
+ ```
193
+
194
+ Notable members and statics:
195
+
196
+ - **`state.id`**, **`state.name`** — identity (`isHalt` is `id === 0`).
197
+ - **`state.withOverrodeHaltState(other)`** — returns a copy whose would-be halt transitions fall through to `other`. The subroutine-call composition mechanism (see `library-binary-numbers/src/index.ts` for examples).
198
+ - **`State.toGraph(state, tapeBlock)`** — walks the reachable graph from `state` and returns a serializable `Graph` (states, transitions, alphabets).
199
+ - **`State.fromGraph(graph)`** — inverse of `toGraph`: rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips together with `toMermaid` / `fromMermaid`.
200
+
201
+ ### Reference
202
+
203
+ 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.
204
+
205
+ ```javascript
206
+ const ref = new Reference();
207
+ const a = new State({ [symbol(['x'])]: { nextState: ref } }, 'a');
208
+ const b = new State({ [symbol(['y'])]: { nextState: a } }, 'b');
209
+ ref.bind(b); // a's transition now resolves to b at run time
210
+ ```
211
+
212
+ `reference.ref` returns the bound state and throws if the reference is still unbound when the machine runs.
213
+
32
214
  ### TuringMachine
33
215
 
216
+ The runtime. Owns one `TapeBlock` and drives a state graph until it reaches `haltState`.
217
+
218
+ ```javascript
219
+ const machine = new TuringMachine({ tapeBlock });
220
+
221
+ // Run to halt:
222
+ machine.run({ initialState, stepsLimit: 1e5 });
223
+
224
+ // Or step-by-step (useful for visualization / debugging):
225
+ for (const step of machine.runStepByStep({ initialState })) {
226
+ console.log(step.state.name, step.currentSymbols, '→', step.nextSymbols, step.movements);
227
+ }
228
+ ```
229
+
230
+ `stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
231
+
34
232
  ## Special objects
35
233
 
36
234
  ### haltState
37
235
 
38
- A special state for stopping the machine
236
+ A singleton `State` (`id === 0`). Transitioning into it stops the run. Imported as a named export from `@turing-machine-js/machine`; do not construct your own — `state.isHalt` checks identity against this single instance.
39
237
 
40
238
  ### ifOtherSymbol
41
239
 
42
- A special symbol for representing the other symbols in `State` class definition
240
+ A sentinel `Symbol` used as a key in a `State` definition to mean *match any symbol not handled by the other keys* (the fallback transition).
43
241
 
44
242
  ### movements
45
243
 
46
- * left - move the head to the left
47
- * right - move the head to the right
48
- * stay - do not move the head
244
+ Per-tape head movement directives passed in `TapeCommand.movement`:
245
+
246
+ * `movements.left` move the head one cell left
247
+ * `movements.right` — move the head one cell right
248
+ * `movements.stay` — leave the head where it is
49
249
 
50
250
  ### symbolCommands
51
251
 
52
- * erase - write the blank symbol
53
- * keep - leave the current symbol
252
+ Special values for `TapeCommand.symbol`:
253
+
254
+ * `symbolCommands.keep` — leave the current cell unchanged (default)
255
+ * `symbolCommands.erase` — write the alphabet's blank symbol
256
+
257
+ ## Introspection and testing
258
+
259
+ `@turing-machine-js/machine` ships two complementary runtime utilities:
260
+
261
+ **`summarize` / `summarizeGraph`** — *structural* analysis. Looks at the state graph without running it.
262
+
263
+ ```javascript
264
+ import { summarize } from '@turing-machine-js/machine';
265
+
266
+ const stats = summarize(myState, myTapeBlock);
267
+ // {
268
+ // stateCount, transitionCount,
269
+ // compositionEdgeCount, maxCompositionDepth,
270
+ // selfLoopCount, hasCycles,
271
+ // tapeCount, alphabetCardinalities,
272
+ // }
273
+ ```
274
+
275
+ `State.inspect(state)` returns the same kind of data for a single state (transitions, override-halt target, etc.) without traversing the graph.
276
+
277
+ **`equivalentOn`** — *behavioral* comparison. Runs two machines on a list of test inputs and reports whether their outputs agree, where they first diverge, and how many steps each took.
278
+
279
+ ```javascript
280
+ import { equivalentOn } from '@turing-machine-js/machine';
281
+
282
+ const report = equivalentOn(
283
+ { state: referenceState, getTapeBlock: () => referenceTapeBlock.clone() },
284
+ { state: candidateState, getTapeBlock: () => candidateTapeBlock.clone() },
285
+ ['^1$', '^10$', '^11$', '^111$'], // test cases
286
+ );
287
+ // report.allAgree → true | false
288
+ // report.results[i] → { agree, referenceOutput, candidateOutput,
289
+ // referenceSteps, candidateSteps, firstDivergenceStep }
290
+ ```
291
+
292
+ For different alphabets, pass `{ reference, candidate }` paired cases plus a custom output comparator. See [`packages/machine/src/utilities/equivalence.spec.ts`](src/utilities/equivalence.spec.ts) for worked examples.
293
+
294
+ Together: use `summarize` to ask "is this machine the right shape?" (size, composition, cycles), and `equivalentOn` to ask "does this machine compute the right thing?" (correctness against a reference). Useful when comparing two implementations of the same algorithm — e.g., the marker-based and bare binary libraries — or when grading student-written machines against a reference.
295
+
296
+ For visualization and round-tripping, see `State.toGraph` / `State.fromGraph` and `toMermaid` / `fromMermaid`.
54
297
 
55
298
  ## Libraries
56
299
 
57
- - [@turing-machine-js/library-binary-numbers](https://github.com/mellonis/turing-machine-js/tree/master/packages/library-binary-numbers)
300
+ - [@turing-machine-js/library-binary-numbers](https://github.com/mellonis/turing-machine-js/tree/master/packages/library-binary-numbers) — binary arithmetic with `^…$` markers, multi-number-per-tape support
301
+ - [@turing-machine-js/library-binary-numbers-bare](https://github.com/mellonis/turing-machine-js/tree/master/packages/library-binary-numbers-bare) — same operations on a 3-symbol alphabet, single-number-per-tape, much smaller state graphs
58
302
 
59
303
  ## Links
60
304
 
@@ -2,6 +2,7 @@ import Command from './Command';
2
2
  import Reference from './Reference';
3
3
  import TapeBlock from './TapeBlock';
4
4
  import TapeCommand from './TapeCommand';
5
+ import { type Graph } from '../utilities/graph';
5
6
  export declare const ifOtherSymbol: unique symbol;
6
7
  export default class State {
7
8
  #private;
@@ -18,5 +19,31 @@ export default class State {
18
19
  getCommand(symbol: symbol): Command;
19
20
  getNextState(symbol: symbol): State | Reference;
20
21
  withOverrodeHaltState(overrodeHaltState: State): State;
22
+ static inspect(state: State): {
23
+ id: number;
24
+ name: string;
25
+ isHalt: boolean;
26
+ overrodeHaltState: {
27
+ id: number;
28
+ name: string;
29
+ } | null;
30
+ transitions: Array<{
31
+ rawPatternDescription: string | undefined;
32
+ command: Array<{
33
+ symbol: string;
34
+ movement: string;
35
+ }>;
36
+ nextState: {
37
+ id: number;
38
+ name: string;
39
+ } | null;
40
+ }>;
41
+ };
42
+ static toGraph(initialState: State, tapeBlock: TapeBlock): Graph;
43
+ static fromGraph(graph: Graph): {
44
+ start: State;
45
+ tapeBlock: TapeBlock;
46
+ states: Record<number, State>;
47
+ };
21
48
  }
22
49
  export declare const haltState: State;
@@ -10,10 +10,13 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
12
  var _State_id, _State_name, _State_overrodeHaltState, _State_symbolToDataMap;
13
+ import Alphabet from './Alphabet';
13
14
  import Command from './Command';
14
15
  import Reference from './Reference';
16
+ import TapeBlock from './TapeBlock';
15
17
  import TapeCommand from './TapeCommand';
16
18
  import { id } from '../utilities/functions';
19
+ import { decodeMovement, decodePatternDescription, decodeWriteSymbol, parseMovementLabel, parsePatternString, parseWriteSymbolLabel, } from '../utilities/graph';
17
20
  export const ifOtherSymbol = Symbol('other symbol');
18
21
  class State {
19
22
  constructor(stateDefinition = null, name) {
@@ -32,7 +35,7 @@ class State {
32
35
  }
33
36
  symbols.forEach((symbol) => {
34
37
  const { nextState } = stateDefinition[symbol];
35
- const nextStateLocal = nextState !== null && nextState !== void 0 ? nextState : this;
38
+ const nextStateLocal = nextState ?? this;
36
39
  if (!(nextStateLocal instanceof State) && !(nextStateLocal instanceof Reference)) {
37
40
  throw new Error('invalid nextState');
38
41
  }
@@ -63,7 +66,7 @@ class State {
63
66
  });
64
67
  });
65
68
  }
66
- __classPrivateFieldSet(this, _State_name, name !== null && name !== void 0 ? name : `id:${__classPrivateFieldGet(this, _State_id, "f")}`, "f");
69
+ __classPrivateFieldSet(this, _State_name, name ?? `id:${__classPrivateFieldGet(this, _State_id, "f")}`, "f");
67
70
  }
68
71
  get id() {
69
72
  return __classPrivateFieldGet(this, _State_id, "f");
@@ -107,6 +110,173 @@ class State {
107
110
  __classPrivateFieldSet(state, _State_overrodeHaltState, overrodeHaltState, "f");
108
111
  return state;
109
112
  }
113
+ // Single-state introspection — no traversal, no tapeBlock required.
114
+ // Returns id, name, halt-status, override-halt target, and the list of
115
+ // transitions out of this state with decoded write/movement labels.
116
+ // Symbol patterns are returned as the raw description string from the
117
+ // interned JS Symbol (decode via decodePatternDescription if needed).
118
+ static inspect(state) {
119
+ const transitions = [];
120
+ for (const [sym, { command, nextState }] of __classPrivateFieldGet(state, _State_symbolToDataMap, "f")) {
121
+ let target = null;
122
+ try {
123
+ target = nextState instanceof State ? nextState : nextState.ref;
124
+ }
125
+ catch {
126
+ target = null; // unbound Reference
127
+ }
128
+ transitions.push({
129
+ rawPatternDescription: sym.description,
130
+ command: command.tapesCommands.map((tc) => ({
131
+ symbol: decodeWriteSymbol(tc.symbol),
132
+ movement: decodeMovement(tc.movement.description),
133
+ })),
134
+ nextState: target ? { id: target.id, name: target.name } : null,
135
+ });
136
+ }
137
+ return {
138
+ id: __classPrivateFieldGet(state, _State_id, "f"),
139
+ name: __classPrivateFieldGet(state, _State_name, "f"),
140
+ isHalt: state.isHalt,
141
+ overrodeHaltState: __classPrivateFieldGet(state, _State_overrodeHaltState, "f")
142
+ ? { id: __classPrivateFieldGet(state, _State_overrodeHaltState, "f").id, name: __classPrivateFieldGet(state, _State_overrodeHaltState, "f").name }
143
+ : null,
144
+ transitions,
145
+ };
146
+ }
147
+ static toGraph(initialState, tapeBlock) {
148
+ const nodes = {};
149
+ const queue = [initialState];
150
+ const alphabets = tapeBlock.alphabets.map((alphabet) => alphabet.symbols);
151
+ while (queue.length > 0) {
152
+ const current = queue.shift();
153
+ if (__classPrivateFieldGet(current, _State_id, "f") in nodes) {
154
+ continue;
155
+ }
156
+ const node = {
157
+ id: __classPrivateFieldGet(current, _State_id, "f"),
158
+ name: __classPrivateFieldGet(current, _State_name, "f"),
159
+ isHalt: current.isHalt,
160
+ transitions: [],
161
+ overrodeHaltStateId: __classPrivateFieldGet(current, _State_overrodeHaltState, "f")?.id ?? null,
162
+ };
163
+ nodes[__classPrivateFieldGet(current, _State_id, "f")] = node;
164
+ if (__classPrivateFieldGet(current, _State_overrodeHaltState, "f")) {
165
+ queue.push(__classPrivateFieldGet(current, _State_overrodeHaltState, "f"));
166
+ }
167
+ for (const [sym, { command, nextState }] of __classPrivateFieldGet(current, _State_symbolToDataMap, "f")) {
168
+ let target;
169
+ try {
170
+ target = nextState instanceof State ? nextState : nextState.ref;
171
+ }
172
+ catch {
173
+ continue;
174
+ }
175
+ node.transitions.push({
176
+ pattern: decodePatternDescription(sym.description, alphabets),
177
+ command: command.tapesCommands.map((tc) => ({
178
+ symbol: decodeWriteSymbol(tc.symbol),
179
+ movement: decodeMovement(tc.movement.description),
180
+ })),
181
+ nextStateId: target.id,
182
+ });
183
+ queue.push(target);
184
+ }
185
+ }
186
+ return { initialId: __classPrivateFieldGet(initialState, _State_id, "f"), alphabets, nodes };
187
+ }
188
+ // Inverse of toGraph: rebuilds a State graph (and a fresh TapeBlock with the
189
+ // graph's alphabets) from a serialized Graph. Round-trips with toGraph in
190
+ // the sense that running the rebuilt machine on the same input gives the
191
+ // same output, but the rebuilt State instances have *new* internal IDs.
192
+ static fromGraph(graph) {
193
+ const alphabetObjs = graph.alphabets.map((syms) => new Alphabet(syms));
194
+ const tapeBlock = TapeBlock.fromAlphabets(alphabetObjs);
195
+ const ids = Object.keys(graph.nodes).map(Number);
196
+ // Pass 1: pre-create a Reference for each non-halt node so transitions can
197
+ // forward-declare their targets.
198
+ const refs = {};
199
+ for (const nodeId of ids) {
200
+ if (!graph.nodes[nodeId].isHalt) {
201
+ refs[nodeId] = new Reference();
202
+ }
203
+ }
204
+ // Convert a parsed pattern back to the symbol key the State expects.
205
+ const patternToKey = (parsed) => {
206
+ if (parsed === null) {
207
+ return ifOtherSymbol;
208
+ }
209
+ const flat = [];
210
+ for (const row of parsed) {
211
+ for (const cell of row) {
212
+ flat.push(cell === null ? ifOtherSymbol : cell);
213
+ }
214
+ }
215
+ return tapeBlock.symbol(flat);
216
+ };
217
+ // Pass 2: build a "bare" State for each non-halt node (no override yet).
218
+ // nextState entries point at refs so cycles work; haltState is used directly.
219
+ const bareStates = {};
220
+ for (const nodeId of ids) {
221
+ const node = graph.nodes[nodeId];
222
+ if (node.isHalt) {
223
+ continue;
224
+ }
225
+ const stateDefinition = {};
226
+ for (const t of node.transitions) {
227
+ const key = patternToKey(parsePatternString(t.pattern, graph.alphabets));
228
+ const target = graph.nodes[t.nextStateId];
229
+ const nextState = target.isHalt ? haltState : refs[t.nextStateId];
230
+ stateDefinition[key] = {
231
+ command: t.command.map((c) => ({
232
+ symbol: parseWriteSymbolLabel(c.symbol),
233
+ movement: parseMovementLabel(c.movement),
234
+ })),
235
+ nextState,
236
+ };
237
+ }
238
+ bareStates[nodeId] = new State(stateDefinition, node.name);
239
+ }
240
+ // Pass 3: apply overrideHaltStates transitively.
241
+ const finalStates = {};
242
+ const inProgress = new Set();
243
+ const getFinal = (nodeId) => {
244
+ if (finalStates[nodeId]) {
245
+ return finalStates[nodeId];
246
+ }
247
+ const node = graph.nodes[nodeId];
248
+ if (node.isHalt) {
249
+ finalStates[nodeId] = haltState;
250
+ return haltState;
251
+ }
252
+ if (inProgress.has(nodeId)) {
253
+ throw new Error(`override-halt cycle at state #${nodeId}`);
254
+ }
255
+ inProgress.add(nodeId);
256
+ let state = bareStates[nodeId];
257
+ if (node.overrodeHaltStateId !== null) {
258
+ state = bareStates[nodeId].withOverrodeHaltState(getFinal(node.overrodeHaltStateId));
259
+ }
260
+ inProgress.delete(nodeId);
261
+ finalStates[nodeId] = state;
262
+ return state;
263
+ };
264
+ for (const nodeId of ids) {
265
+ getFinal(nodeId);
266
+ }
267
+ // Pass 4: bind each ref to the FINAL (possibly wrapped) state so transitions
268
+ // resolve to the version that has its override-halt set.
269
+ for (const nodeId of ids) {
270
+ if (!graph.nodes[nodeId].isHalt) {
271
+ refs[nodeId].bind(finalStates[nodeId]);
272
+ }
273
+ }
274
+ return {
275
+ start: finalStates[graph.initialId],
276
+ tapeBlock,
277
+ states: finalStates,
278
+ };
279
+ }
110
280
  }
111
281
  _State_id = new WeakMap(), _State_name = new WeakMap(), _State_overrodeHaltState = new WeakMap(), _State_symbolToDataMap = new WeakMap();
112
282
  export default State;
@@ -161,7 +161,6 @@ class TapeBlock {
161
161
  return tapeBlock;
162
162
  }
163
163
  isMatched({ currentSymbols = this.currentSymbols, symbol }) {
164
- var _b;
165
164
  if (symbol === ifOtherSymbol) {
166
165
  return true;
167
166
  }
@@ -169,9 +168,9 @@ class TapeBlock {
169
168
  throw new Error('invalid symbol');
170
169
  }
171
170
  const patternList = __classPrivateFieldGet(this, _TapeBlock_symbolToPatternListMap, "f").get(symbol);
172
- return (_b = patternList === null || patternList === void 0 ? void 0 : patternList.some((pattern) => (pattern
171
+ return patternList?.some((pattern) => (pattern
173
172
  .every((everySymbol, ix) => (everySymbol === ifOtherSymbol
174
- || everySymbol === currentSymbols[ix]))))) !== null && _b !== void 0 ? _b : false;
173
+ || everySymbol === currentSymbols[ix])))) ?? false;
175
174
  }
176
175
  replaceTape(tape, tapeIx = 0) {
177
176
  if (__classPrivateFieldGet(this, _TapeBlock_tapes, "f")[tapeIx] == null) {