@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
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { resolve, dirname } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { applyHighlight, applyIndicator } from './applyHighlight';
|
|
6
|
-
import { indexGraph } from './graphIndexes';
|
|
7
|
-
import { recordingOps, type RecordedOp } from './highlightOps';
|
|
8
|
-
import type { GraphHighlight } from './types';
|
|
9
|
-
import type { Graph } from '@turing-machine-js/machine';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Rule tests for `applyHighlight`. Section numbers mirror
|
|
13
|
-
* `docs/graph-highlight-and-breakpoints.md` — any rule change must
|
|
14
|
-
* update both the doc and the matching `describe` block here.
|
|
15
|
-
*
|
|
16
|
-
* Fixtures come from `tests/fixtures/graphs/`, which are committed
|
|
17
|
-
* snapshots of `State.toGraph` output for each bundled example
|
|
18
|
-
* (regenerable via `REGEN_FIXTURES=1 npm test`). Using real engine output
|
|
19
|
-
* means these tests also catch any drift between engine emit and
|
|
20
|
-
* rule expectations.
|
|
21
|
-
*
|
|
22
|
-
* Helpful fixture ids for `turing-callable-subtree`:
|
|
23
|
-
* bare walkToBlank = 3 (frameId 3)
|
|
24
|
-
* writeMarker (override) = 4
|
|
25
|
-
* wrapper walkToBlank(writeMarker) = 5 (bareStateId 3, overriddenHaltStateId 4)
|
|
26
|
-
* halt singleton = 0
|
|
27
|
-
* frame halt marker = -3 (id = -frameId)
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
-
const __dirname = dirname(__filename);
|
|
32
|
-
const FIXTURE_DIR = resolve(__dirname, './fixtures/graphs');
|
|
33
|
-
|
|
34
|
-
function loadGraph(name: string): Graph {
|
|
35
|
-
return JSON.parse(readFileSync(resolve(FIXTURE_DIR, `${name}.json`), 'utf-8'));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Run `applyHighlight` with a recording ops impl, return the ordered ops list. */
|
|
39
|
-
function run(highlight: GraphHighlight | null, graph: Graph, prev: number | 'idle' | null = null): {
|
|
40
|
-
ops: RecordedOp[];
|
|
41
|
-
next: number | 'idle' | null;
|
|
42
|
-
} {
|
|
43
|
-
const indexes = indexGraph(graph);
|
|
44
|
-
const { highlight: opsImpl, record } = recordingOps();
|
|
45
|
-
const { nextPrevStrongId } = applyHighlight(highlight, graph, indexes, prev, opsImpl);
|
|
46
|
-
return { ops: record, next: nextPrevStrongId };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe('applyHighlight', () => {
|
|
50
|
-
describe('null/empty highlight', () => {
|
|
51
|
-
it('returns no-op with nextPrev=null when highlight is null', () => {
|
|
52
|
-
const g = loadGraph('turing-callable-subtree');
|
|
53
|
-
const { ops, next } = run(null, g, 3);
|
|
54
|
-
expect(ops).toEqual([]);
|
|
55
|
-
expect(next).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('§5 halt-target retargeting', () => {
|
|
60
|
-
it('rewrites toId=0 to -frameId when fromId is in a frame', () => {
|
|
61
|
-
const g = loadGraph('turing-callable-subtree');
|
|
62
|
-
// Bare walkToBlank (id 3, frameId 3) halts → engine reports toId=0,
|
|
63
|
-
// should retarget to -3 (frame halt marker).
|
|
64
|
-
const { ops } = run({ fromId: 3, toId: 0, strong: 'from', paused: false }, g);
|
|
65
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
66
|
-
// halt marker -3 gets highlight-to; halt singleton 0 does not.
|
|
67
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: -3, cls: 'mg-highlight-to' });
|
|
68
|
-
expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-to' });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('does NOT retarget when fromId is outside any frame', () => {
|
|
72
|
-
const g = loadGraph('turing-callable-subtree');
|
|
73
|
-
// writeMarker (id 4) is the wrapper's override-target; sits outside
|
|
74
|
-
// the frame (frameId: null), so its halt→halt-singleton stays as id 0.
|
|
75
|
-
const { ops } = run({ fromId: 4, toId: 0, strong: 'from', paused: false }, g);
|
|
76
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
77
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-to' });
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('§2 + §3 wrapper/bare equivalence (asymmetric)', () => {
|
|
82
|
-
it('wrapper-strong expands to [wrapper, bare] — both get highlight-to + strong', () => {
|
|
83
|
-
const g = loadGraph('turing-callable-subtree');
|
|
84
|
-
// Wrapper-entry pause: idle → wrapper(5), pauseBefore strong=to.
|
|
85
|
-
const { ops } = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g);
|
|
86
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
87
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
|
|
88
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-strong' });
|
|
89
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-to' });
|
|
90
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-strong' });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('bare-strong stays bare only — does NOT light up wrapper', () => {
|
|
94
|
-
const g = loadGraph('turing-callable-subtree');
|
|
95
|
-
// Bare-loop pause: prev=bare(3), to=bare(3), pauseBefore strong=to.
|
|
96
|
-
const { ops } = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g);
|
|
97
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
98
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-to' });
|
|
99
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-strong' });
|
|
100
|
-
// Wrapper 5 gets NOTHING from the to-side expansion.
|
|
101
|
-
expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
|
|
102
|
-
expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-strong' });
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('§6 source return chain (toId < 0)', () => {
|
|
107
|
-
it('lights up return arrow + wrapper + override edge + override target', () => {
|
|
108
|
-
const g = loadGraph('turing-callable-subtree');
|
|
109
|
-
// Just-fired halt-bound transition: from=bare(3), to=halt-marker(-3),
|
|
110
|
-
// strong=from (pause-after-style).
|
|
111
|
-
const { ops } = run({ fromId: 3, toId: -3, strong: 'from', paused: true }, g);
|
|
112
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
113
|
-
const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
|
|
114
|
-
// Return arrow w_3 → wrapper(5).
|
|
115
|
-
expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 'w_3', toKey: 's5' });
|
|
116
|
-
// Wrapper gets highlight-to.
|
|
117
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
|
|
118
|
-
// Wrapper → override edge.
|
|
119
|
-
expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's4' });
|
|
120
|
-
// Override (writeMarker) gets highlight-to.
|
|
121
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 4, cls: 'mg-highlight-to' });
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('§7 destination return chain (paused at override after pop)', () => {
|
|
126
|
-
it('lights the full path bare → halt-marker → return → wrapper → override + frame', () => {
|
|
127
|
-
const g = loadGraph('turing-callable-subtree');
|
|
128
|
-
// Paused-before writeMarker: prev=bare(3) (last yield was bare's halt),
|
|
129
|
-
// current=writeMarker(4). strong=to. Bare is in frame 3; wrapper 5's
|
|
130
|
-
// override is 4 → matches.
|
|
131
|
-
const { ops } = run({ fromId: 3, toId: 4, strong: 'to', paused: true }, g);
|
|
132
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
133
|
-
const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
|
|
134
|
-
const frameOps = ops.filter((o) => o.op === 'markFrameActive');
|
|
135
|
-
|
|
136
|
-
// Halt marker lit.
|
|
137
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: -3, cls: 'mg-highlight-to' });
|
|
138
|
-
// bare → halt-marker edge.
|
|
139
|
-
expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's3', toKey: 'c3' });
|
|
140
|
-
// Return arrow.
|
|
141
|
-
expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 'w_3', toKey: 's5' });
|
|
142
|
-
// Wrapper highlight-to.
|
|
143
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
|
|
144
|
-
// Wrapper → override edge.
|
|
145
|
-
expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's4' });
|
|
146
|
-
// Frame active (override sits outside frame, but the chain fired so
|
|
147
|
-
// we still mark the frame to show "we came out of THIS subtree").
|
|
148
|
-
expect(frameOps).toContainEqual({ op: 'markFrameActive', frameId: 3 });
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('does NOT fire when fromId is not in any frame', () => {
|
|
152
|
-
const g = loadGraph('turing-callable-subtree');
|
|
153
|
-
// From idle → writeMarker(4): no frame on the from side, no chain.
|
|
154
|
-
const { ops } = run({ fromId: 'idle', toId: 4, strong: 'to', paused: true }, g);
|
|
155
|
-
const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
|
|
156
|
-
expect(edgeOps).not.toContainEqual({ op: 'highlightEdge', fromKey: 's3', toKey: 'c3' });
|
|
157
|
-
expect(edgeOps).not.toContainEqual({ op: 'highlightEdge', fromKey: 'w_3', toKey: 's5' });
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe('§8 halt singleton (toId === 0, no retarget)', () => {
|
|
162
|
-
it('marks the halt singleton with highlight-to + strong', () => {
|
|
163
|
-
const g = loadGraph('turing-callable-subtree');
|
|
164
|
-
// writeMarker (id 4, no frame) → halt singleton (0). strong=from
|
|
165
|
-
// (pause-after applying writeMarker's command).
|
|
166
|
-
const { ops } = run({ fromId: 4, toId: 0, strong: 'from', paused: true }, g);
|
|
167
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
168
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-to' });
|
|
169
|
-
// strong is 'from', so halt singleton is not strong.
|
|
170
|
-
expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-strong' });
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('§9 frame-active', () => {
|
|
175
|
-
it('marks the frame when canonical strong is inside it (wrapper case)', () => {
|
|
176
|
-
const g = loadGraph('turing-callable-subtree');
|
|
177
|
-
// Wrapper-entry pause: strong=wrapper(5). bareIdOf(5)=3. nodeFrameMap[3]=3.
|
|
178
|
-
const { ops } = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g);
|
|
179
|
-
const frameOps = ops.filter((o) => o.op === 'markFrameActive');
|
|
180
|
-
expect(frameOps).toContainEqual({ op: 'markFrameActive', frameId: 3 });
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('marks the frame when bare is strong', () => {
|
|
184
|
-
const g = loadGraph('turing-callable-subtree');
|
|
185
|
-
const { ops } = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g);
|
|
186
|
-
const frameOps = ops.filter((o) => o.op === 'markFrameActive');
|
|
187
|
-
expect(frameOps).toContainEqual({ op: 'markFrameActive', frameId: 3 });
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('does NOT mark a frame when strong is outside any frame', () => {
|
|
191
|
-
const g = loadGraph('turing-callable-subtree');
|
|
192
|
-
// writeMarker (4, no frame) is strong.
|
|
193
|
-
const { ops } = run({ fromId: 4, toId: 0, strong: 'from', paused: true }, g);
|
|
194
|
-
const frameOps = ops.filter((o) => o.op === 'markFrameActive');
|
|
195
|
-
expect(frameOps).toEqual([]);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe('§10 wrapper-entry call edge', () => {
|
|
200
|
-
it('highlights the wrapper → bare call edge when to-side expanded to both', () => {
|
|
201
|
-
const g = loadGraph('turing-callable-subtree');
|
|
202
|
-
const { ops } = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g);
|
|
203
|
-
const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
|
|
204
|
-
expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's3' });
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('does NOT highlight a call edge when to-side is bare only', () => {
|
|
208
|
-
const g = loadGraph('turing-callable-subtree');
|
|
209
|
-
const { ops } = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g);
|
|
210
|
-
const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
|
|
211
|
-
// No s5→s3 call edge (bare doesn't expand to include wrapper).
|
|
212
|
-
expect(edgeOps).not.toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's3' });
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe('§11 pause-revisit pulse (raw, not canonical)', () => {
|
|
217
|
-
it('does NOT pulse on wrapper-pause → bare-pause transition', () => {
|
|
218
|
-
const g = loadGraph('turing-callable-subtree');
|
|
219
|
-
// First pause: wrapper-entry. prev=null → no pulse. nextPrev=5 (wrapper).
|
|
220
|
-
const r1 = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g, null);
|
|
221
|
-
expect(r1.ops.find((o) => o.op === 'pulse')).toBeUndefined();
|
|
222
|
-
expect(r1.next).toBe(5);
|
|
223
|
-
// Second pause: bare loop iter. prev=5 (wrapper), strong=3 (bare).
|
|
224
|
-
// 5 !== 3 → NO pulse, even though they share #debugRef.
|
|
225
|
-
const r2 = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g, r1.next);
|
|
226
|
-
expect(r2.ops.find((o) => o.op === 'pulse')).toBeUndefined();
|
|
227
|
-
expect(r2.next).toBe(3);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('pulses on same-state revisit (bare → bare)', () => {
|
|
231
|
-
const g = loadGraph('turing-callable-subtree');
|
|
232
|
-
const r1 = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g, null);
|
|
233
|
-
expect(r1.ops.find((o) => o.op === 'pulse')).toBeUndefined();
|
|
234
|
-
const r2 = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g, r1.next);
|
|
235
|
-
expect(r2.ops).toContainEqual({ op: 'pulse', id: 3 });
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('does NOT update prev on non-paused (idle / RUNNING_AUTO) events', () => {
|
|
239
|
-
const g = loadGraph('turing-callable-subtree');
|
|
240
|
-
const r = run({ fromId: 3, toId: 3, strong: 'from', paused: false }, g, 5);
|
|
241
|
-
expect(r.next).toBe(5); // unchanged
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('resets prev to null on null highlight', () => {
|
|
245
|
-
const g = loadGraph('turing-callable-subtree');
|
|
246
|
-
const r = run(null, g, 3);
|
|
247
|
-
expect(r.next).toBeNull();
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe('scroll-into-view', () => {
|
|
252
|
-
it('scrolls to the strong node', () => {
|
|
253
|
-
const g = loadGraph('turing-callable-subtree');
|
|
254
|
-
const { ops } = run({ fromId: 3, toId: 4, strong: 'to', paused: true }, g);
|
|
255
|
-
expect(ops).toContainEqual({ op: 'scrollIntoView', id: 4 });
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('does not scroll when strong is null (no highlight)', () => {
|
|
259
|
-
const g = loadGraph('turing-callable-subtree');
|
|
260
|
-
const { ops } = run(null, g);
|
|
261
|
-
expect(ops.find((o) => o.op === 'scrollIntoView')).toBeUndefined();
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
describe('applyIndicator (§2 canonical breakpoint + §3 indicator class)', () => {
|
|
266
|
-
it('marks every member of the equivalence class when canonical id is in set', () => {
|
|
267
|
-
const g = loadGraph('turing-callable-subtree');
|
|
268
|
-
const { indicator, record } = recordingOps();
|
|
269
|
-
// BP set has the bare id (3). Both wrapper (5) and bare (3) should turn on.
|
|
270
|
-
applyIndicator(new Set([3]), g, [3, 4, 5, -3, 0, 'idle'], indicator);
|
|
271
|
-
const onIds = record
|
|
272
|
-
.filter((r): r is Extract<RecordedOp, { op: 'setBreakpoint' }> => r.op === 'setBreakpoint' && r.on)
|
|
273
|
-
.map((r) => r.id);
|
|
274
|
-
expect(onIds).toContain(3);
|
|
275
|
-
expect(onIds).toContain(5);
|
|
276
|
-
// writeMarker (4), halt-marker (-3), halt (0), idle: never on.
|
|
277
|
-
expect(onIds).not.toContain(4);
|
|
278
|
-
expect(onIds).not.toContain(-3);
|
|
279
|
-
expect(onIds).not.toContain(0);
|
|
280
|
-
expect(onIds).not.toContain('idle');
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('clears every node when the set is empty', () => {
|
|
284
|
-
const g = loadGraph('turing-callable-subtree');
|
|
285
|
-
const { indicator, record } = recordingOps();
|
|
286
|
-
applyIndicator(new Set(), g, [3, 4, 5, 'idle'], indicator);
|
|
287
|
-
const onCount = record.filter((r) => r.op === 'setBreakpoint' && r.on).length;
|
|
288
|
-
expect(onCount).toBe(0);
|
|
289
|
-
// Every node still gets an explicit off call (idempotent clears).
|
|
290
|
-
expect(record.length).toBe(4);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// machines-demo#37 — halt singleton (id 0) is a valid breakpoint
|
|
294
|
-
// target (engine-wide haltState). The set stores canonical id 0;
|
|
295
|
-
// both the halt singleton AND every halt marker (negative ids; one
|
|
296
|
-
// per frame) collapse to that class via `bareIdOf(-N, g) === 0`.
|
|
297
|
-
it('marks halt singleton AND every halt marker when 0 is in the set', () => {
|
|
298
|
-
const g = loadGraph('turing-callable-subtree');
|
|
299
|
-
const { indicator, record } = recordingOps();
|
|
300
|
-
applyIndicator(new Set([0]), g, [3, 4, 5, -3, 0, 'idle'], indicator);
|
|
301
|
-
const onIds = record
|
|
302
|
-
.filter((r): r is Extract<RecordedOp, { op: 'setBreakpoint' }> => r.op === 'setBreakpoint' && r.on)
|
|
303
|
-
.map((r) => r.id);
|
|
304
|
-
expect(onIds).toContain(0); // halt singleton
|
|
305
|
-
expect(onIds).toContain(-3); // halt marker for frame 3
|
|
306
|
-
// Regular and wrapper / bare nodes: never on.
|
|
307
|
-
expect(onIds).not.toContain(3);
|
|
308
|
-
expect(onIds).not.toContain(4);
|
|
309
|
-
expect(onIds).not.toContain(5);
|
|
310
|
-
expect(onIds).not.toContain('idle');
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
describe('regression: simple machines (no callable subtree)', () => {
|
|
315
|
-
it('lights from + to + strong + edge for a plain RUNNING_AUTO tick', () => {
|
|
316
|
-
const g = loadGraph('turing-replace-b');
|
|
317
|
-
// Pick any two ids that have a transition between them.
|
|
318
|
-
const nodes = Object.values(g.nodes).filter((n) => !n.isHalt && !n.isHaltMarker);
|
|
319
|
-
const src = nodes.find((n) => n.transitions.length > 0)!;
|
|
320
|
-
const transition = src.transitions[0];
|
|
321
|
-
const dst = transition.nextStateId;
|
|
322
|
-
const { ops } = run(
|
|
323
|
-
{ fromId: src.id, toId: dst, strong: 'from', paused: false },
|
|
324
|
-
g,
|
|
325
|
-
);
|
|
326
|
-
const classOps = ops.filter((o) => o.op === 'addNodeClass');
|
|
327
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: src.id, cls: 'mg-highlight-from' });
|
|
328
|
-
expect(classOps).toContainEqual({ op: 'addNodeClass', id: src.id, cls: 'mg-highlight-strong' });
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
});
|
package/src/applyHighlight.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import type { GraphHighlight } from './types';
|
|
2
|
-
import type { Graph } from '@turing-machine-js/machine';
|
|
3
|
-
import type { GraphIndexes } from './graphIndexes';
|
|
4
|
-
import type { HighlightOps, IndicatorOps, NodeKey } from './highlightOps';
|
|
5
|
-
import { bareIdOf, highlightExpand } from './graphUtils';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Pure highlight-rule evaluator. Given the current `highlight` (from
|
|
9
|
-
* `MachineView`'s `$derived`), the engine `graph`, derived `indexes`,
|
|
10
|
-
* and the previous strong-id (for pause-revisit pulse detection), emit
|
|
11
|
-
* a sequence of `ops` calls describing the resulting visual state.
|
|
12
|
-
*
|
|
13
|
-
* Strictly additive — the caller is expected to clear previously-applied
|
|
14
|
-
* highlight classes / edge marks / cluster activations BEFORE invoking
|
|
15
|
-
* this function. The function never reads back from the consumer.
|
|
16
|
-
*
|
|
17
|
-
* Returns the new prev-strong-id to thread into the next call. Pulse
|
|
18
|
-
* comparison uses the RAW strong id (not canonical), so wrapper-pause
|
|
19
|
-
* and bare-pause register as different positions and don't pulse each
|
|
20
|
-
* other. Updates only when `highlight.paused === true`; non-paused
|
|
21
|
-
* events (idle / RUNNING_AUTO ticks) leave it untouched. Null highlight
|
|
22
|
-
* resets it to null.
|
|
23
|
-
*
|
|
24
|
-
* See `docs/graph-highlight-and-breakpoints.md` for the 16 rules
|
|
25
|
-
* enumerated.
|
|
26
|
-
*/
|
|
27
|
-
export function applyHighlight(
|
|
28
|
-
highlight: GraphHighlight | null,
|
|
29
|
-
graph: Graph | null,
|
|
30
|
-
indexes: GraphIndexes,
|
|
31
|
-
prevStrongId: NodeKey | null,
|
|
32
|
-
ops: HighlightOps,
|
|
33
|
-
): { nextPrevStrongId: NodeKey | null } {
|
|
34
|
-
if (!highlight || !graph) {
|
|
35
|
-
return { nextPrevStrongId: null };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// §5 Halt-target retargeting: real halt (id 0) reached from an in-frame
|
|
39
|
-
// state retargets to the frame's halt marker (id = -frameId), so the
|
|
40
|
-
// visible edge lands inside the cluster.
|
|
41
|
-
let toId: number | null = highlight.toId;
|
|
42
|
-
if (toId === 0 && typeof highlight.fromId === 'number') {
|
|
43
|
-
const fromFrameId = indexes.nodeFrameMap.get(highlight.fromId);
|
|
44
|
-
if (fromFrameId !== undefined) toId = -fromFrameId;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// §2 Equivalence-class expansion (asymmetric, via highlightExpand):
|
|
48
|
-
// wrapper → [wrapper, bare] (joined visual pair for wrapper-entry pause)
|
|
49
|
-
// bare → [bare] (engine genuinely on the bare; no wrapper sync)
|
|
50
|
-
// From-side expansion only fires for positive numeric ids; the 'idle'
|
|
51
|
-
// sentinel is handled directly below. Halt markers / singleton fall
|
|
52
|
-
// through the direct-lookup branches.
|
|
53
|
-
const fromEqIds = typeof highlight.fromId === 'number'
|
|
54
|
-
? highlightExpand(highlight.fromId, graph)
|
|
55
|
-
: [];
|
|
56
|
-
const toEqIds = toId !== null && toId > 0
|
|
57
|
-
? highlightExpand(toId, graph)
|
|
58
|
-
: [];
|
|
59
|
-
|
|
60
|
-
// §3 Class application — from side.
|
|
61
|
-
if (highlight.fromId === 'idle') {
|
|
62
|
-
ops.addNodeClass('idle', 'mg-highlight-from');
|
|
63
|
-
if (highlight.strong === 'from') ops.addNodeClass('idle', 'mg-highlight-strong');
|
|
64
|
-
}
|
|
65
|
-
for (const id of fromEqIds) {
|
|
66
|
-
ops.addNodeClass(id, 'mg-highlight-from');
|
|
67
|
-
if (highlight.strong === 'from') ops.addNodeClass(id, 'mg-highlight-strong');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// §3 + §8 Class application — to side. Halt markers (toId < 0) and the
|
|
71
|
-
// real halt singleton (toId === 0; only possible when §5 didn't retarget)
|
|
72
|
-
// bypass the equivalence-class expansion via direct lookup.
|
|
73
|
-
if (toId !== null && toId <= 0) {
|
|
74
|
-
ops.addNodeClass(toId, 'mg-highlight-to');
|
|
75
|
-
if (highlight.strong === 'to') ops.addNodeClass(toId, 'mg-highlight-strong');
|
|
76
|
-
}
|
|
77
|
-
for (const id of toEqIds) {
|
|
78
|
-
ops.addNodeClass(id, 'mg-highlight-to');
|
|
79
|
-
if (highlight.strong === 'to') ops.addNodeClass(id, 'mg-highlight-strong');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Edge highlight: the data-id token form mermaid emits.
|
|
83
|
-
const fromKey = highlight.fromId === 'idle' ? 'idle' : `s${highlight.fromId}`;
|
|
84
|
-
const toKey =
|
|
85
|
-
toId === null ? null
|
|
86
|
-
: toId < 0 ? `c${-toId}` // halt marker
|
|
87
|
-
: `s${toId}`;
|
|
88
|
-
if (toKey !== null) ops.highlightEdge(fromKey, toKey);
|
|
89
|
-
|
|
90
|
-
// §10 Wrapper-entry "call" edge: when to-side expanded to [wrapper, bare],
|
|
91
|
-
// light up the wrapper→bare connector so the joined pair has a visible link.
|
|
92
|
-
if (toEqIds.length > 1) {
|
|
93
|
-
const wrapperId = toEqIds.find((id) => graph.nodes[id]?.isWrapper);
|
|
94
|
-
const bareId = toEqIds.find((id) => !graph.nodes[id]?.isWrapper);
|
|
95
|
-
if (wrapperId !== undefined && bareId !== undefined) {
|
|
96
|
-
ops.highlightEdge(`s${wrapperId}`, `s${bareId}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// §6 Source return chain: just-fired transition landed on a frame's
|
|
101
|
-
// halt marker. Light up the post-pop trajectory before the next iter
|
|
102
|
-
// moves the strong node.
|
|
103
|
-
if (toId !== null && toId < 0) {
|
|
104
|
-
const frameId = -toId;
|
|
105
|
-
const wrappers = indexes.frameWrappersMap.get(frameId) ?? [];
|
|
106
|
-
for (const { wrapperId, overrideId } of wrappers) {
|
|
107
|
-
ops.highlightEdge(`w_${frameId}`, `s${wrapperId}`);
|
|
108
|
-
ops.addNodeClass(wrapperId, 'mg-highlight-to');
|
|
109
|
-
if (overrideId !== null) {
|
|
110
|
-
ops.highlightEdge(`s${wrapperId}`, `s${overrideId}`);
|
|
111
|
-
ops.addNodeClass(overrideId, 'mg-highlight-to');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// §7 Destination return chain: paused at a positive toId that's some
|
|
117
|
-
// wrapper W's override AND fromId is in W's frame — the engine just
|
|
118
|
-
// popped. The straight bare→override edge doesn't exist in the graph;
|
|
119
|
-
// light up the actual visible path bare → halt-marker → return →
|
|
120
|
-
// wrapper → override, plus the frame cluster.
|
|
121
|
-
if (typeof highlight.fromId === 'number' && toId !== null && toId > 0) {
|
|
122
|
-
const fromFrameId = indexes.nodeFrameMap.get(highlight.fromId);
|
|
123
|
-
if (fromFrameId !== undefined) {
|
|
124
|
-
const wrappers = indexes.frameWrappersMap.get(fromFrameId) ?? [];
|
|
125
|
-
const matching = wrappers.filter((w) => w.overrideId === toId);
|
|
126
|
-
if (matching.length > 0) {
|
|
127
|
-
ops.addNodeClass(-fromFrameId, 'mg-highlight-to');
|
|
128
|
-
ops.highlightEdge(`s${highlight.fromId}`, `c${fromFrameId}`);
|
|
129
|
-
for (const { wrapperId } of matching) {
|
|
130
|
-
ops.highlightEdge(`w_${fromFrameId}`, `s${wrapperId}`);
|
|
131
|
-
ops.addNodeClass(wrapperId, 'mg-highlight-to');
|
|
132
|
-
ops.highlightEdge(`s${wrapperId}`, `s${toId}`);
|
|
133
|
-
}
|
|
134
|
-
ops.markFrameActive(fromFrameId);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// §9 Frame-active for the strong node. Wrappers are outside any frame
|
|
140
|
-
// so canonicalize via bareIdOf so the wrapper-entry pause still lights
|
|
141
|
-
// up the bare's enclosing cluster.
|
|
142
|
-
const strongId = highlight.strong === 'from' ? highlight.fromId : highlight.toId;
|
|
143
|
-
const strongIdCanonical = typeof strongId === 'number'
|
|
144
|
-
? bareIdOf(strongId, graph)
|
|
145
|
-
: strongId;
|
|
146
|
-
if (typeof strongIdCanonical === 'number') {
|
|
147
|
-
const frameId = indexes.nodeFrameMap.get(strongIdCanonical);
|
|
148
|
-
if (frameId !== undefined) ops.markFrameActive(frameId);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// §11 Pulse on same-state revisit. Uses RAW strongId — wrapper-pause
|
|
152
|
-
// and bare-pause are visually distinct positions even though they
|
|
153
|
-
// share #debugRef; pausing at wrapper then continuing into bare must
|
|
154
|
-
// not pulse. Idles never pulse and never update prevStrongId.
|
|
155
|
-
if (
|
|
156
|
-
highlight.paused
|
|
157
|
-
&& strongId !== null
|
|
158
|
-
&& strongId === prevStrongId
|
|
159
|
-
&& strongId !== undefined
|
|
160
|
-
) {
|
|
161
|
-
ops.pulse(strongId);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Scroll-into-view target: for wrapper-entry pauses, scroll to the
|
|
165
|
-
// BARE (not the wrapper) so the focus matches the displayed state
|
|
166
|
-
// name. The worker's `resolveDisplayName` returns the bare's name
|
|
167
|
-
// for wrapper iters (so the log reads "paused at walkToBlank ..."),
|
|
168
|
-
// but `toId` is the wrapper's id and `highlightExpand` lights up
|
|
169
|
-
// both nodes as strong. Without this canonicalization the scroll
|
|
170
|
-
// lands on the wrapper while the log line and user's mental focus
|
|
171
|
-
// are on the bare. Halt-related ids (≤ 0) are scrolled to as-is —
|
|
172
|
-
// `bareIdOf` would collapse them all to the halt singleton, which
|
|
173
|
-
// is structurally separate from the in-frame halt marker the user
|
|
174
|
-
// is paused near.
|
|
175
|
-
if (strongId !== null) {
|
|
176
|
-
let scrollTarget: NodeKey = strongId;
|
|
177
|
-
if (typeof strongId === 'number' && strongId > 0) {
|
|
178
|
-
const node = graph.nodes[strongId];
|
|
179
|
-
if (node?.isWrapper && node.bareStateId !== null) {
|
|
180
|
-
scrollTarget = node.bareStateId;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
ops.scrollIntoView(scrollTarget);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const nextPrevStrongId = highlight.paused ? strongId : prevStrongId;
|
|
187
|
-
return { nextPrevStrongId };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Pure breakpoint-indicator rule evaluator. For each cached node key,
|
|
192
|
-
* emit `ops.setBreakpoint(key, on)` reflecting whether the node's
|
|
193
|
-
* canonical bare-id is in the `breakpoints` set.
|
|
194
|
-
*
|
|
195
|
-
* The 'idle' string sentinel never carries a breakpoint. All numeric
|
|
196
|
-
* keys are valid BP-class members:
|
|
197
|
-
* - positive id → regular state; canonical via bareIdOf (wrappers
|
|
198
|
-
* collapse to bare)
|
|
199
|
-
* - 0 → haltState singleton (engine-wide; canonical = 0)
|
|
200
|
-
* - negative id → halt marker (per-frame visualization sentinel;
|
|
201
|
-
* bareIdOf maps to 0 — same class as the singleton)
|
|
202
|
-
* Consumers pass their iterable of cached node keys (e.g. `nodeCache.keys()`).
|
|
203
|
-
*/
|
|
204
|
-
export function applyIndicator(
|
|
205
|
-
breakpoints: ReadonlySet<number>,
|
|
206
|
-
graph: Graph | null,
|
|
207
|
-
nodeIds: Iterable<NodeKey>,
|
|
208
|
-
ops: IndicatorOps,
|
|
209
|
-
): void {
|
|
210
|
-
for (const key of nodeIds) {
|
|
211
|
-
const on =
|
|
212
|
-
typeof key === 'number'
|
|
213
|
-
&& graph !== null
|
|
214
|
-
&& breakpoints.has(bareIdOf(key, graph));
|
|
215
|
-
ops.setBreakpoint(key, on);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"initialId": 6,
|
|
3
|
-
"alphabets": [
|
|
4
|
-
[
|
|
5
|
-
"␣",
|
|
6
|
-
"•"
|
|
7
|
-
]
|
|
8
|
-
],
|
|
9
|
-
"nodes": {
|
|
10
|
-
"0": {
|
|
11
|
-
"id": 0,
|
|
12
|
-
"name": "id:0",
|
|
13
|
-
"isHalt": true,
|
|
14
|
-
"isHaltMarker": false,
|
|
15
|
-
"isWrapper": false,
|
|
16
|
-
"bareStateId": null,
|
|
17
|
-
"frameId": null,
|
|
18
|
-
"transitions": [],
|
|
19
|
-
"overriddenHaltStateId": null,
|
|
20
|
-
"tags": []
|
|
21
|
-
},
|
|
22
|
-
"6": {
|
|
23
|
-
"id": 6,
|
|
24
|
-
"name": "10",
|
|
25
|
-
"isHalt": false,
|
|
26
|
-
"isHaltMarker": false,
|
|
27
|
-
"isWrapper": false,
|
|
28
|
-
"bareStateId": null,
|
|
29
|
-
"frameId": null,
|
|
30
|
-
"transitions": [
|
|
31
|
-
{
|
|
32
|
-
"pattern": "'•'",
|
|
33
|
-
"command": [
|
|
34
|
-
{
|
|
35
|
-
"symbol": "K",
|
|
36
|
-
"movement": "S"
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"nextStateId": 7,
|
|
40
|
-
"id": "6.0"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"pattern": "B",
|
|
44
|
-
"command": [
|
|
45
|
-
{
|
|
46
|
-
"symbol": "K",
|
|
47
|
-
"movement": "S"
|
|
48
|
-
}
|
|
49
|
-
],
|
|
50
|
-
"nextStateId": 8,
|
|
51
|
-
"id": "6.1"
|
|
52
|
-
}
|
|
53
|
-
],
|
|
54
|
-
"overriddenHaltStateId": null,
|
|
55
|
-
"tags": [
|
|
56
|
-
"main"
|
|
57
|
-
]
|
|
58
|
-
},
|
|
59
|
-
"7": {
|
|
60
|
-
"id": 7,
|
|
61
|
-
"name": "20",
|
|
62
|
-
"isHalt": false,
|
|
63
|
-
"isHaltMarker": false,
|
|
64
|
-
"isWrapper": false,
|
|
65
|
-
"bareStateId": null,
|
|
66
|
-
"frameId": null,
|
|
67
|
-
"transitions": [
|
|
68
|
-
{
|
|
69
|
-
"pattern": "*",
|
|
70
|
-
"command": [
|
|
71
|
-
{
|
|
72
|
-
"symbol": "K",
|
|
73
|
-
"movement": "R"
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
"nextStateId": 6,
|
|
77
|
-
"id": "7.0"
|
|
78
|
-
}
|
|
79
|
-
],
|
|
80
|
-
"overriddenHaltStateId": null,
|
|
81
|
-
"tags": []
|
|
82
|
-
},
|
|
83
|
-
"8": {
|
|
84
|
-
"id": 8,
|
|
85
|
-
"name": "30",
|
|
86
|
-
"isHalt": false,
|
|
87
|
-
"isHaltMarker": false,
|
|
88
|
-
"isWrapper": false,
|
|
89
|
-
"bareStateId": null,
|
|
90
|
-
"frameId": null,
|
|
91
|
-
"transitions": [
|
|
92
|
-
{
|
|
93
|
-
"pattern": "*",
|
|
94
|
-
"command": [
|
|
95
|
-
{
|
|
96
|
-
"symbol": "'•'",
|
|
97
|
-
"movement": "S"
|
|
98
|
-
}
|
|
99
|
-
],
|
|
100
|
-
"nextStateId": 0,
|
|
101
|
-
"id": "8.0"
|
|
102
|
-
}
|
|
103
|
-
],
|
|
104
|
-
"overriddenHaltStateId": null,
|
|
105
|
-
"tags": []
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|