Below is a small English-like grammar. Type a sentence in the input box and press Parse โ the chart will fill column by column, showing every Predict, Scan, and Complete step. An item like S โ NP โข VP [0] means: "we are trying to build an S, we have matched an NP so far, we still need a VP, and the match started at position 0."
<div class="grammar-box">
<strong>Grammar:</strong>
<code>S โ NP VP</code>
<code>NP โ det noun | noun</code>
<code>VP โ verb NP | verb</code>
<code>det โ "the" | "a"</code>
<code>noun โ "dog" | "cat" | "fish"</code>
<code>verb โ "sees" | "chases" | "ate"</code>
</div>
<div class="input-row">
<input id="sentence" type="text" value="the dog sees a cat" placeholder="e.g. the dog sees a cat" spellcheck="false" />
<button id="parseBtn" type="button">Parse</button>
</div>
<div id="result" class="result"></div>
<div id="chart-wrap"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.grammar-box { background: #f0f4f8; border: 1px solid #cdd5de; border-radius: 8px; padding: .6rem .9rem; margin-bottom: .7rem; }
.grammar-box strong { display: block; margin-bottom: .3rem; font-size: .85rem; color: #4a5568; }
.grammar-box code { display: inline-block; background: #e2e8f0; border-radius: 4px; padding: .1rem .4rem; font-size: .8rem; margin: .15rem .2rem; }
.input-row { display: flex; gap: .5rem; margin-bottom: .6rem; }
#sentence { flex: 1; font: 600 15px ui-monospace,monospace; padding: .45rem .7rem; border: 1px solid #b0bec5; border-radius: 8px; outline: none; }
#sentence:focus { border-color: #2563eb; }
button { font: 600 14px system-ui,sans-serif; padding: .45rem 1rem; background: #1d3557; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #16304f; }
.result { font-weight: 700; font-size: 1rem; min-height: 1.4em; margin-bottom: .5rem; }
.result.ok { color: #0a7d33; }
.result.fail { color: #c92f3c; }
#chart-wrap { overflow-x: auto; }
table { border-collapse: collapse; font-size: .78rem; min-width: 100%; }
th { background: #1d3557; color: #fff; padding: .3rem .7rem; text-align: center; font-weight: 700; }
td { vertical-align: top; padding: .3rem .7rem; border: 1px solid #d1d9e0; min-width: 160px; max-width: 220px; }
td:first-child { background: #f0f4f8; font-weight: 700; color: #1d3557; text-align: center; white-space: nowrap; }
.item { display: block; font-family: ui-monospace,monospace; white-space: nowrap; margin: .1rem 0; font-size: .76rem; }
.item.predict { color: #0066cc; }
.item.scan { color: #0a7d33; }
.item.complete { color: #8b5e00; }
.legend { display: flex; gap: 1rem; margin-bottom: .4rem; font-size: .78rem; }
.legend span { display: flex; align-items: center; gap: .25rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Grammar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Rules stored as { lhs: string, rhs: string[] }
const RULES = [
{ lhs:'S', rhs:['NP','VP'] },
{ lhs:'NP', rhs:['det','noun'] },
{ lhs:'NP', rhs:['noun'] },
{ lhs:'VP', rhs:['verb','NP'] },
{ lhs:'VP', rhs:['verb'] },
];
const TERMINALS = {
det: ['the','a'],
noun: ['dog','cat','fish'],
verb: ['sees','chases','ate'],
};
function termMatches(sym, word) {
return TERMINALS[sym] && TERMINALS[sym].includes(word.toLowerCase());
}
// โโ Earley item: { rule, dot, origin, op }
function itemKey(item) {
return item.rule.lhs + 'โ' + item.rule.rhs.slice(0,item.dot).join(' ') + 'โข' + item.rule.rhs.slice(item.dot).join(' ') + '[' + item.origin + ']';
}
function earley(words) {
const n = words.length;
// chart[i] = array of items at column i
const chart = Array.from({length: n+1}, () => []);
const seen = Array.from({length: n+1}, () => new Set());
function addItem(col, item) {
const k = itemKey(item);
if (seen[col].has(k)) return;
seen[col].add(k);
chart[col].push(item);
}
// Seed: all rules for S at position 0
RULES.filter(r => r.lhs === 'S').forEach(r =>
addItem(0, { rule:r, dot:0, origin:0, op:'predict' }));
for (let i = 0; i <= n; i++) {
let j = 0;
while (j < chart[i].length) {
const item = chart[i][j++];
const nextSym = item.rule.rhs[item.dot]; // symbol after dot
if (nextSym === undefined) {
// COMPLETE: this item is done; advance items waiting for item.rule.lhs
for (const waiting of chart[item.origin]) {
if (waiting.rule.rhs[waiting.dot] === item.rule.lhs) {
addItem(i, { rule:waiting.rule, dot:waiting.dot+1, origin:waiting.origin, op:'complete' });
}
}
} else if (TERMINALS[nextSym]) {
// SCAN: terminal โ advance into next column if the word matches
if (i < n && termMatches(nextSym, words[i])) {
addItem(i+1, { rule:item.rule, dot:item.dot+1, origin:item.origin, op:'scan' });
}
} else {
// PREDICT: non-terminal โ add its rules at column i
RULES.filter(r => r.lhs === nextSym).forEach(r => {
addItem(i, { rule:r, dot:0, origin:i, op:'predict' });
});
}
}
}
const accepted = chart[n].some(it =>
it.rule.lhs === 'S' && it.dot === it.rule.rhs.length && it.origin === 0);
return { chart, accepted };
}
// โโ Render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function formatItem(it) {
const { lhs, rhs } = it.rule;
const left = rhs.slice(0, it.dot).join(' ');
const right = rhs.slice(it.dot).join(' ');
const dot = right ? left + ' โข ' + right : left + ' โข';
return lhs + ' โ ' + dot + ' [' + it.origin + ']';
}
function render(words, chart, accepted) {
const wrap = document.getElementById('chart-wrap');
const res = document.getElementById('result');
if (accepted) { res.textContent = 'โ Parse succeeded โ S spans the whole input!'; res.className = 'result ok'; }
else { res.textContent = 'โ Parse failed โ sentence not in language.'; res.className = 'result fail'; }
// Legend
let html = '<div class="legend"><span><span class="dot" style="background:#0066cc"></span> Predict</span><span><span class="dot" style="background:#0a7d33"></span> Scan</span><span><span class="dot" style="background:#8b5e00"></span> Complete</span></div>';
html += '<table><tr><th>Column</th>';
for (let i = 0; i <= words.length; i++)
html += '<th>' + i + (i < words.length ? '<br><small>"' + words[i] + '"</small>' : '<br><small>(end)</small>') + '</th>';
html += '</tr>';
// One row per "step" (predict / scan / complete labelling)
const maxItems = Math.max(...chart.map(c => c.length));
for (let row = 0; row < maxItems; row++) {
html += '<tr><td>' + (row + 1) + '</td>';
for (let col = 0; col <= words.length; col++) {
const it = chart[col][row];
if (it) {
html += '<td><span class="item ' + it.op + '" title="' + it.op + '">' + formatItem(it) + '</span></td>';
} else {
html += '<td></td>';
}
}
html += '</tr>';
}
html += '</table>';
wrap.innerHTML = html;
}
function doParse() {
const raw = document.getElementById('sentence').value.trim();
const words = raw.split(/\s+/).filter(Boolean);
if (!words.length) { document.getElementById('result').textContent = 'Enter a sentence first.'; document.getElementById('result').className='result'; document.getElementById('chart-wrap').innerHTML=''; return; }
const { chart, accepted } = earley(words);
render(words, chart, accepted);
// console log for node verification
console.log(fmt('parse("%raw%") =', { raw: raw }), accepted, '| items per col:', chart.map(c=>c.length).join(','));
}
document.getElementById('parseBtn').addEventListener('click', doParse);
document.getElementById('sentence').addEventListener('keydown', e => { if (e.key==='Enter') doParse(); });
doParse();
<div class="grammar-box">
<strong>Grammar:</strong>
<code>S โ NP VP</code>
<code>NP โ det noun | noun</code>
<code>VP โ verb NP | verb</code>
<code>det โ "the" | "a"</code>
<code>noun โ "dog" | "cat" | "fish"</code>
<code>verb โ "sees" | "chases" | "ate"</code>
</div>
<div class="input-row">
<input id="sentence" type="text" value="the dog sees a cat" placeholder="e.g. the dog sees a cat" spellcheck="false" />
<button id="parseBtn" type="button">Parse</button>
</div>
<div id="result" class="result"></div>
<div id="chart-wrap"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.grammar-box { background: #f0f4f8; border: 1px solid #cdd5de; border-radius: 8px; padding: .6rem .9rem; margin-bottom: .7rem; }
.grammar-box strong { display: block; margin-bottom: .3rem; font-size: .85rem; color: #4a5568; }
.grammar-box code { display: inline-block; background: #e2e8f0; border-radius: 4px; padding: .1rem .4rem; font-size: .8rem; margin: .15rem .2rem; }
.input-row { display: flex; gap: .5rem; margin-bottom: .6rem; }
#sentence { flex: 1; font: 600 15px ui-monospace,monospace; padding: .45rem .7rem; border: 1px solid #b0bec5; border-radius: 8px; outline: none; }
#sentence:focus { border-color: #2563eb; }
button { font: 600 14px system-ui,sans-serif; padding: .45rem 1rem; background: #1d3557; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #16304f; }
.result { font-weight: 700; font-size: 1rem; min-height: 1.4em; margin-bottom: .5rem; }
.result.ok { color: #0a7d33; }
.result.fail { color: #c92f3c; }
#chart-wrap { overflow-x: auto; }
table { border-collapse: collapse; font-size: .78rem; min-width: 100%; }
th { background: #1d3557; color: #fff; padding: .3rem .7rem; text-align: center; font-weight: 700; }
td { vertical-align: top; padding: .3rem .7rem; border: 1px solid #d1d9e0; min-width: 160px; max-width: 220px; }
td:first-child { background: #f0f4f8; font-weight: 700; color: #1d3557; text-align: center; white-space: nowrap; }
.item { display: block; font-family: ui-monospace,monospace; white-space: nowrap; margin: .1rem 0; font-size: .76rem; }
.item.predict { color: #0066cc; }
.item.scan { color: #0a7d33; }
.item.complete { color: #8b5e00; }
.legend { display: flex; gap: 1rem; margin-bottom: .4rem; font-size: .78rem; }
.legend span { display: flex; align-items: center; gap: .25rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Grammar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Rules stored as { lhs: string, rhs: string[] }
const RULES = [
{ lhs:'S', rhs:['NP','VP'] },
{ lhs:'NP', rhs:['det','noun'] },
{ lhs:'NP', rhs:['noun'] },
{ lhs:'VP', rhs:['verb','NP'] },
{ lhs:'VP', rhs:['verb'] },
];
const TERMINALS = {
det: ['the','a'],
noun: ['dog','cat','fish'],
verb: ['sees','chases','ate'],
};
function termMatches(sym, word) {
return TERMINALS[sym] && TERMINALS[sym].includes(word.toLowerCase());
}
// โโ Earley item: { rule, dot, origin, op }
function itemKey(item) {
return item.rule.lhs + 'โ' + item.rule.rhs.slice(0,item.dot).join(' ') + 'โข' + item.rule.rhs.slice(item.dot).join(' ') + '[' + item.origin + ']';
}
function earley(words) {
const n = words.length;
// chart[i] = array of items at column i
const chart = Array.from({length: n+1}, () => []);
const seen = Array.from({length: n+1}, () => new Set());
function addItem(col, item) {
const k = itemKey(item);
if (seen[col].has(k)) return;
seen[col].add(k);
chart[col].push(item);
}
// Seed: all rules for S at position 0
RULES.filter(r => r.lhs === 'S').forEach(r =>
addItem(0, { rule:r, dot:0, origin:0, op:'predict' }));
for (let i = 0; i <= n; i++) {
let j = 0;
while (j < chart[i].length) {
const item = chart[i][j++];
const nextSym = item.rule.rhs[item.dot]; // symbol after dot
if (nextSym === undefined) {
// COMPLETE: this item is done; advance items waiting for item.rule.lhs
for (const waiting of chart[item.origin]) {
if (waiting.rule.rhs[waiting.dot] === item.rule.lhs) {
addItem(i, { rule:waiting.rule, dot:waiting.dot+1, origin:waiting.origin, op:'complete' });
}
}
} else if (TERMINALS[nextSym]) {
// SCAN: terminal โ advance into next column if the word matches
if (i < n && termMatches(nextSym, words[i])) {
addItem(i+1, { rule:item.rule, dot:item.dot+1, origin:item.origin, op:'scan' });
}
} else {
// PREDICT: non-terminal โ add its rules at column i
RULES.filter(r => r.lhs === nextSym).forEach(r => {
addItem(i, { rule:r, dot:0, origin:i, op:'predict' });
});
}
}
}
const accepted = chart[n].some(it =>
it.rule.lhs === 'S' && it.dot === it.rule.rhs.length && it.origin === 0);
return { chart, accepted };
}
// โโ Render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function formatItem(it) {
const { lhs, rhs } = it.rule;
const left = rhs.slice(0, it.dot).join(' ');
const right = rhs.slice(it.dot).join(' ');
const dot = right ? left + ' โข ' + right : left + ' โข';
return lhs + ' โ ' + dot + ' [' + it.origin + ']';
}
function render(words, chart, accepted) {
const wrap = document.getElementById('chart-wrap');
const res = document.getElementById('result');
if (accepted) { res.textContent = 'โ Parse succeeded โ S spans the whole input!'; res.className = 'result ok'; }
else { res.textContent = 'โ Parse failed โ sentence not in language.'; res.className = 'result fail'; }
// Legend
let html = '<div class="legend"><span><span class="dot" style="background:#0066cc"></span> Predict</span><span><span class="dot" style="background:#0a7d33"></span> Scan</span><span><span class="dot" style="background:#8b5e00"></span> Complete</span></div>';
html += '<table><tr><th>Column</th>';
for (let i = 0; i <= words.length; i++)
html += '<th>' + i + (i < words.length ? '<br><small>"' + words[i] + '"</small>' : '<br><small>(end)</small>') + '</th>';
html += '</tr>';
// One row per "step" (predict / scan / complete labelling)
const maxItems = Math.max(...chart.map(c => c.length));
for (let row = 0; row < maxItems; row++) {
html += '<tr><td>' + (row + 1) + '</td>';
for (let col = 0; col <= words.length; col++) {
const it = chart[col][row];
if (it) {
html += '<td><span class="item ' + it.op + '" title="' + it.op + '">' + formatItem(it) + '</span></td>';
} else {
html += '<td></td>';
}
}
html += '</tr>';
}
html += '</table>';
wrap.innerHTML = html;
}
function doParse() {
const raw = document.getElementById('sentence').value.trim();
const words = raw.split(/\s+/).filter(Boolean);
if (!words.length) { document.getElementById('result').textContent = 'Enter a sentence first.'; document.getElementById('result').className='result'; document.getElementById('chart-wrap').innerHTML=''; return; }
const { chart, accepted } = earley(words);
render(words, chart, accepted);
// console log for node verification
console.log(fmt('parse("%raw%") =', { raw: raw }), accepted, '| items per col:', chart.map(c=>c.length).join(','));
}
document.getElementById('parseBtn').addEventListener('click', doParse);
document.getElementById('sentence').addEventListener('keydown', e => { if (e.key==='Enter') doParse(); });
doParse();
Notice how the chart grows: each column corresponds to one word. A Predict step adds items for rules that could apply next. A Scan step advances items whose next expected symbol matches the current word. A Complete step wakes up all waiting items when a non-terminal is finished. When the full input is consumed and an item S โ โฆ โข [0] sits in the last column, the parse succeeds.
See how it compares to the all-at-once approach of CYK or the restricted grammars required by LR parsers used in most compilers.
Comments
Loading comments...