@turing-machine-js/machine 2.0.2 → 3.0.0
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 +39 -0
- package/README.md +256 -12
- package/dist/classes/State.d.ts +27 -0
- package/dist/classes/State.js +172 -2
- package/dist/classes/TapeBlock.js +2 -3
- package/dist/index.cjs +768 -173
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.mjs +764 -174
- package/dist/utilities/equivalence.d.ts +31 -0
- package/dist/utilities/equivalence.js +68 -0
- package/dist/utilities/graph.d.ts +29 -0
- package/dist/utilities/graph.js +127 -0
- package/dist/utilities/graphFormats.d.ts +3 -0
- package/dist/utilities/graphFormats.js +141 -0
- package/dist/utilities/introspection.d.ts +15 -0
- package/dist/utilities/introspection.js +88 -0
- package/package.json +2 -8
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import State from '../classes/State';
|
|
2
|
+
import TapeBlock from '../classes/TapeBlock';
|
|
3
|
+
export type Runnable = {
|
|
4
|
+
state: State;
|
|
5
|
+
getTapeBlock: () => TapeBlock;
|
|
6
|
+
};
|
|
7
|
+
export type EquivalenceCase = string | {
|
|
8
|
+
reference: string;
|
|
9
|
+
candidate: string;
|
|
10
|
+
};
|
|
11
|
+
export type EquivalenceResult = {
|
|
12
|
+
case: {
|
|
13
|
+
reference: string;
|
|
14
|
+
candidate: string;
|
|
15
|
+
};
|
|
16
|
+
agree: boolean;
|
|
17
|
+
referenceOutput: string;
|
|
18
|
+
candidateOutput: string;
|
|
19
|
+
referenceSteps: number;
|
|
20
|
+
candidateSteps: number;
|
|
21
|
+
firstDivergenceStep: number | null;
|
|
22
|
+
};
|
|
23
|
+
export type EquivalenceReport = {
|
|
24
|
+
results: EquivalenceResult[];
|
|
25
|
+
allAgree: boolean;
|
|
26
|
+
};
|
|
27
|
+
export declare function equivalentOn(reference: Runnable, candidate: Runnable, cases: EquivalenceCase[], options?: {
|
|
28
|
+
compareOutputs?: (refOutput: string, candOutput: string) => boolean;
|
|
29
|
+
compareSnapshots?: ((refSnap: string, candSnap: string) => boolean) | null;
|
|
30
|
+
stepsLimit?: number;
|
|
31
|
+
}): EquivalenceReport;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Tape from '../classes/Tape';
|
|
2
|
+
import TuringMachine from '../classes/TuringMachine';
|
|
3
|
+
const defaultCompare = (a, b) => a === b;
|
|
4
|
+
export function equivalentOn(reference, candidate, cases, options = {}) {
|
|
5
|
+
const compareOutputs = options.compareOutputs ?? defaultCompare;
|
|
6
|
+
const compareSnapshots = options.compareSnapshots === undefined
|
|
7
|
+
? defaultCompare
|
|
8
|
+
: options.compareSnapshots;
|
|
9
|
+
const stepsLimit = options.stepsLimit ?? 1e5;
|
|
10
|
+
const results = cases.map((c) => {
|
|
11
|
+
const pair = typeof c === 'string' ? { reference: c, candidate: c } : c;
|
|
12
|
+
const refRun = runOnce(reference, pair.reference, stepsLimit);
|
|
13
|
+
const candRun = runOnce(candidate, pair.candidate, stepsLimit);
|
|
14
|
+
const agree = compareOutputs(refRun.finalOutput, candRun.finalOutput);
|
|
15
|
+
let firstDivergenceStep = null;
|
|
16
|
+
if (!agree && compareSnapshots !== null) {
|
|
17
|
+
const minLen = Math.min(refRun.snapshots.length, candRun.snapshots.length);
|
|
18
|
+
for (let i = 0; i < minLen; i += 1) {
|
|
19
|
+
if (!compareSnapshots(refRun.snapshots[i], candRun.snapshots[i])) {
|
|
20
|
+
firstDivergenceStep = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (firstDivergenceStep === null && refRun.snapshots.length !== candRun.snapshots.length) {
|
|
25
|
+
firstDivergenceStep = minLen;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
case: pair,
|
|
30
|
+
agree,
|
|
31
|
+
referenceOutput: refRun.finalOutput,
|
|
32
|
+
candidateOutput: candRun.finalOutput,
|
|
33
|
+
referenceSteps: refRun.stepCount,
|
|
34
|
+
candidateSteps: candRun.stepCount,
|
|
35
|
+
firstDivergenceStep,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
results,
|
|
40
|
+
allAgree: results.every((r) => r.agree),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Single-machine runner: snapshots the tape after each step and returns the
|
|
44
|
+
// final output, the snapshot list, and the step count.
|
|
45
|
+
function runOnce(runnable, input, stepsLimit) {
|
|
46
|
+
const tapeBlock = runnable.getTapeBlock();
|
|
47
|
+
const tape = new Tape({
|
|
48
|
+
alphabet: tapeBlock.tapes[0].alphabet,
|
|
49
|
+
symbols: input.split(''),
|
|
50
|
+
});
|
|
51
|
+
tapeBlock.replaceTape(tape);
|
|
52
|
+
const machine = new TuringMachine({ tapeBlock });
|
|
53
|
+
const snapshots = [];
|
|
54
|
+
let stepCount = 0;
|
|
55
|
+
// Inside the for-of body, the tape reflects the state BEFORE the current
|
|
56
|
+
// step's command (i.e. AFTER the previous step's command — or initial for
|
|
57
|
+
// step 1). After the loop, the tape has had every command applied.
|
|
58
|
+
for (const _ of machine.runStepByStep({ initialState: runnable.state, stepsLimit })) {
|
|
59
|
+
snapshots.push(tape.symbols.join(''));
|
|
60
|
+
stepCount += 1;
|
|
61
|
+
}
|
|
62
|
+
snapshots.push(tape.symbols.join(''));
|
|
63
|
+
return {
|
|
64
|
+
finalOutput: tape.symbols.join('').trim(),
|
|
65
|
+
snapshots,
|
|
66
|
+
stepCount,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type GraphCommand = {
|
|
2
|
+
symbol: string;
|
|
3
|
+
movement: string;
|
|
4
|
+
};
|
|
5
|
+
export type GraphTransition = {
|
|
6
|
+
pattern: string;
|
|
7
|
+
command: GraphCommand[];
|
|
8
|
+
nextStateId: number;
|
|
9
|
+
};
|
|
10
|
+
export type GraphNode = {
|
|
11
|
+
id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
isHalt: boolean;
|
|
14
|
+
transitions: GraphTransition[];
|
|
15
|
+
overrodeHaltStateId: number | null;
|
|
16
|
+
};
|
|
17
|
+
export type Graph = {
|
|
18
|
+
initialId: number;
|
|
19
|
+
alphabets: string[][];
|
|
20
|
+
nodes: Record<number, GraphNode>;
|
|
21
|
+
};
|
|
22
|
+
export declare function decodePatternDescription(description: string | undefined, alphabets: string[][]): string;
|
|
23
|
+
export declare function decodeMovement(description: string | undefined): string;
|
|
24
|
+
export type ParsedPattern = null | (string | null)[][];
|
|
25
|
+
export declare function splitUnescaped(s: string, sep: string): string[];
|
|
26
|
+
export declare function parsePatternString(s: string, alphabets: string[][]): ParsedPattern;
|
|
27
|
+
export declare function parseMovementLabel(label: string): symbol;
|
|
28
|
+
export declare function parseWriteSymbolLabel(label: string): string | symbol;
|
|
29
|
+
export declare function decodeWriteSymbol(symbol: string | symbol): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { movements, symbolCommands } from '../classes/TapeCommand';
|
|
2
|
+
const movementDescriptionToLabel = {
|
|
3
|
+
'move caret left command': 'L',
|
|
4
|
+
'move caret right command': 'R',
|
|
5
|
+
'do not move carer': 'S',
|
|
6
|
+
};
|
|
7
|
+
const symbolCommandDescriptionToLabel = {
|
|
8
|
+
'keep symbol command': '·',
|
|
9
|
+
'erase symbol command': '⌫',
|
|
10
|
+
};
|
|
11
|
+
// Reserved characters in the encoded pattern string:
|
|
12
|
+
// '*' per-cell ifOtherSymbol (matches any symbol on that tape)
|
|
13
|
+
// '-' the tape's blank symbol
|
|
14
|
+
// ',' separates per-tape cells inside one pattern
|
|
15
|
+
// '|' separates alternative patterns
|
|
16
|
+
// '\\' escape prefix — to represent any of '*', '-', ',', '|', or '\\' as a
|
|
17
|
+
// *literal* alphabet symbol, prefix it with '\\' (e.g. '\\*' for literal '*').
|
|
18
|
+
function escapeAlphabetSymbol(s) {
|
|
19
|
+
return s
|
|
20
|
+
.replace(/\\/g, '\\\\')
|
|
21
|
+
.replace(/\*/g, '\\*')
|
|
22
|
+
.replace(/-/g, '\\-')
|
|
23
|
+
.replace(/,/g, '\\,')
|
|
24
|
+
.replace(/\|/g, '\\|');
|
|
25
|
+
}
|
|
26
|
+
export function decodePatternDescription(description, alphabets) {
|
|
27
|
+
if (!description) {
|
|
28
|
+
return '?';
|
|
29
|
+
}
|
|
30
|
+
if (description === 'other symbol') {
|
|
31
|
+
return '*';
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const patternList = JSON.parse(description);
|
|
35
|
+
return patternList
|
|
36
|
+
.map((pattern) => pattern
|
|
37
|
+
.map((s, tapeIx) => {
|
|
38
|
+
if (s === null) {
|
|
39
|
+
return '*';
|
|
40
|
+
}
|
|
41
|
+
if (s === alphabets[tapeIx]?.[0]) {
|
|
42
|
+
return '-';
|
|
43
|
+
}
|
|
44
|
+
return escapeAlphabetSymbol(s);
|
|
45
|
+
})
|
|
46
|
+
.join(','))
|
|
47
|
+
.join('|');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return description;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function decodeMovement(description) {
|
|
54
|
+
if (!description) {
|
|
55
|
+
return '?';
|
|
56
|
+
}
|
|
57
|
+
return movementDescriptionToLabel[description] ?? description;
|
|
58
|
+
}
|
|
59
|
+
export function splitUnescaped(s, sep) {
|
|
60
|
+
const parts = [];
|
|
61
|
+
let current = '';
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < s.length) {
|
|
64
|
+
if (s[i] === '\\' && i + 1 < s.length) {
|
|
65
|
+
current += s[i + 1];
|
|
66
|
+
i += 2;
|
|
67
|
+
}
|
|
68
|
+
else if (s[i] === sep) {
|
|
69
|
+
parts.push(current);
|
|
70
|
+
current = '';
|
|
71
|
+
i += 1;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
current += s[i];
|
|
75
|
+
i += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
parts.push(current);
|
|
79
|
+
return parts;
|
|
80
|
+
}
|
|
81
|
+
export function parsePatternString(s, alphabets) {
|
|
82
|
+
if (s === '*') {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const alternatives = splitUnescaped(s, '|');
|
|
86
|
+
return alternatives.map((alt) => {
|
|
87
|
+
const cells = splitUnescaped(alt, ',');
|
|
88
|
+
return cells.map((cell, tapeIx) => {
|
|
89
|
+
if (cell === '*') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (cell === '-') {
|
|
93
|
+
return alphabets[tapeIx]?.[0] ?? cell;
|
|
94
|
+
}
|
|
95
|
+
return cell;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const movementLabelToSymbol = {
|
|
100
|
+
L: movements.left,
|
|
101
|
+
R: movements.right,
|
|
102
|
+
S: movements.stay,
|
|
103
|
+
};
|
|
104
|
+
export function parseMovementLabel(label) {
|
|
105
|
+
const m = movementLabelToSymbol[label];
|
|
106
|
+
if (!m) {
|
|
107
|
+
throw new Error(`unknown movement label: ${label}`);
|
|
108
|
+
}
|
|
109
|
+
return m;
|
|
110
|
+
}
|
|
111
|
+
export function parseWriteSymbolLabel(label) {
|
|
112
|
+
if (label === '·') {
|
|
113
|
+
return symbolCommands.keep;
|
|
114
|
+
}
|
|
115
|
+
if (label === '⌫') {
|
|
116
|
+
return symbolCommands.erase;
|
|
117
|
+
}
|
|
118
|
+
return label;
|
|
119
|
+
}
|
|
120
|
+
export function decodeWriteSymbol(symbol) {
|
|
121
|
+
if (typeof symbol === 'symbol') {
|
|
122
|
+
const description = symbol.description ?? '?';
|
|
123
|
+
return symbolCommandDescriptionToLabel[description] ?? description;
|
|
124
|
+
}
|
|
125
|
+
return symbol;
|
|
126
|
+
}
|
|
127
|
+
// Format converters (toMermaid / fromMermaid) live in ./graphFormats.
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import State from '../classes/State';
|
|
2
|
+
import TapeBlock from '../classes/TapeBlock';
|
|
3
|
+
import { type Graph } from './graph';
|
|
4
|
+
export type GraphSummary = {
|
|
5
|
+
stateCount: number;
|
|
6
|
+
transitionCount: number;
|
|
7
|
+
compositionEdgeCount: number;
|
|
8
|
+
maxCompositionDepth: number;
|
|
9
|
+
selfLoopCount: number;
|
|
10
|
+
hasCycles: boolean;
|
|
11
|
+
tapeCount: number;
|
|
12
|
+
alphabetCardinalities: number[];
|
|
13
|
+
};
|
|
14
|
+
export declare function summarizeGraph(graph: Graph): GraphSummary;
|
|
15
|
+
export declare function summarize(state: State, tapeBlock: TapeBlock): GraphSummary;
|
|
@@ -0,0 +1,88 @@
|
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turing-machine-js/machine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A convenient Turing machine",
|
|
5
5
|
"engines": {
|
|
6
6
|
"npm": ">=7.0.0"
|
|
@@ -32,13 +32,7 @@
|
|
|
32
32
|
"import": "./dist/index.mjs",
|
|
33
33
|
"require": "./dist/index.cjs",
|
|
34
34
|
"default": "./dist/index.mjs"
|
|
35
|
-
},
|
|
36
|
-
"./src": {
|
|
37
|
-
"types": "./dist/index.d.ts",
|
|
38
|
-
"import": "./dist/index.mjs",
|
|
39
|
-
"require": "./dist/index.cjs",
|
|
40
|
-
"default": "./dist/index.mjs"
|
|
41
35
|
}
|
|
42
36
|
},
|
|
43
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "98decf323129d528febafac1bec581a9ee3800ff"
|
|
44
38
|
}
|
package/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["./src/index.ts","./src/classes/alphabet.ts","./src/classes/command.ts","./src/classes/lock.ts","./src/classes/reference.ts","./src/classes/state.ts","./src/classes/tape.ts","./src/classes/tapeblock.ts","./src/classes/tapecommand.ts","./src/classes/turingmachine.ts","./src/utilities/functions.ts"],"version":"5.9.3"}
|