@turing-machine-js/machine 2.0.2 → 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 +39 -0
- package/README.md +256 -12
- package/dist/classes/State.d.ts +27 -0
- package/dist/classes/State.js +172 -2
- package/dist/classes/TapeBlock.js +2 -3
- package/dist/index.cjs +768 -173
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.mjs +764 -174
- package/dist/utilities/equivalence.d.ts +31 -0
- package/dist/utilities/equivalence.js +68 -0
- package/dist/utilities/graph.d.ts +29 -0
- package/dist/utilities/graph.js +127 -0
- package/dist/utilities/graphFormats.d.ts +3 -0
- package/dist/utilities/graphFormats.js +141 -0
- package/dist/utilities/introspection.d.ts +15 -0
- package/dist/utilities/introspection.js +88 -0
- package/package.json +2 -8
- package/tsconfig.tsbuildinfo +0 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
package/dist/classes/State.d.ts
CHANGED
|
@@ -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;
|
package/dist/classes/State.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
171
|
+
return patternList?.some((pattern) => (pattern
|
|
173
172
|
.every((everySymbol, ix) => (everySymbol === ifOtherSymbol
|
|
174
|
-
|| everySymbol === currentSymbols[ix]))))
|
|
173
|
+
|| everySymbol === currentSymbols[ix])))) ?? false;
|
|
175
174
|
}
|
|
176
175
|
replaceTape(tape, tapeIx = 0) {
|
|
177
176
|
if (__classPrivateFieldGet(this, _TapeBlock_tapes, "f")[tapeIx] == null) {
|