@turing-machine-js/visuals 7.0.0-alpha.6 → 7.0.0-alpha.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- 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 +2 -2
- 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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this package will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [7.0.0-alpha.6.1] - 2026-05-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `formatStepNotation(reads, commands, blanks, matchKinds?)` — engine edge-label format primitive, matches `toMermaid` emit byte-for-byte. Per-cell encoding: literal `'X'`, blank shortcut `B`, wildcard `*='X'` (shows what `ifOtherSymbol` caught), keep-with-concrete-symbol `K='X'` / `K=B`, erase `E`. Multi-tape comma-separated within one outer bracket per role. Pass `reads === null` for the manual-Apply path (no transition fired) — output collapses to `[writes]/[moves]`. Folds in the richness machines-demo's local `format.ts` had so demo can drop the local helper and call visuals's primitive directly.
|
|
12
|
+
- `tokenizeStep(reads, commands, blanks, matchKinds?)` + `ReadToken` / `WriteToken` / `StepTokens` types — renderer-agnostic structured form of one step. Same input contract as `formatStepNotation`; returns discriminated-union tokens per cell (`{ kind: 'literal' | 'blank' | 'wildcard', ... }` for reads, `{ kind: 'literal' | 'erase' | 'keep', ... }` for writes). Consumers wanting custom rendering — HTML spans with CSS classes for syntax highlighting, ANSI-colored terminal output, alternative move vocabulary, clickable cells — walk the tokens themselves. `formatStepNotation` is refactored to be a thin string renderer over `tokenizeStep` (output byte-identical).
|
|
13
|
+
- `formatTape(tape)` — inline tape rendering with the head bracketed in place (`a[b]c`).
|
|
14
|
+
- `StepCommand` — plain per-tape command shape (`{ movement: 'L' | 'R' | 'S'; symbol: string | null }`) consumed by `formatStepNotation` and `tokenizeStep`. Distinct from the engine's `TapeCommand` class; matches the shape machines-demo's worker boundary exposes.
|
|
15
|
+
|
|
16
|
+
### Compatibility
|
|
17
|
+
|
|
18
|
+
- alpha.6's `formatCommand(tapeCommand)` and `formatStep(m)` unchanged. Additive release.
|
|
19
|
+
- Engine + builder + library-binary-numbers + library-binary-numbers-bare stay at `7.0.0-alpha.6` — no changes there. Visuals-only follow-up patch; the workspace's lockstep convention is for coordinated peer-dep widening when engine APIs break, not for additive consumer-package enhancements.
|
|
20
|
+
- Peer dep `@turing-machine-js/machine: ^7.0.0-alpha.6` unchanged (semver-prerelease caret already accepts `alpha.6.1`).
|
|
21
|
+
|
|
7
22
|
## [7.0.0-alpha.6] - 2026-05-30
|
|
8
23
|
|
|
9
24
|
### Added
|
package/dist/format.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { type MachineState, type TapeCommand } from '@turing-machine-js/machine';
|
|
2
|
+
import type { TapeSnapshot } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Plain per-tape command shape consumed by `formatStepNotation`. Distinct
|
|
5
|
+
* from the engine's `TapeCommand` class: `symbol === null` means "keep
|
|
6
|
+
* current" (the resolved symbol equals what was already under the head),
|
|
7
|
+
* `movement` is the role letter (not an engine symbol). Matches the shape
|
|
8
|
+
* machines-demo exposes from its worker boundary.
|
|
9
|
+
*/
|
|
10
|
+
export type StepCommand = {
|
|
11
|
+
movement: 'L' | 'R' | 'S';
|
|
12
|
+
symbol: string | null;
|
|
13
|
+
};
|
|
2
14
|
export declare const MOVEMENT_LETTER: Map<symbol, "L" | "R" | "S">;
|
|
3
15
|
/**
|
|
4
16
|
* Render a single tape command in `WRITE/MOVE` form.
|
|
@@ -20,3 +32,106 @@ export declare function formatCommand(tapeCommand: TapeCommand): string;
|
|
|
20
32
|
* by comparing `nextSymbols[i] === currentSymbols[i]`.
|
|
21
33
|
*/
|
|
22
34
|
export declare function formatStep(m: MachineState): string;
|
|
35
|
+
/**
|
|
36
|
+
* Per-tape read-cell token. Discriminated union for renderer-agnostic
|
|
37
|
+
* consumption — UIs map each variant to their own presentation (plain
|
|
38
|
+
* string via `formatStepNotation`, HTML span with CSS class, ANSI color,
|
|
39
|
+
* clickable token, etc.). `formatStepNotation` is the default string
|
|
40
|
+
* renderer over these tokens.
|
|
41
|
+
*
|
|
42
|
+
* - `literal` — the engine matched this exact symbol non-wildcard.
|
|
43
|
+
* - `blank` — the matched symbol is the tape's blank glyph; renderers
|
|
44
|
+
* commonly want a `B`-style shortcut instead of `' '`.
|
|
45
|
+
* - `wildcard` — the engine matched via `ifOtherSymbol`. The literal
|
|
46
|
+
* `symbol` is preserved so renderers can show what the catch-all
|
|
47
|
+
* actually caught (a blank-shortcut would obscure it).
|
|
48
|
+
*/
|
|
49
|
+
export type ReadToken = {
|
|
50
|
+
kind: 'literal';
|
|
51
|
+
symbol: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'blank';
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'wildcard';
|
|
56
|
+
symbol: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Per-tape write-cell token.
|
|
60
|
+
*
|
|
61
|
+
* - `literal` — engine wrote this exact symbol; not a blank, not a keep.
|
|
62
|
+
* - `erase` — engine wrote the tape's blank glyph; rendered as `E` by
|
|
63
|
+
* `formatStepNotation` but structurally distinct from a generic blank
|
|
64
|
+
* write so renderers can style "erase" differently from "write blank as
|
|
65
|
+
* the next interesting symbol."
|
|
66
|
+
* - `keep` — engine left the cell unchanged (`command.symbol === null`).
|
|
67
|
+
* `readContext` carries the kept symbol when caller supplied `reads`;
|
|
68
|
+
* `isBlank` flags whether the kept symbol equals the tape's blank glyph.
|
|
69
|
+
* No `readContext` means manual-Apply path (no transition fired, no
|
|
70
|
+
* per-tape read available).
|
|
71
|
+
*/
|
|
72
|
+
export type WriteToken = {
|
|
73
|
+
kind: 'literal';
|
|
74
|
+
symbol: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'erase';
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'keep';
|
|
79
|
+
readContext?: {
|
|
80
|
+
symbol: string;
|
|
81
|
+
isBlank: boolean;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Structured-token representation of one step. `formatStepNotation` is the
|
|
86
|
+
* default string renderer over this shape; consumers wanting custom
|
|
87
|
+
* rendering (HTML spans, alternative vocabulary, clickable cells, ANSI
|
|
88
|
+
* colors) call `tokenizeStep` and walk the tokens themselves.
|
|
89
|
+
*
|
|
90
|
+
* `reads === null` denotes the manual-Apply path (no transition fired);
|
|
91
|
+
* all read-side encoding is suppressed and `keep` writes carry no
|
|
92
|
+
* `readContext`.
|
|
93
|
+
*/
|
|
94
|
+
export type StepTokens = {
|
|
95
|
+
reads: readonly ReadToken[] | null;
|
|
96
|
+
writes: readonly WriteToken[];
|
|
97
|
+
moves: readonly ('L' | 'R' | 'S')[];
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Tokenize one step's per-tape data into renderer-agnostic structured
|
|
101
|
+
* form. Same input contract as `formatStepNotation` — same engine
|
|
102
|
+
* vocabulary, same null-`reads` manual-Apply handling, same wildcard
|
|
103
|
+
* suppression of the blank shortcut. Use this when you need to render the
|
|
104
|
+
* step in a non-string medium (HTML, terminal escape codes, JSON for
|
|
105
|
+
* embeds) or just want different syntax than the default string output.
|
|
106
|
+
*/
|
|
107
|
+
export declare function tokenizeStep(reads: readonly string[] | null, commands: readonly StepCommand[], blanks: readonly string[], matchKinds?: readonly ('wildcard' | 'literal')[] | null): StepTokens;
|
|
108
|
+
/**
|
|
109
|
+
* Engine edge-label format — `[reads] → [writes]/[moves]`. Matches
|
|
110
|
+
* `toMermaid` emit byte-for-byte so a logged step's notation lines up with
|
|
111
|
+
* the same transition's edge label in the rendered state graph. Thin
|
|
112
|
+
* string renderer over `tokenizeStep`; see that function's docstring +
|
|
113
|
+
* `StepTokens` for the structured form most UIs should prefer.
|
|
114
|
+
*
|
|
115
|
+
* Per-cell rendering:
|
|
116
|
+
* - Read cell: `'X'` (literal) | `B` (blank, NON-wildcard only) | `*='X'`
|
|
117
|
+
* (wildcard — shows what `ifOtherSymbol` caught; the `B` shortcut is
|
|
118
|
+
* suppressed for wildcards so the matched literal is always visible).
|
|
119
|
+
* - Write cell: `'X'` (literal) | `K='X'` (keep, with concrete read
|
|
120
|
+
* appended) | `K=B` (keep, read was blank) | `K` (keep, no read context
|
|
121
|
+
* — only when `reads === null`) | `E` (erase, write equals blank).
|
|
122
|
+
* - Move cell: `L` | `R` | `S`.
|
|
123
|
+
*
|
|
124
|
+
* Multi-tape: per-tape entries comma-separated inside one outer bracket
|
|
125
|
+
* per role — `['1','a'] → ['0','b']/[R,L]`.
|
|
126
|
+
*
|
|
127
|
+
* Pass `reads === null` for the manual-Apply path: output collapses to
|
|
128
|
+
* `[writes]/[moves]` and `K` renders without read context. Pass
|
|
129
|
+
* `matchKinds === null`/omit when no transition fired: every position
|
|
130
|
+
* renders as a literal (no wildcard markers).
|
|
131
|
+
*/
|
|
132
|
+
export declare function formatStepNotation(reads: readonly string[] | null, commands: readonly StepCommand[], blanks: readonly string[], matchKinds?: readonly ('wildcard' | 'literal')[] | null): string;
|
|
133
|
+
/** Inline tape rendering with the head bracketed in place (`a[b]c`).
|
|
134
|
+
* No UI substitution — the user controls the blank glyph. `[<blank>]`
|
|
135
|
+
* may render an invisible space if blank is `' '`; that's the chosen
|
|
136
|
+
* symbol, not a bug. */
|
|
137
|
+
export declare function formatTape(tape: TapeSnapshot): string;
|
package/dist/index.cjs
CHANGED
|
@@ -411,6 +411,103 @@ function formatStep(m) {
|
|
|
411
411
|
const moves = m.movements.map((mv) => MOVEMENT_LETTER.get(mv) ?? '?').join(',');
|
|
412
412
|
return `[${reads}] → [${writes}]/[${moves}]`;
|
|
413
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Tokenize one step's per-tape data into renderer-agnostic structured
|
|
416
|
+
* form. Same input contract as `formatStepNotation` — same engine
|
|
417
|
+
* vocabulary, same null-`reads` manual-Apply handling, same wildcard
|
|
418
|
+
* suppression of the blank shortcut. Use this when you need to render the
|
|
419
|
+
* step in a non-string medium (HTML, terminal escape codes, JSON for
|
|
420
|
+
* embeds) or just want different syntax than the default string output.
|
|
421
|
+
*/
|
|
422
|
+
function tokenizeStep(reads, commands, blanks, matchKinds) {
|
|
423
|
+
const writes = commands.map((c, i) => {
|
|
424
|
+
if (c.symbol === null) {
|
|
425
|
+
if (reads !== null) {
|
|
426
|
+
const r = reads[i];
|
|
427
|
+
if (r !== undefined) {
|
|
428
|
+
return { kind: 'keep', readContext: { symbol: r, isBlank: r === blanks[i] } };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { kind: 'keep' };
|
|
432
|
+
}
|
|
433
|
+
if (c.symbol === blanks[i])
|
|
434
|
+
return { kind: 'erase' };
|
|
435
|
+
return { kind: 'literal', symbol: c.symbol };
|
|
436
|
+
});
|
|
437
|
+
const moves = commands.map((c) => c.movement);
|
|
438
|
+
if (reads === null) {
|
|
439
|
+
return { reads: null, writes, moves };
|
|
440
|
+
}
|
|
441
|
+
const readTokens = reads.map((r, i) => {
|
|
442
|
+
if (matchKinds?.[i] === 'wildcard')
|
|
443
|
+
return { kind: 'wildcard', symbol: r };
|
|
444
|
+
if (r === blanks[i])
|
|
445
|
+
return { kind: 'blank' };
|
|
446
|
+
return { kind: 'literal', symbol: r };
|
|
447
|
+
});
|
|
448
|
+
return { reads: readTokens, writes, moves };
|
|
449
|
+
}
|
|
450
|
+
function renderReadToken(t) {
|
|
451
|
+
if (t.kind === 'wildcard')
|
|
452
|
+
return `*='${t.symbol}'`;
|
|
453
|
+
if (t.kind === 'blank')
|
|
454
|
+
return 'B';
|
|
455
|
+
return `'${t.symbol}'`;
|
|
456
|
+
}
|
|
457
|
+
function renderWriteToken(t) {
|
|
458
|
+
if (t.kind === 'erase')
|
|
459
|
+
return 'E';
|
|
460
|
+
if (t.kind === 'literal')
|
|
461
|
+
return `'${t.symbol}'`;
|
|
462
|
+
if (!t.readContext)
|
|
463
|
+
return 'K';
|
|
464
|
+
if (t.readContext.isBlank)
|
|
465
|
+
return 'K=B';
|
|
466
|
+
return `K='${t.readContext.symbol}'`;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Engine edge-label format — `[reads] → [writes]/[moves]`. Matches
|
|
470
|
+
* `toMermaid` emit byte-for-byte so a logged step's notation lines up with
|
|
471
|
+
* the same transition's edge label in the rendered state graph. Thin
|
|
472
|
+
* string renderer over `tokenizeStep`; see that function's docstring +
|
|
473
|
+
* `StepTokens` for the structured form most UIs should prefer.
|
|
474
|
+
*
|
|
475
|
+
* Per-cell rendering:
|
|
476
|
+
* - Read cell: `'X'` (literal) | `B` (blank, NON-wildcard only) | `*='X'`
|
|
477
|
+
* (wildcard — shows what `ifOtherSymbol` caught; the `B` shortcut is
|
|
478
|
+
* suppressed for wildcards so the matched literal is always visible).
|
|
479
|
+
* - Write cell: `'X'` (literal) | `K='X'` (keep, with concrete read
|
|
480
|
+
* appended) | `K=B` (keep, read was blank) | `K` (keep, no read context
|
|
481
|
+
* — only when `reads === null`) | `E` (erase, write equals blank).
|
|
482
|
+
* - Move cell: `L` | `R` | `S`.
|
|
483
|
+
*
|
|
484
|
+
* Multi-tape: per-tape entries comma-separated inside one outer bracket
|
|
485
|
+
* per role — `['1','a'] → ['0','b']/[R,L]`.
|
|
486
|
+
*
|
|
487
|
+
* Pass `reads === null` for the manual-Apply path: output collapses to
|
|
488
|
+
* `[writes]/[moves]` and `K` renders without read context. Pass
|
|
489
|
+
* `matchKinds === null`/omit when no transition fired: every position
|
|
490
|
+
* renders as a literal (no wildcard markers).
|
|
491
|
+
*/
|
|
492
|
+
function formatStepNotation(reads, commands, blanks, matchKinds) {
|
|
493
|
+
const tokens = tokenizeStep(reads, commands, blanks, matchKinds);
|
|
494
|
+
const writesStr = tokens.writes.map(renderWriteToken).join(',');
|
|
495
|
+
const movesStr = tokens.moves.join(',');
|
|
496
|
+
const writesPart = `[${writesStr}]/[${movesStr}]`;
|
|
497
|
+
if (tokens.reads === null)
|
|
498
|
+
return writesPart;
|
|
499
|
+
const readsStr = tokens.reads.map(renderReadToken).join(',');
|
|
500
|
+
return `[${readsStr}] → ${writesPart}`;
|
|
501
|
+
}
|
|
502
|
+
/** Inline tape rendering with the head bracketed in place (`a[b]c`).
|
|
503
|
+
* No UI substitution — the user controls the blank glyph. `[<blank>]`
|
|
504
|
+
* may render an invisible space if blank is `' '`; that's the chosen
|
|
505
|
+
* symbol, not a bug. */
|
|
506
|
+
function formatTape(tape) {
|
|
507
|
+
return tape.symbols
|
|
508
|
+
.map((sym, i) => (i === tape.position ? `[${sym}]` : sym))
|
|
509
|
+
.join('');
|
|
510
|
+
}
|
|
414
511
|
|
|
415
512
|
const DEFAULT_MAX_STEPS = 1000;
|
|
416
513
|
function snapshotTapes(machine) {
|
|
@@ -508,7 +605,10 @@ exports.bareIdOf = bareIdOf;
|
|
|
508
605
|
exports.equivalentIds = equivalentIds;
|
|
509
606
|
exports.formatCommand = formatCommand;
|
|
510
607
|
exports.formatStep = formatStep;
|
|
608
|
+
exports.formatStepNotation = formatStepNotation;
|
|
609
|
+
exports.formatTape = formatTape;
|
|
511
610
|
exports.highlightExpand = highlightExpand;
|
|
512
611
|
exports.indexGraph = indexGraph;
|
|
513
612
|
exports.recordSnippet = recordSnippet;
|
|
514
613
|
exports.recordingOps = recordingOps;
|
|
614
|
+
exports.tokenizeStep = tokenizeStep;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export type { GraphIndexes } from './graphIndexes';
|
|
|
5
5
|
export { indexGraph } from './graphIndexes';
|
|
6
6
|
export type { GraphHighlight, TapeSnapshot, Frame, Snippet } from './types';
|
|
7
7
|
export { applyHighlight, applyIndicator } from './applyHighlight';
|
|
8
|
-
export { formatCommand, formatStep } from './format';
|
|
8
|
+
export { formatCommand, formatStep, formatStepNotation, formatTape, tokenizeStep, type StepCommand, type ReadToken, type WriteToken, type StepTokens, } from './format';
|
|
9
9
|
export { recordSnippet, type RecordSnippetOptions } from './recordSnippet';
|
package/dist/index.mjs
CHANGED
|
@@ -409,6 +409,103 @@ function formatStep(m) {
|
|
|
409
409
|
const moves = m.movements.map((mv) => MOVEMENT_LETTER.get(mv) ?? '?').join(',');
|
|
410
410
|
return `[${reads}] → [${writes}]/[${moves}]`;
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Tokenize one step's per-tape data into renderer-agnostic structured
|
|
414
|
+
* form. Same input contract as `formatStepNotation` — same engine
|
|
415
|
+
* vocabulary, same null-`reads` manual-Apply handling, same wildcard
|
|
416
|
+
* suppression of the blank shortcut. Use this when you need to render the
|
|
417
|
+
* step in a non-string medium (HTML, terminal escape codes, JSON for
|
|
418
|
+
* embeds) or just want different syntax than the default string output.
|
|
419
|
+
*/
|
|
420
|
+
function tokenizeStep(reads, commands, blanks, matchKinds) {
|
|
421
|
+
const writes = commands.map((c, i) => {
|
|
422
|
+
if (c.symbol === null) {
|
|
423
|
+
if (reads !== null) {
|
|
424
|
+
const r = reads[i];
|
|
425
|
+
if (r !== undefined) {
|
|
426
|
+
return { kind: 'keep', readContext: { symbol: r, isBlank: r === blanks[i] } };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return { kind: 'keep' };
|
|
430
|
+
}
|
|
431
|
+
if (c.symbol === blanks[i])
|
|
432
|
+
return { kind: 'erase' };
|
|
433
|
+
return { kind: 'literal', symbol: c.symbol };
|
|
434
|
+
});
|
|
435
|
+
const moves = commands.map((c) => c.movement);
|
|
436
|
+
if (reads === null) {
|
|
437
|
+
return { reads: null, writes, moves };
|
|
438
|
+
}
|
|
439
|
+
const readTokens = reads.map((r, i) => {
|
|
440
|
+
if (matchKinds?.[i] === 'wildcard')
|
|
441
|
+
return { kind: 'wildcard', symbol: r };
|
|
442
|
+
if (r === blanks[i])
|
|
443
|
+
return { kind: 'blank' };
|
|
444
|
+
return { kind: 'literal', symbol: r };
|
|
445
|
+
});
|
|
446
|
+
return { reads: readTokens, writes, moves };
|
|
447
|
+
}
|
|
448
|
+
function renderReadToken(t) {
|
|
449
|
+
if (t.kind === 'wildcard')
|
|
450
|
+
return `*='${t.symbol}'`;
|
|
451
|
+
if (t.kind === 'blank')
|
|
452
|
+
return 'B';
|
|
453
|
+
return `'${t.symbol}'`;
|
|
454
|
+
}
|
|
455
|
+
function renderWriteToken(t) {
|
|
456
|
+
if (t.kind === 'erase')
|
|
457
|
+
return 'E';
|
|
458
|
+
if (t.kind === 'literal')
|
|
459
|
+
return `'${t.symbol}'`;
|
|
460
|
+
if (!t.readContext)
|
|
461
|
+
return 'K';
|
|
462
|
+
if (t.readContext.isBlank)
|
|
463
|
+
return 'K=B';
|
|
464
|
+
return `K='${t.readContext.symbol}'`;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Engine edge-label format — `[reads] → [writes]/[moves]`. Matches
|
|
468
|
+
* `toMermaid` emit byte-for-byte so a logged step's notation lines up with
|
|
469
|
+
* the same transition's edge label in the rendered state graph. Thin
|
|
470
|
+
* string renderer over `tokenizeStep`; see that function's docstring +
|
|
471
|
+
* `StepTokens` for the structured form most UIs should prefer.
|
|
472
|
+
*
|
|
473
|
+
* Per-cell rendering:
|
|
474
|
+
* - Read cell: `'X'` (literal) | `B` (blank, NON-wildcard only) | `*='X'`
|
|
475
|
+
* (wildcard — shows what `ifOtherSymbol` caught; the `B` shortcut is
|
|
476
|
+
* suppressed for wildcards so the matched literal is always visible).
|
|
477
|
+
* - Write cell: `'X'` (literal) | `K='X'` (keep, with concrete read
|
|
478
|
+
* appended) | `K=B` (keep, read was blank) | `K` (keep, no read context
|
|
479
|
+
* — only when `reads === null`) | `E` (erase, write equals blank).
|
|
480
|
+
* - Move cell: `L` | `R` | `S`.
|
|
481
|
+
*
|
|
482
|
+
* Multi-tape: per-tape entries comma-separated inside one outer bracket
|
|
483
|
+
* per role — `['1','a'] → ['0','b']/[R,L]`.
|
|
484
|
+
*
|
|
485
|
+
* Pass `reads === null` for the manual-Apply path: output collapses to
|
|
486
|
+
* `[writes]/[moves]` and `K` renders without read context. Pass
|
|
487
|
+
* `matchKinds === null`/omit when no transition fired: every position
|
|
488
|
+
* renders as a literal (no wildcard markers).
|
|
489
|
+
*/
|
|
490
|
+
function formatStepNotation(reads, commands, blanks, matchKinds) {
|
|
491
|
+
const tokens = tokenizeStep(reads, commands, blanks, matchKinds);
|
|
492
|
+
const writesStr = tokens.writes.map(renderWriteToken).join(',');
|
|
493
|
+
const movesStr = tokens.moves.join(',');
|
|
494
|
+
const writesPart = `[${writesStr}]/[${movesStr}]`;
|
|
495
|
+
if (tokens.reads === null)
|
|
496
|
+
return writesPart;
|
|
497
|
+
const readsStr = tokens.reads.map(renderReadToken).join(',');
|
|
498
|
+
return `[${readsStr}] → ${writesPart}`;
|
|
499
|
+
}
|
|
500
|
+
/** Inline tape rendering with the head bracketed in place (`a[b]c`).
|
|
501
|
+
* No UI substitution — the user controls the blank glyph. `[<blank>]`
|
|
502
|
+
* may render an invisible space if blank is `' '`; that's the chosen
|
|
503
|
+
* symbol, not a bug. */
|
|
504
|
+
function formatTape(tape) {
|
|
505
|
+
return tape.symbols
|
|
506
|
+
.map((sym, i) => (i === tape.position ? `[${sym}]` : sym))
|
|
507
|
+
.join('');
|
|
508
|
+
}
|
|
412
509
|
|
|
413
510
|
const DEFAULT_MAX_STEPS = 1000;
|
|
414
511
|
function snapshotTapes(machine) {
|
|
@@ -500,4 +597,4 @@ function recordSnippet(opts) {
|
|
|
500
597
|
};
|
|
501
598
|
}
|
|
502
599
|
|
|
503
|
-
export { applyHighlight, applyIndicator, bareIdOf, equivalentIds, formatCommand, formatStep, highlightExpand, indexGraph, recordSnippet, recordingOps };
|
|
600
|
+
export { applyHighlight, applyIndicator, bareIdOf, equivalentIds, formatCommand, formatStep, formatStepNotation, formatTape, highlightExpand, indexGraph, recordSnippet, recordingOps, tokenizeStep };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turing-machine-js/visuals",
|
|
3
|
-
"version": "7.0.0-alpha.6",
|
|
3
|
+
"version": "7.0.0-alpha.6.1",
|
|
4
4
|
"description": "Pure highlight + graph-indexing logic for @turing-machine-js/machine — no DOM, no renderer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"npm": ">=7.0.0"
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"default": "./dist/index.mjs"
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "4f5364293038aabea742235ea0cec2b135f0d6b7"
|
|
46
46
|
}
|
package/dist/applyHighlight.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
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.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
}
|