The classic example is summing 1 + 2 + … + n. Here is the loop in pseudocode:
{ n ≥ 0 }
i := 0; s := 0
while i < n do
i := i + 1
s := s + i
{ s = n*(n+1)/2 }
The loop invariant s = i*(i+1)/2 AND i ≤ n is the key: it holds before the first iteration (when i = 0, s = 0) and is preserved by every step. When the loop exits (i = n), the invariant plus the exit condition give you the post-condition.
<div class="layout">
<div class="left-col">
<div class="pseudo">
<div class="pseudo-line pre" id="line-pre"><span class="brace">{</span> n ≥ 0 <span class="brace">}</span></div>
<div class="pseudo-line" id="line-init">i := 0 s := 0</div>
<div class="pseudo-line loop-kw" id="line-while"><b>while</b> i < n <b>do</b></div>
<div class="pseudo-line indent" id="line-i"> i := i + 1</div>
<div class="pseudo-line indent" id="line-s"> s := s + i</div>
<div class="pseudo-line post" id="line-post"><span class="brace">{</span> s = n·(n+1)/2 <span class="brace">}</span></div>
</div>
<div class="controls">
<label>n = <input id="nval" type="number" min="0" max="10" value="4" style="width:3.5rem"></label>
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="right-col">
<div class="panel">
<div class="panel-title">Current state</div>
<table id="stateTable">
<tr><td>n</td><td id="sv-n">4</td></tr>
<tr><td>i</td><td id="sv-i">0</td></tr>
<tr><td>s</td><td id="sv-s">0</td></tr>
</table>
<div class="inv-box" id="invBox">
<div class="inv-label">Loop invariant</div>
<div class="inv-formula">s = i·(i+1)/2 & i ≤ n</div>
<div class="inv-status" id="invStatus">—</div>
</div>
<div class="post-box" id="postBox" style="display:none">
<div class="inv-label">Post-condition</div>
<div class="inv-formula">s = n·(n+1)/2</div>
<div class="inv-status" id="postStatus">—</div>
</div>
<div class="msg" id="msg"></div>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.layout { display: flex; gap: 1rem; align-items: flex-start; flex-wrap: wrap; }
.left-col { flex: 1 1 260px; }
.right-col { flex: 1 1 220px; }
.pseudo { background: #1e2430; color: #cdd6f4; border-radius: 10px; padding: .8rem 1rem;
font: 13px/1.7 ui-monospace, monospace; margin-bottom: .8rem; }
.pseudo-line { border-radius: 5px; padding: 0 4px; transition: background .2s; }
.pseudo-line.active { background: #3a4459; }
.brace { color: #89b4fa; font-weight: 700; }
.pre, .post { color: #a6e3a1; }
.loop-kw { color: #cba6f7; }
.indent { color: #f5c2e7; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-top: .3rem; }
label { font-size: .9rem; }
input[type=number] { border: 1px solid #aaa; border-radius: 6px; padding: 2px 4px; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.panel { background: #f4f6f9; border: 1px solid #dde3ea; border-radius: 10px; padding: .8rem 1rem; }
.panel-title { font-weight: 700; font-size: .85rem; color: #555; margin-bottom: .5rem; }
#stateTable td { padding: 2px 8px; font: 14px ui-monospace, monospace; }
#stateTable td:first-child { color: #555; }
.inv-box, .post-box { margin-top: .7rem; background: #fff; border: 1px solid #dde3ea;
border-radius: 8px; padding: .5rem .7rem; }
.inv-label { font-size: .75rem; color: #888; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.inv-formula { font: 13px ui-monospace, monospace; color: #333; margin: 2px 0; }
.inv-status { font-size: .85rem; font-weight: 700; min-height: 1.2em; }
.inv-status.ok { color: #0a7d33; }
.inv-status.bad { color: #c92f3c; }
.inv-status.neutral { color: #888; }
.msg { margin-top: .6rem; font-size: .85rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hoare triple demo: verify the sum-to-n loop step by step.
// Triple: { n>=0 } i:=0; s:=0; while i<n do { i:=i+1; s:=s+i } { s=n*(n+1)/2 }
// Loop invariant: s = i*(i+1)/2 AND i <= n
//
// Steps:
// 0: highlight pre-condition (trivially satisfied for n>=0)
// 1: init i=0, s=0
// 2...: each loop body execution
// final: exit condition + post-condition check
let n, i, s, phase; // phase: 'pre'|'init'|'body'|'done'
function getN() {
const v = parseInt(document.getElementById('nval').value, 10);
return isNaN(v) || v < 0 ? 0 : Math.min(v, 10);
}
function reset() {
n = getN();
i = 0; s = 0; phase = 'pre';
clearHighlights();
highlight('line-pre');
updateState();
setMsg('Pre-condition: n ≥ 0. Ready to initialize.');
document.getElementById('postBox').style.display = 'none';
setInv('neutral', '— (before loop starts)');
setPost('neutral', '—');
document.getElementById('btnStep').disabled = false;
document.getElementById('btnRun').disabled = false;
}
function highlight(id) {
document.querySelectorAll('.pseudo-line').forEach(el => el.classList.remove('active'));
const el = document.getElementById(id);
if (el) el.classList.add('active');
}
function clearHighlights() {
document.querySelectorAll('.pseudo-line').forEach(el => el.classList.remove('active'));
}
function updateState() {
document.getElementById('sv-n').textContent = n;
document.getElementById('sv-i').textContent = i;
document.getElementById('sv-s').textContent = s;
}
function invariantHolds() {
return s === i * (i + 1) / 2 && i <= n;
}
function setInv(cls, msg) {
const el = document.getElementById('invStatus');
el.className = 'inv-status ' + cls;
el.textContent = msg;
}
function setPost(cls, msg) {
const el = document.getElementById('postStatus');
el.className = 'inv-status ' + cls;
el.textContent = msg;
}
function setMsg(txt) {
document.getElementById('msg').textContent = txt;
}
function step() {
if (phase === 'pre') {
// init
i = 0; s = 0; phase = 'init';
highlight('line-init');
updateState();
setInv('ok', '✔ s=0=0⋅0⋅1/2, i=0≤0≤n');
setMsg('Initialized i=0, s=0. Invariant holds.');
return;
}
if (phase === 'init') {
// decide: enter loop or skip
if (i < n) {
phase = 'body';
highlight('line-while');
setMsg(fmt('Condition i=%i% < n=%n% is TRUE → entering loop body.', { i: i, n: n }));
} else {
phase = 'done';
finish();
}
return;
}
if (phase === 'body') {
// execute one body iteration
i = i + 1;
highlight('line-i');
updateState();
setMsg(fmt('i := i + 1 → i = %i%', { i: i }));
// s update will be next sub-step; simplify: do both at once
s = s + i;
highlight('line-s');
updateState();
const ok = invariantHolds();
if (ok) {
setInv('ok', '✔ s=' + s + '=' + i + '⋅' + (i+1) + '/2=' + (i*(i+1)/2));
} else {
setInv('bad', '✘ invariant violated!');
}
setMsg(fmt('After iteration %i%: i=%i%, s=%s%. Invariant: %v%', { i: i, s: s, v: ok?'OK':'BROKEN' }));
// check loop guard again
if (i < n) {
highlight('line-while');
} else {
phase = 'done';
finish();
}
return;
}
}
function finish() {
highlight('line-post');
document.getElementById('postBox').style.display = 'block';
const expected = n * (n + 1) / 2;
const ok = s === expected;
setPost(ok ? 'ok' : 'bad',
ok ? '✔ s=' + s + '=n⋅(n+1)/2' : '✘ s=' + s + '≠' + expected);
setMsg(ok
? fmt('Loop exited with i=n=%n%. Post-condition holds! s=%s%=%n%⋅%n%%v%/2', { n: n, s: s, v: 1 })
: 'ERROR: post-condition violated (should never happen for correct n≥0).');
document.getElementById('btnStep').disabled = true;
document.getElementById('btnRun').disabled = true;
}
function runAll() {
document.getElementById('btnStep').disabled = true;
document.getElementById('btnRun').disabled = true;
let ticks = 0;
const maxTicks = 100;
function tick() {
if (phase === 'done' || ticks > maxTicks) return;
step();
ticks++;
if (phase !== 'done') setTimeout(tick, 180);
}
tick();
}
document.getElementById('btnStep').addEventListener('click', step);
document.getElementById('btnRun').addEventListener('click', runAll);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('nval').addEventListener('change', reset);
reset();
// Self-test: verify invariant holds throughout for n=0..6
(function selfTest() {
for (let testN = 0; testN <= 6; testN++) {
let ii = 0, ss = 0;
// invariant before loop
console.assert(ss === ii*(ii+1)/2 && ii <= testN, 'inv pre n=' + testN);
while (ii < testN) {
ii++; ss += ii;
console.assert(ss === ii*(ii+1)/2 && ii <= testN, 'inv body n=' + testN + ' i=' + ii);
}
console.assert(ss === testN*(testN+1)/2, 'post n=' + testN);
}
console.log('Self-test: invariant and post-condition verified for n=0..6');
})();
<div class="layout">
<div class="left-col">
<div class="pseudo">
<div class="pseudo-line pre" id="line-pre"><span class="brace">{</span> n ≥ 0 <span class="brace">}</span></div>
<div class="pseudo-line" id="line-init">i := 0 s := 0</div>
<div class="pseudo-line loop-kw" id="line-while"><b>while</b> i < n <b>do</b></div>
<div class="pseudo-line indent" id="line-i"> i := i + 1</div>
<div class="pseudo-line indent" id="line-s"> s := s + i</div>
<div class="pseudo-line post" id="line-post"><span class="brace">{</span> s = n·(n+1)/2 <span class="brace">}</span></div>
</div>
<div class="controls">
<label>n = <input id="nval" type="number" min="0" max="10" value="4" style="width:3.5rem"></label>
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="right-col">
<div class="panel">
<div class="panel-title">Current state</div>
<table id="stateTable">
<tr><td>n</td><td id="sv-n">4</td></tr>
<tr><td>i</td><td id="sv-i">0</td></tr>
<tr><td>s</td><td id="sv-s">0</td></tr>
</table>
<div class="inv-box" id="invBox">
<div class="inv-label">Loop invariant</div>
<div class="inv-formula">s = i·(i+1)/2 & i ≤ n</div>
<div class="inv-status" id="invStatus">—</div>
</div>
<div class="post-box" id="postBox" style="display:none">
<div class="inv-label">Post-condition</div>
<div class="inv-formula">s = n·(n+1)/2</div>
<div class="inv-status" id="postStatus">—</div>
</div>
<div class="msg" id="msg"></div>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.layout { display: flex; gap: 1rem; align-items: flex-start; flex-wrap: wrap; }
.left-col { flex: 1 1 260px; }
.right-col { flex: 1 1 220px; }
.pseudo { background: #1e2430; color: #cdd6f4; border-radius: 10px; padding: .8rem 1rem;
font: 13px/1.7 ui-monospace, monospace; margin-bottom: .8rem; }
.pseudo-line { border-radius: 5px; padding: 0 4px; transition: background .2s; }
.pseudo-line.active { background: #3a4459; }
.brace { color: #89b4fa; font-weight: 700; }
.pre, .post { color: #a6e3a1; }
.loop-kw { color: #cba6f7; }
.indent { color: #f5c2e7; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-top: .3rem; }
label { font-size: .9rem; }
input[type=number] { border: 1px solid #aaa; border-radius: 6px; padding: 2px 4px; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.panel { background: #f4f6f9; border: 1px solid #dde3ea; border-radius: 10px; padding: .8rem 1rem; }
.panel-title { font-weight: 700; font-size: .85rem; color: #555; margin-bottom: .5rem; }
#stateTable td { padding: 2px 8px; font: 14px ui-monospace, monospace; }
#stateTable td:first-child { color: #555; }
.inv-box, .post-box { margin-top: .7rem; background: #fff; border: 1px solid #dde3ea;
border-radius: 8px; padding: .5rem .7rem; }
.inv-label { font-size: .75rem; color: #888; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.inv-formula { font: 13px ui-monospace, monospace; color: #333; margin: 2px 0; }
.inv-status { font-size: .85rem; font-weight: 700; min-height: 1.2em; }
.inv-status.ok { color: #0a7d33; }
.inv-status.bad { color: #c92f3c; }
.inv-status.neutral { color: #888; }
.msg { margin-top: .6rem; font-size: .85rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hoare triple demo: verify the sum-to-n loop step by step.
// Triple: { n>=0 } i:=0; s:=0; while i<n do { i:=i+1; s:=s+i } { s=n*(n+1)/2 }
// Loop invariant: s = i*(i+1)/2 AND i <= n
//
// Steps:
// 0: highlight pre-condition (trivially satisfied for n>=0)
// 1: init i=0, s=0
// 2...: each loop body execution
// final: exit condition + post-condition check
let n, i, s, phase; // phase: 'pre'|'init'|'body'|'done'
function getN() {
const v = parseInt(document.getElementById('nval').value, 10);
return isNaN(v) || v < 0 ? 0 : Math.min(v, 10);
}
function reset() {
n = getN();
i = 0; s = 0; phase = 'pre';
clearHighlights();
highlight('line-pre');
updateState();
setMsg('Pre-condition: n ≥ 0. Ready to initialize.');
document.getElementById('postBox').style.display = 'none';
setInv('neutral', '— (before loop starts)');
setPost('neutral', '—');
document.getElementById('btnStep').disabled = false;
document.getElementById('btnRun').disabled = false;
}
function highlight(id) {
document.querySelectorAll('.pseudo-line').forEach(el => el.classList.remove('active'));
const el = document.getElementById(id);
if (el) el.classList.add('active');
}
function clearHighlights() {
document.querySelectorAll('.pseudo-line').forEach(el => el.classList.remove('active'));
}
function updateState() {
document.getElementById('sv-n').textContent = n;
document.getElementById('sv-i').textContent = i;
document.getElementById('sv-s').textContent = s;
}
function invariantHolds() {
return s === i * (i + 1) / 2 && i <= n;
}
function setInv(cls, msg) {
const el = document.getElementById('invStatus');
el.className = 'inv-status ' + cls;
el.textContent = msg;
}
function setPost(cls, msg) {
const el = document.getElementById('postStatus');
el.className = 'inv-status ' + cls;
el.textContent = msg;
}
function setMsg(txt) {
document.getElementById('msg').textContent = txt;
}
function step() {
if (phase === 'pre') {
// init
i = 0; s = 0; phase = 'init';
highlight('line-init');
updateState();
setInv('ok', '✔ s=0=0⋅0⋅1/2, i=0≤0≤n');
setMsg('Initialized i=0, s=0. Invariant holds.');
return;
}
if (phase === 'init') {
// decide: enter loop or skip
if (i < n) {
phase = 'body';
highlight('line-while');
setMsg(fmt('Condition i=%i% < n=%n% is TRUE → entering loop body.', { i: i, n: n }));
} else {
phase = 'done';
finish();
}
return;
}
if (phase === 'body') {
// execute one body iteration
i = i + 1;
highlight('line-i');
updateState();
setMsg(fmt('i := i + 1 → i = %i%', { i: i }));
// s update will be next sub-step; simplify: do both at once
s = s + i;
highlight('line-s');
updateState();
const ok = invariantHolds();
if (ok) {
setInv('ok', '✔ s=' + s + '=' + i + '⋅' + (i+1) + '/2=' + (i*(i+1)/2));
} else {
setInv('bad', '✘ invariant violated!');
}
setMsg(fmt('After iteration %i%: i=%i%, s=%s%. Invariant: %v%', { i: i, s: s, v: ok?'OK':'BROKEN' }));
// check loop guard again
if (i < n) {
highlight('line-while');
} else {
phase = 'done';
finish();
}
return;
}
}
function finish() {
highlight('line-post');
document.getElementById('postBox').style.display = 'block';
const expected = n * (n + 1) / 2;
const ok = s === expected;
setPost(ok ? 'ok' : 'bad',
ok ? '✔ s=' + s + '=n⋅(n+1)/2' : '✘ s=' + s + '≠' + expected);
setMsg(ok
? fmt('Loop exited with i=n=%n%. Post-condition holds! s=%s%=%n%⋅%n%%v%/2', { n: n, s: s, v: 1 })
: 'ERROR: post-condition violated (should never happen for correct n≥0).');
document.getElementById('btnStep').disabled = true;
document.getElementById('btnRun').disabled = true;
}
function runAll() {
document.getElementById('btnStep').disabled = true;
document.getElementById('btnRun').disabled = true;
let ticks = 0;
const maxTicks = 100;
function tick() {
if (phase === 'done' || ticks > maxTicks) return;
step();
ticks++;
if (phase !== 'done') setTimeout(tick, 180);
}
tick();
}
document.getElementById('btnStep').addEventListener('click', step);
document.getElementById('btnRun').addEventListener('click', runAll);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('nval').addEventListener('change', reset);
reset();
// Self-test: verify invariant holds throughout for n=0..6
(function selfTest() {
for (let testN = 0; testN <= 6; testN++) {
let ii = 0, ss = 0;
// invariant before loop
console.assert(ss === ii*(ii+1)/2 && ii <= testN, 'inv pre n=' + testN);
while (ii < testN) {
ii++; ss += ii;
console.assert(ss === ii*(ii+1)/2 && ii <= testN, 'inv body n=' + testN + ' i=' + ii);
}
console.assert(ss === testN*(testN+1)/2, 'post n=' + testN);
}
console.log('Self-test: invariant and post-condition verified for n=0..6');
})();
Use the Step button to advance one iteration at a time. The panel on the right shows the current state and highlights whether the invariant holds. Press Run all to execute to completion, or Reset to start over. Notice how the invariant s = i*(i+1)/2 stays green through every step — that is the proof.
Comments
Loading comments...