When you learned that uncall
actually does. Does it interpret the program in some exotic reversed mode? No — it does
something far more satisfying: it rewrites the source code. There is a purely
syntactic transformation, a handful of rewrite rules you can apply with a pencil, that turns any
reversible program text
Write
| Program | Its inverse | Why |
|---|---|---|
x += e | x -= e | subtraction undoes addition |
x -= e | x += e | and vice versa |
x ^= e | x ^= e | XOR is its own inverse |
x <=> y | x <=> y | swapping twice is the identity |
A; B | ℐ[B]; ℐ[A] | socks and shoes: undo the last thing first |
if c then A else B fi e | if e then ℐ[A] else ℐ[B] fi c | condition and assertion swap roles |
from a do A loop B until c | from c do ℐ[A] loop ℐ[B] until a | entry assertion and exit condition swap |
call p | uncall p | by definition |
uncall p | call p | inverting twice returns the original |
skip | skip | doing nothing undoes itself |
Two of these rows carry all the intellectual weight. The sequence rule reverses the
order (you must undo the most recent step first). And the conditional rule is the
assertion idea made mechanical: forwards, c chooses the branch and e
records the choice; backwards, e chooses which branch to un-run and c
becomes the thing to verify. The information flows through the join in one direction, and inversion
turns it around.
Because every rule maps a legal statement to a legal statement,
Take this four-line program:
Apply the sequence rule (reverse the lines), then the statement rules (flip each operator):
To verify an inversion, compose the two: running the original and then the candidate inverse
must be the identity on every starting state. Let's make the machine do both the inverting and the
checking. Below, a mini-language is represented as a data structure, invert() implements
the rule table, and an interpreter traces the round trip:
The inverter is four lines long. That is the punchline of this whole module: in a reversible language, inversion is not a research problem, it is a compiler pass.
The classic slip is to invert each statement but forget to reverse the order:
x += 5; y += x: keeping the order gives x -= 5; y -= x, which subtracts the
wrong x from y (the already-restored one). The correct inverse,
y -= x; x -= 5, undoes the most recent effect first, while the state it depends on is
still in place. Exactly the same as undressing: shoes off before socks. If your hand-derived
inverse fails the round-trip test, an un-reversed sequence is the first thing to check.
Everything above leaned on the program being reversible in the first place. What about inverting an
arbitrary program — given code for
Contrast the two worlds. For a reversible program: inversion is syntactic, runs in time proportional to the length of the text, and yields a program exactly as fast as the original. For an irreversible program: inversion needs search over the input space, may find many answers or none, and in general is not computable at all. The whole discipline of reversible-language design is the price paid to stay in the first world.
Yes — it's a lively research area, precisely because it is hard. Grammar-based inverters can turn a
printer into a parser; the logic-programming world gets partial inversion "for free" by running
relations sideways (a Prolog append can split a list as happily as it joins one); and
program-synthesis tools search for inverses with clever pruning rather than brute force. But every
such technique hits the same wall sooner or later: where the forward program destroyed
information, no amount of cleverness can conjure it back — the inverter must either search,
guess, or give up. Reversible languages don't dodge that wall; they simply never build it, by
refusing to destroy the information in the first place.