The single most common statement in all of programming is also the most destructive.
x = 42 looks harmless — but whatever x held a moment ago is now
gone, unrecoverable, erased. Every ordinary program is a long chain of these little acts of
forgetting. We have spent this course building hardware that never forgets — all the way up to a
The answer is a small, beautiful discipline: build every program out of steps that are individually invertible, and the whole program inherits an inverse automatically. This lesson lays out the two pillars of that discipline — invertible updates and assertions at joins — and the rest of the module builds real languages on top of them.
Recall from x = e replaces the value of x. That
replacement is a many-to-one funnel: every possible old value of x leads to the same new
state, so the old value cannot be deduced afterwards. Irreversible.
But not every assignment forgets. An update that combines the old value with something else, using an operation that can be unpicked, keeps the past recoverable:
| Update | Meaning | Inverse |
|---|---|---|
x += e | add e to x | x -= e |
x -= e | subtract e from x | x += e |
x ^= e | XOR e into x | x ^= e (its own inverse) |
swap x, y | exchange two variables | swap again (its own inverse) |
with one crucial side condition: the updated variable must not occur in the expression
e. As long as that holds, the new value of x together with the
(unchanged) value of e pins down the old value of x exactly — the update is
a bijection on the program state. In the language of maths,
The side condition — the updated variable may not appear in its own update expression — is
not bureaucratic fussiness. Consider x += x. It doubles x, and doubling
integers is one-to-one… but its inverse is halving, which only lands on an integer
when x is even. The update sneaks you out of the tidy world where
+= is undone by -=: mechanically applying the rule "invert
x += e as x -= e" would give x -= x, which zeroes
x — a genuinely destructive step! And x ^= x is even worse: it sets
x to x outright. So
reversible languages enforce the rule syntactically: the variable being updated must not be
free in the right-hand side. Then, and only then, every update is invertible by the simple
symbol-for-symbol swap in the table above.
Here is a three-step program written as explicit forward/backward pairs. Watch the state go out and come back — the backward pass runs the inverse updates in reverse order:
The final line is the starting state, bit for bit. Nothing was journalled, nothing was backed up — the current state alone was enough to walk home.
That reverse-order rule is worth staring at. You put on socks, then shoes; to undo it you remove
shoes, then socks. Formally, if a program is the composition
— and each += ↔ -=, ^= ↔ ^=). This is the payoff of the
discipline: every reversible program comes with a derivable inverse, for free. You
never write the "undo" code; the compiler can spell it out from the forward text. The next lessons
exploit this relentlessly — a whole language (Janus) where calling a procedure backwards is one
keyword, and a syntactic algorithm (program inversion) that flips any program in one pass.
A fun corollary: because swap and ^= are legal, the classic XOR-swap trick is a
perfectly good reversible program — three invertible updates that exchange two variables with no
temporary at all:
Updates are only half the story. Programs also branch and loop — and control flow
forgets too, in a sneakier way. Think about a plain
if: two different execution paths run through it and then join into one
continuation. Standing just after the if, a conventional program has no idea which
branch it took. That knowledge — one whole bit of it, per join — has been discarded. The same happens
every time a
Running backwards, that lost bit is exactly what you need: arriving at a join from below, the inverse program must decide which branch to un-run. So reversible languages impose a matching discipline on control flow:
if, at a loop's entry), the
program must supply an assertion — a condition that is true on one incoming path
and false on the other.
This is the deep symmetry of reversible programming: conditions (tested on the way in) and
assertions (established on the way out) are mirror images, and backwards execution swaps
their roles. In the next lesson we meet Janus, the canonical language built exactly
on this idea — its if has an exit assertion, its loop has an entry assertion, and every
procedure can be called in either direction.
Absolutely — that is what an undo stack, a database journal, and a time-travel debugger all do: before
each destructive write, squirrel away the value being destroyed. It works, and later in this module we
will see industrial tools built on precisely that trick. But notice the cost: the log grows with the
length of the run, without bound, and the destructive steps still physically happened (with
their