Base CS from zero
Conditionals as branches
You can write if (x > 0) { ... } else { ... } in any language. But what does the CPU
actually do when it reaches that line?
The answer is not “the CPU checks the condition”. The CPU does not have a circuit that
understands > as an abstract idea. What the CPU has is two much simpler mechanisms
working together: a compare instruction that records the relationship between two
values into a set of single-bit result flags, and a conditional jump instruction that
reads one of those flags and decides — purely on whether a single bit is 0 or 1 — whether
to jump or to fall through.
That two-step sequence — compare, then conditional jump — is how every if in every
program has been implemented on every CPU architecture since the 1950s. Once you see it,
you will never look at an if statement the same way again.
After this lesson you can describe how a compare instruction records its result into a flags register, explain what “fall through” means for a conditional jump, trace the machine-level execution of a simple if/else through both branches, and explain why if/else is two possible destinations for the program counter.
The flags register: recording comparison results. Most CPUs include a special register called the flags register (also called the condition codes register or status register on various architectures). Unlike a general-purpose register such as R0 or R1, the flags register is not a single large value — it is a collection of individual bits, each bit representing the answer to one yes/no question about the most recent arithmetic or comparison operation.
The three most important flags for conditionals are:
- Z (Zero flag): set to 1 if the result of the last operation was zero; set to 0 otherwise.
- N (Negative flag): set to 1 if the result was negative (the high bit was 1 in signed arithmetic); set to 0 otherwise.
- C (Carry flag): set to 1 if the last unsigned addition produced a carry out of the highest bit (i.e., the result overflowed the register width).
These flags are updated automatically by many instructions — arithmetic, logical, and compare instructions — as a side effect of their normal execution. They are not set by LOAD or STORE instructions, which do not produce numeric results.
The compare instruction: subtraction that only updates flags. A CMP (compare) instruction takes two operands — typically two registers or a register and a constant — and subtracts one from the other. The key feature: CMP discards the subtraction result. It does not write the result into any register. Its sole purpose is to update the flags.
For example, CMP R0, R1 computes R0 − R1 internally, throws away the numerical result,
and updates the flags:
- If R0 = R1, then R0 − R1 = 0, so the Z flag is set to 1.
- If R0 < R1 (in signed arithmetic), the result is negative, so the N flag is set to 1.
- If R0 > R1 (in signed arithmetic), the result is positive and non-zero, so Z = 0 and N = 0.
No register is modified. R0 and R1 keep their values. Only the flags register changes.
The compare instruction is the mechanism by which the CPU records a relationship between two values in a form that the next instruction — the conditional jump — can read.
The conditional jump: reading one flag bit. A conditional jump instruction (sometimes written Jxx, where xx names the condition — for example, JZ for “jump if zero”, JNZ for “jump if not zero”, JL for “jump if less”) does one thing: it reads a specific flag from the flags register and makes a binary decision:
- If the flag is 1 (the condition holds): jump — set the program counter to the jump’s target address.
- If the flag is 0 (the condition does not hold): fall through — do nothing to the program counter; let it advance normally to the next instruction.
Fall through means “continue to the immediately following instruction in memory”. It is not a separate instruction; it is the absence of a jump. When the condition is false, the conditional jump behaves identically to a no-op: the PC advances by the instruction size and execution continues with the next instruction.
The conditional jump reads exactly one flag bit. It does not re-evaluate the original
expression x > 0. The comparison already happened; the flag is the residue of that
comparison.
Why this works
Why separate CMP from the conditional jump? The separation keeps both instructions simple. CMP is a pure flag-setter that requires no control-flow logic. The conditional jump is a pure PC-changer that requires no arithmetic logic. Combining them into one instruction would require the instruction decoder to handle both arithmetic and branching simultaneously, which complicates the pipeline. Many real architectures (x86, ARM, RISC-V) follow this exact CMP + conditional-jump split. The nand2tetris Hack machine teaches the same split in its minimal instruction set.
if/else as two destinations. An if/else in source code compiles to a pattern with
two possible paths through memory. Here is the general shape for any if (condition) { A } else { B }:
[CMP instruction] ; compare the operands
[conditional jump to ELSE] ; jump to the else block if condition is FALSE
[instructions for A] ; the if-true block
[unconditional JUMP to END] ; skip past the else block
[ELSE: instructions for B] ; the if-false block
[END: ...] ; execution resumes here after either branchTwo things to notice:
- The conditional jump is written as “jump if the condition is false” because the true block comes immediately after the jump instruction (fall-through case), and the else block comes later. If the condition is true, the jump is not taken (fall through), and the true block runs. If the condition is false, the jump is taken, and the else block runs.
- After the true block, an unconditional jump skips over the else block. Without this jump, execution would fall through into the else block after finishing the true block.
This is a concrete example of the program counter being driven to two different destinations depending on the value of a single flag bit.
Tracing if (R0 == R1) through both paths.
Suppose this program is compiled to the following instructions (each 4 bytes):
Address 100: CMP R0, R1 ; compare R0 and R1, update flags
Address 104: JNE 116 ; jump to 116 if Z=0 (R0 ≠ R1)
Address 108: STORE R0, 200 ; if-true: store R0's value at address 200
Address 112: JUMP 120 ; skip the else block
Address 116: STORE R1, 200 ; if-false (else): store R1's value at address 200
Address 120: ... ; program continues hereCase 1: R0 = R1 = 7 (condition is true).
- CMP at 100: compute R0 − R1 = 7 − 7 = 0. Z flag ← 1. PC → 104.
- JNE at 104: read Z flag = 1. Condition “not equal” requires Z = 0. Z = 1, so the condition is FALSE. Fall through. PC → 108.
- STORE at 108: write R0 (= 7) to memory address 200. PC → 112.
- JUMP at 112: unconditional. PC ← 120. Execution continues at 120.
- The else block at 116 was never reached.
Case 2: R0 = 3, R1 = 9 (condition is false).
- CMP at 100: compute R0 − R1 = 3 − 9 = −6. Z flag ← 0. N flag ← 1. PC → 104.
- JNE at 104: read Z flag = 0. Condition “not equal” requires Z = 0. Z = 0, so the condition is TRUE. Jump. PC ← 116.
- STORE at 116: write R1 (= 9) to memory address 200. PC → 120.
- Execution continues at 120.
- The if-true block at 108–112 was never reached.
In both cases, a single flag bit (Z) determined which of two paths through memory the program counter followed. The if/else has zero runtime cost beyond one compare instruction and one conditional jump.
Edge cases
What about else if? An else if chain is just multiple CMP + conditional-jump pairs
in sequence. The first condition is tested; if it is false, a conditional jump skips past
the first block to the next CMP. The pattern repeats until one condition is true or all are
exhausted. Each additional else if adds one CMP and one conditional jump — the chain
length is limited only by the program size, not by any CPU constraint.
CMP R0, R1 computes R0 − R1 and discards the result. If R0 = 5 and R1 = 5, what is the value of the Z (Zero) flag after CMP executes?
CMP R0, R1 is executed with R0 = 3 and R1 = 7. What is the value of the Z flag?
A JZ (jump if zero) instruction targets address 300. The Z flag is currently 0. Does the CPU jump to 300 or fall through to the next instruction? Type 0 for fall-through, 1 for jump.
A JZ instruction targets address 300. The Z flag is 1. The JZ instruction itself is at address 200 (4 bytes). After JZ executes, what does the PC hold?
In an if/else block compiled to machine code, after the if-true block finishes, an unconditional JUMP is placed. What does it skip over?
What is 'fall through' in the context of a conditional jump instruction?
An if statement compiles to two machine instructions working in sequence. First, a
CMP (compare) instruction subtracts one value from another, discards the numeric
result, and records the relationship as single bits in the flags register — most
importantly the Z (Zero) flag and the N (Negative) flag. Second, a conditional
jump instruction reads one of those flag bits: if the condition is true it sets the
program counter to a target address (jumps); if the condition is false it does nothing
and lets the PC advance to the next instruction (falls through). An if/else is
exactly two destinations for the PC: the if-true block (reached via fall-through) and the
if-false block (reached via jump). After the if-true block, an unconditional jump skips
over the else block. At runtime, the choice between these two paths costs exactly one
compare and one conditional jump.