<div class="controls">
<label for="expr">Expression:</label>
<input id="expr" type="text" value="3+2*4" spellcheck="false" autocomplete="off" />
<button id="btn-parse" type="button">Parse</button>
</div>
<div class="steps-header">
<span class="col-stack">Stack</span>
<span class="col-input">Remaining input</span>
<span class="col-action">Action</span>
</div>
<div id="steps-list"></div>
<div class="nav-row">
<button id="btn-prev" type="button" disabled>◀ Prev</button>
<span id="step-counter">—</span>
<button id="btn-next" type="button" disabled>Next ▶</button>
<button id="btn-end" type="button" disabled>End ↦</button>
</div>
<div id="msg" class="msg"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; margin: 0; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .7rem; }
label { font-weight: 600; }
#expr { font: 600 15px ui-monospace, monospace; border: 1.5px solid #adb1b8; border-radius: 6px;
padding: .35rem .6rem; width: 170px; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button#btn-parse { background: #2a6eb5; border-color: #2a6eb5; }
.steps-header { display: grid; grid-template-columns: 1fr 1fr 1.2fr;
gap: 4px; font-weight: 700; font-size: 12px; color: #555;
border-bottom: 2px solid #dde3ea; padding: 0 0 4px; margin-bottom: 4px; }
#steps-list { max-height: 240px; overflow-y: auto; }
.step-row { display: grid; grid-template-columns: 1fr 1fr 1.2fr; gap: 4px;
padding: 4px 2px; border-radius: 5px; transition: background .15s; }
.step-row.active { background: #e6f0fb; }
.step-row.done { background: #e8f7ee; }
.step-row.err { background: #fdecea; }
.col-stack, .col-input, .col-action { font: 13px ui-monospace, monospace; word-break: break-all; }
.col-action { font-family: system-ui, sans-serif; font-size: 12px; color: #1d3557; }
.col-action.shift { color: #2a6eb5; font-weight: 700; }
.col-action.reduce { color: #a0522d; font-weight: 700; }
.col-action.accept { color: #0a7d33; font-weight: 700; }
.col-action.error { color: #c92f3c; font-weight: 700; }
.nav-row { display: flex; gap: .5rem; align-items: center; margin: .6rem 0 .4rem; flex-wrap: wrap; }
#step-counter { font-size: 13px; color: #555; min-width: 60px; text-align: center; }
.msg { font-weight: 700; font-size: 13px; min-height: 1.4em; }
.msg.ok { color: #0a7d33; } .msg.err { color: #c92f3c; }
.col-stack, .col-input, .col-action { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// LR(0) shift-reduce parser for a simple arithmetic grammar.
// Grammar (augmented):
// S' -> E
// E -> E + T | T
// T -> T * F | F
// F -> ( E ) | num
//
// The LR(0) items, states and action/goto tables are hard-coded from
// the canonical LR(0) automaton for this grammar.
// Productions (0-indexed):
// 0: S' -> E
// 1: E -> E + T
// 2: E -> T
// 3: T -> T * F
// 4: T -> F
// 5: F -> ( E )
// 6: F -> num
// --- Tokeniser ---
function tokenise(src) {
const tokens = [];
let i = 0;
while (i < src.length) {
const ch = src[i];
if (' \t'.includes(ch)) { i++; continue; }
if (ch >= '0' && ch <= '9') {
let n = '';
while (i < src.length && src[i] >= '0' && src[i] <= '9') n += src[i++];
tokens.push({ type: 'num', val: n });
} else if ('()+*'.includes(ch)) {
tokens.push({ type: ch, val: ch }); i++;
} else {
return { error: fmt('Unexpected character: %ch%', { ch: ch }) };
}
}
tokens.push({ type: '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
, val: '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
});
return tokens;
}
// --- LR(0) table (hand-constructed) ---
// States 0..11
// action[state][token] = { op: 'shift'|'reduce'|'accept'|'error', arg }
// shift: arg = next state
// reduce: arg = production index { lhs, rhsLen, label }
// goto[state][nonterm] = next state
const PRODS = [
{ lhs: "S'", rhsLen: 1, label: "S' → E" }, // 0
{ lhs: 'E', rhsLen: 3, label: 'E → E + T' }, // 1
{ lhs: 'E', rhsLen: 1, label: 'E → T' }, // 2
{ lhs: 'T', rhsLen: 3, label: 'T → T * F' }, // 3
{ lhs: 'T', rhsLen: 1, label: 'T → F' }, // 4
{ lhs: 'F', rhsLen: 3, label: 'F → ( E )' }, // 5
{ lhs: 'F', rhsLen: 1, label: 'F → num' }, // 6
];
// LR(0) states for the augmented grammar.
// This is a standard textbook LR(0) automaton.
const ACTION = [
// s0: start
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s1: E on stack – accept or shift +
{ '+': {op:'shift',arg:6}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'accept',arg:0} },
// s2: T on stack – reduce E->T or shift *
{ '+': {op:'reduce',arg:2}, '*': {op:'shift',arg:7}, ')': {op:'reduce',arg:2}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:2} },
// s3: F on stack – reduce T->F
{ '+': {op:'reduce',arg:4}, '*': {op:'reduce',arg:4}, ')': {op:'reduce',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:4} },
// s4: ( on stack
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s5: num on stack – reduce F->num
{ '+': {op:'reduce',arg:6}, '*': {op:'reduce',arg:6}, ')': {op:'reduce',arg:6}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:6} },
// s6: E + on stack
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s7: T * on stack
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s8: ( E on stack – shift +, shift )
{ '+': {op:'shift',arg:6}, ')': {op:'shift',arg:11} },
// s9: E + T on stack – reduce E->E+T, or shift *
{ '+': {op:'reduce',arg:1}, '*': {op:'shift',arg:7}, ')': {op:'reduce',arg:1}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:1} },
// s10: T * F on stack – reduce T->T*F
{ '+': {op:'reduce',arg:3}, '*': {op:'reduce',arg:3}, ')': {op:'reduce',arg:3}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:3} },
// s11: ( E ) on stack – reduce F->(E)
{ '+': {op:'reduce',arg:5}, '*': {op:'reduce',arg:5}, ')': {op:'reduce',arg:5}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:5} },
];
const GOTO = [
// s0
{ E:1, T:2, F:3 },
// s1
{},
// s2
{},
// s3
{},
// s4
{ E:8, T:2, F:3 },
// s5
{},
// s6
{ T:9, F:3 },
// s7
{ F:10 },
// s8
{},
// s9
{},
// s10
{},
// s11
{},
];
function run(tokens) {
const steps = [];
let stateStack = [0];
let symStack = [];
let pos = 0;
function stackStr() {
return (symStack.length === 0 ? '·' : symStack.join(' ')) +
' [' + stateStack.join(',') + ']';
}
function inputStr() {
return tokens.slice(pos).map(t => t.val).join(' ');
}
for (let iter = 0; iter < 300; iter++) {
const state = stateStack[stateStack.length - 1];
const tok = tokens[pos];
const act = ACTION[state] && ACTION[state][tok.type];
if (!act || act.op === 'error') {
steps.push({ stack: stackStr(), input: inputStr(), action: fmt('Error: unexpected "%val%"', { val: tok.val }), cls: 'error' });
return { steps, ok: false };
}
if (act.op === 'accept') {
steps.push({ stack: stackStr(), input: inputStr(), action: 'Accept ✓', cls: 'accept' });
return { steps, ok: true };
}
if (act.op === 'shift') {
steps.push({ stack: stackStr(), input: inputStr(), action: fmt('Shift %val%', { val: tok.val }), cls: 'shift' });
symStack.push(tok.val);
stateStack.push(act.arg);
pos++;
} else if (act.op === 'reduce') {
const prod = PRODS[act.arg];
steps.push({ stack: stackStr(), input: inputStr(), action: fmt('Reduce %label%', { label: prod.label }), cls: 'reduce' });
for (let i = 0; i < prod.rhsLen; i++) { symStack.pop(); stateStack.pop(); }
symStack.push(prod.lhs);
const topState = stateStack[stateStack.length - 1];
const gotoState = GOTO[topState] && GOTO[topState][prod.lhs];
if (gotoState === undefined) {
steps.push({ stack: stackStr(), input: inputStr(), action: 'Error: goto undefined', cls: 'error' });
return { steps, ok: false };
}
stateStack.push(gotoState);
}
}
steps.push({ stack: stackStr(), input: inputStr(), action: 'Error: too many steps', cls: 'error' });
return { steps, ok: false };
}
// --- UI ---
let allSteps = [];
let cursor = 0;
function render() {
const list = document.getElementById('steps-list');
list.innerHTML = '';
allSteps.forEach((s, i) => {
const row = document.createElement('div');
row.className = 'step-row' + (i === cursor ? ' active' : (i < cursor ? (allSteps[i].cls === 'accept' ? ' done' : '') : ''));
if (i > cursor) row.style.opacity = '0.35';
const cls = s.cls || '';
row.innerHTML =
'<span class="col-stack">' + esc(s.stack) + '</span>' +
'<span class="col-input">' + esc(s.input) + '</span>' +
'<span class="col-action ' + cls + '">' + esc(s.action) + '</span>';
list.appendChild(row);
});
// scroll active row into view
const activeEl = list.querySelectorAll('.step-row')[cursor];
if (activeEl) activeEl.scrollIntoView({ block: 'nearest' });
document.getElementById('step-counter').textContent = (cursor + 1) + ' / ' + allSteps.length;
document.getElementById('btn-prev').disabled = cursor === 0;
document.getElementById('btn-next').disabled = cursor >= allSteps.length - 1;
document.getElementById('btn-end').disabled = cursor >= allSteps.length - 1;
}
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
function parse() {
const src = document.getElementById('expr').value.trim();
const res = tokenise(src);
const msg = document.getElementById('msg');
if (res.error) { msg.textContent = '✗ ' + res.error; msg.className = 'msg err'; allSteps=[]; render(); return; }
const { steps, ok } = run(res);
allSteps = steps;
cursor = 0;
msg.textContent = ok ? '✓ Parsed successfully!' : '✗ Parse error.';
msg.className = ok ? 'msg ok' : 'msg err';
document.getElementById('btn-next').disabled = false;
document.getElementById('btn-end').disabled = false;
render();
}
document.getElementById('btn-parse').addEventListener('click', parse);
document.getElementById('btn-prev').addEventListener('click', () => { if (cursor > 0) { cursor--; render(); } });
document.getElementById('btn-next').addEventListener('click', () => { if (cursor < allSteps.length - 1) { cursor++; render(); } });
document.getElementById('btn-end').addEventListener('click', () => { cursor = allSteps.length - 1; render(); });
document.getElementById('expr').addEventListener('keydown', e => { if (e.key === 'Enter') parse(); });
// Auto-parse on load
parse();
<div class="controls">
<label for="expr">Expression:</label>
<input id="expr" type="text" value="3+2*4" spellcheck="false" autocomplete="off" />
<button id="btn-parse" type="button">Parse</button>
</div>
<div class="steps-header">
<span class="col-stack">Stack</span>
<span class="col-input">Remaining input</span>
<span class="col-action">Action</span>
</div>
<div id="steps-list"></div>
<div class="nav-row">
<button id="btn-prev" type="button" disabled>◀ Prev</button>
<span id="step-counter">—</span>
<button id="btn-next" type="button" disabled>Next ▶</button>
<button id="btn-end" type="button" disabled>End ↦</button>
</div>
<div id="msg" class="msg"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; margin: 0; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .7rem; }
label { font-weight: 600; }
#expr { font: 600 15px ui-monospace, monospace; border: 1.5px solid #adb1b8; border-radius: 6px;
padding: .35rem .6rem; width: 170px; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button#btn-parse { background: #2a6eb5; border-color: #2a6eb5; }
.steps-header { display: grid; grid-template-columns: 1fr 1fr 1.2fr;
gap: 4px; font-weight: 700; font-size: 12px; color: #555;
border-bottom: 2px solid #dde3ea; padding: 0 0 4px; margin-bottom: 4px; }
#steps-list { max-height: 240px; overflow-y: auto; }
.step-row { display: grid; grid-template-columns: 1fr 1fr 1.2fr; gap: 4px;
padding: 4px 2px; border-radius: 5px; transition: background .15s; }
.step-row.active { background: #e6f0fb; }
.step-row.done { background: #e8f7ee; }
.step-row.err { background: #fdecea; }
.col-stack, .col-input, .col-action { font: 13px ui-monospace, monospace; word-break: break-all; }
.col-action { font-family: system-ui, sans-serif; font-size: 12px; color: #1d3557; }
.col-action.shift { color: #2a6eb5; font-weight: 700; }
.col-action.reduce { color: #a0522d; font-weight: 700; }
.col-action.accept { color: #0a7d33; font-weight: 700; }
.col-action.error { color: #c92f3c; font-weight: 700; }
.nav-row { display: flex; gap: .5rem; align-items: center; margin: .6rem 0 .4rem; flex-wrap: wrap; }
#step-counter { font-size: 13px; color: #555; min-width: 60px; text-align: center; }
.msg { font-weight: 700; font-size: 13px; min-height: 1.4em; }
.msg.ok { color: #0a7d33; } .msg.err { color: #c92f3c; }
.col-stack, .col-input, .col-action { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// LR(0) shift-reduce parser for a simple arithmetic grammar.
// Grammar (augmented):
// S' -> E
// E -> E + T | T
// T -> T * F | F
// F -> ( E ) | num
//
// The LR(0) items, states and action/goto tables are hard-coded from
// the canonical LR(0) automaton for this grammar.
// Productions (0-indexed):
// 0: S' -> E
// 1: E -> E + T
// 2: E -> T
// 3: T -> T * F
// 4: T -> F
// 5: F -> ( E )
// 6: F -> num
// --- Tokeniser ---
function tokenise(src) {
const tokens = [];
let i = 0;
while (i < src.length) {
const ch = src[i];
if (' \t'.includes(ch)) { i++; continue; }
if (ch >= '0' && ch <= '9') {
let n = '';
while (i < src.length && src[i] >= '0' && src[i] <= '9') n += src[i++];
tokens.push({ type: 'num', val: n });
} else if ('()+*'.includes(ch)) {
tokens.push({ type: ch, val: ch }); i++;
} else {
return { error: fmt('Unexpected character: %ch%', { ch: ch }) };
}
}
tokens.push({ type: '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
, val: '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
});
return tokens;
}
// --- LR(0) table (hand-constructed) ---
// States 0..11
// action[state][token] = { op: 'shift'|'reduce'|'accept'|'error', arg }
// shift: arg = next state
// reduce: arg = production index { lhs, rhsLen, label }
// goto[state][nonterm] = next state
const PRODS = [
{ lhs: "S'", rhsLen: 1, label: "S' → E" }, // 0
{ lhs: 'E', rhsLen: 3, label: 'E → E + T' }, // 1
{ lhs: 'E', rhsLen: 1, label: 'E → T' }, // 2
{ lhs: 'T', rhsLen: 3, label: 'T → T * F' }, // 3
{ lhs: 'T', rhsLen: 1, label: 'T → F' }, // 4
{ lhs: 'F', rhsLen: 3, label: 'F → ( E )' }, // 5
{ lhs: 'F', rhsLen: 1, label: 'F → num' }, // 6
];
// LR(0) states for the augmented grammar.
// This is a standard textbook LR(0) automaton.
const ACTION = [
// s0: start
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s1: E on stack – accept or shift +
{ '+': {op:'shift',arg:6}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'accept',arg:0} },
// s2: T on stack – reduce E->T or shift *
{ '+': {op:'reduce',arg:2}, '*': {op:'shift',arg:7}, ')': {op:'reduce',arg:2}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:2} },
// s3: F on stack – reduce T->F
{ '+': {op:'reduce',arg:4}, '*': {op:'reduce',arg:4}, ')': {op:'reduce',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:4} },
// s4: ( on stack
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s5: num on stack – reduce F->num
{ '+': {op:'reduce',arg:6}, '*': {op:'reduce',arg:6}, ')': {op:'reduce',arg:6}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:6} },
// s6: E + on stack
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s7: T * on stack
{ num: {op:'shift',arg:5}, '(': {op:'shift',arg:4}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'error'} },
// s8: ( E on stack – shift +, shift )
{ '+': {op:'shift',arg:6}, ')': {op:'shift',arg:11} },
// s9: E + T on stack – reduce E->E+T, or shift *
{ '+': {op:'reduce',arg:1}, '*': {op:'shift',arg:7}, ')': {op:'reduce',arg:1}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:1} },
// s10: T * F on stack – reduce T->T*F
{ '+': {op:'reduce',arg:3}, '*': {op:'reduce',arg:3}, ')': {op:'reduce',arg:3}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:3} },
// s11: ( E ) on stack – reduce F->(E)
{ '+': {op:'reduce',arg:5}, '*': {op:'reduce',arg:5}, ')': {op:'reduce',arg:5}, '
Notice the two operations the parser alternates between: shift (move the next token from input onto the stack) and reduce (pop a matched right-hand side and push the non-terminal it produces). The stack grows left to right, and when the grammar symbol on top matches the start symbol with an empty input, parsing succeeds. Each reduce step is one node being added to the parse tree.
: {op:'reduce',arg:5} },
];
const GOTO = [
// s0
{ E:1, T:2, F:3 },
// s1
{},
// s2
{},
// s3
{},
// s4
{ E:8, T:2, F:3 },
// s5
{},
// s6
{ T:9, F:3 },
// s7
{ F:10 },
// s8
{},
// s9
{},
// s10
{},
// s11
{},
];
function run(tokens) {
const steps = [];
let stateStack = [0];
let symStack = [];
let pos = 0;
function stackStr() {
return (symStack.length === 0 ? '·' : symStack.join(' ')) +
' [' + stateStack.join(',') + ']';
}
function inputStr() {
return tokens.slice(pos).map(t => t.val).join(' ');
}
for (let iter = 0; iter < 300; iter++) {
const state = stateStack[stateStack.length - 1];
const tok = tokens[pos];
const act = ACTION[state] && ACTION[state][tok.type];
if (!act || act.op === 'error') {
steps.push({ stack: stackStr(), input: inputStr(), action: fmt('Error: unexpected "%val%"', { val: tok.val }), cls: 'error' });
return { steps, ok: false };
}
if (act.op === 'accept') {
steps.push({ stack: stackStr(), input: inputStr(), action: 'Accept ✓', cls: 'accept' });
return { steps, ok: true };
}
if (act.op === 'shift') {
steps.push({ stack: stackStr(), input: inputStr(), action: fmt('Shift %val%', { val: tok.val }), cls: 'shift' });
symStack.push(tok.val);
stateStack.push(act.arg);
pos++;
} else if (act.op === 'reduce') {
const prod = PRODS[act.arg];
steps.push({ stack: stackStr(), input: inputStr(), action: fmt('Reduce %label%', { label: prod.label }), cls: 'reduce' });
for (let i = 0; i < prod.rhsLen; i++) { symStack.pop(); stateStack.pop(); }
symStack.push(prod.lhs);
const topState = stateStack[stateStack.length - 1];
const gotoState = GOTO[topState] && GOTO[topState][prod.lhs];
if (gotoState === undefined) {
steps.push({ stack: stackStr(), input: inputStr(), action: 'Error: goto undefined', cls: 'error' });
return { steps, ok: false };
}
stateStack.push(gotoState);
}
}
steps.push({ stack: stackStr(), input: inputStr(), action: 'Error: too many steps', cls: 'error' });
return { steps, ok: false };
}
// --- UI ---
let allSteps = [];
let cursor = 0;
function render() {
const list = document.getElementById('steps-list');
list.innerHTML = '';
allSteps.forEach((s, i) => {
const row = document.createElement('div');
row.className = 'step-row' + (i === cursor ? ' active' : (i < cursor ? (allSteps[i].cls === 'accept' ? ' done' : '') : ''));
if (i > cursor) row.style.opacity = '0.35';
const cls = s.cls || '';
row.innerHTML =
'<span class="col-stack">' + esc(s.stack) + '</span>' +
'<span class="col-input">' + esc(s.input) + '</span>' +
'<span class="col-action ' + cls + '">' + esc(s.action) + '</span>';
list.appendChild(row);
});
// scroll active row into view
const activeEl = list.querySelectorAll('.step-row')[cursor];
if (activeEl) activeEl.scrollIntoView({ block: 'nearest' });
document.getElementById('step-counter').textContent = (cursor + 1) + ' / ' + allSteps.length;
document.getElementById('btn-prev').disabled = cursor === 0;
document.getElementById('btn-next').disabled = cursor >= allSteps.length - 1;
document.getElementById('btn-end').disabled = cursor >= allSteps.length - 1;
}
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
function parse() {
const src = document.getElementById('expr').value.trim();
const res = tokenise(src);
const msg = document.getElementById('msg');
if (res.error) { msg.textContent = '✗ ' + res.error; msg.className = 'msg err'; allSteps=[]; render(); return; }
const { steps, ok } = run(res);
allSteps = steps;
cursor = 0;
msg.textContent = ok ? '✓ Parsed successfully!' : '✗ Parse error.';
msg.className = ok ? 'msg ok' : 'msg err';
document.getElementById('btn-next').disabled = false;
document.getElementById('btn-end').disabled = false;
render();
}
document.getElementById('btn-parse').addEventListener('click', parse);
document.getElementById('btn-prev').addEventListener('click', () => { if (cursor > 0) { cursor--; render(); } });
document.getElementById('btn-next').addEventListener('click', () => { if (cursor < allSteps.length - 1) { cursor++; render(); } });
document.getElementById('btn-end').addEventListener('click', () => { cursor = allSteps.length - 1; render(); });
document.getElementById('expr').addEventListener('keydown', e => { if (e.key === 'Enter') parse(); });
// Auto-parse on load
parse();
Comments
Loading comments...