Below are small loops. To prove one always stops, find a measure — a value built from the variables — that strictly drops on every iteration yet never goes negative. If you find one, the loop is certified to terminate. Pick a measure and press Run & test to watch it tick down.
<p class="hint">To prove a loop ends, find a <b>measure</b> — a value made from the variables — that strictly <b>decreases</b> every iteration but never goes below zero. Pick a loop, pick a candidate measure, then run it and watch the measure tick down.</p>
<div class="row">
<label>Loop:
<select id="loop">
<option value="countdown">A · countdown: while (n > 0) n = n - 1</option>
<option value="twovar">B · two vars: while (x > 0) { x = x - 1; y = y + 3 }</option>
<option value="collatz">C · Collatz: while (n > 1) n = n is even ? n/2 : 3n+1</option>
</select>
</label>
<label>Candidate measure:
<select id="measure"></select>
</label>
<label>Start value: <input id="start" type="number" min="1" max="200" value="7"></label>
</div>
<div class="btns">
<button id="run" type="button">Run & test</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div class="status" id="status">Choose a loop and a measure, then run.</div>
<div class="trace" id="trace"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.row { display: flex; flex-direction: column; gap: .6rem; margin-bottom: .7rem; }
label { font-size: .85rem; font-weight: 600; color: #1d3557; display: flex; flex-direction: column; gap: .25rem; }
select, input { font: 500 14px system-ui, sans-serif; padding: .4rem .5rem; border: 1px solid #cdd9e3;
border-radius: 8px; background: #fff; color: #222; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.trace { font: 13px ui-monospace, monospace; background: #f3f6f9; border: 1px solid #e0e8ef;
border-radius: 8px; padding: .5rem .7rem; max-height: 180px; overflow:auto; white-space: pre; }
.trace .down { color: #0a7d33; }
.trace .up { color: #c92f3c; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Termination analysis: a loop terminates if some "ranking function" (measure)
// built from the variables strictly DECREASES every iteration and stays >= 0
// (a well-founded order). Such a measure can only fall finitely often, so the
// loop must end. Deciding termination for EVERY program is the halting problem, proven undecidable by Turing (1936) — Collatz below has no known measure.
// Each loop: step(state) -> next state or null when the guard fails (loop ends).
const LOOPS = {
countdown: {
init: n => ({ n }),
step: s => s.n > 0 ? { n: s.n - 1 } : null,
measures: [
{ id: 'n', label: 'n', f: s => s.n },
{ id: 'neg', label: '-n (wrong direction)', f: s => -s.n },
{ id: 'const', label: '100 (constant)', f: s => 100 }
]
},
twovar: {
init: x => ({ x, y: 0 }),
step: s => s.x > 0 ? { x: s.x - 1, y: s.y + 3 } : null,
measures: [
{ id: 'x', label: 'x', f: s => s.x },
{ id: 'sum', label: 'x + y (y grows!)', f: s => s.x + s.y },
{ id: 'y', label: 'y', f: s => s.y }
]
},
collatz: {
init: n => ({ n }),
step: s => s.n > 1 ? { n: s.n % 2 === 0 ? s.n / 2 : 3 * s.n + 1 } : null,
measures: [
{ id: 'n', label: 'n (jumps up on odd n)', f: s => s.n },
{ id: 'log', label: 'floor(log2 n) (still jumps)', f: s => Math.floor(Math.log2(s.n)) },
{ id: 'none', label: '(no measure is known)', f: s => s.n }
]
}
};
const $ = id => document.getElementById(id);
function fillMeasures(){
const m = LOOPS[$('loop').value].measures;
$('measure').innerHTML = m.map(x => '<option value="'+x.id+'">'+x.label+'</option>').join('');
}
function setStatus(t, cls){ const s=$('status'); s.innerHTML=t; s.className='status '+cls; }
function run(){
const loopName = $('loop').value;
const L = LOOPS[loopName];
let start = Math.max(1, Math.min(200, parseInt($('start').value,10) || 1));
const measDef = L.measures.find(m => m.id === $('measure').value);
let s = L.init(start);
let prev = measDef.f(s);
const lines = [];
let iters = 0, decreasedEveryStep = true, wentNegative = false;
const CAP = 1000;
while (true) {
const cur = measDef.f(s);
if (cur < 0) wentNegative = true;
let tag = iters === 0 ? '' : (cur < prev ? 'down' : 'up');
const arrow = iters === 0 ? '' : (cur < prev ? ' ↓' : ' ↑ NOT smaller');
lines.push(fmt('<span class="%tag%">state %stringify% measure = %cur%%arrow%</span>', { tag: tag, stringify: JSON.stringify(s), cur: cur, arrow: arrow }));
if (iters > 0 && cur >= prev) decreasedEveryStep = false;
prev = cur;
const next = L.step(s);
if (next === null) { lines.push(fmt('loop guard false → terminated after %iters% iterations', { iters: iters })); break; }
s = next; iters++;
if (iters > CAP) { lines.push(fmt('… stopped tracing at %CAP% iterations', { CAP: CAP })); break; }
}
$('trace').innerHTML = lines.join('\n');
const proves = decreasedEveryStep && !wentNegative;
if (proves) {
setStatus('✓ The measure strictly decreased every step and stayed ≥ 0 — this is a valid ranking function. The loop is <b>proven to terminate</b> for every start.', 'ok');
console.log('Valid ranking function for', loopName, '- decreased every step, bounded below.');
} else if (loopName === 'collatz') {
setStatus('✗ This measure jumps up on some steps, so it proves nothing. <b>No decreasing measure for Collatz is known</b> — whether it always halts is an open problem, and termination in general is undecidable.', 'bad');
console.log('Collatz: chosen measure failed to decrease monotonically (expected).');
} else if (wentNegative) {
setStatus('✗ The measure went negative, so it is not bounded below — it cannot serve as a ranking function. Try another.', 'bad');
} else {
setStatus('✗ The measure did not strictly decrease on every step (it stayed equal or grew). It proves nothing here — try another.', 'bad');
}
}
$('loop').addEventListener('change', () => { fillMeasures(); $('trace').innerHTML=''; setStatus('Choose a measure, then run.',''); });
$('run').addEventListener('click', run);
$('reset').addEventListener('click', () => { $('trace').innerHTML=''; setStatus('Choose a loop and a measure, then run.',''); });
fillMeasures();
<p class="hint">To prove a loop ends, find a <b>measure</b> — a value made from the variables — that strictly <b>decreases</b> every iteration but never goes below zero. Pick a loop, pick a candidate measure, then run it and watch the measure tick down.</p>
<div class="row">
<label>Loop:
<select id="loop">
<option value="countdown">A · countdown: while (n > 0) n = n - 1</option>
<option value="twovar">B · two vars: while (x > 0) { x = x - 1; y = y + 3 }</option>
<option value="collatz">C · Collatz: while (n > 1) n = n is even ? n/2 : 3n+1</option>
</select>
</label>
<label>Candidate measure:
<select id="measure"></select>
</label>
<label>Start value: <input id="start" type="number" min="1" max="200" value="7"></label>
</div>
<div class="btns">
<button id="run" type="button">Run & test</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div class="status" id="status">Choose a loop and a measure, then run.</div>
<div class="trace" id="trace"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.row { display: flex; flex-direction: column; gap: .6rem; margin-bottom: .7rem; }
label { font-size: .85rem; font-weight: 600; color: #1d3557; display: flex; flex-direction: column; gap: .25rem; }
select, input { font: 500 14px system-ui, sans-serif; padding: .4rem .5rem; border: 1px solid #cdd9e3;
border-radius: 8px; background: #fff; color: #222; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.trace { font: 13px ui-monospace, monospace; background: #f3f6f9; border: 1px solid #e0e8ef;
border-radius: 8px; padding: .5rem .7rem; max-height: 180px; overflow:auto; white-space: pre; }
.trace .down { color: #0a7d33; }
.trace .up { color: #c92f3c; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Termination analysis: a loop terminates if some "ranking function" (measure)
// built from the variables strictly DECREASES every iteration and stays >= 0
// (a well-founded order). Such a measure can only fall finitely often, so the
// loop must end. Deciding termination for EVERY program is the halting problem, proven undecidable by Turing (1936) — Collatz below has no known measure.
// Each loop: step(state) -> next state or null when the guard fails (loop ends).
const LOOPS = {
countdown: {
init: n => ({ n }),
step: s => s.n > 0 ? { n: s.n - 1 } : null,
measures: [
{ id: 'n', label: 'n', f: s => s.n },
{ id: 'neg', label: '-n (wrong direction)', f: s => -s.n },
{ id: 'const', label: '100 (constant)', f: s => 100 }
]
},
twovar: {
init: x => ({ x, y: 0 }),
step: s => s.x > 0 ? { x: s.x - 1, y: s.y + 3 } : null,
measures: [
{ id: 'x', label: 'x', f: s => s.x },
{ id: 'sum', label: 'x + y (y grows!)', f: s => s.x + s.y },
{ id: 'y', label: 'y', f: s => s.y }
]
},
collatz: {
init: n => ({ n }),
step: s => s.n > 1 ? { n: s.n % 2 === 0 ? s.n / 2 : 3 * s.n + 1 } : null,
measures: [
{ id: 'n', label: 'n (jumps up on odd n)', f: s => s.n },
{ id: 'log', label: 'floor(log2 n) (still jumps)', f: s => Math.floor(Math.log2(s.n)) },
{ id: 'none', label: '(no measure is known)', f: s => s.n }
]
}
};
const $ = id => document.getElementById(id);
function fillMeasures(){
const m = LOOPS[$('loop').value].measures;
$('measure').innerHTML = m.map(x => '<option value="'+x.id+'">'+x.label+'</option>').join('');
}
function setStatus(t, cls){ const s=$('status'); s.innerHTML=t; s.className='status '+cls; }
function run(){
const loopName = $('loop').value;
const L = LOOPS[loopName];
let start = Math.max(1, Math.min(200, parseInt($('start').value,10) || 1));
const measDef = L.measures.find(m => m.id === $('measure').value);
let s = L.init(start);
let prev = measDef.f(s);
const lines = [];
let iters = 0, decreasedEveryStep = true, wentNegative = false;
const CAP = 1000;
while (true) {
const cur = measDef.f(s);
if (cur < 0) wentNegative = true;
let tag = iters === 0 ? '' : (cur < prev ? 'down' : 'up');
const arrow = iters === 0 ? '' : (cur < prev ? ' ↓' : ' ↑ NOT smaller');
lines.push(fmt('<span class="%tag%">state %stringify% measure = %cur%%arrow%</span>', { tag: tag, stringify: JSON.stringify(s), cur: cur, arrow: arrow }));
if (iters > 0 && cur >= prev) decreasedEveryStep = false;
prev = cur;
const next = L.step(s);
if (next === null) { lines.push(fmt('loop guard false → terminated after %iters% iterations', { iters: iters })); break; }
s = next; iters++;
if (iters > CAP) { lines.push(fmt('… stopped tracing at %CAP% iterations', { CAP: CAP })); break; }
}
$('trace').innerHTML = lines.join('\n');
const proves = decreasedEveryStep && !wentNegative;
if (proves) {
setStatus('✓ The measure strictly decreased every step and stayed ≥ 0 — this is a valid ranking function. The loop is <b>proven to terminate</b> for every start.', 'ok');
console.log('Valid ranking function for', loopName, '- decreased every step, bounded below.');
} else if (loopName === 'collatz') {
setStatus('✗ This measure jumps up on some steps, so it proves nothing. <b>No decreasing measure for Collatz is known</b> — whether it always halts is an open problem, and termination in general is undecidable.', 'bad');
console.log('Collatz: chosen measure failed to decrease monotonically (expected).');
} else if (wentNegative) {
setStatus('✗ The measure went negative, so it is not bounded below — it cannot serve as a ranking function. Try another.', 'bad');
} else {
setStatus('✗ The measure did not strictly decrease on every step (it stayed equal or grew). It proves nothing here — try another.', 'bad');
}
}
$('loop').addEventListener('change', () => { fillMeasures(); $('trace').innerHTML=''; setStatus('Choose a measure, then run.',''); });
$('run').addEventListener('click', run);
$('reset').addEventListener('click', () => { $('trace').innerHTML=''; setStatus('Choose a loop and a measure, then run.',''); });
fillMeasures();
Comments
Loading comments...