A file passes through states: Closed → Open → Closed. Each state allows only certain operations. Build a sequence of operations using the buttons, then press Check to see whether the typestate checker accepts or rejects your program.
<!-- Interactive typestate checker — file lifecycle demo -->
<p class="hint">A file starts <b>Closed</b>. Add operations using the buttons, then press <b>Check</b> to verify the sequence. Each state allows only certain operations: <code>open()</code> from Closed; <code>read()</code>, <code>write()</code>, <code>close()</code> from Open.</p>
<div class="state-bar">
<span class="state-label">Current state</span>
<span id="state-display" class="state-chip closed">Closed</span>
</div>
<div id="program" class="program">
<span class="placeholder">(empty program — add operations)</span>
</div>
<div class="op-buttons">
<button id="btn-open" type="button" title="open() — valid only in Closed state">open()</button>
<button id="btn-read" type="button" title="read() — valid only in Open state">read()</button>
<button id="btn-write" type="button" title="write() — valid only in Open state">write()</button>
<button id="btn-close" type="button" title="close() — valid only in Open state">close()</button>
</div>
<div class="action-row">
<button id="btn-check" type="button">Check</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
<div id="trace" class="trace"></div>
/* Styles for the typestate checker demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.state-bar { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.state-label { font-size: .8rem; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: .05em; }
.state-chip { font: 700 .82rem ui-monospace, monospace; padding: .18rem .55rem; border-radius: 5px; }
.state-chip.closed { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.state-chip.open { background: #d4edda; color: #155724; border: 1px solid #b8d9c3; }
.program { min-height: 2.4rem; background: #f5f7fa; border: 1px solid #dce3ec; border-radius: 8px;
padding: .45rem .65rem; font: .85rem ui-monospace, monospace; display: flex; flex-wrap: wrap;
gap: .3rem; align-items: center; margin-bottom: .7rem; }
.placeholder { color: #aaa; font-style: italic; font-size: .82rem; }
.op { padding: .2rem .5rem; border-radius: 5px; font: 600 .82rem ui-monospace, monospace;
display: inline-flex; align-items: center; gap: .25rem; }
.op.ok { background: #d4edda; color: #155724; border: 1px solid #b8d9c3; }
.op.err { background: #f8d7da; color: #721c24; border: 1px solid #f1b0b7; }
.op .x { cursor: pointer; font-size: .95rem; opacity: .6; }
.op .x:hover { opacity: 1; }
.op-buttons { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.op-buttons button { font: 600 .82rem ui-monospace, monospace; padding: .3rem .7rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
.op-buttons button:hover { opacity: .85; }
.action-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.action-row button { font: 600 14px system-ui, sans-serif; padding: .4rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
.action-row button.ghost { background: #fff; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
.trace { font: .8rem ui-monospace, monospace; color: #555; margin-top: .4rem; line-height: 1.6; white-space: pre-wrap; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Typestate transitions: each state maps allowed ops to their next state.
// 'closed' allows only open(); 'open' allows read/write/close.
const TRANSITIONS = {
closed: { open: 'open' },
open: { read: 'open', write: 'open', close: 'closed' }
};
// ops = list of operation names the user has added
const ops = [];
let simState = 'closed';
const programEl = document.getElementById('program');
const stateEl = document.getElementById('state-display');
const statusEl = document.getElementById('status');
const traceEl = document.getElementById('trace');
function updateStateChip(state) {
// update the state chip label and color class
stateEl.textContent = state === 'closed' ? 'Closed' : 'Open';
stateEl.className = 'state-chip ' + state;
}
function renderProgram() {
// re-draw the program strip from the ops array
programEl.innerHTML = '';
if (ops.length === 0) {
programEl.innerHTML = '<span class="placeholder">(empty program — add operations)</span>';
return;
}
ops.forEach((op, i) => {
const span = document.createElement('span');
span.className = 'op ok';
span.innerHTML = op + '() <span class="x" data-i="' + i + '">✕</span>';
span.querySelector('.x').addEventListener('click', () => { ops.splice(i, 1); renderProgram(); });
programEl.appendChild(span);
if (i < ops.length - 1) {
const arr = document.createElement('span');
arr.textContent = '→';
arr.style.color = '#888';
programEl.appendChild(arr);
}
});
}
function addOp(op) {
// append one operation and clear previous check result
ops.push(op);
renderProgram();
statusEl.textContent = '';
statusEl.className = 'status';
traceEl.textContent = '';
}
function check() {
// walk the typestate machine and report accept or reject
let state = 'closed';
const lines = [];
let errorIdx = -1;
for (let i = 0; i < ops.length; i++) {
const op = ops[i];
const next = TRANSITIONS[state] && TRANSITIONS[state][op];
if (next === undefined) {
// operation is not in TRANSITIONS[state] — type error
lines.push(i18nFmt('✗ %op% — illegal in state '%state%'', { op: op + '()', state: state }));
errorIdx = i;
break;
}
lines.push(i18nFmt('✓ %op%: %from% → %to%', { op: op + '()', from: state, to: next }));
state = next;
}
// a well-formed program must return the file to Closed
if (errorIdx === -1 && state !== 'closed') {
lines.push(i18nFmt('✗ Program ends in state '%state%' (must end Closed)', { state: state }));
errorIdx = ops.length;
}
traceEl.textContent = lines.join('\n');
if (errorIdx === -1) {
statusEl.textContent = '✓ Accepted — valid operation sequence.';
statusEl.className = 'status ok';
} else {
// show the offending op in red
statusEl.textContent = i18nFmt('✗ Rejected — %op% is illegal here.', { op: ops[errorIdx] !== undefined ? ops[errorIdx] + '()' : '?' });
statusEl.className = 'status err';
markError(errorIdx);
}
simState = state;
updateStateChip(simState);
}
function markError(idx) {
// style the op chip at the error index as an error
const spans = programEl.querySelectorAll('.op');
spans.forEach((sp, i) => {
if (i === idx) sp.className = 'op err';
});
}
function reset() {
ops.length = 0;
simState = 'closed';
renderProgram();
updateStateChip('closed');
statusEl.textContent = '';
statusEl.className = 'status';
traceEl.textContent = '';
}
document.getElementById('btn-open').addEventListener('click', () => addOp('open'));
document.getElementById('btn-read').addEventListener('click', () => addOp('read'));
document.getElementById('btn-write').addEventListener('click', () => addOp('write'));
document.getElementById('btn-close').addEventListener('click', () => addOp('close'));
document.getElementById('btn-check').addEventListener('click', check);
document.getElementById('btn-reset').addEventListener('click', reset);
updateStateChip('closed');
<!-- Interactive typestate checker — file lifecycle demo -->
<p class="hint">A file starts <b>Closed</b>. Add operations using the buttons, then press <b>Check</b> to verify the sequence. Each state allows only certain operations: <code>open()</code> from Closed; <code>read()</code>, <code>write()</code>, <code>close()</code> from Open.</p>
<div class="state-bar">
<span class="state-label">Current state</span>
<span id="state-display" class="state-chip closed">Closed</span>
</div>
<div id="program" class="program">
<span class="placeholder">(empty program — add operations)</span>
</div>
<div class="op-buttons">
<button id="btn-open" type="button" title="open() — valid only in Closed state">open()</button>
<button id="btn-read" type="button" title="read() — valid only in Open state">read()</button>
<button id="btn-write" type="button" title="write() — valid only in Open state">write()</button>
<button id="btn-close" type="button" title="close() — valid only in Open state">close()</button>
</div>
<div class="action-row">
<button id="btn-check" type="button">Check</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
<div id="trace" class="trace"></div>
/* Styles for the typestate checker demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.state-bar { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.state-label { font-size: .8rem; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: .05em; }
.state-chip { font: 700 .82rem ui-monospace, monospace; padding: .18rem .55rem; border-radius: 5px; }
.state-chip.closed { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.state-chip.open { background: #d4edda; color: #155724; border: 1px solid #b8d9c3; }
.program { min-height: 2.4rem; background: #f5f7fa; border: 1px solid #dce3ec; border-radius: 8px;
padding: .45rem .65rem; font: .85rem ui-monospace, monospace; display: flex; flex-wrap: wrap;
gap: .3rem; align-items: center; margin-bottom: .7rem; }
.placeholder { color: #aaa; font-style: italic; font-size: .82rem; }
.op { padding: .2rem .5rem; border-radius: 5px; font: 600 .82rem ui-monospace, monospace;
display: inline-flex; align-items: center; gap: .25rem; }
.op.ok { background: #d4edda; color: #155724; border: 1px solid #b8d9c3; }
.op.err { background: #f8d7da; color: #721c24; border: 1px solid #f1b0b7; }
.op .x { cursor: pointer; font-size: .95rem; opacity: .6; }
.op .x:hover { opacity: 1; }
.op-buttons { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.op-buttons button { font: 600 .82rem ui-monospace, monospace; padding: .3rem .7rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
.op-buttons button:hover { opacity: .85; }
.action-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.action-row button { font: 600 14px system-ui, sans-serif; padding: .4rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
.action-row button.ghost { background: #fff; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
.trace { font: .8rem ui-monospace, monospace; color: #555; margin-top: .4rem; line-height: 1.6; white-space: pre-wrap; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Typestate transitions: each state maps allowed ops to their next state.
// 'closed' allows only open(); 'open' allows read/write/close.
const TRANSITIONS = {
closed: { open: 'open' },
open: { read: 'open', write: 'open', close: 'closed' }
};
// ops = list of operation names the user has added
const ops = [];
let simState = 'closed';
const programEl = document.getElementById('program');
const stateEl = document.getElementById('state-display');
const statusEl = document.getElementById('status');
const traceEl = document.getElementById('trace');
function updateStateChip(state) {
// update the state chip label and color class
stateEl.textContent = state === 'closed' ? 'Closed' : 'Open';
stateEl.className = 'state-chip ' + state;
}
function renderProgram() {
// re-draw the program strip from the ops array
programEl.innerHTML = '';
if (ops.length === 0) {
programEl.innerHTML = '<span class="placeholder">(empty program — add operations)</span>';
return;
}
ops.forEach((op, i) => {
const span = document.createElement('span');
span.className = 'op ok';
span.innerHTML = op + '() <span class="x" data-i="' + i + '">✕</span>';
span.querySelector('.x').addEventListener('click', () => { ops.splice(i, 1); renderProgram(); });
programEl.appendChild(span);
if (i < ops.length - 1) {
const arr = document.createElement('span');
arr.textContent = '→';
arr.style.color = '#888';
programEl.appendChild(arr);
}
});
}
function addOp(op) {
// append one operation and clear previous check result
ops.push(op);
renderProgram();
statusEl.textContent = '';
statusEl.className = 'status';
traceEl.textContent = '';
}
function check() {
// walk the typestate machine and report accept or reject
let state = 'closed';
const lines = [];
let errorIdx = -1;
for (let i = 0; i < ops.length; i++) {
const op = ops[i];
const next = TRANSITIONS[state] && TRANSITIONS[state][op];
if (next === undefined) {
// operation is not in TRANSITIONS[state] — type error
lines.push(i18nFmt('✗ %op% — illegal in state '%state%'', { op: op + '()', state: state }));
errorIdx = i;
break;
}
lines.push(i18nFmt('✓ %op%: %from% → %to%', { op: op + '()', from: state, to: next }));
state = next;
}
// a well-formed program must return the file to Closed
if (errorIdx === -1 && state !== 'closed') {
lines.push(i18nFmt('✗ Program ends in state '%state%' (must end Closed)', { state: state }));
errorIdx = ops.length;
}
traceEl.textContent = lines.join('\n');
if (errorIdx === -1) {
statusEl.textContent = '✓ Accepted — valid operation sequence.';
statusEl.className = 'status ok';
} else {
// show the offending op in red
statusEl.textContent = i18nFmt('✗ Rejected — %op% is illegal here.', { op: ops[errorIdx] !== undefined ? ops[errorIdx] + '()' : '?' });
statusEl.className = 'status err';
markError(errorIdx);
}
simState = state;
updateStateChip(simState);
}
function markError(idx) {
// style the op chip at the error index as an error
const spans = programEl.querySelectorAll('.op');
spans.forEach((sp, i) => {
if (i === idx) sp.className = 'op err';
});
}
function reset() {
ops.length = 0;
simState = 'closed';
renderProgram();
updateStateChip('closed');
statusEl.textContent = '';
statusEl.className = 'status';
traceEl.textContent = '';
}
document.getElementById('btn-open').addEventListener('click', () => addOp('open'));
document.getElementById('btn-read').addEventListener('click', () => addOp('read'));
document.getElementById('btn-write').addEventListener('click', () => addOp('write'));
document.getElementById('btn-close').addEventListener('click', () => addOp('close'));
document.getElementById('btn-check').addEventListener('click', check);
document.getElementById('btn-reset').addEventListener('click', reset);
updateStateChip('closed');
Notice the asymmetry. Checking whether a sequence is valid is linear — walk the states one step at a time. But writing a correct program means respecting the protocol in every execution path, including branches, loops, and error handlers. That is the hard part that typestate analysis automates.
Comments
Loading comments...