turing machine
A simulator for a real Turing machine — an unbounded tape, a read/write head, and a handful of states — running four small preset programs.
Scans right to find the end of the number, then carries leftward through the 1s the way you would by hand.
A Turing machine is barely more than the NFA from the regex visualizer: a set of states and a transition table, same as before. The difference is what it has to work with. An NFA's entire memory is which handful of states are currently lit up — that's it, nothing else persists between input characters. A Turing machine gets a tape it can read, write, and move along in either direction, and that tape is exactly as long as it needs to be. Unbounded memory is the whole story: it's the difference between a machine that can only recognize patterns and one that can actually compute.
The aⁿbⁿ preset is a direct callback to that earlier page, which explained why no NFA can recognize strings like "aabb" or "aaabbb" — doing that requires counting how many a's showed up before checking that the same number of b's follow, and an NFA has nowhere to put a running count. A Turing machine does: this preset crosses off one a and one matching b per pass, using the tape itself as the counter, and only accepts if both sides run out at exactly the same time.
The busy beaver preset is a different kind of demonstration. There's no algorithm that can look at an arbitrary Turing machine and say in advance whether it will ever halt — that's the halting problem, and it's provably undecidable in general. But for small enough machines, you can just try every one of them: the included 3-state, 2-symbol machine was found by brute-force simulating all roughly one million possible 3-state machines and keeping the one that writes the most 1s (six) before halting, starting from a completely blank tape. It's a real answer to "how much can a machine this small actually do" — you just can't get that answer any way other than checking.
view source
import { useEffect, useMemo, useRef, useState } from 'react';
type Move = 'L' | 'R' | 'S';
type NextState = string | 'HALT' | 'ACCEPT' | 'REJECT';
interface Transition {
write: string;
move: Move;
next: NextState;
}
type Rules = Record<string, Record<string, Transition>>;
interface Program {
id: string;
label: string;
blank: string;
startState: string;
rules: Rules;
defaultTape: string;
blurb: string;
}
type Status = 'running' | 'halted' | 'accepted' | 'rejected';
interface MachineState {
tape: Record<number, string>;
head: number;
state: string;
steps: number;
status: Status;
}
const PROGRAMS: Program[] = [
{
id: 'inc',
label: 'binary +1',
blank: '_',
startState: 'right',
defaultTape: '1011',
blurb:
'Scans right to find the end of the number, then carries leftward through the 1s the way you would by hand.',
rules: {
right: {
'0': { write: '0', move: 'R', next: 'right' },
'1': { write: '1', move: 'R', next: 'right' },
_: { write: '_', move: 'L', next: 'carry' },
},
carry: {
'1': { write: '0', move: 'L', next: 'carry' },
'0': { write: '1', move: 'S', next: 'HALT' },
_: { write: '1', move: 'S', next: 'HALT' },
},
},
},
{
id: 'add',
label: 'unary add',
blank: '_',
startState: 'seekPlus',
defaultTape: '111+11',
blurb:
'Turns "m 1s + n 1s" into one run of m+n+1 1s by replacing the + with a 1, then erases the last 1 to correct for it.',
rules: {
seekPlus: {
'1': { write: '1', move: 'R', next: 'seekPlus' },
'+': { write: '1', move: 'R', next: 'seekEnd' },
},
seekEnd: {
'1': { write: '1', move: 'R', next: 'seekEnd' },
_: { write: '_', move: 'L', next: 'erase' },
},
erase: {
'1': { write: '_', move: 'S', next: 'HALT' },
},
},
},
{
id: 'anbn',
label: 'aⁿbⁿ',
blank: '_',
startState: 'start',
defaultTape: 'aaabbb',
blurb:
'Repeatedly crosses off one a and the next unmatched b, then rewinds -- accepts only if both run out at exactly the same time.',
rules: {
start: {
X: { write: 'X', move: 'R', next: 'start' },
a: { write: 'X', move: 'R', next: 'findB' },
Y: { write: 'Y', move: 'R', next: 'checkNoMoreA' },
b: { write: 'b', move: 'S', next: 'REJECT' },
_: { write: '_', move: 'S', next: 'ACCEPT' },
},
findB: {
a: { write: 'a', move: 'R', next: 'findB' },
X: { write: 'X', move: 'R', next: 'findB' },
Y: { write: 'Y', move: 'R', next: 'findB' },
b: { write: 'Y', move: 'L', next: 'returnLeft' },
_: { write: '_', move: 'S', next: 'REJECT' },
},
returnLeft: {
X: { write: 'X', move: 'L', next: 'returnLeft' },
a: { write: 'a', move: 'L', next: 'returnLeft' },
Y: { write: 'Y', move: 'L', next: 'returnLeft' },
b: { write: 'b', move: 'L', next: 'returnLeft' },
_: { write: '_', move: 'R', next: 'start' },
},
checkNoMoreA: {
Y: { write: 'Y', move: 'R', next: 'checkNoMoreA' },
_: { write: '_', move: 'S', next: 'ACCEPT' },
a: { write: 'a', move: 'S', next: 'REJECT' },
X: { write: 'X', move: 'S', next: 'REJECT' },
b: { write: 'b', move: 'S', next: 'REJECT' },
},
},
},
{
id: 'bb3',
label: 'busy beaver',
blank: '0',
startState: 'A',
defaultTape: '',
blurb:
'The 3-state, 2-symbol machine that writes the most possible 1s (six) before halting, starting from a completely blank tape.',
rules: {
A: {
'0': { write: '1', move: 'R', next: 'B' },
'1': { write: '1', move: 'L', next: 'C' },
},
B: {
'0': { write: '1', move: 'L', next: 'A' },
'1': { write: '1', move: 'R', next: 'B' },
},
C: {
'0': { write: '1', move: 'L', next: 'B' },
'1': { write: '1', move: 'L', next: 'HALT' },
},
},
},
];
function initMachine(program: Program, tapeStr: string): MachineState {
const tape: Record<number, string> = {};
for (let i = 0; i < tapeStr.length; i++) {
if (tapeStr[i] !== program.blank) tape[i] = tapeStr[i];
}
return {
tape,
head: 0,
state: program.startState,
steps: 0,
status: 'running',
};
}
function stepMachine(program: Program, m: MachineState): MachineState {
if (m.status !== 'running') return m;
const symbol = m.tape[m.head] ?? program.blank;
const transition = program.rules[m.state]?.[symbol];
if (!transition) return { ...m, status: 'halted' };
const newTape = { ...m.tape, [m.head]: transition.write };
const newHead =
transition.move === 'L'
? m.head - 1
: transition.move === 'R'
? m.head + 1
: m.head;
const status: Status =
transition.next === 'HALT'
? 'halted'
: transition.next === 'ACCEPT'
? 'accepted'
: transition.next === 'REJECT'
? 'rejected'
: 'running';
return {
tape: newTape,
head: newHead,
state: transition.next,
steps: m.steps + 1,
status,
};
}
const STATUS_LABEL: Record<Status, string> = {
running: 'running',
halted: 'halted',
accepted: 'accepted',
rejected: 'rejected',
};
const PAD = 3;
interface Props {
initialProgram?: string;
}
export default function TuringMachine({ initialProgram = 'inc' }: Props) {
const initial = PROGRAMS.find((p) => p.id === initialProgram) ?? PROGRAMS[0];
const [programId, setProgramId] = useState(initial.id);
const [tapeInput, setTapeInput] = useState(initial.defaultTape);
const [machine, setMachine] = useState(() =>
initMachine(initial, initial.defaultTape),
);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(200);
const program = PROGRAMS.find((p) => p.id === programId)!;
const headRef = useRef<HTMLDivElement>(null);
const selectProgram = (id: string) => {
const next = PROGRAMS.find((p) => p.id === id)!;
setProgramId(id);
setTapeInput(next.defaultTape);
setMachine(initMachine(next, next.defaultTape));
setPlaying(false);
};
const reset = () => {
setMachine(initMachine(program, tapeInput));
setPlaying(false);
};
const stepOnce = () => setMachine((m) => stepMachine(program, m));
useEffect(() => {
if (!playing) return;
const id = setInterval(() => {
setMachine((m) => (m.status === 'running' ? stepMachine(program, m) : m));
}, speed);
return () => clearInterval(id);
}, [playing, speed, program]);
useEffect(() => {
if (machine.status !== 'running') setPlaying(false);
}, [machine.status]);
useEffect(() => {
headRef.current?.scrollIntoView({
inline: 'center',
block: 'nearest',
behavior: 'smooth',
});
}, [machine.head]);
const cellRange = useMemo(() => {
const keys = Object.keys(machine.tape).map(Number);
const min = Math.min(machine.head, ...keys, 0) - PAD;
const max = Math.max(machine.head, ...keys, 0) + PAD;
const range: number[] = [];
for (let i = min; i <= max; i++) range.push(i);
return range;
}, [machine.tape, machine.head]);
return (
<div className="turing-machine">
<div className="tape-scroll">
<div className="tape">
{cellRange.map((i) => (
<div
key={i}
ref={i === machine.head ? headRef : undefined}
className={`cell ${i === machine.head ? 'head' : ''}`}
>
{machine.tape[i] ?? program.blank}
</div>
))}
</div>
</div>
<div className="status-line">
<span>
state <strong>{machine.state}</strong>
</span>
<span>
step <strong>{machine.steps}</strong>
</span>
<span className={`status ${machine.status}`}>
{STATUS_LABEL[machine.status]}
</span>
</div>
<div className="controls">
<div className="group">
<span className="group-label">program</span>
{PROGRAMS.map((p) => (
<button
key={p.id}
type="button"
className={programId === p.id ? 'active' : ''}
onClick={() => selectProgram(p.id)}
>
{p.label}
</button>
))}
</div>
<div className="group">
<button
type="button"
onClick={stepOnce}
disabled={machine.status !== 'running'}
>
step
</button>
<button
type="button"
onClick={() => setPlaying((p) => !p)}
disabled={machine.status !== 'running'}
>
{playing ? 'pause' : 'play'}
</button>
<button type="button" onClick={reset}>
reset
</button>
</div>
</div>
<div className="field">
<label htmlFor="tm-tape">
initial tape
<input
id="tm-tape"
type="text"
value={tapeInput}
maxLength={30}
spellCheck={false}
onChange={(e) => setTapeInput(e.target.value)}
/>
</label>
<label className="speed">
speed
<input
type="range"
min={50}
max={600}
step={25}
value={650 - speed}
onChange={(e) => setSpeed(650 - Number(e.target.value))}
/>
</label>
</div>
<p className="blurb">{program.blurb}</p>
<style>{`
.turing-machine {
border: 1px solid #2a2d31;
background: #0e1113;
}
.turing-machine .tape-scroll {
overflow-x: auto;
border-bottom: 1px solid #2a2d31;
padding: 1.5rem 0;
}
.turing-machine .tape {
display: flex;
width: max-content;
margin: 0 auto;
padding: 0 1rem;
}
.turing-machine .cell {
flex-shrink: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #2a2d31;
margin-right: -1px;
font-size: 0.9rem;
color: var(--muted);
}
.turing-machine .cell.head {
border: 1px solid var(--accent);
color: #0b0d0f;
background: var(--accent);
position: relative;
z-index: 1;
}
.turing-machine .status-line {
display: flex;
gap: 1.25rem;
padding: 0.6rem 1rem;
border-bottom: 1px solid #2a2d31;
font-size: 0.8rem;
color: var(--muted);
}
.turing-machine .status-line strong {
color: var(--fg);
font-weight: normal;
}
.turing-machine .status.accepted {
color: var(--accent);
}
.turing-machine .status.rejected {
color: #e58a8a;
}
.turing-machine .controls {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
padding: 0.75rem 1rem;
}
.turing-machine .group {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
}
.turing-machine .group-label {
color: var(--muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-right: 0.15rem;
}
.turing-machine .controls button {
font-family: inherit;
font-size: 0.75rem;
color: #8a8f98;
background: rgba(0, 0, 0, 0.4);
border: 1px solid #2a2d31;
padding: 0.25rem 0.6rem;
cursor: pointer;
}
.turing-machine .controls button:hover:not(:disabled) {
color: #e6e6e6;
}
.turing-machine .controls button:disabled {
opacity: 0.4;
cursor: default;
}
.turing-machine .controls button.active {
color: #0b0d0f;
background: var(--accent);
border-color: var(--accent);
}
.turing-machine .field {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
padding: 0 1rem 0.75rem;
}
.turing-machine .field label {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--muted);
font-size: 0.75rem;
}
.turing-machine .field input[type='text'] {
font-family: var(--mono);
font-size: 0.85rem;
color: #e6e6e6;
background: #000;
border: 1px solid #2a2d31;
padding: 0.3rem 0.5rem;
width: 12rem;
}
.turing-machine .field input[type='text']:focus {
outline: none;
border-color: var(--accent);
}
.turing-machine .blurb {
color: var(--muted);
font-size: 0.8rem;
margin: 0;
padding: 0 1rem 1rem;
max-width: 60ch;
}
`}</style>
</div>
);
}