@turing-machine-js/machine 2.0.2 → 3.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/CHANGELOG.md ADDED
@@ -0,0 +1,54 @@
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.1] - 2026-04-30
8
+
9
+ ### Fixed
10
+
11
+ - **`graphFormats.ts` — polynomial-time regex (CodeQL `js/polynomial-redos`).** `alphabetsRegex` was `/^%%\s*alphabets:\s*(.+)$/`, where the `\s*` and `(.+)` both match whitespace, letting the engine try N+1 split points on inputs like `"%%alphabets:"` followed by many trailing spaces. Anchored the first captured char as `\S` to remove the ambiguity.
12
+
13
+ ### Added
14
+
15
+ - **`MachineState` re-export** from `index.ts`. The type was always `export type MachineState = ...` in `classes/TuringMachine.ts`, but the package barrel didn't surface it. Consumers (notably `@post-machine-js/machine`'s `PostMachine` overrides of `run` / `runStepByStep`) can now `import { type MachineState } from '@turing-machine-js/machine'` directly, dropping any local `Generator<infer T>` workaround.
16
+
17
+ ### Changed (internal)
18
+
19
+ - Removed the unreachable `if (hasCycles) return` guard at the top of `summarizeGraph`'s `visit()` cycle-detection. The recursive call pattern (outer for-loop checks before calling, inner loop checks after each recursive call) ensures `visit()` is never invoked when `hasCycles` is already true. Static analysis confirmed the guard was dead code.
20
+ - Tightened test coverage on `State.ts` and the v3 utilities — overall coverage rose from 95.64% / 88.39% / 95.5% (statements/branches/lines) to 98.39% / 94.01% / 98.34%. New tests cover invalid-input paths in the `State` constructor (string-keyed definitions, non-`State`/`Reference` `nextState`, empty-array commands), the `getSymbol` fallback to `ifOtherSymbol`, the `toGraph` skip of unbound `Reference` transitions, the `fromGraph` cyclic-override-halt error, and the previously-unexercised branches in `splitUnescaped`, `parsePatternString`, `parseMovementLabel`, and `fromMermaid`'s ensureNode update / error paths.
21
+
22
+ ## [3.0.0] - 2026-04-30
23
+
24
+ ### Added
25
+
26
+ - **`State.toGraph(state, tapeBlock)`** static — walks the reachable graph from a state and returns a serializable `Graph` (states, transitions, alphabets).
27
+ - **`State.fromGraph(graph)`** static — inverse of `toGraph`; rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips losslessly via `toMermaid` / `fromMermaid`.
28
+ - **`State.inspect(state)`** static — single-state introspection (id, name, isHalt, override-halt target, transitions) without graph traversal or a tapeBlock.
29
+ - **`toMermaid(graph)`** — renders a `Graph` to Mermaid flowchart syntax.
30
+ - **`fromMermaid(text)`** — parses Mermaid produced by `toMermaid` back into a `Graph`.
31
+ - **`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.
32
+ - **`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.
33
+ - New type exports: `Graph`, `GraphNode`, `GraphTransition`, `GraphCommand`, `GraphSummary`, `Runnable`, `EquivalenceCase`, `EquivalenceResult`, `EquivalenceReport`.
34
+
35
+ ### Changed
36
+
37
+ - TypeScript `target` and `module` raised from `ES6` to `ES2020` (consumers see compiled `dist/` only — no observable difference).
38
+
39
+ ### Removed
40
+
41
+ - **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'`.
42
+
43
+ ### Migration
44
+
45
+ ```diff
46
+ - import { ... } from '@turing-machine-js/machine/src';
47
+ + import { ... } from '@turing-machine-js/machine';
48
+ ```
49
+
50
+ The `/src` subpath was an in-monorepo dev-time shim that never had a real reason to be on the npm tarball.
51
+
52
+ ## [2.0.2] - earlier
53
+
54
+ 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;