We use essential cookies to run the site (session, security, and your theme/language preferences). With your permission we also load embedded third-party content, such as YouTube videos. Cookie Policy
Why you can never be fully sure your refactor is safe
Author(s):Elier Rodríguez García
Index
Introduction
Every programmer knows the small anxiety of a refactor: you rewrite a function to be cleaner or faster, and you believe it does the same thing — but does it, on every input? That question — do two programs always produce identical outputs? — is program equivalence.
The obvious check is to test: run both on a bunch of inputs and compare. If they ever disagree, they're not equivalent. But if they always agree on your tests... you've proven nothing. There are infinitely many possible inputs, and a difference might hide at exactly the one you didn't try — the value 42, or every input above a million.
And here's the deep result: there is no general procedure that can decide, for arbitrary programs, whether they're equivalent. The problem is undecidable — a direct relative of the halting problem. Not slow: impossible.
Spot the Difference
Try it. Pick a pair of small programs that look like they might do the same thing. Type an input and compare their outputs, or let the demo auto-test a range. Hunt for a value where they disagree.
<p class="hint">Pick two programs that look alike. Test an input to compare their outputs, or auto-test a range. A single mismatch proves they differ — but matching every test you try proves <b>nothing</b>.</p>
<select id="pair" class="pair"></select>
<div class="progs">
<pre class="prog" id="pa"></pre>
<pre class="prog" id="pb"></pre>
</div>
<div class="row">
<label>input x = <input id="x" type="number" value="3" /></label>
<button id="test" type="button">Test</button>
<button id="auto" type="button">Auto-test −50…200</button>
</div>
<div id="out" class="out"></div>
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Program equivalence: deciding if two programs always agree is undecidable.
// Testing can only ever find a difference, never prove sameness — a sneaky
// input (here 42, or anything above 1000) can hide the disagreement.
const PAIRS = [
{ name: 'double a number', a: 'function f(x) {\n return 2 * x;\n}', b: 'function g(x) {\n return x + x;\n}', f: x => 2 * x, g: x => x + x },
{ name: 'square (one has a bug)', a: 'function f(x) {\n return x * x;\n}', b: 'function g(x) {\n if (x === 42)\n return 1729;\n return x * x;\n}', f: x => x * x, g: x => x === 42 ? 1729 : x * x },
{ name: 'absolute value', a: 'function f(x) {\n return Math.abs(x);\n}', b: 'function g(x) {\n return x < 0 ? -x : x;\n}', f: x => Math.abs(x), g: x => x < 0 ? -x : x },
{ name: 'identity (off by one when big)', a: 'function f(x) {\n return x;\n}', b: 'function g(x) {\n if (x > 1000)\n return x + 1;\n return x;\n}', f: x => x, g: x => x > 1000 ? x + 1 : x }
];
let cur = 0;
function show() {
const p = PAIRS[cur];
document.getElementById('pa').textContent = p.a;
document.getElementById('pb').textContent = p.b;
document.getElementById('out').innerHTML = '';
}
function test() {
const p = PAIRS[cur]; const x = +document.getElementById('x').value;
const a = p.f(x), b = p.g(x);
const out = document.getElementById('out');
if (a === b) out.innerHTML = fmt('<span>f(%x%) = <code>%a%</code>, g(%x%) = <code>%b%</code> — <span class="ok">match.</span> (Still no proof of equivalence.)</span>', { x: x, a: a, b: b });
else out.innerHTML = fmt('<span>f(%x%) = <code>%a%</code>, g(%x%) = <code>%b%</code> — <span class="bad">DIFFER! Found a counterexample → not equivalent.</span></span>', { x: x, a: a, b: b });
}
function auto() {
const p = PAIRS[cur]; let diff = null;
for (let x = -50; x <= 200; x++) { if (p.f(x) !== p.g(x)) { diff = x; break; } }
const out = document.getElementById('out');
if (diff !== null) out.innerHTML = fmt('<span class="bad">Mismatch at x = %diff%</span> — f = <code>%f%</code>, g = <code>%g%</code>. Definitely <b>not</b> equivalent.', { diff: diff, f: p.f(diff), g: p.g(diff) });
else out.innerHTML = '<span class="ok">No difference found in −50…200.</span> Tempting to call them equal — but that proves nothing. The disagreement (if any) could hide at a larger input. There’s no finite test that settles it.';
}
function init() {
const sel = document.getElementById('pair');
sel.innerHTML = PAIRS.map((p, i) => '<option value="' + i + '">' + p.name + '</option>').join('');
sel.addEventListener('change', () => { cur = +sel.value; show(); });
document.getElementById('test').addEventListener('click', test);
document.getElementById('auto').addEventListener('click', auto);
document.getElementById('x').addEventListener('keydown', e => { if (e.key === 'Enter') test(); });
show();
}
init();
<p class="hint">Pick two programs that look alike. Test an input to compare their outputs, or auto-test a range. A single mismatch proves they differ — but matching every test you try proves <b>nothing</b>.</p>
<select id="pair" class="pair"></select>
<div class="progs">
<pre class="prog" id="pa"></pre>
<pre class="prog" id="pb"></pre>
</div>
<div class="row">
<label>input x = <input id="x" type="number" value="3" /></label>
<button id="test" type="button">Test</button>
<button id="auto" type="button">Auto-test −50…200</button>
</div>
<div id="out" class="out"></div>
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Program equivalence: deciding if two programs always agree is undecidable.
// Testing can only ever find a difference, never prove sameness — a sneaky
// input (here 42, or anything above 1000) can hide the disagreement.
const PAIRS = [
{ name: 'double a number', a: 'function f(x) {\n return 2 * x;\n}', b: 'function g(x) {\n return x + x;\n}', f: x => 2 * x, g: x => x + x },
{ name: 'square (one has a bug)', a: 'function f(x) {\n return x * x;\n}', b: 'function g(x) {\n if (x === 42)\n return 1729;\n return x * x;\n}', f: x => x * x, g: x => x === 42 ? 1729 : x * x },
{ name: 'absolute value', a: 'function f(x) {\n return Math.abs(x);\n}', b: 'function g(x) {\n return x < 0 ? -x : x;\n}', f: x => Math.abs(x), g: x => x < 0 ? -x : x },
{ name: 'identity (off by one when big)', a: 'function f(x) {\n return x;\n}', b: 'function g(x) {\n if (x > 1000)\n return x + 1;\n return x;\n}', f: x => x, g: x => x > 1000 ? x + 1 : x }
];
let cur = 0;
function show() {
const p = PAIRS[cur];
document.getElementById('pa').textContent = p.a;
document.getElementById('pb').textContent = p.b;
document.getElementById('out').innerHTML = '';
}
function test() {
const p = PAIRS[cur]; const x = +document.getElementById('x').value;
const a = p.f(x), b = p.g(x);
const out = document.getElementById('out');
if (a === b) out.innerHTML = fmt('<span>f(%x%) = <code>%a%</code>, g(%x%) = <code>%b%</code> — <span class="ok">match.</span> (Still no proof of equivalence.)</span>', { x: x, a: a, b: b });
else out.innerHTML = fmt('<span>f(%x%) = <code>%a%</code>, g(%x%) = <code>%b%</code> — <span class="bad">DIFFER! Found a counterexample → not equivalent.</span></span>', { x: x, a: a, b: b });
}
function auto() {
const p = PAIRS[cur]; let diff = null;
for (let x = -50; x <= 200; x++) { if (p.f(x) !== p.g(x)) { diff = x; break; } }
const out = document.getElementById('out');
if (diff !== null) out.innerHTML = fmt('<span class="bad">Mismatch at x = %diff%</span> — f = <code>%f%</code>, g = <code>%g%</code>. Definitely <b>not</b> equivalent.', { diff: diff, f: p.f(diff), g: p.g(diff) });
else out.innerHTML = '<span class="ok">No difference found in −50…200.</span> Tempting to call them equal — but that proves nothing. The disagreement (if any) could hide at a larger input. There’s no finite test that settles it.';
}
function init() {
const sel = document.getElementById('pair');
sel.innerHTML = PAIRS.map((p, i) => '<option value="' + i + '">' + p.name + '</option>').join('');
sel.addEventListener('change', () => { cur = +sel.value; show(); });
document.getElementById('test').addEventListener('click', test);
document.getElementById('auto').addEventListener('click', auto);
document.getElementById('x').addEventListener('keydown', e => { if (e.key === 'Enter') test(); });
show();
}
init();