Inverting Programs

Every information-preserving program can be run backward — and the inverse is built mechanically from the pieces of the forward program. Gries’ chapter on inversion turns “undo” into a small, provable calculus.

This is an original study summary for quick reference, following the treatment of program inversion in David Gries’ The Science of Programming (Ch. 21). Notation follows Gries and Dijkstra: {P} S {Q} for a Hoare triple, guarded commands (if…fi, do…od) for the programming language, and S⁻¹ for the inverse of S. See the book for the full, rigorous treatment.

Contents

  1. What it means to invert a program
  2. Inverting the basic commands
  3. Why invert
  4. Worked example: an accumulation loop
  5. Conditions for invertibility
  6. Summary

Most reasoning about programs runs one way: given a start state, what does the program produce? Inversion asks the opposite question — given the result, can we recover the state we came from, and can we do it with an actual program? When the answer is yes, the inverse is not something to invent from scratch. It is assembled, construct by construct, from the forward program. This page walks the definition, the inversion rules for each command, the reasons the technique earns its place, a worked loop, and the exact conditions under which a program can be inverted at all. It builds on the basic commands and the iterative command.

What it means to invert a program

A program S is invertible if there exists a program S⁻¹ — its inverse — that, run immediately after S, restores the state that held before S ran. Formally, for any predicate P describing the starting state:

{P}  S ; S⁻¹  {P}

Read it plainly: run S, then run S⁻¹, and you are exactly where you started. S⁻¹ undoes whatever S did. This is the same idea as ctrl-Z, but stated precisely enough to prove.

S⁻¹ undoes S  ⇔  S ; S⁻¹ behaves like skip  (over the reachable states)

Not every program qualifies. Inversion requires that S throw away no information: the final state must uniquely determine the initial state. Two consequences follow immediately.

So the invertible programs are exactly the deterministic, information-preserving ones — those that compute a one-to-one mapping from initial to final states. For such programs, S ; S⁻¹ is the identity and, symmetrically, S⁻¹ ; S is too, so the inverse of the inverse is the original: (S⁻¹)⁻¹ = S.

Inverting the basic commands

The power of the technique is that inversion is compositional: to invert a program, invert each of its parts and reassemble them. The rules below are the whole toolkit — there is one per construct in the language.

ConstructInverseNote
skipskip⁻¹ = skipDoing nothing is undone by doing nothing.
S1 ; S2(S1 ; S2)⁻¹ = S2⁻¹ ; S1⁻¹Reverse the order, invert each step — undo the last thing first, like taking off shoes then socks.
x := einvertible only if e lets you recover the old xSee the discussion below — this is where information is usually lost.
if … fiinvert each branch; guards must be reconstructable from the post-stateYou must be able to tell, after the fact, which branch ran.
do B → S oda loop running S⁻¹ the same number of timesNeeds a backward stopping test — a “co-guard.”

Sequential composition — reverse the order

The order flip is the rule people forget. If the forward program does A then B, its inverse does B⁻¹ then A⁻¹. This is forced: B ran last against the state A left, so B⁻¹ is the only thing that can restore that intermediate state, and only then can A⁻¹ restore the original.

# forward:   x := x + 1 ;  x := x * 2
# inverse:   x := x / 2 ;  x := x - 1     (reversed order, each step inverted)
# check:  start x=5 → +1 → 6 → *2 → 12 → /2 → 6 → -1 → 5   ✓

Assignment — the pivot of invertibility

An assignment x := e is invertible only when the expression e lets you compute the old value of x back from the new one. The invertible cases are exactly the reversible arithmetic ones.

✓ Invertible assignments
# add a constant → subtract it
x := x + c        inverse:  x := x - c

# double → halve  (only if division is exact)
x := x * 2        inverse:  x := x / 2

# negate → negate
x := -x           inverse:  x := -x
✗ Not invertible (information lost)
# overwrites x with a constant:
# the old x is gone forever
x := 5

# unless the old value was saved first:
old := x ;  x := 5
# inverse:  x := old  (now recoverable)

The lesson: x := e is invertible when e depends on the old x in a one-to-one way. x := x + c and x := x * 2 (with exact division) qualify; x := 5 does not, because after it runs the old x is unrecoverable. Saving the old value first — old := x — restores invertibility, at the cost of extra state.

Selection (IF) — the branch taken must be recoverable

To invert an alternative command, invert each branch. But there is a catch the assignment case did not have: after the command runs, you must be able to tell which branch was taken, purely from the post-state. Each forward guard Bi needs a matching post-guard that is true exactly when branch i ran.

# forward
if x < 0 → x := x + 100
[] x ≥ 0 → x := x + 200
fi
# after branch 1: x is in [100,199];  after branch 2: x ≥ 200
# the ranges do not overlap → the branch taken IS recoverable

# inverse: guard on the post-state, undo the matching branch
if x < 200 → x := x - 100
[] x ≥ 200 → x := x - 200
fi

If two branches could leave the state in overlapping conditions, the post-guards cannot be written and the IF is not invertible — that overlap is precisely information loss (you can no longer tell which path produced the result).

Iteration (DO) — walk it back with a co-guard

The inverse of a loop is another loop. If the forward loop do B → S od executes its body n times, the inverse must execute S⁻¹ exactly n times — no more, no fewer. The difficulty is knowing when to stop going backward. The forward guard B tells you when to keep going forward; the inverse needs its own test — a co-guard BB that is true on every state the forward loop passed through and becomes false exactly when you reach the original starting state.

# forward loop, body run n times
do B → S od

# inverse loop: run S⁻¹ the same n times, stop via the co-guard BB
do BB → S⁻¹ od

Choosing BB is the real work of inverting a loop: it must fire the inverse body precisely n times. Often a counter or a monotone quantity from the loop gives it to you directly (as in the worked example below, where the co-guard is simply i ≠ 0).

Why invert

Inversion is not a curiosity. It shows up wherever “go back” or “the other direction” is a real requirement.

The classic example. It is straightforward to write a program that, given a combination (say a k-subset of {0..n-1}), computes its rank — its position in the standard ordering of all such combinations. Going the other way — given a rank, produce the corresponding combination (“unranking”) — looks harder. But the ranking program is invertible, so you do not design the unranker separately: you invert the ranker. The inverse reads a rank and reconstructs the combination that produced it, and it is correct because inversion preserves the specification.

Worked example: an accumulation loop

Take a simple loop that walks a counter forward, accumulating as it goes, and derive its inverse mechanically. Forward, we start at i = 0 and count up to n, adding i into a running sum:

✓ Forward — accumulate
# pre:  i = 0  and  s = 0
# post: i = n  and  s = 0+1+...+(n-1)
i := 0 ;  s := 0 ;
do i ≠ n →
     s := s + i ;
     i := i + 1
od
✓ Inverse — walk it back
# pre:  i = n  and  s = 0+1+...+(n-1)
# post: i = 0  and  s = 0
# co-guard: i ≠ 0 (loop back exactly n times)
do i ≠ 0 →
     i := i - 1 ;
     s := s - i
od

Every piece of the inverse comes straight from the rules. The forward body is the sequence s := s + i ; i := i + 1; by the composition rule its inverse reverses the order and inverts each step, giving i := i - 1 ; s := s - i — and note the inner order matters, we must roll i back before subtracting so we subtract the same value that was added. The forward loop ran its body n times (until i = n); the co-guard i ≠ 0 makes the inverse run its body n times too, stopping exactly when i returns to 0.

Now the correctness claim {P} forward ; inverse {P}. Let P assert i = 0 ∧ s = 0. Running forward lands in the state i = n ∧ s = 0+1+…+(n-1). The inverse loop then subtracts each i back out in the reverse order it was added and decrements i each pass, terminating with i = 0 and s reduced back to 0 — exactly P. Trace it for n = 3:

# forward:  (i,s): (0,0)→(1,0)→(2,1)→(3,3)     s = 0+1+2 = 3
# inverse:  (3,3)→ i=2,s=3-2=1 → i=1,s=1-1=0 → i=0,s=0-0=0
# back to (0,0) = P                              ✓

Conditions for invertibility

Pulling the rules together, a program is invertible exactly when it destroys no information at any step. Concretely, three conditions must all hold.

ConstructInvertible?Because
skipAlwaysChanges nothing, so nothing to recover.
x := x + c, x := -xYesOld x recomputable from the new value.
x := x * 2Yes, if exactHalving recovers it, provided division is exact (no truncation).
x := 5 (constant)NoOld x overwritten and lost.
x := x * 0, integer x := x / 2NoMany inputs map to one output — not one-to-one.
S1 ; S2If both parts areInverse is S2⁻¹ ; S1⁻¹.
if … fiIf branch is recoverablePost-guards must identify the branch taken.
do … odIf a co-guard existsMust run the inverse body the same number of times.
Nondeterministic SGenerally noSame result reachable from many starts — ambiguous.

Summary

IdeaThe one-line takeaway
What inversion isS is invertible if some S⁻¹ gives {P} S ; S⁻¹ {P} — it undoes S.
PreconditionOnly deterministic, information-preserving (one-to-one) programs can be inverted.
skip and sequenceskip⁻¹ = skip; (S1;S2)⁻¹ = S2⁻¹ ; S1⁻¹ — reverse order, invert each.
AssignmentInvertible only if e recovers the old x: x:=x+c yes, x:=5 no.
Selection (IF)Invert each branch; the branch taken must be recoverable from the post-state.
Iteration (DO)Inverse is a loop running S⁻¹ the same number of times, stopped by a co-guard.
Why invertUndo/redo, reversible computing, deriving a hard algorithm as the inverse of an easy one, enumeration.
Unranking exampleInvert the rank-of-combination program to get combination-from-rank for free.
Invertibility testNo unrecoverable overwrite, guards recoverable, loops with a backward stop.
The recurring theme: inversion is assembly, not invention. If a program destroys no information, its inverse already exists inside it — reverse the sequence, invert each command, and supply a co-guard for every loop. The moment a step throws information away, inversion stops being possible right there, which makes it a sharp lens for spotting exactly where a computation is lossy. See also the basic commands and the iterative command.