<!-- Demo: a tiny stack VM with switch and threaded dispatch -->
<div class="toolbar">
<label for="prog-select">Program:</label>
<select id="prog-select">
<option value="0">(3 + 4) × 2</option>
<option value="1">5 × 6</option>
<option value="2">1 + 2 + 3 + 4</option>
</select>
<label for="mode-select">Dispatch:</label>
<select id="mode-select">
<option value="switch">Switch dispatch</option>
<option value="threaded">Threaded dispatch</option>
</select>
<button id="run-btn" type="button">Run</button>
<button id="step-btn" type="button" class="ghost">Step</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="panels">
<div class="pane">
<div class="pane-title">Program</div>
<div id="prog-view" class="prog-view"></div>
</div>
<div class="pane">
<div class="pane-title">Stack</div>
<div id="stack-view" class="stack-view"><em>(empty)</em></div>
</div>
</div>
<div id="status" class="status">Select a program and press Run or Step.</div>
<div class="trace-label">Dispatch trace</div>
<div id="trace" class="trace"></div>
/* Layout for the toolbar and panels */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .6rem; margin-bottom: .7rem; }
label { font-size: .8rem; color: #555; }
select { font-size: .85rem; padding: .25rem .4rem; border: 1px solid #bbb; border-radius: 6px; background: #fff; }
button { font: 600 13px system-ui, sans-serif; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.panels { display: flex; gap: .6rem; margin-bottom: .5rem; }
.pane { flex: 1; border: 1px solid #d0d7de; border-radius: 8px; overflow: hidden; min-width: 0; }
.pane-title { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: #555; background: #f0f4f8; padding: .3rem .6rem; border-bottom: 1px solid #d0d7de; }
/* Program listing styles */
.prog-view { padding: .4rem .5rem; font: 13px/1.7 ui-monospace, monospace; }
.instr { display: flex; align-items: center; gap: .4rem; padding: .1rem .3rem; border-radius: 5px; }
.instr.active { background: #fff3cd; }
.instr.done { opacity: .45; }
.ip-arrow { color: #e63946; font-size: 1rem; width: 1rem; flex-shrink: 0; }
.op-name { color: #1d3557; font-weight: 700; min-width: 3.5rem; }
.op-arg { color: #6a737d; }
/* Stack display styles */
.stack-view { padding: .5rem .6rem; font: 13px ui-monospace, monospace; min-height: 60px; }
.stack-item { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 5px;
padding: .15rem .5rem; margin-bottom: .25rem; color: #1d3557; font-weight: 600; }
/* Status bar styles */
.status { font-size: .95rem; font-weight: 600; margin: .35rem 0; min-height: 1.3em; color: #333; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
.trace-label { font-size: .75rem; color: #555; font-weight: 700; text-transform: uppercase;
letter-spacing: .04em; margin-top: .3rem; }
.trace { font: 11px/1.6 ui-monospace, monospace; color: #444; background: #f6f8fa;
border: 1px solid #d0d7de; border-radius: 7px; padding: .4rem .6rem;
max-height: 90px; overflow-y: auto; white-space: pre; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Opcode constants — four ops: PUSH, ADD, MUL, HALT
const OP = { PUSH: 0, ADD: 1, MUL: 2, HALT: 3 };
const OP_NAME = ['PUSH', 'ADD', 'MUL', 'HALT'];
// Three canned programs. Each is a flat array of {op, arg} instructions.
const PROGRAMS = [
// Program 0: PUSH 3, PUSH 4, ADD, PUSH 2, MUL, HALT => (3+4)*2 = 14
[{ op: OP.PUSH, arg: 3 }, { op: OP.PUSH, arg: 4 }, { op: OP.ADD, arg: null },
{ op: OP.PUSH, arg: 2 }, { op: OP.MUL, arg: null }, { op: OP.HALT, arg: null }],
// Program 1: PUSH 5, PUSH 6, MUL, HALT => 5*6 = 30
[{ op: OP.PUSH, arg: 5 }, { op: OP.PUSH, arg: 6 }, { op: OP.MUL, arg: null },
{ op: OP.HALT, arg: null }],
// Program 2: PUSH 1, PUSH 2, ADD, PUSH 3, ADD, PUSH 4, ADD, HALT => 1+2+3+4 = 10
[{ op: OP.PUSH, arg: 1 }, { op: OP.PUSH, arg: 2 }, { op: OP.ADD, arg: null },
{ op: OP.PUSH, arg: 3 }, { op: OP.ADD, arg: null }, { op: OP.PUSH, arg: 4 },
{ op: OP.ADD, arg: null }, { op: OP.HALT, arg: null }],
];
let program = PROGRAMS[0];
let mode = 'switch';
let ip = 0, stack = [], halted = false, traceLines = [];
let dispatchCount = 0, mispredCount = 0;
let prevOpIdx = -1;
// Switch dispatch: after every handler we jump back to the top of the switch.
function switchStep() {
if (ip >= program.length) return false;
const instr = program[ip];
let jumped = true;
// Simplified misprediction model: consecutive opcodes of different types
// approximate a branch prediction miss at the inner case-select branch.
if (prevOpIdx !== -1 && instr.op !== prevOpIdx) mispredCount++;
switch (instr.op) {
case OP.PUSH: stack.push(instr.arg); break;
case OP.ADD: { const b = stack.pop(), a = stack.pop(); stack.push(a + b); break; }
case OP.MUL: { const b = stack.pop(), a = stack.pop(); stack.push(a * b); break; }
case OP.HALT: halted = true; jumped = false; break;
}
prevOpIdx = instr.op;
dispatchCount++;
ip++;
return jumped;
}
// Threaded dispatch: each handler address is resolved from a dispatch table.
// In a real system the next-instruction address is embedded in the bytecode stream.
function threadedStep() {
if (ip >= program.length) return false;
const instr = program[ip];
// Same misprediction model — compare per-pair vs per-site in a real CPU.
if (prevOpIdx !== -1 && instr.op !== prevOpIdx) mispredCount++;
const handlers = [
() => { stack.push(instr.arg); },
() => { const b = stack.pop(), a = stack.pop(); stack.push(a + b); },
() => { const b = stack.pop(), a = stack.pop(); stack.push(a * b); },
() => { halted = true; },
];
handlers[instr.op]();
prevOpIdx = instr.op;
dispatchCount++;
ip++;
return !halted;
}
function step() {
if (halted || ip >= program.length) return;
const instr = program[ip];
const opLabel = OP_NAME[instr.op] + (instr.arg !== null ? ' ' + instr.arg : '');
const moved = mode === 'switch' ? switchStep() : threadedStep();
const modeLabel = mode === 'switch' ? 'SW' : 'TH';
traceLines.push(i18nFmt('[%mode%] ip=%ip% %op%', { mode: modeLabel, op: opLabel, ip: ip - 1 }));
renderAll();
}
function runAll() {
while (!halted && ip < program.length) step();
}
function resetVM() {
ip = 0; stack = []; halted = false; traceLines = [];
dispatchCount = 0; mispredCount = 0; prevOpIdx = -1;
document.getElementById('status').textContent = 'Select a program and press Run or Step.';
document.getElementById('status').className = 'status';
renderAll();
}
function renderProg() {
const el = document.getElementById('prog-view');
el.innerHTML = program.map((ins, i) => {
let cls = 'instr';
if (i < ip) cls += ' done';
if (i === ip && !halted) cls += ' active';
const arrow = (i === ip && !halted) ? '<span class="ip-arrow">▶</span>' : '<span class="ip-arrow"> </span>';
const arg = ins.arg !== null ? `<span class="op-arg">${ins.arg}</span>` : '';
return `<div class="${cls}">${arrow}<span class="op-name">${OP_NAME[ins.op]}</span>${arg}</div>`;
}).join('');
}
function renderStack() {
const el = document.getElementById('stack-view');
if (stack.length === 0) { el.innerHTML = '<em>(empty)</em>'; return; }
el.innerHTML = [...stack].reverse().map(v => `<div class="stack-item">${v}</div>`).join('');
}
function renderStatus() {
const el = document.getElementById('status');
if (halted) {
const top = stack.length > 0 ? stack[stack.length - 1] : '?';
el.textContent = i18nFmt('Result: %result% (dispatched %ops% instructions)', { result: top, ops: dispatchCount });
el.className = 'status ok';
} else if (ip > 0) {
el.textContent = i18nFmt('ip=%ip%, dispatched %ops% so far', { ip: ip, ops: dispatchCount });
el.className = 'status';
}
}
function renderTrace() {
const el = document.getElementById('trace');
el.textContent = traceLines.join('\n');
el.scrollTop = el.scrollHeight;
}
function renderAll() {
renderProg(); renderStack(); renderStatus(); renderTrace();
document.getElementById('run-btn').disabled = halted;
document.getElementById('step-btn').disabled = halted;
}
document.getElementById('prog-select').addEventListener('change', e => {
program = PROGRAMS[+e.target.value]; resetVM();
});
document.getElementById('mode-select').addEventListener('change', e => {
mode = e.target.value; resetVM();
});
document.getElementById('run-btn').addEventListener('click', runAll);
document.getElementById('step-btn').addEventListener('click', step);
document.getElementById('reset-btn').addEventListener('click', resetVM);
renderAll();
<!-- Demo: a tiny stack VM with switch and threaded dispatch -->
<div class="toolbar">
<label for="prog-select">Program:</label>
<select id="prog-select">
<option value="0">(3 + 4) × 2</option>
<option value="1">5 × 6</option>
<option value="2">1 + 2 + 3 + 4</option>
</select>
<label for="mode-select">Dispatch:</label>
<select id="mode-select">
<option value="switch">Switch dispatch</option>
<option value="threaded">Threaded dispatch</option>
</select>
<button id="run-btn" type="button">Run</button>
<button id="step-btn" type="button" class="ghost">Step</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="panels">
<div class="pane">
<div class="pane-title">Program</div>
<div id="prog-view" class="prog-view"></div>
</div>
<div class="pane">
<div class="pane-title">Stack</div>
<div id="stack-view" class="stack-view"><em>(empty)</em></div>
</div>
</div>
<div id="status" class="status">Select a program and press Run or Step.</div>
<div class="trace-label">Dispatch trace</div>
<div id="trace" class="trace"></div>
/* Layout for the toolbar and panels */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .6rem; margin-bottom: .7rem; }
label { font-size: .8rem; color: #555; }
select { font-size: .85rem; padding: .25rem .4rem; border: 1px solid #bbb; border-radius: 6px; background: #fff; }
button { font: 600 13px system-ui, sans-serif; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.panels { display: flex; gap: .6rem; margin-bottom: .5rem; }
.pane { flex: 1; border: 1px solid #d0d7de; border-radius: 8px; overflow: hidden; min-width: 0; }
.pane-title { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: #555; background: #f0f4f8; padding: .3rem .6rem; border-bottom: 1px solid #d0d7de; }
/* Program listing styles */
.prog-view { padding: .4rem .5rem; font: 13px/1.7 ui-monospace, monospace; }
.instr { display: flex; align-items: center; gap: .4rem; padding: .1rem .3rem; border-radius: 5px; }
.instr.active { background: #fff3cd; }
.instr.done { opacity: .45; }
.ip-arrow { color: #e63946; font-size: 1rem; width: 1rem; flex-shrink: 0; }
.op-name { color: #1d3557; font-weight: 700; min-width: 3.5rem; }
.op-arg { color: #6a737d; }
/* Stack display styles */
.stack-view { padding: .5rem .6rem; font: 13px ui-monospace, monospace; min-height: 60px; }
.stack-item { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 5px;
padding: .15rem .5rem; margin-bottom: .25rem; color: #1d3557; font-weight: 600; }
/* Status bar styles */
.status { font-size: .95rem; font-weight: 600; margin: .35rem 0; min-height: 1.3em; color: #333; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
.trace-label { font-size: .75rem; color: #555; font-weight: 700; text-transform: uppercase;
letter-spacing: .04em; margin-top: .3rem; }
.trace { font: 11px/1.6 ui-monospace, monospace; color: #444; background: #f6f8fa;
border: 1px solid #d0d7de; border-radius: 7px; padding: .4rem .6rem;
max-height: 90px; overflow-y: auto; white-space: pre; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Opcode constants — four ops: PUSH, ADD, MUL, HALT
const OP = { PUSH: 0, ADD: 1, MUL: 2, HALT: 3 };
const OP_NAME = ['PUSH', 'ADD', 'MUL', 'HALT'];
// Three canned programs. Each is a flat array of {op, arg} instructions.
const PROGRAMS = [
// Program 0: PUSH 3, PUSH 4, ADD, PUSH 2, MUL, HALT => (3+4)*2 = 14
[{ op: OP.PUSH, arg: 3 }, { op: OP.PUSH, arg: 4 }, { op: OP.ADD, arg: null },
{ op: OP.PUSH, arg: 2 }, { op: OP.MUL, arg: null }, { op: OP.HALT, arg: null }],
// Program 1: PUSH 5, PUSH 6, MUL, HALT => 5*6 = 30
[{ op: OP.PUSH, arg: 5 }, { op: OP.PUSH, arg: 6 }, { op: OP.MUL, arg: null },
{ op: OP.HALT, arg: null }],
// Program 2: PUSH 1, PUSH 2, ADD, PUSH 3, ADD, PUSH 4, ADD, HALT => 1+2+3+4 = 10
[{ op: OP.PUSH, arg: 1 }, { op: OP.PUSH, arg: 2 }, { op: OP.ADD, arg: null },
{ op: OP.PUSH, arg: 3 }, { op: OP.ADD, arg: null }, { op: OP.PUSH, arg: 4 },
{ op: OP.ADD, arg: null }, { op: OP.HALT, arg: null }],
];
let program = PROGRAMS[0];
let mode = 'switch';
let ip = 0, stack = [], halted = false, traceLines = [];
let dispatchCount = 0, mispredCount = 0;
let prevOpIdx = -1;
// Switch dispatch: after every handler we jump back to the top of the switch.
function switchStep() {
if (ip >= program.length) return false;
const instr = program[ip];
let jumped = true;
// Simplified misprediction model: consecutive opcodes of different types
// approximate a branch prediction miss at the inner case-select branch.
if (prevOpIdx !== -1 && instr.op !== prevOpIdx) mispredCount++;
switch (instr.op) {
case OP.PUSH: stack.push(instr.arg); break;
case OP.ADD: { const b = stack.pop(), a = stack.pop(); stack.push(a + b); break; }
case OP.MUL: { const b = stack.pop(), a = stack.pop(); stack.push(a * b); break; }
case OP.HALT: halted = true; jumped = false; break;
}
prevOpIdx = instr.op;
dispatchCount++;
ip++;
return jumped;
}
// Threaded dispatch: each handler address is resolved from a dispatch table.
// In a real system the next-instruction address is embedded in the bytecode stream.
function threadedStep() {
if (ip >= program.length) return false;
const instr = program[ip];
// Same misprediction model — compare per-pair vs per-site in a real CPU.
if (prevOpIdx !== -1 && instr.op !== prevOpIdx) mispredCount++;
const handlers = [
() => { stack.push(instr.arg); },
() => { const b = stack.pop(), a = stack.pop(); stack.push(a + b); },
() => { const b = stack.pop(), a = stack.pop(); stack.push(a * b); },
() => { halted = true; },
];
handlers[instr.op]();
prevOpIdx = instr.op;
dispatchCount++;
ip++;
return !halted;
}
function step() {
if (halted || ip >= program.length) return;
const instr = program[ip];
const opLabel = OP_NAME[instr.op] + (instr.arg !== null ? ' ' + instr.arg : '');
const moved = mode === 'switch' ? switchStep() : threadedStep();
const modeLabel = mode === 'switch' ? 'SW' : 'TH';
traceLines.push(i18nFmt('[%mode%] ip=%ip% %op%', { mode: modeLabel, op: opLabel, ip: ip - 1 }));
renderAll();
}
function runAll() {
while (!halted && ip < program.length) step();
}
function resetVM() {
ip = 0; stack = []; halted = false; traceLines = [];
dispatchCount = 0; mispredCount = 0; prevOpIdx = -1;
document.getElementById('status').textContent = 'Select a program and press Run or Step.';
document.getElementById('status').className = 'status';
renderAll();
}
function renderProg() {
const el = document.getElementById('prog-view');
el.innerHTML = program.map((ins, i) => {
let cls = 'instr';
if (i < ip) cls += ' done';
if (i === ip && !halted) cls += ' active';
const arrow = (i === ip && !halted) ? '<span class="ip-arrow">▶</span>' : '<span class="ip-arrow"> </span>';
const arg = ins.arg !== null ? `<span class="op-arg">${ins.arg}</span>` : '';
return `<div class="${cls}">${arrow}<span class="op-name">${OP_NAME[ins.op]}</span>${arg}</div>`;
}).join('');
}
function renderStack() {
const el = document.getElementById('stack-view');
if (stack.length === 0) { el.innerHTML = '<em>(empty)</em>'; return; }
el.innerHTML = [...stack].reverse().map(v => `<div class="stack-item">${v}</div>`).join('');
}
function renderStatus() {
const el = document.getElementById('status');
if (halted) {
const top = stack.length > 0 ? stack[stack.length - 1] : '?';
el.textContent = i18nFmt('Result: %result% (dispatched %ops% instructions)', { result: top, ops: dispatchCount });
el.className = 'status ok';
} else if (ip > 0) {
el.textContent = i18nFmt('ip=%ip%, dispatched %ops% so far', { ip: ip, ops: dispatchCount });
el.className = 'status';
}
}
function renderTrace() {
const el = document.getElementById('trace');
el.textContent = traceLines.join('\n');
el.scrollTop = el.scrollHeight;
}
function renderAll() {
renderProg(); renderStack(); renderStatus(); renderTrace();
document.getElementById('run-btn').disabled = halted;
document.getElementById('step-btn').disabled = halted;
}
document.getElementById('prog-select').addEventListener('change', e => {
program = PROGRAMS[+e.target.value]; resetVM();
});
document.getElementById('mode-select').addEventListener('change', e => {
mode = e.target.value; resetVM();
});
document.getElementById('run-btn').addEventListener('click', runAll);
document.getElementById('step-btn').addEventListener('click', step);
document.getElementById('reset-btn').addEventListener('click', resetVM);
renderAll();
Notice what changes between the two modes. Switch dispatch jumps back to the top of a single big switch after every opcode — one indirect branch per instruction that always targets the same place, making it easy to mispredict. Threaded dispatch stores the address of each handler directly in the instruction stream and jumps straight there — each branch target differs, letting the CPU predict each handler independently. On modern hardware that difference compounds across millions of instructions.
Comments
Loading comments...