The demo below simulates a PDA that accepts strings with perfectly balanced parentheses — any mix of (, ), [, and ]. Type a string, then step through the automaton one character at a time to watch the stack grow and shrink.
<p class="hint">Type a string using <code>(</code> <code>)</code> <code>[</code> <code>]</code> then step through the PDA one character at a time. The stack shows the machine's memory.</p>
<div class="input-row">
<input id="inputStr" type="text" value="(([])())" maxlength="30" placeholder="e.g. (([])())"/>
<button id="btnLoad" type="button">Load</button>
</div>
<div class="tape-wrap">
<div id="tape" class="tape"></div>
</div>
<div class="main-row">
<div class="stack-box">
<div class="box-label">Stack <span class="tip">(top → bottom)</span></div>
<div id="stack" class="stack"></div>
<div class="stack-bottom">⊥ bottom</div>
</div>
<div class="state-box">
<div class="box-label">State</div>
<div id="stateDisplay" class="state-display">q0</div>
<div class="box-label" style="margin-top:1rem">Step</div>
<div id="stepDisplay" class="step-display">—</div>
</div>
</div>
<div id="verdict" class="verdict"></div>
<div class="btns">
<button id="btnStep" type="button">Step →</button>
<button id="btnRun" type="button">Run all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.5; }
code { background: #eef; padding: .05em .3em; border-radius: 4px; font-size: .9em; }
.input-row { display: flex; gap: .5rem; margin-bottom: .75rem; }
#inputStr { flex: 1; font: 600 15px ui-monospace, monospace; padding: .4rem .6rem;
border: 1px solid #aaa; border-radius: 8px; letter-spacing: .08em; }
.tape-wrap { overflow-x: auto; margin-bottom: .75rem; }
.tape { display: flex; gap: 3px; min-height: 46px; align-items: center; }
.tape-cell { width: 38px; height: 38px; display: flex; align-items: center; justify-content: center;
font: 700 17px ui-monospace, monospace; border-radius: 7px;
border: 1.5px solid #bbb; background: #e8eef3; flex-shrink: 0; transition: background .15s; }
.tape-cell.current { background: #1d3557; color: #fff; border-color: #1d3557; }
.tape-cell.done { background: #c5d8e8; color: #444; border-color: #9ab; }
.tape-cell.upcoming { opacity: .55; }
.tape-end { font-size: .8rem; color: #888; padding: 0 .4rem; }
.main-row { display: flex; gap: 1rem; margin-bottom: .75rem; }
.stack-box, .state-box { background: #f3f6f9; border: 1px solid #dde3ea; border-radius: 10px;
padding: .6rem .8rem; }
.stack-box { flex: 1; min-width: 0; }
.state-box { min-width: 110px; display: flex; flex-direction: column; align-items: center; }
.box-label { font-size: .78rem; font-weight: 700; color: #6b7e99; letter-spacing: .04em;
text-transform: uppercase; margin-bottom: .35rem; }
.tip { font-weight: 400; text-transform: none; letter-spacing: 0; }
.stack { display: flex; flex-direction: column; gap: 3px; min-height: 80px; }
.stack-item { background: #1d3557; color: #fff; border-radius: 6px; padding: .25rem .5rem;
font: 700 14px ui-monospace, monospace; text-align: center; animation: pop-in .12s ease; }
.stack-item.bracket { background: #457b9d; }
@keyframes pop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.stack-bottom { font-size: .75rem; color: #888; margin-top: .3rem; text-align: center; }
.state-display { font: 700 22px ui-monospace, monospace; background: #1d3557; color: #fff;
border-radius: 50%; width: 58px; height: 58px; display: flex;
align-items: center; justify-content: center; }
.state-display.accept { background: #0a7d33; }
.state-display.reject { background: #c92f3c; }
.step-display { font-size: .82rem; color: #1d3557; font-weight: 600; text-align: center;
min-height: 2.5em; line-height: 1.4; }
.verdict { font-size: 1rem; font-weight: 700; min-height: 1.4em; margin-bottom: .5rem; }
.verdict.ok { color: #0a7d33; }
.verdict.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// PDA for balanced parentheses: accepts strings in {(,),[,]}*
// where every opener has a matching closer in correct order.
// States: q0 (running), qAccept, qReject.
// Transitions (deterministic):
// q0, read '(' or '[' → push symbol, stay q0
// q0, read ')' → if top=='(' pop and stay q0, else → qReject
// q0, read ']' → if top=='[' pop and stay q0, else → qReject
// q0, end of input → if stack empty → qAccept, else → qReject
let input = '', pos = 0, stack = [], state = 'q0', lastMsg = '';
function matchSymbol(open, close) {
return (close === ')' && open === '(') || (close === ']' && open === '[');
}
function initMachine(str) {
input = str.replace(/[^()[\]]/g, '');
pos = 0; stack = []; state = 'q0'; lastMsg = '—';
render();
}
function stepMachine() {
if (state !== 'q0') return;
if (pos >= input.length) {
// end of input
if (stack.length === 0) { state = 'qAccept'; lastMsg = 'Input exhausted, stack empty → ACCEPT'; }
else { state = 'qReject'; lastMsg = 'Input exhausted, stack not empty → REJECT'; }
render(); return;
}
const ch = input[pos];
if (ch === '(' || ch === '[') {
stack.push(ch);
lastMsg = fmt('Read "%ch%" → push onto stack', { ch: ch });
} else if (ch === ')' || ch === ']') {
if (stack.length === 0) {
state = 'qReject'; lastMsg = fmt('Read "%ch%" but stack empty → REJECT', { ch: ch });
pos++; render(); return;
}
const top = stack[stack.length - 1];
if (matchSymbol(top, ch)) {
stack.pop();
lastMsg = fmt('Read "%ch%" matches "%top%" → pop', { ch: ch, top: top });
} else {
state = 'qReject'; lastMsg = fmt('Read "%ch%" but top is "%top%" → REJECT', { ch: ch, top: top });
pos++; render(); return;
}
}
pos++;
render();
}
function render() {
// tape
const tapeEl = document.getElementById('tape');
tapeEl.innerHTML = '';
for (let i = 0; i < input.length; i++) {
const cell = document.createElement('div');
cell.className = 'tape-cell' +
(i < pos ? ' done' : i === pos ? ' current' : ' upcoming');
cell.textContent = input[i];
tapeEl.appendChild(cell);
}
if (input.length === 0) {
const empty = document.createElement('span');
empty.className = 'tape-end'; empty.textContent = '(empty string)';
tapeEl.appendChild(empty);
} else if (pos >= input.length && state === 'q0') {
const end = document.createElement('span');
end.className = 'tape-end'; end.textContent = '← end';
tapeEl.appendChild(end);
}
// stack (display top first)
const stackEl = document.getElementById('stack');
stackEl.innerHTML = '';
for (let i = stack.length - 1; i >= 0; i--) {
const item = document.createElement('div');
item.className = 'stack-item bracket';
item.textContent = stack[i];
stackEl.appendChild(item);
}
// state
const sd = document.getElementById('stateDisplay');
sd.textContent = state === 'qAccept' ? '✓' : state === 'qReject' ? '✗' : 'q0';
sd.className = 'state-display' +
(state === 'qAccept' ? ' accept' : state === 'qReject' ? ' reject' : '');
// step message
document.getElementById('stepDisplay').textContent = lastMsg;
// verdict
const v = document.getElementById('verdict');
if (state === 'qAccept') { v.textContent = '✓ Accepted — parentheses are balanced!'; v.className = 'verdict ok'; }
else if (state === 'qReject') { v.textContent = '✗ Rejected — brackets do not balance.'; v.className = 'verdict bad'; }
else { v.textContent = ''; v.className = 'verdict'; }
const done = state !== 'q0';
document.getElementById('btnStep').disabled = done;
document.getElementById('btnRun').disabled = done;
}
function runAll() {
while (state === 'q0') stepMachine();
}
document.getElementById('btnLoad').addEventListener('click', () => {
initMachine(document.getElementById('inputStr').value);
});
document.getElementById('btnStep').addEventListener('click', stepMachine);
document.getElementById('btnRun').addEventListener('click', runAll);
document.getElementById('btnReset').addEventListener('click', () => {
initMachine(document.getElementById('inputStr').value);
});
document.getElementById('inputStr').addEventListener('keydown', (e) => {
if (e.key === 'Enter') initMachine(e.target.value);
});
// boot
initMachine(document.getElementById('inputStr').value);
// node-verifiable assertions (run with node to confirm correctness)
// const cases = [
// ['(([])())', true],
// ['((()))', true],
// ['', true],
// ['()', true],
// ['[]', true],
// ['([)]', false],
// ['(((', false],
// [')))', false],
// ['([[]()])', true],
// ['(]', false],
// ];
<p class="hint">Type a string using <code>(</code> <code>)</code> <code>[</code> <code>]</code> then step through the PDA one character at a time. The stack shows the machine's memory.</p>
<div class="input-row">
<input id="inputStr" type="text" value="(([])())" maxlength="30" placeholder="e.g. (([])())"/>
<button id="btnLoad" type="button">Load</button>
</div>
<div class="tape-wrap">
<div id="tape" class="tape"></div>
</div>
<div class="main-row">
<div class="stack-box">
<div class="box-label">Stack <span class="tip">(top → bottom)</span></div>
<div id="stack" class="stack"></div>
<div class="stack-bottom">⊥ bottom</div>
</div>
<div class="state-box">
<div class="box-label">State</div>
<div id="stateDisplay" class="state-display">q0</div>
<div class="box-label" style="margin-top:1rem">Step</div>
<div id="stepDisplay" class="step-display">—</div>
</div>
</div>
<div id="verdict" class="verdict"></div>
<div class="btns">
<button id="btnStep" type="button">Step →</button>
<button id="btnRun" type="button">Run all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.5; }
code { background: #eef; padding: .05em .3em; border-radius: 4px; font-size: .9em; }
.input-row { display: flex; gap: .5rem; margin-bottom: .75rem; }
#inputStr { flex: 1; font: 600 15px ui-monospace, monospace; padding: .4rem .6rem;
border: 1px solid #aaa; border-radius: 8px; letter-spacing: .08em; }
.tape-wrap { overflow-x: auto; margin-bottom: .75rem; }
.tape { display: flex; gap: 3px; min-height: 46px; align-items: center; }
.tape-cell { width: 38px; height: 38px; display: flex; align-items: center; justify-content: center;
font: 700 17px ui-monospace, monospace; border-radius: 7px;
border: 1.5px solid #bbb; background: #e8eef3; flex-shrink: 0; transition: background .15s; }
.tape-cell.current { background: #1d3557; color: #fff; border-color: #1d3557; }
.tape-cell.done { background: #c5d8e8; color: #444; border-color: #9ab; }
.tape-cell.upcoming { opacity: .55; }
.tape-end { font-size: .8rem; color: #888; padding: 0 .4rem; }
.main-row { display: flex; gap: 1rem; margin-bottom: .75rem; }
.stack-box, .state-box { background: #f3f6f9; border: 1px solid #dde3ea; border-radius: 10px;
padding: .6rem .8rem; }
.stack-box { flex: 1; min-width: 0; }
.state-box { min-width: 110px; display: flex; flex-direction: column; align-items: center; }
.box-label { font-size: .78rem; font-weight: 700; color: #6b7e99; letter-spacing: .04em;
text-transform: uppercase; margin-bottom: .35rem; }
.tip { font-weight: 400; text-transform: none; letter-spacing: 0; }
.stack { display: flex; flex-direction: column; gap: 3px; min-height: 80px; }
.stack-item { background: #1d3557; color: #fff; border-radius: 6px; padding: .25rem .5rem;
font: 700 14px ui-monospace, monospace; text-align: center; animation: pop-in .12s ease; }
.stack-item.bracket { background: #457b9d; }
@keyframes pop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.stack-bottom { font-size: .75rem; color: #888; margin-top: .3rem; text-align: center; }
.state-display { font: 700 22px ui-monospace, monospace; background: #1d3557; color: #fff;
border-radius: 50%; width: 58px; height: 58px; display: flex;
align-items: center; justify-content: center; }
.state-display.accept { background: #0a7d33; }
.state-display.reject { background: #c92f3c; }
.step-display { font-size: .82rem; color: #1d3557; font-weight: 600; text-align: center;
min-height: 2.5em; line-height: 1.4; }
.verdict { font-size: 1rem; font-weight: 700; min-height: 1.4em; margin-bottom: .5rem; }
.verdict.ok { color: #0a7d33; }
.verdict.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// PDA for balanced parentheses: accepts strings in {(,),[,]}*
// where every opener has a matching closer in correct order.
// States: q0 (running), qAccept, qReject.
// Transitions (deterministic):
// q0, read '(' or '[' → push symbol, stay q0
// q0, read ')' → if top=='(' pop and stay q0, else → qReject
// q0, read ']' → if top=='[' pop and stay q0, else → qReject
// q0, end of input → if stack empty → qAccept, else → qReject
let input = '', pos = 0, stack = [], state = 'q0', lastMsg = '';
function matchSymbol(open, close) {
return (close === ')' && open === '(') || (close === ']' && open === '[');
}
function initMachine(str) {
input = str.replace(/[^()[\]]/g, '');
pos = 0; stack = []; state = 'q0'; lastMsg = '—';
render();
}
function stepMachine() {
if (state !== 'q0') return;
if (pos >= input.length) {
// end of input
if (stack.length === 0) { state = 'qAccept'; lastMsg = 'Input exhausted, stack empty → ACCEPT'; }
else { state = 'qReject'; lastMsg = 'Input exhausted, stack not empty → REJECT'; }
render(); return;
}
const ch = input[pos];
if (ch === '(' || ch === '[') {
stack.push(ch);
lastMsg = fmt('Read "%ch%" → push onto stack', { ch: ch });
} else if (ch === ')' || ch === ']') {
if (stack.length === 0) {
state = 'qReject'; lastMsg = fmt('Read "%ch%" but stack empty → REJECT', { ch: ch });
pos++; render(); return;
}
const top = stack[stack.length - 1];
if (matchSymbol(top, ch)) {
stack.pop();
lastMsg = fmt('Read "%ch%" matches "%top%" → pop', { ch: ch, top: top });
} else {
state = 'qReject'; lastMsg = fmt('Read "%ch%" but top is "%top%" → REJECT', { ch: ch, top: top });
pos++; render(); return;
}
}
pos++;
render();
}
function render() {
// tape
const tapeEl = document.getElementById('tape');
tapeEl.innerHTML = '';
for (let i = 0; i < input.length; i++) {
const cell = document.createElement('div');
cell.className = 'tape-cell' +
(i < pos ? ' done' : i === pos ? ' current' : ' upcoming');
cell.textContent = input[i];
tapeEl.appendChild(cell);
}
if (input.length === 0) {
const empty = document.createElement('span');
empty.className = 'tape-end'; empty.textContent = '(empty string)';
tapeEl.appendChild(empty);
} else if (pos >= input.length && state === 'q0') {
const end = document.createElement('span');
end.className = 'tape-end'; end.textContent = '← end';
tapeEl.appendChild(end);
}
// stack (display top first)
const stackEl = document.getElementById('stack');
stackEl.innerHTML = '';
for (let i = stack.length - 1; i >= 0; i--) {
const item = document.createElement('div');
item.className = 'stack-item bracket';
item.textContent = stack[i];
stackEl.appendChild(item);
}
// state
const sd = document.getElementById('stateDisplay');
sd.textContent = state === 'qAccept' ? '✓' : state === 'qReject' ? '✗' : 'q0';
sd.className = 'state-display' +
(state === 'qAccept' ? ' accept' : state === 'qReject' ? ' reject' : '');
// step message
document.getElementById('stepDisplay').textContent = lastMsg;
// verdict
const v = document.getElementById('verdict');
if (state === 'qAccept') { v.textContent = '✓ Accepted — parentheses are balanced!'; v.className = 'verdict ok'; }
else if (state === 'qReject') { v.textContent = '✗ Rejected — brackets do not balance.'; v.className = 'verdict bad'; }
else { v.textContent = ''; v.className = 'verdict'; }
const done = state !== 'q0';
document.getElementById('btnStep').disabled = done;
document.getElementById('btnRun').disabled = done;
}
function runAll() {
while (state === 'q0') stepMachine();
}
document.getElementById('btnLoad').addEventListener('click', () => {
initMachine(document.getElementById('inputStr').value);
});
document.getElementById('btnStep').addEventListener('click', stepMachine);
document.getElementById('btnRun').addEventListener('click', runAll);
document.getElementById('btnReset').addEventListener('click', () => {
initMachine(document.getElementById('inputStr').value);
});
document.getElementById('inputStr').addEventListener('keydown', (e) => {
if (e.key === 'Enter') initMachine(e.target.value);
});
// boot
initMachine(document.getElementById('inputStr').value);
// node-verifiable assertions (run with node to confirm correctness)
// const cases = [
// ['(([])())', true],
// ['((()))', true],
// ['', true],
// ['()', true],
// ['[]', true],
// ['([)]', false],
// ['(((', false],
// [')))', false],
// ['([[]()])', true],
// ['(]', false],
// ];
Notice what happens when brackets mismatch or the stack still holds items at the end: the PDA rejects. The stack is the machine's entire memory — it has no registers, no random access, no way to look back. Yet that single column of symbols is enough to handle any context-free grammar. Try nesting deeply: ((([]))()) — the stack faithfully mirrors the depth.
Comments
Loading comments...