Below is a miniature linear-type checker. Each token is a linear resource that must be consumed exactly once. Click the actions to build a small program, then run the checker to see whether every resource is used exactly once.
<!-- <!-- Linear-type checker demo --> -->
<p class="hint">Each token is a <b>linear resource</b> — it must be used exactly once. Add tokens, build a program with use/move/drop operations, then run the checker. It catches leaks (unused), double-use, and use-after-move.</p>
<div class="workspace">
<div class="panel">
<div class="panel-title">Resources</div>
<div id="resource-list" class="resource-list"></div>
<button id="btn-add" type="button">+ New token</button>
</div>
<div class="panel">
<div class="panel-title">Program</div>
<div id="stmt-list" class="stmt-list"></div>
<div class="action-row">
<select id="sel-action">
<option value="use">use (read)</option>
<option value="move">move (transfer)</option>
<option value="drop">drop (free)</option>
</select>
<select id="sel-token"></select>
<button id="btn-add-stmt" type="button">Add step</button>
</div>
</div>
</div>
<div class="check-row">
<button id="btn-check" type="button">Check types</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="result" class="result"></div>
/* Linear-type checker styles */
* { 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; }
.workspace { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; margin-bottom: .7rem; }
.panel { border: 1px solid #cdd9e3; border-radius: 8px; padding: .6rem .7rem; background: #f7f9fb; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
color: #5a7088; margin-bottom: .45rem; }
.resource-list, .stmt-list { min-height: 48px; margin-bottom: .45rem; display: flex;
flex-direction: column; gap: .25rem; }
.token-chip { display: inline-flex; align-items: center; gap: .3rem; padding: .2rem .5rem;
border-radius: 5px; font: 600 .82rem ui-monospace, monospace; background: #dce8f5;
color: #1d3557; border: 1px solid #b0c9e3; }
.token-chip.consumed { background: #fde8e8; color: #b91c1c; border-color: #f5b4b4; text-decoration: line-through; }
.token-chip.moved { background: #e8f3e8; color: #166534; border-color: #a7d3a9; }
.stmt-chip { font: .82rem ui-monospace, monospace; padding: .18rem .45rem; border-radius: 5px;
background: #eef1f5; border: 1px solid #d0d8e3; color: #374151; }
.action-row { display: flex; gap: .35rem; flex-wrap: wrap; }
select { font: .82rem system-ui, sans-serif; padding: .3rem .4rem; border: 1px solid #b0c9e3;
border-radius: 6px; background: #fff; }
.check-row { display: flex; gap: .5rem; margin-bottom: .5rem; }
button { font: 600 .82rem system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.small { font-size: .75rem; padding: .22rem .5rem; }
.result { font-size: .9rem; font-weight: 600; min-height: 1.5em; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.result.warn { color: #b45309; }
@media (max-width: 420px) { .workspace { grid-template-columns: 1fr; } }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Tiny linear-type checker: resources have three states.
// States: fresh (allocated, not yet used), consumed (use/drop applied), moved (ownership transferred).
// Ops: create = allocate; use = read (consumes); drop = free (consumes); move = transfer ownership (marks as moved).
let tokenCounter = 0;
// name -> state ('fresh' | 'consumed' | 'moved')
const tokens = {};
// ordered list of program steps
const stmts = [];
function freshName() {
// Generate short single-letter names, then a+1, b+1 etc.
const letters = 'abcdefghijklmnopqrstuvwxyz';
return letters[tokenCounter++ % 26] + (tokenCounter > 26 ? String(Math.floor((tokenCounter - 1) / 26)) : '');
}
function renderResources() {
// Render token chips and rebuild the token selector.
const list = document.getElementById('resource-list');
list.innerHTML = '';
const sel = document.getElementById('sel-token');
sel.innerHTML = '';
for (const [name, state] of Object.entries(tokens)) {
const chip = document.createElement('div');
chip.className = 'token-chip' + (state === 'consumed' ? ' consumed' : state === 'moved' ? ' moved' : '');
chip.textContent = name + ' : T';
list.appendChild(chip);
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
sel.appendChild(opt);
}
if (!Object.keys(tokens).length) {
const placeholder = document.createElement('div');
placeholder.style.color = '#9aabba';
placeholder.style.fontSize = '.8rem';
placeholder.textContent = 'No tokens yet.';
list.appendChild(placeholder);
}
}
function renderStmts() {
// Render the statement list.
const list = document.getElementById('stmt-list');
list.innerHTML = '';
if (!stmts.length) {
const placeholder = document.createElement('div');
placeholder.style.color = '#9aabba';
placeholder.style.fontSize = '.8rem';
placeholder.textContent = 'No steps yet.';
list.appendChild(placeholder);
return;
}
stmts.forEach((s) => {
const chip = document.createElement('div');
chip.className = 'stmt-chip';
chip.textContent = s.label;
list.appendChild(chip);
});
}
document.getElementById('btn-add').addEventListener('click', () => {
// Allocate a new fresh token and record a create step.
const name = freshName();
tokens[name] = 'fresh';
stmts.push({ op: 'create', token: name, label: 'let ' + name + ' = alloc()' });
renderResources();
renderStmts();
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
document.getElementById('btn-add-stmt').addEventListener('click', () => {
// Append a use/move/drop step for the selected token.
const action = document.getElementById('sel-action').value;
const name = document.getElementById('sel-token').value;
if (!name) return;
if (action === 'use') {
stmts.push({ op: 'use', token: name, label: 'read(' + name + ')' });
} else if (action === 'move') {
stmts.push({ op: 'move', token: name, label: 'let b = move(' + name + ')' });
} else if (action === 'drop') {
stmts.push({ op: 'drop', token: name, label: 'drop(' + name + ')' });
}
renderStmts();
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
document.getElementById('btn-check').addEventListener('click', () => {
// Re-simulate from the start and collect type errors.
// Replay all statements to find type violations.
const live = {};
for (const [n] of Object.entries(tokens)) live[n] = 'fresh';
const errors = [];
for (const s of stmts) {
if (s.op === 'create') {
// Reset state on re-run (handles reset + re-check).
live[s.token] = 'fresh';
} else if (s.op === 'use' || s.op === 'move' || s.op === 'drop') {
const state = live[s.token];
if (state === 'consumed') {
errors.push(i18nFmt('✗ Error: '%name%' already consumed — double-use via %op%.', { name: s.token, op: s.op }));
} else if (state === 'moved') {
errors.push(i18nFmt('✗ Error: '%name%' was moved — cannot %op% an invalidated value.', { name: s.token, op: s.op }));
} else {
// Mark as consumed or moved.
live[s.token] = s.op === 'move' ? 'moved' : 'consumed';
}
}
}
// Any still-fresh token at the end is a leak.
for (const [n, state] of Object.entries(live)) {
if (state === 'fresh') {
errors.push(i18nFmt('✗ Error: '%name%' was never consumed — resource leak.', { name: n }));
}
}
const result = document.getElementById('result');
if (!errors.length && Object.keys(live).length > 0) {
result.textContent = '✓ All resources used exactly once — linear types satisfied!';
result.className = 'result ok';
} else if (!errors.length) {
result.textContent = 'Add at least one token to check.';
result.className = 'result warn';
} else {
result.textContent = errors[0];
result.className = 'result bad';
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
// Clear all tokens and statements.
tokenCounter = 0;
for (const k of Object.keys(tokens)) delete tokens[k];
stmts.length = 0;
renderResources();
renderStmts();
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
renderResources();
renderStmts();
<!-- <!-- Linear-type checker demo --> -->
<p class="hint">Each token is a <b>linear resource</b> — it must be used exactly once. Add tokens, build a program with use/move/drop operations, then run the checker. It catches leaks (unused), double-use, and use-after-move.</p>
<div class="workspace">
<div class="panel">
<div class="panel-title">Resources</div>
<div id="resource-list" class="resource-list"></div>
<button id="btn-add" type="button">+ New token</button>
</div>
<div class="panel">
<div class="panel-title">Program</div>
<div id="stmt-list" class="stmt-list"></div>
<div class="action-row">
<select id="sel-action">
<option value="use">use (read)</option>
<option value="move">move (transfer)</option>
<option value="drop">drop (free)</option>
</select>
<select id="sel-token"></select>
<button id="btn-add-stmt" type="button">Add step</button>
</div>
</div>
</div>
<div class="check-row">
<button id="btn-check" type="button">Check types</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="result" class="result"></div>
/* Linear-type checker styles */
* { 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; }
.workspace { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; margin-bottom: .7rem; }
.panel { border: 1px solid #cdd9e3; border-radius: 8px; padding: .6rem .7rem; background: #f7f9fb; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
color: #5a7088; margin-bottom: .45rem; }
.resource-list, .stmt-list { min-height: 48px; margin-bottom: .45rem; display: flex;
flex-direction: column; gap: .25rem; }
.token-chip { display: inline-flex; align-items: center; gap: .3rem; padding: .2rem .5rem;
border-radius: 5px; font: 600 .82rem ui-monospace, monospace; background: #dce8f5;
color: #1d3557; border: 1px solid #b0c9e3; }
.token-chip.consumed { background: #fde8e8; color: #b91c1c; border-color: #f5b4b4; text-decoration: line-through; }
.token-chip.moved { background: #e8f3e8; color: #166534; border-color: #a7d3a9; }
.stmt-chip { font: .82rem ui-monospace, monospace; padding: .18rem .45rem; border-radius: 5px;
background: #eef1f5; border: 1px solid #d0d8e3; color: #374151; }
.action-row { display: flex; gap: .35rem; flex-wrap: wrap; }
select { font: .82rem system-ui, sans-serif; padding: .3rem .4rem; border: 1px solid #b0c9e3;
border-radius: 6px; background: #fff; }
.check-row { display: flex; gap: .5rem; margin-bottom: .5rem; }
button { font: 600 .82rem system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.small { font-size: .75rem; padding: .22rem .5rem; }
.result { font-size: .9rem; font-weight: 600; min-height: 1.5em; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.result.warn { color: #b45309; }
@media (max-width: 420px) { .workspace { grid-template-columns: 1fr; } }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Tiny linear-type checker: resources have three states.
// States: fresh (allocated, not yet used), consumed (use/drop applied), moved (ownership transferred).
// Ops: create = allocate; use = read (consumes); drop = free (consumes); move = transfer ownership (marks as moved).
let tokenCounter = 0;
// name -> state ('fresh' | 'consumed' | 'moved')
const tokens = {};
// ordered list of program steps
const stmts = [];
function freshName() {
// Generate short single-letter names, then a+1, b+1 etc.
const letters = 'abcdefghijklmnopqrstuvwxyz';
return letters[tokenCounter++ % 26] + (tokenCounter > 26 ? String(Math.floor((tokenCounter - 1) / 26)) : '');
}
function renderResources() {
// Render token chips and rebuild the token selector.
const list = document.getElementById('resource-list');
list.innerHTML = '';
const sel = document.getElementById('sel-token');
sel.innerHTML = '';
for (const [name, state] of Object.entries(tokens)) {
const chip = document.createElement('div');
chip.className = 'token-chip' + (state === 'consumed' ? ' consumed' : state === 'moved' ? ' moved' : '');
chip.textContent = name + ' : T';
list.appendChild(chip);
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
sel.appendChild(opt);
}
if (!Object.keys(tokens).length) {
const placeholder = document.createElement('div');
placeholder.style.color = '#9aabba';
placeholder.style.fontSize = '.8rem';
placeholder.textContent = 'No tokens yet.';
list.appendChild(placeholder);
}
}
function renderStmts() {
// Render the statement list.
const list = document.getElementById('stmt-list');
list.innerHTML = '';
if (!stmts.length) {
const placeholder = document.createElement('div');
placeholder.style.color = '#9aabba';
placeholder.style.fontSize = '.8rem';
placeholder.textContent = 'No steps yet.';
list.appendChild(placeholder);
return;
}
stmts.forEach((s) => {
const chip = document.createElement('div');
chip.className = 'stmt-chip';
chip.textContent = s.label;
list.appendChild(chip);
});
}
document.getElementById('btn-add').addEventListener('click', () => {
// Allocate a new fresh token and record a create step.
const name = freshName();
tokens[name] = 'fresh';
stmts.push({ op: 'create', token: name, label: 'let ' + name + ' = alloc()' });
renderResources();
renderStmts();
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
document.getElementById('btn-add-stmt').addEventListener('click', () => {
// Append a use/move/drop step for the selected token.
const action = document.getElementById('sel-action').value;
const name = document.getElementById('sel-token').value;
if (!name) return;
if (action === 'use') {
stmts.push({ op: 'use', token: name, label: 'read(' + name + ')' });
} else if (action === 'move') {
stmts.push({ op: 'move', token: name, label: 'let b = move(' + name + ')' });
} else if (action === 'drop') {
stmts.push({ op: 'drop', token: name, label: 'drop(' + name + ')' });
}
renderStmts();
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
document.getElementById('btn-check').addEventListener('click', () => {
// Re-simulate from the start and collect type errors.
// Replay all statements to find type violations.
const live = {};
for (const [n] of Object.entries(tokens)) live[n] = 'fresh';
const errors = [];
for (const s of stmts) {
if (s.op === 'create') {
// Reset state on re-run (handles reset + re-check).
live[s.token] = 'fresh';
} else if (s.op === 'use' || s.op === 'move' || s.op === 'drop') {
const state = live[s.token];
if (state === 'consumed') {
errors.push(i18nFmt('✗ Error: '%name%' already consumed — double-use via %op%.', { name: s.token, op: s.op }));
} else if (state === 'moved') {
errors.push(i18nFmt('✗ Error: '%name%' was moved — cannot %op% an invalidated value.', { name: s.token, op: s.op }));
} else {
// Mark as consumed or moved.
live[s.token] = s.op === 'move' ? 'moved' : 'consumed';
}
}
}
// Any still-fresh token at the end is a leak.
for (const [n, state] of Object.entries(live)) {
if (state === 'fresh') {
errors.push(i18nFmt('✗ Error: '%name%' was never consumed — resource leak.', { name: n }));
}
}
const result = document.getElementById('result');
if (!errors.length && Object.keys(live).length > 0) {
result.textContent = '✓ All resources used exactly once — linear types satisfied!';
result.className = 'result ok';
} else if (!errors.length) {
result.textContent = 'Add at least one token to check.';
result.className = 'result warn';
} else {
result.textContent = errors[0];
result.className = 'result bad';
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
// Clear all tokens and statements.
tokenCounter = 0;
for (const k of Object.keys(tokens)) delete tokens[k];
stmts.length = 0;
renderResources();
renderStmts();
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
renderResources();
renderStmts();
Notice how the checker distinguishes three error modes: unused (a leaked resource), double-use (a freed-then-reused resource), and use-after-consume (borrowing a value that was already moved). Every real-world memory safety bug falls into one of these three categories — and a linear type checker rules out all of them statically.
Comments
Loading comments...