@turing-machine-js/visuals 7.0.0-alpha.6 → 7.0.0-alpha.7
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 +19 -0
- package/README.md +237 -11
- package/dist/format.d.ts +115 -0
- package/dist/index.cjs +100 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +98 -1
- package/package.json +3 -3
- package/dist/applyHighlight.js +0 -191
- package/dist/format.js +0 -46
- package/dist/graphIndexes.js +0 -58
- package/dist/graphUtils.js +0 -76
- package/dist/highlightOps.js +0 -36
- package/dist/index.js +0 -6
- package/dist/recordSnippet.js +0 -92
- package/dist/types.js +0 -1
- package/src/applyHighlight.spec.ts +0 -331
- package/src/applyHighlight.ts +0 -217
- package/src/fixtures/graphs/post-walk-mark.json +0 -108
- package/src/fixtures/graphs/turing-callable-subtree.json +0 -108
- package/src/fixtures/graphs/turing-copy-two-tapes.json +0 -87
- package/src/fixtures/graphs/turing-replace-b.json +0 -72
- package/src/format.spec.ts +0 -100
- package/src/format.ts +0 -51
- package/src/graphIndexes.ts +0 -84
- package/src/graphUtils.spec.ts +0 -112
- package/src/graphUtils.ts +0 -74
- package/src/highlightOps.ts +0 -94
- package/src/index.ts +0 -10
- package/src/recordSnippet.spec.ts +0 -275
- package/src/recordSnippet.ts +0 -141
- package/src/types.ts +0 -96
- package/tsconfig.build.json +0 -11
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -10
package/src/highlightOps.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract between the pure highlight logic (`applyHighlight`,
|
|
3
|
-
* `applyIndicator`) and any consumer that actually renders the graph
|
|
4
|
-
* (Svelte component, vanilla embed, server-side snapshot, etc.).
|
|
5
|
-
*
|
|
6
|
-
* The pure functions decide *what* should happen (which node gets a class,
|
|
7
|
-
* which edge lights up, where to pulse); the consumer's `HighlightOps`
|
|
8
|
-
* implementation decides *how* (DOM mutation, recording for tests, etc.).
|
|
9
|
-
*
|
|
10
|
-
* See `docs/graph-highlight-and-breakpoints.md` for the rules each
|
|
11
|
-
* implementation must respect.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type NodeKey = number | 'idle';
|
|
15
|
-
|
|
16
|
-
/** Classes the apply-highlight pass may add to a `g.node` element. */
|
|
17
|
-
export type HighlightClass =
|
|
18
|
-
| 'mg-highlight-from'
|
|
19
|
-
| 'mg-highlight-to'
|
|
20
|
-
| 'mg-highlight-strong';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Operations the highlight logic invokes on the rendered graph. Purely
|
|
24
|
-
* additive — the consumer is expected to clear previous highlight state
|
|
25
|
-
* (classes, marker swaps) BEFORE invoking `applyHighlight`. The pure
|
|
26
|
-
* function never reads back from the consumer; it just emits ops.
|
|
27
|
-
*
|
|
28
|
-
* Edge keys follow mermaid's data-id token form: `'idle'` for the
|
|
29
|
-
* synthetic entry sentinel, `'s${id}'` for regular/wrapper/bare states,
|
|
30
|
-
* `'c${id}'` for halt markers (id = `-frameId`), `'w_${id}'` for callable-
|
|
31
|
-
* subtree subgraph clusters. Mermaid emits `L_${from}_${to}_${ix}` per
|
|
32
|
-
* edge; ix-resolution is the consumer's concern (multiple edges between
|
|
33
|
-
* the same pair are rare; the consumer typically picks the first match).
|
|
34
|
-
*/
|
|
35
|
-
export interface HighlightOps {
|
|
36
|
-
/** Add a highlight class to the node identified by `id`. */
|
|
37
|
-
addNodeClass(id: NodeKey, cls: HighlightClass): void;
|
|
38
|
-
|
|
39
|
-
/** Highlight the edge whose data-id matches `L_${fromKey}_${toKey}_*`. */
|
|
40
|
-
highlightEdge(fromKey: string, toKey: string): void;
|
|
41
|
-
|
|
42
|
-
/** Mark the callable-subtree cluster for `frameId` as active. */
|
|
43
|
-
markFrameActive(frameId: number): void;
|
|
44
|
-
|
|
45
|
-
/** Fire a one-shot pulse animation on the given node. */
|
|
46
|
-
pulse(id: NodeKey): void;
|
|
47
|
-
|
|
48
|
-
/** Scroll the given node into the visible area of its container. */
|
|
49
|
-
scrollIntoView(id: NodeKey): void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Operations the indicator (breakpoint dot) pass invokes. */
|
|
53
|
-
export interface IndicatorOps {
|
|
54
|
-
/** Set or clear the breakpoint indicator on the given node. */
|
|
55
|
-
setBreakpoint(id: NodeKey, on: boolean): void;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** A single recorded op — serializable, suitable for snapshot tests. */
|
|
59
|
-
export type RecordedOp =
|
|
60
|
-
| { op: 'addNodeClass'; id: NodeKey; cls: HighlightClass }
|
|
61
|
-
| { op: 'highlightEdge'; fromKey: string; toKey: string }
|
|
62
|
-
| { op: 'markFrameActive'; frameId: number }
|
|
63
|
-
| { op: 'pulse'; id: NodeKey }
|
|
64
|
-
| { op: 'scrollIntoView'; id: NodeKey }
|
|
65
|
-
| { op: 'setBreakpoint'; id: NodeKey; on: boolean };
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Build a recording `HighlightOps` + `IndicatorOps` pair plus the shared
|
|
69
|
-
* `record` array of calls in invocation order. Used by tests to assert
|
|
70
|
-
* what the pure logic would have done without running a real DOM.
|
|
71
|
-
*
|
|
72
|
-
* Snapshot-friendly: the record contains only plain JSON-serializable
|
|
73
|
-
* values (no DOM nodes, no function refs).
|
|
74
|
-
*/
|
|
75
|
-
export function recordingOps(): {
|
|
76
|
-
highlight: HighlightOps;
|
|
77
|
-
indicator: IndicatorOps;
|
|
78
|
-
record: RecordedOp[];
|
|
79
|
-
} {
|
|
80
|
-
const record: RecordedOp[] = [];
|
|
81
|
-
return {
|
|
82
|
-
record,
|
|
83
|
-
highlight: {
|
|
84
|
-
addNodeClass(id, cls) { record.push({ op: 'addNodeClass', id, cls }); },
|
|
85
|
-
highlightEdge(fromKey, toKey) { record.push({ op: 'highlightEdge', fromKey, toKey }); },
|
|
86
|
-
markFrameActive(frameId) { record.push({ op: 'markFrameActive', frameId }); },
|
|
87
|
-
pulse(id) { record.push({ op: 'pulse', id }); },
|
|
88
|
-
scrollIntoView(id) { record.push({ op: 'scrollIntoView', id }); },
|
|
89
|
-
},
|
|
90
|
-
indicator: {
|
|
91
|
-
setBreakpoint(id, on) { record.push({ op: 'setBreakpoint', id, on }); },
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Public API — extracted modules + Snippet recording surface.
|
|
2
|
-
export type { NodeKey, HighlightClass, HighlightOps, IndicatorOps, RecordedOp } from './highlightOps';
|
|
3
|
-
export { recordingOps } from './highlightOps';
|
|
4
|
-
export { bareIdOf, highlightExpand, equivalentIds } from './graphUtils';
|
|
5
|
-
export type { GraphIndexes } from './graphIndexes';
|
|
6
|
-
export { indexGraph } from './graphIndexes';
|
|
7
|
-
export type { GraphHighlight, TapeSnapshot, Frame, Snippet } from './types';
|
|
8
|
-
export { applyHighlight, applyIndicator } from './applyHighlight';
|
|
9
|
-
export { formatCommand, formatStep } from './format';
|
|
10
|
-
export { recordSnippet, type RecordSnippetOptions } from './recordSnippet';
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
Alphabet,
|
|
4
|
-
State,
|
|
5
|
-
Tape,
|
|
6
|
-
TapeBlock,
|
|
7
|
-
TuringMachine,
|
|
8
|
-
haltState,
|
|
9
|
-
ifOtherSymbol,
|
|
10
|
-
movements,
|
|
11
|
-
symbolCommands,
|
|
12
|
-
} from '@turing-machine-js/machine';
|
|
13
|
-
import { recordSnippet } from './recordSnippet';
|
|
14
|
-
|
|
15
|
-
/** Build a machine that converts each 'a' → 'b' (move right) until blank (halt). */
|
|
16
|
-
function buildTwoAMachine() {
|
|
17
|
-
const alphabet = new Alphabet([' ', 'a', 'b']);
|
|
18
|
-
const tape = new Tape({ alphabet, symbols: ['a', 'a'] });
|
|
19
|
-
const tapeBlock = TapeBlock.fromTapes([tape]);
|
|
20
|
-
const machine = new TuringMachine({ tapeBlock });
|
|
21
|
-
|
|
22
|
-
const initialState = new State({
|
|
23
|
-
[tapeBlock.symbol(['a'])]: {
|
|
24
|
-
command: [{ symbol: 'b', movement: movements.right }],
|
|
25
|
-
},
|
|
26
|
-
[ifOtherSymbol]: {
|
|
27
|
-
nextState: haltState,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const graph = State.toGraph(initialState, tapeBlock);
|
|
32
|
-
const alphabets = [alphabet.symbols.filter((s) => s !== alphabet.blankSymbol).concat(alphabet.blankSymbol)];
|
|
33
|
-
|
|
34
|
-
return { machine, initialState, graph, alphabets, alphabet };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('recordSnippet', () => {
|
|
38
|
-
describe('frame 0 (initial state)', () => {
|
|
39
|
-
it('step is 0', () => {
|
|
40
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
41
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
42
|
-
expect(snippet.frames[0].step).toBe(0);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('commands is undefined', () => {
|
|
46
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
47
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
48
|
-
expect(snippet.frames[0].commands).toBeUndefined();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('highlight is null', () => {
|
|
52
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
53
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
54
|
-
expect(snippet.frames[0].highlight).toBeNull();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('tape reflects initial state (position 0, contains "a")', () => {
|
|
58
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
59
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
60
|
-
const frame0Tape = snippet.frames[0].tape[0];
|
|
61
|
-
expect(frame0Tape.position).toBe(0);
|
|
62
|
-
expect(frame0Tape.symbols).toContain('a');
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('frame count', () => {
|
|
67
|
-
it('2 "a"s + halt iter = 3 iters → 4 frames (frame 0 + 3)', () => {
|
|
68
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
69
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
70
|
-
// iter 1: read 'a', write 'b', move R
|
|
71
|
-
// iter 2: read 'a', write 'b', move R
|
|
72
|
-
// iter 3: read blank, nextState = haltState
|
|
73
|
-
expect(snippet.frames).toHaveLength(4);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('per-iter frame commands', () => {
|
|
78
|
-
it('frame 1: read "a", write "b", move R', () => {
|
|
79
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
80
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
81
|
-
expect(snippet.frames[1].commands).toEqual([{ movement: 'R', read: 'a', write: 'b' }]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('frame 2: read "a", write "b", move R', () => {
|
|
85
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
86
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
87
|
-
expect(snippet.frames[2].commands).toEqual([{ movement: 'R', read: 'a', write: 'b' }]);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('frame 3 (halt iter): read===write (keep), movement S (stay)', () => {
|
|
91
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
92
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
93
|
-
// halt-bound transition has default command: keep + stay; blank cell read+written
|
|
94
|
-
const blank = snippet.frames[3].commands![0].read;
|
|
95
|
-
expect(snippet.frames[3].commands).toEqual([{ movement: 'S', read: blank, write: blank }]);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('per-iter frame tape (post-command snapshots)', () => {
|
|
100
|
-
it('frame 1 tape: "b" written at position 0, head at position 1', () => {
|
|
101
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
102
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
103
|
-
const tape = snippet.frames[1].tape[0];
|
|
104
|
-
expect(tape.symbols[0]).toBe('b');
|
|
105
|
-
expect(tape.position).toBe(1);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('frame 2 tape: both cells "b", head at position 2', () => {
|
|
109
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
110
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
111
|
-
const tape = snippet.frames[2].tape[0];
|
|
112
|
-
expect(tape.symbols[0]).toBe('b');
|
|
113
|
-
expect(tape.symbols[1]).toBe('b');
|
|
114
|
-
expect(tape.position).toBe(2);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('snippet metadata', () => {
|
|
119
|
-
it('version is 1', () => {
|
|
120
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
121
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
122
|
-
expect(snippet.version).toBe(1);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('graph is the passed-in graph (referential equality)', () => {
|
|
126
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
127
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
128
|
-
expect(snippet.graph).toBe(graph);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('alphabets is the passed-in alphabets (referential equality)', () => {
|
|
132
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
133
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
134
|
-
expect(snippet.alphabets).toBe(alphabets);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('name is set when provided', () => {
|
|
138
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
139
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets, name: 'test snippet' });
|
|
140
|
-
expect(snippet.name).toBe('test snippet');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('name is absent when not provided', () => {
|
|
144
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
145
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
146
|
-
expect('name' in snippet).toBe(false);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('log option', () => {
|
|
151
|
-
it('attaches log to non-frame-0 frames when log returns a string', () => {
|
|
152
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
153
|
-
const snippet = recordSnippet({
|
|
154
|
-
machine,
|
|
155
|
-
initialState,
|
|
156
|
-
graph,
|
|
157
|
-
alphabets,
|
|
158
|
-
log: () => 'step line',
|
|
159
|
-
});
|
|
160
|
-
// frame 0 has no log
|
|
161
|
-
expect(snippet.frames[0].log).toBeUndefined();
|
|
162
|
-
// frames 1-3 all have log
|
|
163
|
-
for (let i = 1; i < snippet.frames.length; i++) {
|
|
164
|
-
expect(snippet.frames[i].log).toBe('step line');
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('omits log when log returns undefined', () => {
|
|
169
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
170
|
-
const snippet = recordSnippet({
|
|
171
|
-
machine,
|
|
172
|
-
initialState,
|
|
173
|
-
graph,
|
|
174
|
-
alphabets,
|
|
175
|
-
log: () => undefined,
|
|
176
|
-
});
|
|
177
|
-
for (const frame of snippet.frames) {
|
|
178
|
-
expect('log' in frame).toBe(false);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('passes current and previous MachineState to log', () => {
|
|
183
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
184
|
-
const calls: [unknown, unknown][] = [];
|
|
185
|
-
recordSnippet({
|
|
186
|
-
machine,
|
|
187
|
-
initialState,
|
|
188
|
-
graph,
|
|
189
|
-
alphabets,
|
|
190
|
-
log: (m, prev) => { calls.push([m.step, prev ? prev.step : null]); return undefined; },
|
|
191
|
-
});
|
|
192
|
-
// 3 iters → 3 log calls
|
|
193
|
-
expect(calls).toHaveLength(3);
|
|
194
|
-
expect(calls[0]).toEqual([1, null]);
|
|
195
|
-
expect(calls[1]).toEqual([2, 1]);
|
|
196
|
-
expect(calls[2]).toEqual([3, 2]);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('maxSteps truncation', () => {
|
|
201
|
-
it('with maxSteps: 1 → 2 frames (frame 0 + frame 1)', () => {
|
|
202
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
203
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets, maxSteps: 1 });
|
|
204
|
-
expect(snippet.frames).toHaveLength(2);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('with maxSteps: 1, frame 1 tape reflects the first command applied', () => {
|
|
208
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
209
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets, maxSteps: 1 });
|
|
210
|
-
const tape = snippet.frames[1].tape[0];
|
|
211
|
-
// 'a' was written to 'b', head moved right
|
|
212
|
-
expect(tape.symbols[0]).toBe('b');
|
|
213
|
-
expect(tape.position).toBe(1);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('highlight', () => {
|
|
218
|
-
it('non-frame-0 frames have a non-null highlight with strong: "from" and paused: false', () => {
|
|
219
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
220
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
221
|
-
for (let i = 1; i < snippet.frames.length; i++) {
|
|
222
|
-
const h = snippet.frames[i].highlight;
|
|
223
|
-
expect(h).not.toBeNull();
|
|
224
|
-
expect(h!.strong).toBe('from');
|
|
225
|
-
expect(h!.paused).toBe(false);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('halt-iter frame highlight has toId: 0 (haltState)', () => {
|
|
230
|
-
const { machine, initialState, graph, alphabets } = buildTwoAMachine();
|
|
231
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
232
|
-
const lastFrame = snippet.frames[snippet.frames.length - 1];
|
|
233
|
-
expect(lastFrame.highlight!.toId).toBe(0);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe('single-step machine (halts immediately)', () => {
|
|
238
|
-
it('produces 2 frames when machine halts on step 1', () => {
|
|
239
|
-
const alphabet = new Alphabet([' ', 'x']);
|
|
240
|
-
const tape = new Tape({ alphabet, symbols: ['x'] });
|
|
241
|
-
const tapeBlock = TapeBlock.fromTapes([tape]);
|
|
242
|
-
const machine = new TuringMachine({ tapeBlock });
|
|
243
|
-
const initialState = new State({
|
|
244
|
-
[ifOtherSymbol]: { nextState: haltState },
|
|
245
|
-
});
|
|
246
|
-
const graph = State.toGraph(initialState, tapeBlock);
|
|
247
|
-
const alphabets = [['x', ' ']];
|
|
248
|
-
|
|
249
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
250
|
-
expect(snippet.frames).toHaveLength(2);
|
|
251
|
-
expect(snippet.frames[0].commands).toBeUndefined();
|
|
252
|
-
expect(snippet.frames[1].commands).toBeDefined();
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe('keep command → read === write', () => {
|
|
257
|
-
it('a keep+stay command yields read === write in commands', () => {
|
|
258
|
-
const alphabet = new Alphabet([' ', 'a']);
|
|
259
|
-
const tape = new Tape({ alphabet, symbols: ['a'] });
|
|
260
|
-
const tapeBlock = TapeBlock.fromTapes([tape]);
|
|
261
|
-
const machine = new TuringMachine({ tapeBlock });
|
|
262
|
-
const initialState = new State({
|
|
263
|
-
[tapeBlock.symbol(['a'])]: {
|
|
264
|
-
command: [{ symbol: symbolCommands.keep, movement: movements.stay }],
|
|
265
|
-
nextState: haltState,
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
const graph = State.toGraph(initialState, tapeBlock);
|
|
269
|
-
const alphabets = [['a', ' ']];
|
|
270
|
-
|
|
271
|
-
const snippet = recordSnippet({ machine, initialState, graph, alphabets });
|
|
272
|
-
expect(snippet.frames[1].commands![0]).toEqual({ movement: 'S', read: 'a', write: 'a' });
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
});
|
package/src/recordSnippet.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
haltState,
|
|
3
|
-
type Graph,
|
|
4
|
-
type MachineState,
|
|
5
|
-
type State,
|
|
6
|
-
type TuringMachine,
|
|
7
|
-
} from '@turing-machine-js/machine';
|
|
8
|
-
import { MOVEMENT_LETTER } from './format';
|
|
9
|
-
import { bareIdOf } from './graphUtils';
|
|
10
|
-
import type { Frame, GraphHighlight, Snippet, TapeSnapshot } from './types';
|
|
11
|
-
|
|
12
|
-
export type RecordSnippetOptions = {
|
|
13
|
-
machine: TuringMachine;
|
|
14
|
-
initialState: State;
|
|
15
|
-
graph: Graph;
|
|
16
|
-
alphabets: string[][];
|
|
17
|
-
name?: string;
|
|
18
|
-
/**
|
|
19
|
-
* Maximum number of iteration steps to record. Defaults to 1000.
|
|
20
|
-
* If the machine hasn't halted after `maxSteps` iters, recording stops
|
|
21
|
-
* and the snippet contains `maxSteps + 1` frames (frame 0 plus one per iter).
|
|
22
|
-
*/
|
|
23
|
-
maxSteps?: number;
|
|
24
|
-
/**
|
|
25
|
-
* Optional per-frame log formatter. Called with the current and previous
|
|
26
|
-
* `MachineState`; return a string to attach as `frame.log`, or `undefined`
|
|
27
|
-
* to omit. Not called for frame 0 (initial state — no transition has fired).
|
|
28
|
-
*/
|
|
29
|
-
log?: (m: MachineState, prev: MachineState | null) => string | undefined;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const DEFAULT_MAX_STEPS = 1000;
|
|
33
|
-
|
|
34
|
-
function snapshotTapes(machine: TuringMachine): TapeSnapshot[] {
|
|
35
|
-
return machine.tapeBlock.tapes.map((t) => ({
|
|
36
|
-
symbols: [...t.symbols],
|
|
37
|
-
position: t.position,
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function deriveCommands(
|
|
42
|
-
m: MachineState,
|
|
43
|
-
): NonNullable<Frame['commands']> {
|
|
44
|
-
return m.movements.map((mv, i) => ({
|
|
45
|
-
movement: MOVEMENT_LETTER.get(mv) ?? 'S',
|
|
46
|
-
read: m.currentSymbols[i],
|
|
47
|
-
// nextSymbols is already resolved (keep → current symbol, erase → blank);
|
|
48
|
-
// when write === read the command was a keep (UI suppresses the flash).
|
|
49
|
-
write: m.nextSymbols[i],
|
|
50
|
-
}));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function deriveHighlight(m: MachineState, graph: Graph): GraphHighlight {
|
|
54
|
-
return {
|
|
55
|
-
fromId: bareIdOf(m.state.id, graph),
|
|
56
|
-
toId: m.nextState === haltState ? 0 : m.nextState.id,
|
|
57
|
-
strong: 'from',
|
|
58
|
-
paused: false,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Record a full machine run into a `Snippet` — a self-contained playback
|
|
64
|
-
* artifact suitable for embeds, articles, or landing-page panels.
|
|
65
|
-
*
|
|
66
|
-
* The returned snippet contains one frame per iteration plus a frame-0
|
|
67
|
-
* initial-state snapshot. Recording stops when the machine halts or when
|
|
68
|
-
* `maxSteps` iterations have been consumed (default 1000).
|
|
69
|
-
*
|
|
70
|
-
* Tape-timing note: `runStepByStep` yields BEFORE applying its command
|
|
71
|
-
* (the command is applied after the yield resumes). The recorder uses a
|
|
72
|
-
* one-step-delayed snapshot so each frame's `tape` reflects the
|
|
73
|
-
* post-command state for that frame's iter.
|
|
74
|
-
*/
|
|
75
|
-
export function recordSnippet(opts: RecordSnippetOptions): Snippet {
|
|
76
|
-
const {
|
|
77
|
-
machine,
|
|
78
|
-
initialState,
|
|
79
|
-
graph,
|
|
80
|
-
alphabets,
|
|
81
|
-
name,
|
|
82
|
-
maxSteps = DEFAULT_MAX_STEPS,
|
|
83
|
-
log,
|
|
84
|
-
} = opts;
|
|
85
|
-
|
|
86
|
-
const frames: Frame[] = [
|
|
87
|
-
{ step: 0, tape: snapshotTapes(machine), highlight: null },
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
// pending holds everything for the frame whose tape snapshot is not yet
|
|
91
|
-
// available (because applyCommand hasn't fired yet). It is flushed at the
|
|
92
|
-
// start of the NEXT iter (when the tape reflects the previous command) and
|
|
93
|
-
// after the loop (when the final command has been applied).
|
|
94
|
-
let pending: Omit<Frame, 'tape'> | null = null;
|
|
95
|
-
let prev: MachineState | null = null;
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
for (const m of machine.runStepByStep({ initialState, stepsLimit: maxSteps })) {
|
|
99
|
-
// At this point applyCommand for the PREVIOUS iter has already run
|
|
100
|
-
// (the generator called applyCommand before looping back to yield).
|
|
101
|
-
// So the current tape state = post-command of the previous iter.
|
|
102
|
-
if (pending !== null) {
|
|
103
|
-
frames.push({ ...pending, tape: snapshotTapes(machine) });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const commands = deriveCommands(m);
|
|
107
|
-
const highlight = deriveHighlight(m, graph);
|
|
108
|
-
const logLine = log ? log(m, prev) : undefined;
|
|
109
|
-
|
|
110
|
-
pending = {
|
|
111
|
-
step: m.step,
|
|
112
|
-
commands,
|
|
113
|
-
highlight,
|
|
114
|
-
...(logLine !== undefined ? { log: logLine } : {}),
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
prev = m;
|
|
118
|
-
}
|
|
119
|
-
} catch (e) {
|
|
120
|
-
// runStepByStep throws 'Long execution' when stepsLimit is hit.
|
|
121
|
-
// At that point applyCommand for the last yielded iter has run, so the
|
|
122
|
-
// tape is in the post-command state we want for the pending frame.
|
|
123
|
-
if (!(e instanceof Error) || e.message !== 'Long execution') {
|
|
124
|
-
throw e;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Flush the last pending frame. After the loop (or after the catch), the
|
|
129
|
-
// tape reflects the post-command state of the final yielded iter.
|
|
130
|
-
if (pending !== null) {
|
|
131
|
-
frames.push({ ...pending, tape: snapshotTapes(machine) });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
version: 1,
|
|
136
|
-
...(name !== undefined ? { name } : {}),
|
|
137
|
-
graph,
|
|
138
|
-
alphabets,
|
|
139
|
-
frames,
|
|
140
|
-
};
|
|
141
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type { Graph } from '@turing-machine-js/machine';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* State-graph highlight descriptor (machines-demo#10). MachineView derives it
|
|
5
|
-
* from `executionMode` + the latest pause-response data; MachineGraph reads
|
|
6
|
-
* it to light up the `from → edge → to` triple in the rendered SVG.
|
|
7
|
-
*
|
|
8
|
-
* - `fromId: 'idle'` represents the synthetic `idle([idle])` sentinel that
|
|
9
|
-
* `toMermaid` emits at the entry point. Used in IDLE mode to mark "where
|
|
10
|
-
* execution would start".
|
|
11
|
-
* - `fromId: number` is an engine `GraphNode.id` — the source state.
|
|
12
|
-
* - `toId: number | null` is the destination state's id (or `null` at halt).
|
|
13
|
-
* - `strong` selects which end of the triple gets the bolder/stronger
|
|
14
|
-
* accent. Per the (B) rule: `from` strong at `before` pause; `to` strong
|
|
15
|
-
* at `after` / iter-end pause / IDLE (destination feels current).
|
|
16
|
-
*/
|
|
17
|
-
export type GraphHighlight = {
|
|
18
|
-
fromId: number | 'idle';
|
|
19
|
-
toId: number | null;
|
|
20
|
-
strong: 'from' | 'to';
|
|
21
|
-
/**
|
|
22
|
-
* True when this highlight reflects a paused-event apply (RUNNING_PAUSED),
|
|
23
|
-
* false for per-iter idle applies (RUNNING_AUTO). MachineGraph uses this
|
|
24
|
-
* to detect cross-pause same-state revisits — pulsing the strong node when
|
|
25
|
-
* the current paused apply lands on the same state as the previous paused
|
|
26
|
-
* apply, even when intermediate idle applies pointed elsewhere
|
|
27
|
-
* (e.g., stateA breakpoint → continue → run through stateB/C → stateA
|
|
28
|
-
* breakpoint fires again). The simpler "previous apply's strong matches"
|
|
29
|
-
* check already covers AUTO self-loops; this flag drives the second pulse
|
|
30
|
-
* trigger.
|
|
31
|
-
*/
|
|
32
|
-
paused: boolean;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Per-tape snapshot: the cells visible/usable plus the head's index into them.
|
|
37
|
-
* Same shape as machines-demo's TapeSnapshot. Pure data — no library handles.
|
|
38
|
-
*/
|
|
39
|
-
export type TapeSnapshot = {
|
|
40
|
-
symbols: string[];
|
|
41
|
-
position: number;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* One frame of a recorded snippet — the state of the machine at iter `step`.
|
|
46
|
-
* Frame 0 = initial state (before any transition); frame N = state after iter N's transition.
|
|
47
|
-
*
|
|
48
|
-
* `tape` is per-tape (single-tape machines: length 1). `highlight` describes
|
|
49
|
-
* what to render on the state graph at this moment (null when no highlight).
|
|
50
|
-
* `log` is optional pre-formatted text — a caption / status line consumers can render.
|
|
51
|
-
*/
|
|
52
|
-
export type Frame = {
|
|
53
|
-
step: number;
|
|
54
|
-
tape: TapeSnapshot[];
|
|
55
|
-
/**
|
|
56
|
-
* Per-tape engine command for the iter that produced this frame. Carries
|
|
57
|
-
* both sides of the cell so players can step bi-directionally without
|
|
58
|
-
* recomputing from neighbouring frames:
|
|
59
|
-
*
|
|
60
|
-
* - `movement` — `'L' | 'R' | 'S'`. Forward step slides the tape this way;
|
|
61
|
-
* backward step slides the opposite.
|
|
62
|
-
* - `read` — symbol on the head's cell BEFORE this iter (what the engine
|
|
63
|
-
* matched). Backward step writes this back.
|
|
64
|
-
* - `write` — symbol on the cell AFTER this iter (=== `read` if no write
|
|
65
|
-
* happened). Forward step writes this; UI triggers the per-cell flash
|
|
66
|
-
* iff `write !== read`.
|
|
67
|
-
*
|
|
68
|
-
* Undefined on frame 0 (initial state — no transition has fired yet).
|
|
69
|
-
*/
|
|
70
|
-
commands?: {
|
|
71
|
-
movement: 'L' | 'R' | 'S';
|
|
72
|
-
read: string;
|
|
73
|
-
write: string;
|
|
74
|
-
}[];
|
|
75
|
-
highlight: GraphHighlight | null;
|
|
76
|
-
log?: string;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Recorded run of a machine — playback artifact for embeds, articles,
|
|
81
|
-
* landing-page panels. Engine-agnostic (no `engine` field; identity lives
|
|
82
|
-
* at the caller bucket level).
|
|
83
|
-
*
|
|
84
|
-
* - `version: 1` — schema integer. Additive fields don't bump it;
|
|
85
|
-
* shape-breaking changes do.
|
|
86
|
-
* - `graph` — engine `State.toGraph` output captured at recording time.
|
|
87
|
-
* - `alphabets` — per-tape alphabet list (single-tape: length 1).
|
|
88
|
-
* - `frames` — length === `stepsApplied + 1`; frame 0 is the initial state.
|
|
89
|
-
*/
|
|
90
|
-
export type Snippet = {
|
|
91
|
-
version: 1;
|
|
92
|
-
name?: string;
|
|
93
|
-
graph: Graph;
|
|
94
|
-
alphabets: string[][];
|
|
95
|
-
frames: Frame[];
|
|
96
|
-
};
|