@turing-machine-js/visuals 7.0.0-alpha.6
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 +21 -0
- package/LICENSE +674 -0
- package/README.md +21 -0
- package/dist/applyHighlight.d.ts +42 -0
- package/dist/applyHighlight.js +191 -0
- package/dist/format.d.ts +22 -0
- package/dist/format.js +46 -0
- package/dist/graphIndexes.d.ts +31 -0
- package/dist/graphIndexes.js +58 -0
- package/dist/graphUtils.d.ts +37 -0
- package/dist/graphUtils.js +76 -0
- package/dist/highlightOps.d.ts +81 -0
- package/dist/highlightOps.js +36 -0
- package/dist/index.cjs +514 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.mjs +503 -0
- package/dist/recordSnippet.d.ts +35 -0
- package/dist/recordSnippet.js +92 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.js +1 -0
- package/docs/graph-highlight-and-breakpoints.md +272 -0
- package/package.json +46 -0
- package/src/applyHighlight.spec.ts +331 -0
- package/src/applyHighlight.ts +217 -0
- package/src/fixtures/graphs/post-walk-mark.json +108 -0
- package/src/fixtures/graphs/turing-callable-subtree.json +108 -0
- package/src/fixtures/graphs/turing-copy-two-tapes.json +87 -0
- package/src/fixtures/graphs/turing-replace-b.json +72 -0
- package/src/format.spec.ts +100 -0
- package/src/format.ts +51 -0
- package/src/graphIndexes.ts +84 -0
- package/src/graphUtils.spec.ts +112 -0
- package/src/graphUtils.ts +74 -0
- package/src/highlightOps.ts +94 -0
- package/src/index.ts +10 -0
- package/src/recordSnippet.spec.ts +275 -0
- package/src/recordSnippet.ts +141 -0
- package/src/types.ts +96 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @turing-machine-js/visuals
|
|
2
|
+
|
|
3
|
+
Pure highlight + graph-indexing logic for [`@turing-machine-js/machine`](../machine). No DOM, no Svelte, no Mermaid — consumers bring their own renderer and DOM applier.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
Types and pure functions for:
|
|
8
|
+
- Indexing an engine `Graph` for wrapper/bare lookup (`indexGraph`, `bareIdOf`, `highlightExpand`).
|
|
9
|
+
- Applying highlight + indicator operations against a renderer-agnostic `HighlightOps` interface (`applyHighlight`, `applyIndicator`).
|
|
10
|
+
|
|
11
|
+
See [`docs/graph-highlight-and-breakpoints.md`](./docs/graph-highlight-and-breakpoints.md) for the full set of rules these functions satisfy.
|
|
12
|
+
|
|
13
|
+
## Versioning
|
|
14
|
+
|
|
15
|
+
Lockstep with `@turing-machine-js/machine`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @turing-machine-js/visuals @turing-machine-js/machine
|
|
21
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Pure highlight-rule evaluator. Given the current `highlight` (from
|
|
7
|
+
* `MachineView`'s `$derived`), the engine `graph`, derived `indexes`,
|
|
8
|
+
* and the previous strong-id (for pause-revisit pulse detection), emit
|
|
9
|
+
* a sequence of `ops` calls describing the resulting visual state.
|
|
10
|
+
*
|
|
11
|
+
* Strictly additive — the caller is expected to clear previously-applied
|
|
12
|
+
* highlight classes / edge marks / cluster activations BEFORE invoking
|
|
13
|
+
* this function. The function never reads back from the consumer.
|
|
14
|
+
*
|
|
15
|
+
* Returns the new prev-strong-id to thread into the next call. Pulse
|
|
16
|
+
* comparison uses the RAW strong id (not canonical), so wrapper-pause
|
|
17
|
+
* and bare-pause register as different positions and don't pulse each
|
|
18
|
+
* other. Updates only when `highlight.paused === true`; non-paused
|
|
19
|
+
* events (idle / RUNNING_AUTO ticks) leave it untouched. Null highlight
|
|
20
|
+
* resets it to null.
|
|
21
|
+
*
|
|
22
|
+
* See `docs/graph-highlight-and-breakpoints.md` for the 16 rules
|
|
23
|
+
* enumerated.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyHighlight(highlight: GraphHighlight | null, graph: Graph | null, indexes: GraphIndexes, prevStrongId: NodeKey | null, ops: HighlightOps): {
|
|
26
|
+
nextPrevStrongId: NodeKey | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Pure breakpoint-indicator rule evaluator. For each cached node key,
|
|
30
|
+
* emit `ops.setBreakpoint(key, on)` reflecting whether the node's
|
|
31
|
+
* canonical bare-id is in the `breakpoints` set.
|
|
32
|
+
*
|
|
33
|
+
* The 'idle' string sentinel never carries a breakpoint. All numeric
|
|
34
|
+
* keys are valid BP-class members:
|
|
35
|
+
* - positive id → regular state; canonical via bareIdOf (wrappers
|
|
36
|
+
* collapse to bare)
|
|
37
|
+
* - 0 → haltState singleton (engine-wide; canonical = 0)
|
|
38
|
+
* - negative id → halt marker (per-frame visualization sentinel;
|
|
39
|
+
* bareIdOf maps to 0 — same class as the singleton)
|
|
40
|
+
* Consumers pass their iterable of cached node keys (e.g. `nodeCache.keys()`).
|
|
41
|
+
*/
|
|
42
|
+
export declare function applyIndicator(breakpoints: ReadonlySet<number>, graph: Graph | null, nodeIds: Iterable<NodeKey>, ops: IndicatorOps): void;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { bareIdOf, highlightExpand } from './graphUtils';
|
|
2
|
+
/**
|
|
3
|
+
* Pure highlight-rule evaluator. Given the current `highlight` (from
|
|
4
|
+
* `MachineView`'s `$derived`), the engine `graph`, derived `indexes`,
|
|
5
|
+
* and the previous strong-id (for pause-revisit pulse detection), emit
|
|
6
|
+
* a sequence of `ops` calls describing the resulting visual state.
|
|
7
|
+
*
|
|
8
|
+
* Strictly additive — the caller is expected to clear previously-applied
|
|
9
|
+
* highlight classes / edge marks / cluster activations BEFORE invoking
|
|
10
|
+
* this function. The function never reads back from the consumer.
|
|
11
|
+
*
|
|
12
|
+
* Returns the new prev-strong-id to thread into the next call. Pulse
|
|
13
|
+
* comparison uses the RAW strong id (not canonical), so wrapper-pause
|
|
14
|
+
* and bare-pause register as different positions and don't pulse each
|
|
15
|
+
* other. Updates only when `highlight.paused === true`; non-paused
|
|
16
|
+
* events (idle / RUNNING_AUTO ticks) leave it untouched. Null highlight
|
|
17
|
+
* resets it to null.
|
|
18
|
+
*
|
|
19
|
+
* See `docs/graph-highlight-and-breakpoints.md` for the 16 rules
|
|
20
|
+
* enumerated.
|
|
21
|
+
*/
|
|
22
|
+
export function applyHighlight(highlight, graph, indexes, prevStrongId, ops) {
|
|
23
|
+
if (!highlight || !graph) {
|
|
24
|
+
return { nextPrevStrongId: null };
|
|
25
|
+
}
|
|
26
|
+
// §5 Halt-target retargeting: real halt (id 0) reached from an in-frame
|
|
27
|
+
// state retargets to the frame's halt marker (id = -frameId), so the
|
|
28
|
+
// visible edge lands inside the cluster.
|
|
29
|
+
let toId = highlight.toId;
|
|
30
|
+
if (toId === 0 && typeof highlight.fromId === 'number') {
|
|
31
|
+
const fromFrameId = indexes.nodeFrameMap.get(highlight.fromId);
|
|
32
|
+
if (fromFrameId !== undefined)
|
|
33
|
+
toId = -fromFrameId;
|
|
34
|
+
}
|
|
35
|
+
// §2 Equivalence-class expansion (asymmetric, via highlightExpand):
|
|
36
|
+
// wrapper → [wrapper, bare] (joined visual pair for wrapper-entry pause)
|
|
37
|
+
// bare → [bare] (engine genuinely on the bare; no wrapper sync)
|
|
38
|
+
// From-side expansion only fires for positive numeric ids; the 'idle'
|
|
39
|
+
// sentinel is handled directly below. Halt markers / singleton fall
|
|
40
|
+
// through the direct-lookup branches.
|
|
41
|
+
const fromEqIds = typeof highlight.fromId === 'number'
|
|
42
|
+
? highlightExpand(highlight.fromId, graph)
|
|
43
|
+
: [];
|
|
44
|
+
const toEqIds = toId !== null && toId > 0
|
|
45
|
+
? highlightExpand(toId, graph)
|
|
46
|
+
: [];
|
|
47
|
+
// §3 Class application — from side.
|
|
48
|
+
if (highlight.fromId === 'idle') {
|
|
49
|
+
ops.addNodeClass('idle', 'mg-highlight-from');
|
|
50
|
+
if (highlight.strong === 'from')
|
|
51
|
+
ops.addNodeClass('idle', 'mg-highlight-strong');
|
|
52
|
+
}
|
|
53
|
+
for (const id of fromEqIds) {
|
|
54
|
+
ops.addNodeClass(id, 'mg-highlight-from');
|
|
55
|
+
if (highlight.strong === 'from')
|
|
56
|
+
ops.addNodeClass(id, 'mg-highlight-strong');
|
|
57
|
+
}
|
|
58
|
+
// §3 + §8 Class application — to side. Halt markers (toId < 0) and the
|
|
59
|
+
// real halt singleton (toId === 0; only possible when §5 didn't retarget)
|
|
60
|
+
// bypass the equivalence-class expansion via direct lookup.
|
|
61
|
+
if (toId !== null && toId <= 0) {
|
|
62
|
+
ops.addNodeClass(toId, 'mg-highlight-to');
|
|
63
|
+
if (highlight.strong === 'to')
|
|
64
|
+
ops.addNodeClass(toId, 'mg-highlight-strong');
|
|
65
|
+
}
|
|
66
|
+
for (const id of toEqIds) {
|
|
67
|
+
ops.addNodeClass(id, 'mg-highlight-to');
|
|
68
|
+
if (highlight.strong === 'to')
|
|
69
|
+
ops.addNodeClass(id, 'mg-highlight-strong');
|
|
70
|
+
}
|
|
71
|
+
// Edge highlight: the data-id token form mermaid emits.
|
|
72
|
+
const fromKey = highlight.fromId === 'idle' ? 'idle' : `s${highlight.fromId}`;
|
|
73
|
+
const toKey = toId === null ? null
|
|
74
|
+
: toId < 0 ? `c${-toId}` // halt marker
|
|
75
|
+
: `s${toId}`;
|
|
76
|
+
if (toKey !== null)
|
|
77
|
+
ops.highlightEdge(fromKey, toKey);
|
|
78
|
+
// §10 Wrapper-entry "call" edge: when to-side expanded to [wrapper, bare],
|
|
79
|
+
// light up the wrapper→bare connector so the joined pair has a visible link.
|
|
80
|
+
if (toEqIds.length > 1) {
|
|
81
|
+
const wrapperId = toEqIds.find((id) => graph.nodes[id]?.isWrapper);
|
|
82
|
+
const bareId = toEqIds.find((id) => !graph.nodes[id]?.isWrapper);
|
|
83
|
+
if (wrapperId !== undefined && bareId !== undefined) {
|
|
84
|
+
ops.highlightEdge(`s${wrapperId}`, `s${bareId}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// §6 Source return chain: just-fired transition landed on a frame's
|
|
88
|
+
// halt marker. Light up the post-pop trajectory before the next iter
|
|
89
|
+
// moves the strong node.
|
|
90
|
+
if (toId !== null && toId < 0) {
|
|
91
|
+
const frameId = -toId;
|
|
92
|
+
const wrappers = indexes.frameWrappersMap.get(frameId) ?? [];
|
|
93
|
+
for (const { wrapperId, overrideId } of wrappers) {
|
|
94
|
+
ops.highlightEdge(`w_${frameId}`, `s${wrapperId}`);
|
|
95
|
+
ops.addNodeClass(wrapperId, 'mg-highlight-to');
|
|
96
|
+
if (overrideId !== null) {
|
|
97
|
+
ops.highlightEdge(`s${wrapperId}`, `s${overrideId}`);
|
|
98
|
+
ops.addNodeClass(overrideId, 'mg-highlight-to');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// §7 Destination return chain: paused at a positive toId that's some
|
|
103
|
+
// wrapper W's override AND fromId is in W's frame — the engine just
|
|
104
|
+
// popped. The straight bare→override edge doesn't exist in the graph;
|
|
105
|
+
// light up the actual visible path bare → halt-marker → return →
|
|
106
|
+
// wrapper → override, plus the frame cluster.
|
|
107
|
+
if (typeof highlight.fromId === 'number' && toId !== null && toId > 0) {
|
|
108
|
+
const fromFrameId = indexes.nodeFrameMap.get(highlight.fromId);
|
|
109
|
+
if (fromFrameId !== undefined) {
|
|
110
|
+
const wrappers = indexes.frameWrappersMap.get(fromFrameId) ?? [];
|
|
111
|
+
const matching = wrappers.filter((w) => w.overrideId === toId);
|
|
112
|
+
if (matching.length > 0) {
|
|
113
|
+
ops.addNodeClass(-fromFrameId, 'mg-highlight-to');
|
|
114
|
+
ops.highlightEdge(`s${highlight.fromId}`, `c${fromFrameId}`);
|
|
115
|
+
for (const { wrapperId } of matching) {
|
|
116
|
+
ops.highlightEdge(`w_${fromFrameId}`, `s${wrapperId}`);
|
|
117
|
+
ops.addNodeClass(wrapperId, 'mg-highlight-to');
|
|
118
|
+
ops.highlightEdge(`s${wrapperId}`, `s${toId}`);
|
|
119
|
+
}
|
|
120
|
+
ops.markFrameActive(fromFrameId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// §9 Frame-active for the strong node. Wrappers are outside any frame
|
|
125
|
+
// so canonicalize via bareIdOf so the wrapper-entry pause still lights
|
|
126
|
+
// up the bare's enclosing cluster.
|
|
127
|
+
const strongId = highlight.strong === 'from' ? highlight.fromId : highlight.toId;
|
|
128
|
+
const strongIdCanonical = typeof strongId === 'number'
|
|
129
|
+
? bareIdOf(strongId, graph)
|
|
130
|
+
: strongId;
|
|
131
|
+
if (typeof strongIdCanonical === 'number') {
|
|
132
|
+
const frameId = indexes.nodeFrameMap.get(strongIdCanonical);
|
|
133
|
+
if (frameId !== undefined)
|
|
134
|
+
ops.markFrameActive(frameId);
|
|
135
|
+
}
|
|
136
|
+
// §11 Pulse on same-state revisit. Uses RAW strongId — wrapper-pause
|
|
137
|
+
// and bare-pause are visually distinct positions even though they
|
|
138
|
+
// share #debugRef; pausing at wrapper then continuing into bare must
|
|
139
|
+
// not pulse. Idles never pulse and never update prevStrongId.
|
|
140
|
+
if (highlight.paused
|
|
141
|
+
&& strongId !== null
|
|
142
|
+
&& strongId === prevStrongId
|
|
143
|
+
&& strongId !== undefined) {
|
|
144
|
+
ops.pulse(strongId);
|
|
145
|
+
}
|
|
146
|
+
// Scroll-into-view target: for wrapper-entry pauses, scroll to the
|
|
147
|
+
// BARE (not the wrapper) so the focus matches the displayed state
|
|
148
|
+
// name. The worker's `resolveDisplayName` returns the bare's name
|
|
149
|
+
// for wrapper iters (so the log reads "paused at walkToBlank ..."),
|
|
150
|
+
// but `toId` is the wrapper's id and `highlightExpand` lights up
|
|
151
|
+
// both nodes as strong. Without this canonicalization the scroll
|
|
152
|
+
// lands on the wrapper while the log line and user's mental focus
|
|
153
|
+
// are on the bare. Halt-related ids (≤ 0) are scrolled to as-is —
|
|
154
|
+
// `bareIdOf` would collapse them all to the halt singleton, which
|
|
155
|
+
// is structurally separate from the in-frame halt marker the user
|
|
156
|
+
// is paused near.
|
|
157
|
+
if (strongId !== null) {
|
|
158
|
+
let scrollTarget = strongId;
|
|
159
|
+
if (typeof strongId === 'number' && strongId > 0) {
|
|
160
|
+
const node = graph.nodes[strongId];
|
|
161
|
+
if (node?.isWrapper && node.bareStateId !== null) {
|
|
162
|
+
scrollTarget = node.bareStateId;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
ops.scrollIntoView(scrollTarget);
|
|
166
|
+
}
|
|
167
|
+
const nextPrevStrongId = highlight.paused ? strongId : prevStrongId;
|
|
168
|
+
return { nextPrevStrongId };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Pure breakpoint-indicator rule evaluator. For each cached node key,
|
|
172
|
+
* emit `ops.setBreakpoint(key, on)` reflecting whether the node's
|
|
173
|
+
* canonical bare-id is in the `breakpoints` set.
|
|
174
|
+
*
|
|
175
|
+
* The 'idle' string sentinel never carries a breakpoint. All numeric
|
|
176
|
+
* keys are valid BP-class members:
|
|
177
|
+
* - positive id → regular state; canonical via bareIdOf (wrappers
|
|
178
|
+
* collapse to bare)
|
|
179
|
+
* - 0 → haltState singleton (engine-wide; canonical = 0)
|
|
180
|
+
* - negative id → halt marker (per-frame visualization sentinel;
|
|
181
|
+
* bareIdOf maps to 0 — same class as the singleton)
|
|
182
|
+
* Consumers pass their iterable of cached node keys (e.g. `nodeCache.keys()`).
|
|
183
|
+
*/
|
|
184
|
+
export function applyIndicator(breakpoints, graph, nodeIds, ops) {
|
|
185
|
+
for (const key of nodeIds) {
|
|
186
|
+
const on = typeof key === 'number'
|
|
187
|
+
&& graph !== null
|
|
188
|
+
&& breakpoints.has(bareIdOf(key, graph));
|
|
189
|
+
ops.setBreakpoint(key, on);
|
|
190
|
+
}
|
|
191
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type MachineState, type TapeCommand } from '@turing-machine-js/machine';
|
|
2
|
+
export declare const MOVEMENT_LETTER: Map<symbol, "L" | "R" | "S">;
|
|
3
|
+
/**
|
|
4
|
+
* Render a single tape command in `WRITE/MOVE` form.
|
|
5
|
+
* - Write: `'X'` (literal symbol) | `K` (keep) | `E` (erase = write blank).
|
|
6
|
+
* - Move: `L` / `R` / `S` from `movements.*`.
|
|
7
|
+
*
|
|
8
|
+
* Matches the engine's edge-label vocabulary so formatted commands line up
|
|
9
|
+
* with the write/move cells in `toMermaid`-emitted edge labels.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatCommand(tapeCommand: TapeCommand): string;
|
|
12
|
+
/**
|
|
13
|
+
* Render one step's edge-label notation: `[reads] → [writes]/[moves]`.
|
|
14
|
+
* Each role is wrapped in a single `[…]`; multi-tape entries are
|
|
15
|
+
* comma-separated inside the brackets.
|
|
16
|
+
*
|
|
17
|
+
* Matches the engine's `toMermaid` emit so logged steps line up with
|
|
18
|
+
* graph edge labels. Note: `nextSymbols` in `MachineState` is already
|
|
19
|
+
* resolved (keep → current symbol, erase → blank) — `K` is inferred
|
|
20
|
+
* by comparing `nextSymbols[i] === currentSymbols[i]`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatStep(m: MachineState): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { movements, symbolCommands } from '@turing-machine-js/machine';
|
|
2
|
+
export const MOVEMENT_LETTER = new Map([
|
|
3
|
+
[movements.left, 'L'],
|
|
4
|
+
[movements.right, 'R'],
|
|
5
|
+
[movements.stay, 'S'],
|
|
6
|
+
]);
|
|
7
|
+
/**
|
|
8
|
+
* Render a single tape command in `WRITE/MOVE` form.
|
|
9
|
+
* - Write: `'X'` (literal symbol) | `K` (keep) | `E` (erase = write blank).
|
|
10
|
+
* - Move: `L` / `R` / `S` from `movements.*`.
|
|
11
|
+
*
|
|
12
|
+
* Matches the engine's edge-label vocabulary so formatted commands line up
|
|
13
|
+
* with the write/move cells in `toMermaid`-emitted edge labels.
|
|
14
|
+
*/
|
|
15
|
+
export function formatCommand(tapeCommand) {
|
|
16
|
+
let write;
|
|
17
|
+
if (tapeCommand.symbol === symbolCommands.keep) {
|
|
18
|
+
write = 'K';
|
|
19
|
+
}
|
|
20
|
+
else if (tapeCommand.symbol === symbolCommands.erase) {
|
|
21
|
+
write = 'E';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
write = `'${tapeCommand.symbol}'`;
|
|
25
|
+
}
|
|
26
|
+
const move = MOVEMENT_LETTER.get(tapeCommand.movement) ?? '?';
|
|
27
|
+
return `${write}/${move}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Render one step's edge-label notation: `[reads] → [writes]/[moves]`.
|
|
31
|
+
* Each role is wrapped in a single `[…]`; multi-tape entries are
|
|
32
|
+
* comma-separated inside the brackets.
|
|
33
|
+
*
|
|
34
|
+
* Matches the engine's `toMermaid` emit so logged steps line up with
|
|
35
|
+
* graph edge labels. Note: `nextSymbols` in `MachineState` is already
|
|
36
|
+
* resolved (keep → current symbol, erase → blank) — `K` is inferred
|
|
37
|
+
* by comparing `nextSymbols[i] === currentSymbols[i]`.
|
|
38
|
+
*/
|
|
39
|
+
export function formatStep(m) {
|
|
40
|
+
const reads = m.currentSymbols.map((s) => `'${s}'`).join(',');
|
|
41
|
+
const writes = m.nextSymbols
|
|
42
|
+
.map((s, i) => (s === m.currentSymbols[i] ? 'K' : `'${s}'`))
|
|
43
|
+
.join(',');
|
|
44
|
+
const moves = m.movements.map((mv) => MOVEMENT_LETTER.get(mv) ?? '?').join(',');
|
|
45
|
+
return `[${reads}] → [${writes}]/[${moves}]`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Graph } from '@turing-machine-js/machine';
|
|
2
|
+
/**
|
|
3
|
+
* Derived lookups over an engine `Graph` that the highlight + indicator
|
|
4
|
+
* passes need. Recomputed once per Build; consumed read-only thereafter.
|
|
5
|
+
*
|
|
6
|
+
* Pure transformation of `graph` — same input always produces deep-equal
|
|
7
|
+
* output. See `docs/graph-highlight-and-breakpoints.md` for how each
|
|
8
|
+
* field is consumed.
|
|
9
|
+
*/
|
|
10
|
+
export type GraphIndexes = {
|
|
11
|
+
/** `GraphNode.id` → containing callable-subtree frameId. Only nodes
|
|
12
|
+
* with `frameId !== null` (i.e. in-frame states) are present. */
|
|
13
|
+
nodeFrameMap: Map<number, number>;
|
|
14
|
+
/** frameId → list of wrappers calling into that frame, each with the
|
|
15
|
+
* wrapper's id and its override-target id. Used by both source and
|
|
16
|
+
* destination return-chain passes. */
|
|
17
|
+
frameWrappersMap: Map<number, Array<{
|
|
18
|
+
wrapperId: number;
|
|
19
|
+
overrideId: number | null;
|
|
20
|
+
}>>;
|
|
21
|
+
/** Cluster label text (as emitted by `toMermaid`) → frameId. The
|
|
22
|
+
* rendered SVG's `g.cluster` carries the label inside a
|
|
23
|
+
* `<foreignObject>`; consumers match by `label.textContent.trim()` to
|
|
24
|
+
* build their own `clusterCache: Map<frameId, SVGElement>`. */
|
|
25
|
+
frameLabelToId: Map<string, number>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Walk the engine graph once and build all derived lookups. Cheap;
|
|
29
|
+
* intended to run on every Build (graph identity changes per build).
|
|
30
|
+
*/
|
|
31
|
+
export declare function indexGraph(graph: Graph | null): GraphIndexes;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk the engine graph once and build all derived lookups. Cheap;
|
|
3
|
+
* intended to run on every Build (graph identity changes per build).
|
|
4
|
+
*/
|
|
5
|
+
export function indexGraph(graph) {
|
|
6
|
+
const nodeFrameMap = new Map();
|
|
7
|
+
const frameWrappersMap = new Map();
|
|
8
|
+
const frameLabelToId = new Map();
|
|
9
|
+
if (!graph)
|
|
10
|
+
return { nodeFrameMap, frameWrappersMap, frameLabelToId };
|
|
11
|
+
for (const node of Object.values(graph.nodes)) {
|
|
12
|
+
if (node.frameId !== null)
|
|
13
|
+
nodeFrameMap.set(node.id, node.frameId);
|
|
14
|
+
}
|
|
15
|
+
// For each wrapper, append to its bare's frame entry. Multiple wrappers
|
|
16
|
+
// can share the same bare with different overrides; we record them all
|
|
17
|
+
// so the return-chain passes can highlight every candidate.
|
|
18
|
+
for (const node of Object.values(graph.nodes)) {
|
|
19
|
+
if (!node.isWrapper || node.bareStateId === null)
|
|
20
|
+
continue;
|
|
21
|
+
const bare = graph.nodes[node.bareStateId];
|
|
22
|
+
if (!bare || bare.frameId === null)
|
|
23
|
+
continue;
|
|
24
|
+
const entry = { wrapperId: node.id, overrideId: node.overriddenHaltStateId };
|
|
25
|
+
const arr = frameWrappersMap.get(bare.frameId);
|
|
26
|
+
if (arr)
|
|
27
|
+
arr.push(entry);
|
|
28
|
+
else
|
|
29
|
+
frameWrappersMap.set(bare.frameId, [entry]);
|
|
30
|
+
}
|
|
31
|
+
// Cluster label reconstruction: mirrors the engine's `toMermaid` emit
|
|
32
|
+
// (`callable subtree of NAME` for single-bare frames, `callable scope:
|
|
33
|
+
// A ∪ B ∪ …` for union frames; bare names sorted by id). Consumers
|
|
34
|
+
// need this to map mermaid's rendered cluster (whose own SVG id is the
|
|
35
|
+
// useless literal `[object Object]`) back to a frameId.
|
|
36
|
+
const bareIds = new Set();
|
|
37
|
+
for (const n of Object.values(graph.nodes)) {
|
|
38
|
+
if (n.isWrapper && n.bareStateId !== null)
|
|
39
|
+
bareIds.add(n.bareStateId);
|
|
40
|
+
}
|
|
41
|
+
const frameToBareNames = new Map();
|
|
42
|
+
for (const n of Object.values(graph.nodes).sort((a, b) => a.id - b.id)) {
|
|
43
|
+
if (n.isWrapper || n.isHaltMarker || n.frameId === null)
|
|
44
|
+
continue;
|
|
45
|
+
if (!bareIds.has(n.id))
|
|
46
|
+
continue;
|
|
47
|
+
const arr = frameToBareNames.get(n.frameId) ?? [];
|
|
48
|
+
arr.push(n.name);
|
|
49
|
+
frameToBareNames.set(n.frameId, arr);
|
|
50
|
+
}
|
|
51
|
+
for (const [frameId, names] of frameToBareNames) {
|
|
52
|
+
const label = names.length > 1
|
|
53
|
+
? `callable scope: ${names.join(' ∪ ')}`
|
|
54
|
+
: `callable subtree of ${names[0] ?? frameId}`;
|
|
55
|
+
frameLabelToId.set(label, frameId);
|
|
56
|
+
}
|
|
57
|
+
return { nodeFrameMap, frameWrappersMap, frameLabelToId };
|
|
58
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Graph } from '@turing-machine-js/machine';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize an engine `GraphNode.id` to its canonical representative for
|
|
4
|
+
* breakpoint-class lookups (machines-demo#37). Wrappers produced by
|
|
5
|
+
* `State.withOverriddenHaltState` share `#debugRef` with their bare state
|
|
6
|
+
* engine-side (turing-machine-js v7 `State.ts`: `state.#debugRef =
|
|
7
|
+
* bare.#debugRef`), so they form a single breakpoint from the user's POV.
|
|
8
|
+
* This collapses any wrapper id to its bare's id; non-wrapper ids return
|
|
9
|
+
* self. Used so the demo can store ONE canonical id per equivalence class
|
|
10
|
+
* in its breakpoint set, and expand to all class members for indicator
|
|
11
|
+
* rendering — keeping the worker-side toggle count to one per class
|
|
12
|
+
* (multiple toggles on the shared ref would double-flip).
|
|
13
|
+
*/
|
|
14
|
+
export declare function bareIdOf(id: number, graph: Graph | null): number;
|
|
15
|
+
/**
|
|
16
|
+
* Asymmetric expansion for the highlight effect (machines-demo#37).
|
|
17
|
+
* Wrapper → `[wrapper, bare]` (the wrapper-entry pause is visually joined
|
|
18
|
+
* to its bare, since the user thinks of them as one call site).
|
|
19
|
+
* Bare → `[bare]` only (when the engine is genuinely on the bare — e.g. a
|
|
20
|
+
* loop iter — the wrapper is not the "active" state and shouldn't get the
|
|
21
|
+
* strong highlight).
|
|
22
|
+
* Non-wrapper / non-bare ids return `[id]`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function highlightExpand(id: number, graph: Graph | null): number[];
|
|
25
|
+
/**
|
|
26
|
+
* All GraphNode ids in the same breakpoint equivalence class as `id`.
|
|
27
|
+
* Symmetric — gives consumers the full list of nodes that share an engine
|
|
28
|
+
* breakpoint, regardless of which class member is the input. Used by the
|
|
29
|
+
* context-menu's "Shared with" info line so the user can see at a glance
|
|
30
|
+
* which other nodes flip together.
|
|
31
|
+
*
|
|
32
|
+
* Halt class (canonical id 0): the halt singleton + every halt marker in
|
|
33
|
+
* the graph. Wrapper/bare class: the bare + every wrapper pointing at it.
|
|
34
|
+
* Singleton classes (regular states, idle sentinel proxies) return just
|
|
35
|
+
* the input id.
|
|
36
|
+
*/
|
|
37
|
+
export declare function equivalentIds(id: number, graph: Graph | null): number[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize an engine `GraphNode.id` to its canonical representative for
|
|
3
|
+
* breakpoint-class lookups (machines-demo#37). Wrappers produced by
|
|
4
|
+
* `State.withOverriddenHaltState` share `#debugRef` with their bare state
|
|
5
|
+
* engine-side (turing-machine-js v7 `State.ts`: `state.#debugRef =
|
|
6
|
+
* bare.#debugRef`), so they form a single breakpoint from the user's POV.
|
|
7
|
+
* This collapses any wrapper id to its bare's id; non-wrapper ids return
|
|
8
|
+
* self. Used so the demo can store ONE canonical id per equivalence class
|
|
9
|
+
* in its breakpoint set, and expand to all class members for indicator
|
|
10
|
+
* rendering — keeping the worker-side toggle count to one per class
|
|
11
|
+
* (multiple toggles on the shared ref would double-flip).
|
|
12
|
+
*/
|
|
13
|
+
export function bareIdOf(id, graph) {
|
|
14
|
+
if (!graph)
|
|
15
|
+
return id;
|
|
16
|
+
// Halt markers (negative ids, one per frame) are visualization sentinels;
|
|
17
|
+
// at runtime they all collapse to the haltState singleton (id 0). For
|
|
18
|
+
// breakpoint purposes they're a single class — setting BP on any
|
|
19
|
+
// halt-related node sets it on the global haltState.
|
|
20
|
+
if (id < 0)
|
|
21
|
+
return 0;
|
|
22
|
+
const node = graph.nodes[id];
|
|
23
|
+
if (node && node.isWrapper && node.bareStateId !== null) {
|
|
24
|
+
return node.bareStateId;
|
|
25
|
+
}
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Asymmetric expansion for the highlight effect (machines-demo#37).
|
|
30
|
+
* Wrapper → `[wrapper, bare]` (the wrapper-entry pause is visually joined
|
|
31
|
+
* to its bare, since the user thinks of them as one call site).
|
|
32
|
+
* Bare → `[bare]` only (when the engine is genuinely on the bare — e.g. a
|
|
33
|
+
* loop iter — the wrapper is not the "active" state and shouldn't get the
|
|
34
|
+
* strong highlight).
|
|
35
|
+
* Non-wrapper / non-bare ids return `[id]`.
|
|
36
|
+
*/
|
|
37
|
+
export function highlightExpand(id, graph) {
|
|
38
|
+
if (!graph)
|
|
39
|
+
return [id];
|
|
40
|
+
const node = graph.nodes[id];
|
|
41
|
+
if (node?.isWrapper && node.bareStateId !== null) {
|
|
42
|
+
return [id, node.bareStateId];
|
|
43
|
+
}
|
|
44
|
+
return [id];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* All GraphNode ids in the same breakpoint equivalence class as `id`.
|
|
48
|
+
* Symmetric — gives consumers the full list of nodes that share an engine
|
|
49
|
+
* breakpoint, regardless of which class member is the input. Used by the
|
|
50
|
+
* context-menu's "Shared with" info line so the user can see at a glance
|
|
51
|
+
* which other nodes flip together.
|
|
52
|
+
*
|
|
53
|
+
* Halt class (canonical id 0): the halt singleton + every halt marker in
|
|
54
|
+
* the graph. Wrapper/bare class: the bare + every wrapper pointing at it.
|
|
55
|
+
* Singleton classes (regular states, idle sentinel proxies) return just
|
|
56
|
+
* the input id.
|
|
57
|
+
*/
|
|
58
|
+
export function equivalentIds(id, graph) {
|
|
59
|
+
if (!graph)
|
|
60
|
+
return [id];
|
|
61
|
+
const canonical = bareIdOf(id, graph);
|
|
62
|
+
if (canonical === 0) {
|
|
63
|
+
const ids = [0];
|
|
64
|
+
for (const node of Object.values(graph.nodes)) {
|
|
65
|
+
if (node.isHaltMarker)
|
|
66
|
+
ids.push(node.id);
|
|
67
|
+
}
|
|
68
|
+
return ids;
|
|
69
|
+
}
|
|
70
|
+
const result = new Set([canonical]);
|
|
71
|
+
for (const node of Object.values(graph.nodes)) {
|
|
72
|
+
if (node.isWrapper && node.bareStateId === canonical)
|
|
73
|
+
result.add(node.id);
|
|
74
|
+
}
|
|
75
|
+
return [...result];
|
|
76
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
export type NodeKey = number | 'idle';
|
|
14
|
+
/** Classes the apply-highlight pass may add to a `g.node` element. */
|
|
15
|
+
export type HighlightClass = 'mg-highlight-from' | 'mg-highlight-to' | 'mg-highlight-strong';
|
|
16
|
+
/**
|
|
17
|
+
* Operations the highlight logic invokes on the rendered graph. Purely
|
|
18
|
+
* additive — the consumer is expected to clear previous highlight state
|
|
19
|
+
* (classes, marker swaps) BEFORE invoking `applyHighlight`. The pure
|
|
20
|
+
* function never reads back from the consumer; it just emits ops.
|
|
21
|
+
*
|
|
22
|
+
* Edge keys follow mermaid's data-id token form: `'idle'` for the
|
|
23
|
+
* synthetic entry sentinel, `'s${id}'` for regular/wrapper/bare states,
|
|
24
|
+
* `'c${id}'` for halt markers (id = `-frameId`), `'w_${id}'` for callable-
|
|
25
|
+
* subtree subgraph clusters. Mermaid emits `L_${from}_${to}_${ix}` per
|
|
26
|
+
* edge; ix-resolution is the consumer's concern (multiple edges between
|
|
27
|
+
* the same pair are rare; the consumer typically picks the first match).
|
|
28
|
+
*/
|
|
29
|
+
export interface HighlightOps {
|
|
30
|
+
/** Add a highlight class to the node identified by `id`. */
|
|
31
|
+
addNodeClass(id: NodeKey, cls: HighlightClass): void;
|
|
32
|
+
/** Highlight the edge whose data-id matches `L_${fromKey}_${toKey}_*`. */
|
|
33
|
+
highlightEdge(fromKey: string, toKey: string): void;
|
|
34
|
+
/** Mark the callable-subtree cluster for `frameId` as active. */
|
|
35
|
+
markFrameActive(frameId: number): void;
|
|
36
|
+
/** Fire a one-shot pulse animation on the given node. */
|
|
37
|
+
pulse(id: NodeKey): void;
|
|
38
|
+
/** Scroll the given node into the visible area of its container. */
|
|
39
|
+
scrollIntoView(id: NodeKey): void;
|
|
40
|
+
}
|
|
41
|
+
/** Operations the indicator (breakpoint dot) pass invokes. */
|
|
42
|
+
export interface IndicatorOps {
|
|
43
|
+
/** Set or clear the breakpoint indicator on the given node. */
|
|
44
|
+
setBreakpoint(id: NodeKey, on: boolean): void;
|
|
45
|
+
}
|
|
46
|
+
/** A single recorded op — serializable, suitable for snapshot tests. */
|
|
47
|
+
export type RecordedOp = {
|
|
48
|
+
op: 'addNodeClass';
|
|
49
|
+
id: NodeKey;
|
|
50
|
+
cls: HighlightClass;
|
|
51
|
+
} | {
|
|
52
|
+
op: 'highlightEdge';
|
|
53
|
+
fromKey: string;
|
|
54
|
+
toKey: string;
|
|
55
|
+
} | {
|
|
56
|
+
op: 'markFrameActive';
|
|
57
|
+
frameId: number;
|
|
58
|
+
} | {
|
|
59
|
+
op: 'pulse';
|
|
60
|
+
id: NodeKey;
|
|
61
|
+
} | {
|
|
62
|
+
op: 'scrollIntoView';
|
|
63
|
+
id: NodeKey;
|
|
64
|
+
} | {
|
|
65
|
+
op: 'setBreakpoint';
|
|
66
|
+
id: NodeKey;
|
|
67
|
+
on: boolean;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Build a recording `HighlightOps` + `IndicatorOps` pair plus the shared
|
|
71
|
+
* `record` array of calls in invocation order. Used by tests to assert
|
|
72
|
+
* what the pure logic would have done without running a real DOM.
|
|
73
|
+
*
|
|
74
|
+
* Snapshot-friendly: the record contains only plain JSON-serializable
|
|
75
|
+
* values (no DOM nodes, no function refs).
|
|
76
|
+
*/
|
|
77
|
+
export declare function recordingOps(): {
|
|
78
|
+
highlight: HighlightOps;
|
|
79
|
+
indicator: IndicatorOps;
|
|
80
|
+
record: RecordedOp[];
|
|
81
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
* Build a recording `HighlightOps` + `IndicatorOps` pair plus the shared
|
|
15
|
+
* `record` array of calls in invocation order. Used by tests to assert
|
|
16
|
+
* what the pure logic would have done without running a real DOM.
|
|
17
|
+
*
|
|
18
|
+
* Snapshot-friendly: the record contains only plain JSON-serializable
|
|
19
|
+
* values (no DOM nodes, no function refs).
|
|
20
|
+
*/
|
|
21
|
+
export function recordingOps() {
|
|
22
|
+
const record = [];
|
|
23
|
+
return {
|
|
24
|
+
record,
|
|
25
|
+
highlight: {
|
|
26
|
+
addNodeClass(id, cls) { record.push({ op: 'addNodeClass', id, cls }); },
|
|
27
|
+
highlightEdge(fromKey, toKey) { record.push({ op: 'highlightEdge', fromKey, toKey }); },
|
|
28
|
+
markFrameActive(frameId) { record.push({ op: 'markFrameActive', frameId }); },
|
|
29
|
+
pulse(id) { record.push({ op: 'pulse', id }); },
|
|
30
|
+
scrollIntoView(id) { record.push({ op: 'scrollIntoView', id }); },
|
|
31
|
+
},
|
|
32
|
+
indicator: {
|
|
33
|
+
setBreakpoint(id, on) { record.push({ op: 'setBreakpoint', id, on }); },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|