@turing-machine-js/machine 3.0.0 → 3.0.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.
@@ -1,141 +0,0 @@
1
- // Format converters between a Graph (the data model produced by State.toGraph
2
- // and consumed by State.fromGraph) and external string representations.
3
- //
4
- // Currently only Mermaid flowchart syntax is supported. Future formats
5
- // (Graphviz, JSON-LD, custom DSL) belong here too.
6
- export function toMermaid(graph) {
7
- const lines = [
8
- 'flowchart TD',
9
- `%% alphabets: ${JSON.stringify(graph.alphabets)}`,
10
- ];
11
- for (const node of Object.values(graph.nodes)) {
12
- const id = `s${node.id}`;
13
- if (node.isHalt) {
14
- lines.push(` ${id}(((halt)))`);
15
- }
16
- else if (node.id === graph.initialId) {
17
- lines.push(` ${id}(("${node.name}"))`);
18
- }
19
- else {
20
- lines.push(` ${id}["${node.name}"]`);
21
- }
22
- }
23
- for (const node of Object.values(graph.nodes)) {
24
- for (const t of node.transitions) {
25
- // Per-tape commands separated with ',' to mirror the pattern syntax.
26
- const cmd = t.command.map((c) => `${c.symbol}/${c.movement}`).join(',');
27
- const label = `${t.pattern} → ${cmd}`;
28
- lines.push(` s${node.id} -- "${label}" --> s${t.nextStateId}`);
29
- }
30
- if (node.overrodeHaltStateId !== null) {
31
- lines.push(` s${node.id} -. onHalt .-> s${node.overrodeHaltStateId}`);
32
- }
33
- }
34
- return lines.join('\n');
35
- }
36
- // Inverse of toMermaid: parses the Mermaid output produced by toMermaid back
37
- // into a Graph. The parser is strict to the dialect toMermaid emits — it
38
- // recognises the specific node/edge shapes and the leading
39
- // `%% alphabets: [...]` comment. Hand-edited Mermaid that uses different
40
- // arrow styles or shapes will not parse.
41
- //
42
- // Caveats:
43
- // - Write-symbol cells in commands are split on '/' (last occurrence) and
44
- // per-tape segments are split on ','. If your alphabet contains '/' or ','
45
- // as literal symbols, the parser cannot disambiguate. Stick to alphabets
46
- // without those characters when round-tripping through Mermaid.
47
- const haltNodeRegex = /^s(\d+)\(\(\(halt\)\)\)$/;
48
- const initialNodeRegex = /^s(\d+)\(\("([^"]*)"\)\)$/;
49
- const regularNodeRegex = /^s(\d+)\["([^"]*)"\]$/;
50
- const transitionRegex = /^s(\d+)\s+--\s+"(.*)"\s+-->\s+s(\d+)$/;
51
- const onHaltRegex = /^s(\d+)\s+-\.\s+onHalt\s+\.->\s+s(\d+)$/;
52
- const alphabetsRegex = /^%%\s*alphabets:\s*(.+)$/;
53
- export function fromMermaid(text) {
54
- const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
55
- let alphabets = [];
56
- let initialId = null;
57
- const nodes = {};
58
- const ensureNode = (id, opts = {}) => {
59
- if (!nodes[id]) {
60
- nodes[id] = {
61
- id,
62
- name: opts.name ?? `s${id}`,
63
- isHalt: opts.isHalt ?? false,
64
- transitions: [],
65
- overrodeHaltStateId: null,
66
- };
67
- }
68
- else {
69
- if (opts.name !== undefined) {
70
- nodes[id].name = opts.name;
71
- }
72
- if (opts.isHalt !== undefined) {
73
- nodes[id].isHalt = opts.isHalt;
74
- }
75
- }
76
- return nodes[id];
77
- };
78
- // First pass: alphabets + nodes.
79
- for (const line of lines) {
80
- if (line === 'flowchart TD') {
81
- continue;
82
- }
83
- const am = line.match(alphabetsRegex);
84
- if (am) {
85
- alphabets = JSON.parse(am[1]);
86
- continue;
87
- }
88
- const hm = line.match(haltNodeRegex);
89
- if (hm) {
90
- ensureNode(Number(hm[1]), { name: 'halt', isHalt: true });
91
- continue;
92
- }
93
- const im = line.match(initialNodeRegex);
94
- if (im) {
95
- const id = Number(im[1]);
96
- initialId = id;
97
- ensureNode(id, { name: im[2] });
98
- continue;
99
- }
100
- const rm = line.match(regularNodeRegex);
101
- if (rm) {
102
- ensureNode(Number(rm[1]), { name: rm[2] });
103
- continue;
104
- }
105
- }
106
- // Second pass: edges.
107
- for (const line of lines) {
108
- const om = line.match(onHaltRegex);
109
- if (om) {
110
- ensureNode(Number(om[1])).overrodeHaltStateId = Number(om[2]);
111
- continue;
112
- }
113
- const tm = line.match(transitionRegex);
114
- if (tm) {
115
- const fromId = Number(tm[1]);
116
- const label = tm[2];
117
- const toId = Number(tm[3]);
118
- const arrowIx = label.indexOf(' → ');
119
- if (arrowIx === -1) {
120
- throw new Error(`fromMermaid: malformed edge label: "${label}"`);
121
- }
122
- const pattern = label.slice(0, arrowIx);
123
- const commandStr = label.slice(arrowIx + ' → '.length);
124
- const command = commandStr.split(',').map((part) => {
125
- const slashIx = part.lastIndexOf('/');
126
- if (slashIx === -1) {
127
- throw new Error(`fromMermaid: malformed command part: "${part}"`);
128
- }
129
- return {
130
- symbol: part.slice(0, slashIx),
131
- movement: part.slice(slashIx + 1),
132
- };
133
- });
134
- ensureNode(fromId).transitions.push({ pattern, command, nextStateId: toId });
135
- }
136
- }
137
- if (initialId === null) {
138
- throw new Error('fromMermaid: no initial state (double-paren node) found');
139
- }
140
- return { initialId, alphabets, nodes };
141
- }
@@ -1,88 +0,0 @@
1
- import State from '../classes/State';
2
- export function summarizeGraph(graph) {
3
- const nodes = Object.values(graph.nodes);
4
- let transitionCount = 0;
5
- let compositionEdgeCount = 0;
6
- let selfLoopCount = 0;
7
- for (const node of nodes) {
8
- transitionCount += node.transitions.length;
9
- if (node.overrodeHaltStateId !== null) {
10
- compositionEdgeCount += 1;
11
- }
12
- for (const t of node.transitions) {
13
- if (t.nextStateId === node.id) {
14
- selfLoopCount += 1;
15
- }
16
- }
17
- }
18
- // Longest withOverrodeHaltState chain. Walks node → overrodeHaltState recursively;
19
- // a Set guards against cycles in the override graph (which throw at construction
20
- // time anyway, but being defensive costs little).
21
- const overrideDepthFrom = (id, visited) => {
22
- if (visited.has(id)) {
23
- return 0;
24
- }
25
- visited.add(id);
26
- const node = graph.nodes[id];
27
- if (!node || node.overrodeHaltStateId === null) {
28
- return 0;
29
- }
30
- return 1 + overrideDepthFrom(node.overrodeHaltStateId, visited);
31
- };
32
- const maxCompositionDepth = nodes.reduce((max, node) => Math.max(max, overrideDepthFrom(node.id, new Set())), 0);
33
- // Cycle detection: tri-color DFS over the transition graph.
34
- const WHITE = 0;
35
- const GREY = 1;
36
- const BLACK = 2;
37
- const color = new Map();
38
- for (const node of nodes) {
39
- color.set(node.id, WHITE);
40
- }
41
- let hasCycles = false;
42
- const visit = (id) => {
43
- if (hasCycles) {
44
- return;
45
- }
46
- if (color.get(id) === GREY) {
47
- hasCycles = true;
48
- return;
49
- }
50
- if (color.get(id) === BLACK) {
51
- return;
52
- }
53
- color.set(id, GREY);
54
- const node = graph.nodes[id];
55
- if (node) {
56
- for (const t of node.transitions) {
57
- visit(t.nextStateId);
58
- if (hasCycles) {
59
- return;
60
- }
61
- }
62
- }
63
- color.set(id, BLACK);
64
- };
65
- for (const node of nodes) {
66
- if (hasCycles) {
67
- break;
68
- }
69
- visit(node.id);
70
- }
71
- return {
72
- stateCount: nodes.length,
73
- transitionCount,
74
- compositionEdgeCount,
75
- maxCompositionDepth,
76
- selfLoopCount,
77
- hasCycles,
78
- tapeCount: graph.alphabets.length,
79
- alphabetCardinalities: graph.alphabets.map((a) => a.length),
80
- };
81
- }
82
- // Convenience: build the graph and summarize in one step.
83
- export function summarize(state, tapeBlock) {
84
- return summarizeGraph(State.toGraph(state, tapeBlock));
85
- }
86
- // Behavioral equivalence checking (the testing-tool counterpart to introspection)
87
- // lives in ./equivalence — kept separate because it runs machines and compares
88
- // outputs rather than examining structure.