Booleans live a double life. Sometimes a program wants a boolean's value —
flag = (a < b) stores a 1 or a 0. But far more often a boolean is used only to
steer control: if (a < b && c < d) … never needs the number, only
the branch it decides. A good compiler translates these two uses differently, and the second — the
jumping-code translation — is where the deep and delightful technique of
backpatching lives. This page is about generating short-circuit boolean code in a
single pass, even though, at the moment you emit a jump, you do not yet know where it should go.
t = a < b then flag = t.
if/while guards, and it is what makes short
circuiting natural.
Short-circuit semantics are the reason control-flow code is more than an optimisation — it is often
required. In
Here is the bind. Generating code in a single forward pass, consider
if (a < b) goto ???. At the moment the parser reduces the condition
if). We know a jump is needed; we do not yet know its label. Two escapes exist: make a second
pass (wasteful), or emit the jump with the target field left blank and remember to fill it in
later, once the target's address is known. That deferred filling-in is backpatching.
Every boolean sub-expression
The semantic rules wire these together. For && we know that if || is the exact
mirror: backpatch
if (a < b || c < d) then S
Trace the lists as the code is emitted top to bottom. Instructions are numbered; _ marks an
as-yet-unknown target waiting to be backpatched.
| # | emitted | on the list | action |
|---|---|---|---|
| 100 | if a < b goto _ | B1.truelist = {100} | true ⇒ whole OR is true |
| 101 | goto _ | B1.falselist = {101} | false ⇒ must test B2 |
| — | label M = 102 | backpatch B1.falselist→102 | |
| 102 | if c < d goto _ | B2.truelist = {102} | true ⇒ whole OR is true |
| 103 | goto _ | B2.falselist = {103} | false ⇒ OR is false |
After the || rule: then S, it backpatches the whole truelist if. Every blank _ is now filled — in a single forward
pass.
We model the machine directly: an array of instructions with mutable target fields, and the three
operations. We build the code for a < b || c < d guarding a statement
S, then backpatch and print the finished, fully-targeted TAC.
Run it: the two if … goto jumps (100 and 102) end up patched to the start of
S, the first goto (101) points into the test of the second operand, and the last
goto (103) skips past S. No second pass, no placeholder left blank.
It predates high-level compilers — it comes from assemblers and early linkers coping with
forward references. When an assembler met JMP done before it had seen the label
done:, it could not know the address, so it left the operand as a hole and kept a list of
such holes. When done: finally appeared, the assembler walked back and "patched" every hole
with the now-known address — literally writing back into instructions it had already emitted. Compilers
inherited the trick wholesale for short-circuit booleans and jumping code. It is the same idea a modern
linker uses when it resolves
The whole point of jumping code for && and || is preserving
short-circuit semantics, and the classic bug is to break them by evaluating both
operands unconditionally (as a numerical translation of t = (B1 & B2) would). Consider
if (p != null && p.x > 0). If you evaluate p.x whenever you compute
the boolean, you dereference null exactly when the guard was meant to protect you —
crash. Correct jumping code emits B2's test only on the path where B1 was
true, so p.x is never touched when p is null. When you hand-lower
&&, always route B1's false-exit around B2's
code, never through it.