<!-- Type Soundness demo — expression evaluator with type checker -->
<p class="hint">Type a small expression and press <b>Type-check</b>. If it passes, press <b>Evaluate</b>. A type error blocks evaluation — that is progress and preservation at work.</p>
<div class="editor-row">
<div class="expr-group">
<label for="expr-input">Expression</label>
<input id="expr-input" type="text" placeholder="e.g. 1 + 2 or if true then 10 else 20" spellcheck="false" autocomplete="off" />
</div>
<div class="btn-row">
<button id="btn-check" type="button">Type-check</button>
<button id="btn-run" type="button">Evaluate</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="result-area">
<div class="result-box" id="type-box">
<span class="box-label">Inferred type</span>
<span class="box-value" id="type-out">—</span>
</div>
<div class="result-box" id="eval-box">
<span class="box-label">Value</span>
<span class="box-value" id="eval-out">—</span>
</div>
</div>
<div class="status" id="status"></div>
<div class="examples-section">
<p class="examples-label">Quick examples (click to load)</p>
<div class="examples-row" id="examples-row"></div>
</div>
/* Styles for the type soundness demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .9rem; line-height: 1.5; }
.editor-row { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .7rem; }
.expr-group { display: flex; flex-direction: column; gap: .3rem; }
label { font-size: .82rem; font-weight: 600; color: #555; }
input { font: 15px ui-monospace, monospace; padding: .45rem .7rem; border: 1.5px solid #b0bec5; border-radius: 8px; width: 100%; outline: none; transition: border-color .15s; }
input:focus { border-color: #1d3557; }
.btn-row { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1.5px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; transition: opacity .15s; }
button:hover { opacity: .85; }
button.ghost { background: #fff; color: #1d3557; }
.result-area { display: flex; gap: .6rem; margin-bottom: .5rem; }
.result-box { flex: 1; border: 1.5px solid #cdd9e3; border-radius: 10px; padding: .5rem .7rem; background: #f4f8fb; min-height: 52px; }
.result-box.ok { border-color: #0a7d33; background: #e8f7ee; }
.result-box.bad { border-color: #c92f3c; background: #fdecea; }
.box-label { display: block; font-size: .74rem; font-weight: 700; color: #6b7a8d; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .2rem; }
.box-value { font: 14px ui-monospace, monospace; word-break: break-all; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.examples-label { font-size: .8rem; font-weight: 600; color: #555; margin: .3rem 0 .4rem; }
.examples-row { display: flex; flex-wrap: wrap; gap: .4rem; }
.ex-btn { font: 13px ui-monospace, monospace; padding: .25rem .6rem; border: 1px solid #adb1b8; background: #e8eef3; color: #1d3557; border-radius: 6px; cursor: pointer; }
.ex-btn:hover { background: #cdd9e3; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Type soundness demo: tiny expression language with type checker + evaluator.
// Grammar
// Expr = Num | Bool | Add(Expr,Expr) | If(Expr,Expr,Expr)
// Types: "Int" | "Bool" | "TypeError"
// Tokenizer and recursive-descent parser
function tokenize(src) {
const tokens = [];
let i = 0;
while (i < src.length) {
if (/\s/.test(src[i])) { i++; continue; }
if (/\d/.test(src[i])) {
let n = '';
while (i < src.length && /\d/.test(src[i])) n += src[i++];
tokens.push({ t: 'num', v: parseInt(n, 10) });
continue;
}
if (src.slice(i, i + 4) === 'true') { tokens.push({ t: 'bool', v: true }); i += 4; continue; }
if (src.slice(i, i + 5) === 'false') { tokens.push({ t: 'bool', v: false }); i += 5; continue; }
if (src.slice(i, i + 2) === 'if') { tokens.push({ t: 'if' }); i += 2; continue; }
if (src.slice(i, i + 4) === 'then') { tokens.push({ t: 'then' }); i += 4; continue; }
if (src.slice(i, i + 4) === 'else') { tokens.push({ t: 'else' }); i += 4; continue; }
if (src[i] === '+') { tokens.push({ t: '+' }); i++; continue; }
if (src[i] === '(') { tokens.push({ t: '(' }); i++; continue; }
if (src[i] === ')') { tokens.push({ t: ')' }); i++; continue; }
throw new Error(i18nFmt('Unexpected character: '%ch%'', { ch: src[i] }));
}
return tokens;
}
// Parse a full expression
function parse(tokens) {
let pos = 0;
function peek() { return tokens[pos]; }
function consume(type) {
const tok = tokens[pos];
if (!tok || (type && tok.t !== type)) throw new Error(i18nFmt('Expected '%expected%' but got '%got%'', { expected: type || '?', got: tok ? tok.t : 'EOF' }));
pos++;
return tok;
}
function parseExpr() {
const left = parsePrimary();
if (peek() && peek().t === '+') { consume('+'); const right = parsePrimary(); return { kind: 'add', left, right }; }
return left;
}
function parsePrimary() {
const tok = peek();
if (!tok) throw new Error('Unexpected end of input');
if (tok.t === 'num') { consume('num'); return { kind: 'num', v: tok.v }; }
if (tok.t === 'bool') { consume('bool'); return { kind: 'bool', v: tok.v }; }
if (tok.t === 'if') {
consume('if'); const cond = parseExpr();
consume('then'); const thenE = parseExpr();
consume('else'); const elseE = parseExpr();
return { kind: 'if', cond, thenE, elseE };
}
if (tok.t === '(') { consume('('); const e = parseExpr(); consume(')'); return e; }
throw new Error(i18nFmt('Unexpected token: '%tok%'', { tok: tok.t }));
}
const result = parseExpr();
if (pos !== tokens.length) throw new Error('Unexpected extra input after expression');
return result;
}
// Type checker — enforces progress and preservation
// Progress: if typeOf(e) != TypeError, then e can step or is a value.
// Preservation: if typeOf(e) = T and e -> e', then typeOf(e') = T.
function typeOf(e) {
if (e.kind === 'num') return 'Int';
if (e.kind === 'bool') return 'Bool';
if (e.kind === 'add') {
// T-Add: both operands must be Int
if (typeOf(e.left) === 'Int' && typeOf(e.right) === 'Int') return 'Int';
return 'TypeError';
}
if (e.kind === 'if') {
const tc = typeOf(e.cond), tt = typeOf(e.thenE), te = typeOf(e.elseE);
// T-If: condition must be Bool, branches must share the same type
if (tc === 'Bool' && tt !== 'TypeError' && te !== 'TypeError' && tt === te) return tt;
return 'TypeError';
}
return 'TypeError';
}
// Evaluator — only called after type check passes
function evaluate(e) {
if (e.kind === 'num') return e.v;
if (e.kind === 'bool') return e.v;
if (e.kind === 'add') return evaluate(e.left) + evaluate(e.right);
if (e.kind === 'if') return evaluate(e.cond) ? evaluate(e.thenE) : evaluate(e.elseE);
throw new Error('Evaluator stuck — should not happen for a well-typed expression');
}
// UI wiring
const examples = [
{ expr: '1 + 2', label: '1 + 2' },
{ expr: 'true', label: 'true' },
{ expr: 'if true then 10 else 20', label: 'if true then 10 else 20' },
{ expr: 'if false then 1 else 2', label: 'if false then 1 else 2' },
{ expr: '(1 + 2) + 3', label: '(1+2)+3' },
{ expr: 'true + 1', label: 'true + 1' },
{ expr: 'if 1 then 2 else 3', label: 'if 1 then 2 else 3' },
{ expr: 'if true then 1 else false', label: 'if true then 1 else false' },
];
const exRow = document.getElementById('examples-row');
examples.forEach(({ expr, label }) => {
const b = document.createElement('button');
b.className = 'ex-btn'; b.textContent = label;
b.addEventListener('click', () => { document.getElementById('expr-input').value = expr; runCheck(); });
exRow.appendChild(b);
});
function setResult(typeStr, evalStr, typeOk, evalOk) {
const tb = document.getElementById('type-box'), to = document.getElementById('type-out');
const eb = document.getElementById('eval-box'), eo = document.getElementById('eval-out');
to.textContent = typeStr; tb.className = 'result-box ' + (typeOk ? 'ok' : 'bad');
eo.textContent = evalStr; eb.className = 'result-box ' + (evalOk ? 'ok' : 'bad');
}
function setStatus(msg, cls) {
const s = document.getElementById('status'); s.textContent = msg; s.className = 'status ' + cls;
}
function runCheck() {
const src = document.getElementById('expr-input').value.trim();
if (!src) { setResult('—', '—', false, false); setStatus('', ''); return; }
let ast;
try { ast = parse(tokenize(src)); }
catch (err) { setResult('?', '?', false, false); setStatus(i18nFmt('Parse error: %msg%', { msg: err.message }), 'bad'); return; }
const ty = typeOf(ast);
if (ty === 'TypeError') {
setResult('TypeError', 'blocked by type checker', false, false);
setStatus('Type error — evaluation blocked. The type system caught this before running.', 'bad');
return;
}
setResult(ty, '—', true, false);
setStatus(i18nFmt('Well-typed: %ty%. Press Evaluate to run safely.', { ty: ty }), 'ok');
}
function runEval() {
const src = document.getElementById('expr-input').value.trim();
if (!src) { setResult('—', '—', false, false); setStatus('', ''); return; }
let ast;
try { ast = parse(tokenize(src)); }
catch (err) { setResult('?', '?', false, false); setStatus(i18nFmt('Parse error: %msg%', { msg: err.message }), 'bad'); return; }
const ty = typeOf(ast);
if (ty === 'TypeError') {
setResult('TypeError', 'blocked by type checker', false, false);
setStatus('Type error — the type checker blocks evaluation. No runtime crash possible.', 'bad');
return;
}
// Progress theorem: typeOf(ast) != TypeError => evaluate never gets stuck
const val = evaluate(ast);
setResult(ty, String(val), true, true);
setStatus(i18nFmt('Evaluated to %val% (type: %ty%). Progress + preservation held every step.', { ty: ty, val: String(val) }), 'ok');
}
document.getElementById('btn-check').addEventListener('click', runCheck);
document.getElementById('btn-run').addEventListener('click', runEval);
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('expr-input').value = '';
setResult('—', '—', false, false); setStatus('', '');
});
<!-- Type Soundness demo — expression evaluator with type checker -->
<p class="hint">Type a small expression and press <b>Type-check</b>. If it passes, press <b>Evaluate</b>. A type error blocks evaluation — that is progress and preservation at work.</p>
<div class="editor-row">
<div class="expr-group">
<label for="expr-input">Expression</label>
<input id="expr-input" type="text" placeholder="e.g. 1 + 2 or if true then 10 else 20" spellcheck="false" autocomplete="off" />
</div>
<div class="btn-row">
<button id="btn-check" type="button">Type-check</button>
<button id="btn-run" type="button">Evaluate</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="result-area">
<div class="result-box" id="type-box">
<span class="box-label">Inferred type</span>
<span class="box-value" id="type-out">—</span>
</div>
<div class="result-box" id="eval-box">
<span class="box-label">Value</span>
<span class="box-value" id="eval-out">—</span>
</div>
</div>
<div class="status" id="status"></div>
<div class="examples-section">
<p class="examples-label">Quick examples (click to load)</p>
<div class="examples-row" id="examples-row"></div>
</div>
/* Styles for the type soundness demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .9rem; line-height: 1.5; }
.editor-row { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .7rem; }
.expr-group { display: flex; flex-direction: column; gap: .3rem; }
label { font-size: .82rem; font-weight: 600; color: #555; }
input { font: 15px ui-monospace, monospace; padding: .45rem .7rem; border: 1.5px solid #b0bec5; border-radius: 8px; width: 100%; outline: none; transition: border-color .15s; }
input:focus { border-color: #1d3557; }
.btn-row { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1.5px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; transition: opacity .15s; }
button:hover { opacity: .85; }
button.ghost { background: #fff; color: #1d3557; }
.result-area { display: flex; gap: .6rem; margin-bottom: .5rem; }
.result-box { flex: 1; border: 1.5px solid #cdd9e3; border-radius: 10px; padding: .5rem .7rem; background: #f4f8fb; min-height: 52px; }
.result-box.ok { border-color: #0a7d33; background: #e8f7ee; }
.result-box.bad { border-color: #c92f3c; background: #fdecea; }
.box-label { display: block; font-size: .74rem; font-weight: 700; color: #6b7a8d; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .2rem; }
.box-value { font: 14px ui-monospace, monospace; word-break: break-all; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.examples-label { font-size: .8rem; font-weight: 600; color: #555; margin: .3rem 0 .4rem; }
.examples-row { display: flex; flex-wrap: wrap; gap: .4rem; }
.ex-btn { font: 13px ui-monospace, monospace; padding: .25rem .6rem; border: 1px solid #adb1b8; background: #e8eef3; color: #1d3557; border-radius: 6px; cursor: pointer; }
.ex-btn:hover { background: #cdd9e3; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Type soundness demo: tiny expression language with type checker + evaluator.
// Grammar
// Expr = Num | Bool | Add(Expr,Expr) | If(Expr,Expr,Expr)
// Types: "Int" | "Bool" | "TypeError"
// Tokenizer and recursive-descent parser
function tokenize(src) {
const tokens = [];
let i = 0;
while (i < src.length) {
if (/\s/.test(src[i])) { i++; continue; }
if (/\d/.test(src[i])) {
let n = '';
while (i < src.length && /\d/.test(src[i])) n += src[i++];
tokens.push({ t: 'num', v: parseInt(n, 10) });
continue;
}
if (src.slice(i, i + 4) === 'true') { tokens.push({ t: 'bool', v: true }); i += 4; continue; }
if (src.slice(i, i + 5) === 'false') { tokens.push({ t: 'bool', v: false }); i += 5; continue; }
if (src.slice(i, i + 2) === 'if') { tokens.push({ t: 'if' }); i += 2; continue; }
if (src.slice(i, i + 4) === 'then') { tokens.push({ t: 'then' }); i += 4; continue; }
if (src.slice(i, i + 4) === 'else') { tokens.push({ t: 'else' }); i += 4; continue; }
if (src[i] === '+') { tokens.push({ t: '+' }); i++; continue; }
if (src[i] === '(') { tokens.push({ t: '(' }); i++; continue; }
if (src[i] === ')') { tokens.push({ t: ')' }); i++; continue; }
throw new Error(i18nFmt('Unexpected character: '%ch%'', { ch: src[i] }));
}
return tokens;
}
// Parse a full expression
function parse(tokens) {
let pos = 0;
function peek() { return tokens[pos]; }
function consume(type) {
const tok = tokens[pos];
if (!tok || (type && tok.t !== type)) throw new Error(i18nFmt('Expected '%expected%' but got '%got%'', { expected: type || '?', got: tok ? tok.t : 'EOF' }));
pos++;
return tok;
}
function parseExpr() {
const left = parsePrimary();
if (peek() && peek().t === '+') { consume('+'); const right = parsePrimary(); return { kind: 'add', left, right }; }
return left;
}
function parsePrimary() {
const tok = peek();
if (!tok) throw new Error('Unexpected end of input');
if (tok.t === 'num') { consume('num'); return { kind: 'num', v: tok.v }; }
if (tok.t === 'bool') { consume('bool'); return { kind: 'bool', v: tok.v }; }
if (tok.t === 'if') {
consume('if'); const cond = parseExpr();
consume('then'); const thenE = parseExpr();
consume('else'); const elseE = parseExpr();
return { kind: 'if', cond, thenE, elseE };
}
if (tok.t === '(') { consume('('); const e = parseExpr(); consume(')'); return e; }
throw new Error(i18nFmt('Unexpected token: '%tok%'', { tok: tok.t }));
}
const result = parseExpr();
if (pos !== tokens.length) throw new Error('Unexpected extra input after expression');
return result;
}
// Type checker — enforces progress and preservation
// Progress: if typeOf(e) != TypeError, then e can step or is a value.
// Preservation: if typeOf(e) = T and e -> e', then typeOf(e') = T.
function typeOf(e) {
if (e.kind === 'num') return 'Int';
if (e.kind === 'bool') return 'Bool';
if (e.kind === 'add') {
// T-Add: both operands must be Int
if (typeOf(e.left) === 'Int' && typeOf(e.right) === 'Int') return 'Int';
return 'TypeError';
}
if (e.kind === 'if') {
const tc = typeOf(e.cond), tt = typeOf(e.thenE), te = typeOf(e.elseE);
// T-If: condition must be Bool, branches must share the same type
if (tc === 'Bool' && tt !== 'TypeError' && te !== 'TypeError' && tt === te) return tt;
return 'TypeError';
}
return 'TypeError';
}
// Evaluator — only called after type check passes
function evaluate(e) {
if (e.kind === 'num') return e.v;
if (e.kind === 'bool') return e.v;
if (e.kind === 'add') return evaluate(e.left) + evaluate(e.right);
if (e.kind === 'if') return evaluate(e.cond) ? evaluate(e.thenE) : evaluate(e.elseE);
throw new Error('Evaluator stuck — should not happen for a well-typed expression');
}
// UI wiring
const examples = [
{ expr: '1 + 2', label: '1 + 2' },
{ expr: 'true', label: 'true' },
{ expr: 'if true then 10 else 20', label: 'if true then 10 else 20' },
{ expr: 'if false then 1 else 2', label: 'if false then 1 else 2' },
{ expr: '(1 + 2) + 3', label: '(1+2)+3' },
{ expr: 'true + 1', label: 'true + 1' },
{ expr: 'if 1 then 2 else 3', label: 'if 1 then 2 else 3' },
{ expr: 'if true then 1 else false', label: 'if true then 1 else false' },
];
const exRow = document.getElementById('examples-row');
examples.forEach(({ expr, label }) => {
const b = document.createElement('button');
b.className = 'ex-btn'; b.textContent = label;
b.addEventListener('click', () => { document.getElementById('expr-input').value = expr; runCheck(); });
exRow.appendChild(b);
});
function setResult(typeStr, evalStr, typeOk, evalOk) {
const tb = document.getElementById('type-box'), to = document.getElementById('type-out');
const eb = document.getElementById('eval-box'), eo = document.getElementById('eval-out');
to.textContent = typeStr; tb.className = 'result-box ' + (typeOk ? 'ok' : 'bad');
eo.textContent = evalStr; eb.className = 'result-box ' + (evalOk ? 'ok' : 'bad');
}
function setStatus(msg, cls) {
const s = document.getElementById('status'); s.textContent = msg; s.className = 'status ' + cls;
}
function runCheck() {
const src = document.getElementById('expr-input').value.trim();
if (!src) { setResult('—', '—', false, false); setStatus('', ''); return; }
let ast;
try { ast = parse(tokenize(src)); }
catch (err) { setResult('?', '?', false, false); setStatus(i18nFmt('Parse error: %msg%', { msg: err.message }), 'bad'); return; }
const ty = typeOf(ast);
if (ty === 'TypeError') {
setResult('TypeError', 'blocked by type checker', false, false);
setStatus('Type error — evaluation blocked. The type system caught this before running.', 'bad');
return;
}
setResult(ty, '—', true, false);
setStatus(i18nFmt('Well-typed: %ty%. Press Evaluate to run safely.', { ty: ty }), 'ok');
}
function runEval() {
const src = document.getElementById('expr-input').value.trim();
if (!src) { setResult('—', '—', false, false); setStatus('', ''); return; }
let ast;
try { ast = parse(tokenize(src)); }
catch (err) { setResult('?', '?', false, false); setStatus(i18nFmt('Parse error: %msg%', { msg: err.message }), 'bad'); return; }
const ty = typeOf(ast);
if (ty === 'TypeError') {
setResult('TypeError', 'blocked by type checker', false, false);
setStatus('Type error — the type checker blocks evaluation. No runtime crash possible.', 'bad');
return;
}
// Progress theorem: typeOf(ast) != TypeError => evaluate never gets stuck
const val = evaluate(ast);
setResult(ty, String(val), true, true);
setStatus(i18nFmt('Evaluated to %val% (type: %ty%). Progress + preservation held every step.', { ty: ty, val: String(val) }), 'ok');
}
document.getElementById('btn-check').addEventListener('click', runCheck);
document.getElementById('btn-run').addEventListener('click', runEval);
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('expr-input').value = '';
setResult('—', '—', false, false); setStatus('', '');
});
Comments
Loading comments...