The demo below parses a simple arithmetic expression using a tiny PEG grammar with two rules. The Naïve mode applies rules repeatedly from scratch; the Packrat mode consults a memo table first. Type any expression and press Parse to watch the call counts diverge.
<!-- Packrat parsing demo: naive recursive descent vs packrat with memoization -->
<div class="controls">
<label for="expr">Expression to parse:</label>
<input id="expr" type="text" value="1+2+3" placeholder="e.g. 1+2+3" />
<div class="mode-row">
<label class="mode-label">Mode:</label>
<button id="btn-naive" class="mode-btn active" type="button">Naïve</button>
<button id="btn-packrat" class="mode-btn" type="button">Packrat</button>
<button id="btn-parse" type="button">Parse</button>
</div>
</div>
<div class="stats" id="stats"></div>
<div class="grid-wrap">
<div id="legend" class="legend"></div>
<div id="grid" class="grid"></div>
</div>
/* global reset / box-sizing */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .6rem; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
label { font-size: .85rem; color: #555; }
input { font-size: 1rem; padding: .35rem .5rem; border: 1px solid #ccc; border-radius: 6px; width: 100%; }
.mode-row { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.mode-label { font-size: .85rem; color: #555; margin-right: .2rem; }
.mode-btn { font: 600 13px system-ui; padding: .3rem .7rem; border: 1px solid #1d3557;
border-radius: 6px; background: #fff; color: #1d3557; cursor: pointer; }
.mode-btn.active { background: #1d3557; color: #fff; }
#btn-parse { font: 600 13px system-ui; padding: .3rem .9rem; border: 1px solid #1d3557;
border-radius: 6px; background: #e63946; color: #fff; cursor: pointer; margin-left: auto; }
.stats { font-size: .92rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.stats.ok { color: #0a7d33; }
.stats.warn { color: #b45309; }
.grid-wrap { overflow-x: auto; }
.legend { font-size: .78rem; color: #555; margin-bottom: .3rem; display: flex; gap: .8rem; flex-wrap: wrap; }
.legend span { display: inline-flex; align-items: center; gap: .25rem; }
.swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid #ccc; }
.swatch.hit { background: #a8d8a8; border-color: #5aaa5a; }
.swatch.computed { background: #b8d0e8; border-color: #5a90c8; }
.swatch.fail { background: #f0c0c0; border-color: #d07070; }
.swatch.empty { background: #eee; }
/* memo grid cells */
.grid { display: grid; gap: 2px; min-width: max-content; }
.cell { width: 34px; height: 28px; border-radius: 4px; font-size: .68rem; display: flex;
align-items: center; justify-content: center; border: 1px solid #ddd;
background: #eee; color: #444; text-align: center; overflow: hidden; white-space: nowrap; }
.cell.hit { background: #a8d8a8; border-color: #5aaa5a; color: #1a501a; }
.cell.computed { background: #b8d0e8; border-color: #5a90c8; color: #1a3a5a; }
.cell.fail { background: #f0c0c0; border-color: #d07070; color: #7a1010; }
.row-label { font-size: .75rem; font-weight: 600; color: #555; display: flex;
align-items: center; padding-right: .3rem; white-space: nowrap; }
.col-header { font-size: .68rem; color: #777; text-align: center; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Packrat demo: compare naïve recursive descent vs memoized (packrat) parsing
// Grammar (simplified, no operator precedence):
// Expr <- Term ('+' Term)*
// Term <- [0-9]+
// Each rule is applied at every position of the input string
const RULES = ['Expr', 'Term'];
// Naïve parser: no memoization — re-parses the same substrings repeatedly
let naiveCalls = 0;
function naiveExpr(s, pos) {
naiveCalls++;
// try to parse Term at this position
const t0 = naiveTerm(s, pos);
if (t0 === null) return null;
let cur = t0;
while (cur < s.length && s[cur] === '+') {
const t1 = naiveTerm(s, cur + 1);
if (t1 === null) break;
cur = t1;
}
return cur;
}
function naiveTerm(s, pos) {
naiveCalls++;
if (pos >= s.length || !/[0-9]/.test(s[pos])) return null;
let i = pos;
while (i < s.length && /[0-9]/.test(s[i])) i++;
return i;
}
// Packrat parser: memo table stores (rule, position) -> end position
// memo[rule][pos] = { end, hit } where end is the end position or null on failure
let memo = {};
let packratCalls = 0;
let memoHits = 0;
function packratExpr(s, pos) {
packratCalls++;
const key = 'Expr,' + pos;
if (key in memo) { memoHits++; return memo[key]; }
// compute result, store in memo before returning
const t0 = packratTerm(s, pos);
let result = null;
if (t0 !== null) {
let cur = t0;
while (cur < s.length && s[cur] === '+') {
const t1 = packratTerm(s, cur + 1);
if (t1 === null) break;
cur = t1;
}
result = cur;
}
memo[key] = result;
return result;
}
function packratTerm(s, pos) {
packratCalls++;
const key = 'Term,' + pos;
if (key in memo) { memoHits++; return memo[key]; }
// compute result, store in memo before returning
let result = null;
if (pos < s.length && /[0-9]/.test(s[pos])) {
let i = pos;
while (i < s.length && /[0-9]/.test(s[i])) i++;
result = i;
}
memo[key] = result;
return result;
}
// UI state and event wiring
let mode = 'naive';
let lastGrid = null;
document.getElementById('btn-naive').addEventListener('click', () => {
mode = 'naive';
document.getElementById('btn-naive').classList.add('active');
document.getElementById('btn-packrat').classList.remove('active');
});
document.getElementById('btn-packrat').addEventListener('click', () => {
mode = 'packrat';
document.getElementById('btn-packrat').classList.add('active');
document.getElementById('btn-naive').classList.remove('active');
});
function parse() {
const s = document.getElementById('expr').value.replace(/\s/g, '');
const stats = document.getElementById('stats');
const gridEl = document.getElementById('grid');
const legendEl = document.getElementById('legend');
if (!s) { stats.textContent = 'Please enter an expression.'; stats.className = 'stats warn'; return; }
// run the selected parser on the trimmed input
let result, calls, hits;
if (mode === 'naive') {
naiveCalls = 0;
result = naiveExpr(s, 0);
calls = naiveCalls;
hits = 0;
} else {
memo = {};
packratCalls = 0;
memoHits = 0;
result = packratExpr(s, 0);
calls = packratCalls;
hits = memoHits;
}
const ok = result === s.length;
// show result stats
if (mode === 'naive') {
stats.textContent = ok
? i18nFmt('Parsed OK — %calls% rule calls (naïve, no memo).', { calls })
: i18nFmt('Parse failed — %calls% rule calls (naïve, no memo).', { calls });
} else {
stats.textContent = ok
? i18nFmt('Parsed OK — %calls% rule calls, %hits% memo hits (packrat).', { calls, hits })
: i18nFmt('Parse failed — %calls% rule calls, %hits% memo hits (packrat).', { calls, hits });
}
stats.className = 'stats ' + (ok ? 'ok' : 'warn');
// draw the memo table grid (packrat) or blank grid (naive)
// Populate the memo grid for packrat, or a call-coverage grid for naive
const cols = s.length + 1;
gridEl.style.gridTemplateColumns = '80px ' + RULES.map(() => 'repeat(' + cols + ', 34px)').join(' 6px ');
gridEl.innerHTML = '';
// column headers: one char per input position
const empty = document.createElement('div');
gridEl.appendChild(empty);
RULES.forEach((rule, ri) => {
for (let p = 0; p <= s.length; p++) {
const h = document.createElement('div');
h.className = 'col-header';
h.textContent = p < s.length ? s[p] : '#';
gridEl.appendChild(h);
}
if (ri < RULES.length - 1) {
const sep = document.createElement('div');
gridEl.appendChild(sep);
}
});
RULES.forEach((rule, ri) => {
// row label for this rule
const label = document.createElement('div');
label.className = 'row-label';
label.textContent = rule;
gridEl.style.gridTemplateRows = 'auto auto';
// We use a flat grid — rebuild with one row per rule
// (rebuilt below as a proper 2-row grid)
});
// rebuild grid DOM with proper 2-row layout
// Build a proper 2-row grid: header row + one row per rule
gridEl.innerHTML = '';
const totalCols = 1 + RULES.length * cols + (RULES.length - 1);
gridEl.style.gridTemplateColumns =
'80px ' + Array(cols).fill('34px').join(' ') + ' 6px ' + Array(cols).fill('34px').join(' ');
// emit column header cells
const hSpacer = document.createElement('div');
gridEl.appendChild(hSpacer);
RULES.forEach((rule, ri) => {
for (let p = 0; p <= s.length; p++) {
const h = document.createElement('div');
h.className = 'col-header';
h.textContent = p < s.length ? s[p] : '#';
gridEl.appendChild(h);
}
if (ri < RULES.length - 1) gridEl.appendChild(document.createElement('div'));
});
// emit one data row per grammar rule
RULES.forEach((rule, ri) => {
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = rule;
gridEl.appendChild(lbl);
for (let p = 0; p <= s.length; p++) {
const cell = document.createElement('div');
cell.className = 'cell';
if (mode === 'packrat') {
const key = rule + ',' + p;
if (key in memo) {
const end = memo[key];
if (end === null) { cell.classList.add('fail'); cell.textContent = '✗'; }
else { cell.classList.add('computed'); cell.textContent = end; }
}
} else {
// naive mode: no memo table to show
cell.textContent = '';
}
gridEl.appendChild(cell);
}
if (ri < RULES.length - 1) gridEl.appendChild(document.createElement('div'));
});
// draw color legend below the grid
legendEl.innerHTML = '';
if (mode === 'packrat') {
legendEl.innerHTML =
'<span><span class="swatch computed"></span>computed & stored</span>' +
'<span><span class="swatch fail"></span>failed (null)</span>' +
'<span><span class="swatch empty"></span>not reached</span>';
} else {
legendEl.innerHTML = '<span>Naïve mode has no memo table — switch to Packrat to see it.</span>';
}
}
document.getElementById('btn-parse').addEventListener('click', parse);
document.getElementById('expr').addEventListener('keydown', e => { if (e.key === 'Enter') parse(); });
parse();
<!-- Packrat parsing demo: naive recursive descent vs packrat with memoization -->
<div class="controls">
<label for="expr">Expression to parse:</label>
<input id="expr" type="text" value="1+2+3" placeholder="e.g. 1+2+3" />
<div class="mode-row">
<label class="mode-label">Mode:</label>
<button id="btn-naive" class="mode-btn active" type="button">Naïve</button>
<button id="btn-packrat" class="mode-btn" type="button">Packrat</button>
<button id="btn-parse" type="button">Parse</button>
</div>
</div>
<div class="stats" id="stats"></div>
<div class="grid-wrap">
<div id="legend" class="legend"></div>
<div id="grid" class="grid"></div>
</div>
/* global reset / box-sizing */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .6rem; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
label { font-size: .85rem; color: #555; }
input { font-size: 1rem; padding: .35rem .5rem; border: 1px solid #ccc; border-radius: 6px; width: 100%; }
.mode-row { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.mode-label { font-size: .85rem; color: #555; margin-right: .2rem; }
.mode-btn { font: 600 13px system-ui; padding: .3rem .7rem; border: 1px solid #1d3557;
border-radius: 6px; background: #fff; color: #1d3557; cursor: pointer; }
.mode-btn.active { background: #1d3557; color: #fff; }
#btn-parse { font: 600 13px system-ui; padding: .3rem .9rem; border: 1px solid #1d3557;
border-radius: 6px; background: #e63946; color: #fff; cursor: pointer; margin-left: auto; }
.stats { font-size: .92rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.stats.ok { color: #0a7d33; }
.stats.warn { color: #b45309; }
.grid-wrap { overflow-x: auto; }
.legend { font-size: .78rem; color: #555; margin-bottom: .3rem; display: flex; gap: .8rem; flex-wrap: wrap; }
.legend span { display: inline-flex; align-items: center; gap: .25rem; }
.swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid #ccc; }
.swatch.hit { background: #a8d8a8; border-color: #5aaa5a; }
.swatch.computed { background: #b8d0e8; border-color: #5a90c8; }
.swatch.fail { background: #f0c0c0; border-color: #d07070; }
.swatch.empty { background: #eee; }
/* memo grid cells */
.grid { display: grid; gap: 2px; min-width: max-content; }
.cell { width: 34px; height: 28px; border-radius: 4px; font-size: .68rem; display: flex;
align-items: center; justify-content: center; border: 1px solid #ddd;
background: #eee; color: #444; text-align: center; overflow: hidden; white-space: nowrap; }
.cell.hit { background: #a8d8a8; border-color: #5aaa5a; color: #1a501a; }
.cell.computed { background: #b8d0e8; border-color: #5a90c8; color: #1a3a5a; }
.cell.fail { background: #f0c0c0; border-color: #d07070; color: #7a1010; }
.row-label { font-size: .75rem; font-weight: 600; color: #555; display: flex;
align-items: center; padding-right: .3rem; white-space: nowrap; }
.col-header { font-size: .68rem; color: #777; text-align: center; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Packrat demo: compare naïve recursive descent vs memoized (packrat) parsing
// Grammar (simplified, no operator precedence):
// Expr <- Term ('+' Term)*
// Term <- [0-9]+
// Each rule is applied at every position of the input string
const RULES = ['Expr', 'Term'];
// Naïve parser: no memoization — re-parses the same substrings repeatedly
let naiveCalls = 0;
function naiveExpr(s, pos) {
naiveCalls++;
// try to parse Term at this position
const t0 = naiveTerm(s, pos);
if (t0 === null) return null;
let cur = t0;
while (cur < s.length && s[cur] === '+') {
const t1 = naiveTerm(s, cur + 1);
if (t1 === null) break;
cur = t1;
}
return cur;
}
function naiveTerm(s, pos) {
naiveCalls++;
if (pos >= s.length || !/[0-9]/.test(s[pos])) return null;
let i = pos;
while (i < s.length && /[0-9]/.test(s[i])) i++;
return i;
}
// Packrat parser: memo table stores (rule, position) -> end position
// memo[rule][pos] = { end, hit } where end is the end position or null on failure
let memo = {};
let packratCalls = 0;
let memoHits = 0;
function packratExpr(s, pos) {
packratCalls++;
const key = 'Expr,' + pos;
if (key in memo) { memoHits++; return memo[key]; }
// compute result, store in memo before returning
const t0 = packratTerm(s, pos);
let result = null;
if (t0 !== null) {
let cur = t0;
while (cur < s.length && s[cur] === '+') {
const t1 = packratTerm(s, cur + 1);
if (t1 === null) break;
cur = t1;
}
result = cur;
}
memo[key] = result;
return result;
}
function packratTerm(s, pos) {
packratCalls++;
const key = 'Term,' + pos;
if (key in memo) { memoHits++; return memo[key]; }
// compute result, store in memo before returning
let result = null;
if (pos < s.length && /[0-9]/.test(s[pos])) {
let i = pos;
while (i < s.length && /[0-9]/.test(s[i])) i++;
result = i;
}
memo[key] = result;
return result;
}
// UI state and event wiring
let mode = 'naive';
let lastGrid = null;
document.getElementById('btn-naive').addEventListener('click', () => {
mode = 'naive';
document.getElementById('btn-naive').classList.add('active');
document.getElementById('btn-packrat').classList.remove('active');
});
document.getElementById('btn-packrat').addEventListener('click', () => {
mode = 'packrat';
document.getElementById('btn-packrat').classList.add('active');
document.getElementById('btn-naive').classList.remove('active');
});
function parse() {
const s = document.getElementById('expr').value.replace(/\s/g, '');
const stats = document.getElementById('stats');
const gridEl = document.getElementById('grid');
const legendEl = document.getElementById('legend');
if (!s) { stats.textContent = 'Please enter an expression.'; stats.className = 'stats warn'; return; }
// run the selected parser on the trimmed input
let result, calls, hits;
if (mode === 'naive') {
naiveCalls = 0;
result = naiveExpr(s, 0);
calls = naiveCalls;
hits = 0;
} else {
memo = {};
packratCalls = 0;
memoHits = 0;
result = packratExpr(s, 0);
calls = packratCalls;
hits = memoHits;
}
const ok = result === s.length;
// show result stats
if (mode === 'naive') {
stats.textContent = ok
? i18nFmt('Parsed OK — %calls% rule calls (naïve, no memo).', { calls })
: i18nFmt('Parse failed — %calls% rule calls (naïve, no memo).', { calls });
} else {
stats.textContent = ok
? i18nFmt('Parsed OK — %calls% rule calls, %hits% memo hits (packrat).', { calls, hits })
: i18nFmt('Parse failed — %calls% rule calls, %hits% memo hits (packrat).', { calls, hits });
}
stats.className = 'stats ' + (ok ? 'ok' : 'warn');
// draw the memo table grid (packrat) or blank grid (naive)
// Populate the memo grid for packrat, or a call-coverage grid for naive
const cols = s.length + 1;
gridEl.style.gridTemplateColumns = '80px ' + RULES.map(() => 'repeat(' + cols + ', 34px)').join(' 6px ');
gridEl.innerHTML = '';
// column headers: one char per input position
const empty = document.createElement('div');
gridEl.appendChild(empty);
RULES.forEach((rule, ri) => {
for (let p = 0; p <= s.length; p++) {
const h = document.createElement('div');
h.className = 'col-header';
h.textContent = p < s.length ? s[p] : '#';
gridEl.appendChild(h);
}
if (ri < RULES.length - 1) {
const sep = document.createElement('div');
gridEl.appendChild(sep);
}
});
RULES.forEach((rule, ri) => {
// row label for this rule
const label = document.createElement('div');
label.className = 'row-label';
label.textContent = rule;
gridEl.style.gridTemplateRows = 'auto auto';
// We use a flat grid — rebuild with one row per rule
// (rebuilt below as a proper 2-row grid)
});
// rebuild grid DOM with proper 2-row layout
// Build a proper 2-row grid: header row + one row per rule
gridEl.innerHTML = '';
const totalCols = 1 + RULES.length * cols + (RULES.length - 1);
gridEl.style.gridTemplateColumns =
'80px ' + Array(cols).fill('34px').join(' ') + ' 6px ' + Array(cols).fill('34px').join(' ');
// emit column header cells
const hSpacer = document.createElement('div');
gridEl.appendChild(hSpacer);
RULES.forEach((rule, ri) => {
for (let p = 0; p <= s.length; p++) {
const h = document.createElement('div');
h.className = 'col-header';
h.textContent = p < s.length ? s[p] : '#';
gridEl.appendChild(h);
}
if (ri < RULES.length - 1) gridEl.appendChild(document.createElement('div'));
});
// emit one data row per grammar rule
RULES.forEach((rule, ri) => {
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = rule;
gridEl.appendChild(lbl);
for (let p = 0; p <= s.length; p++) {
const cell = document.createElement('div');
cell.className = 'cell';
if (mode === 'packrat') {
const key = rule + ',' + p;
if (key in memo) {
const end = memo[key];
if (end === null) { cell.classList.add('fail'); cell.textContent = '✗'; }
else { cell.classList.add('computed'); cell.textContent = end; }
}
} else {
// naive mode: no memo table to show
cell.textContent = '';
}
gridEl.appendChild(cell);
}
if (ri < RULES.length - 1) gridEl.appendChild(document.createElement('div'));
});
// draw color legend below the grid
legendEl.innerHTML = '';
if (mode === 'packrat') {
legendEl.innerHTML =
'<span><span class="swatch computed"></span>computed & stored</span>' +
'<span><span class="swatch fail"></span>failed (null)</span>' +
'<span><span class="swatch empty"></span>not reached</span>';
} else {
legendEl.innerHTML = '<span>Naïve mode has no memo table — switch to Packrat to see it.</span>';
}
}
document.getElementById('btn-parse').addEventListener('click', parse);
document.getElementById('expr').addEventListener('keydown', e => { if (e.key === 'Enter') parse(); });
parse();
Notice that the naïve parser's call count grows rapidly with input length, while the packrat parser's call count stays proportional to the input length. Each cell in the grid represents one (rule, position) pair; a green cell means the memo table was hit — work that was skipped entirely.
Comments
Loading comments...