01/11Sequence

LEARN TO PROGRAM · CHAPTER 03 · BASICS

Control flow

In chapter 1 the program ran from top to bottom, one line after another. Real programs don't: they decide (do this or that) and they repeat (do this ten thousand times). The order in which the lines actually run is the program's control flow.

Picture the code as a railway and the running program as a token riding it. Arches are statements. Switches are decisions, with a lamp that goes green for true and red for false. Loops are tracks that bend back on themselves. Scroll, and the token rides each layout, running a real program with real values.

Every code block runs in your browser. Edit it, break it, press Run.

01 · SEQUENCE

Sequence: one thing after another

The simplest flow is a straight line. Each statement runs once, in order, and each one sees the world the one before it left behind. On the stage, the token passes under four arches, and the chip above it shows x changing: 5, then 6, then 12.

sequence.js

Press Step through: the highlighted line is the arch the token is under.

Order matters. The same two steps, swapped, give a different answer, because x is a different number by the time each one runs:

order.js
3ways to arrange steps are all you ever need: sequence, selection (choose a path) and iteration (repeat). Böhm and Jacopini proved in 1966 that any flowchart can be rewritten using only these three. The rest of this chapter is the other two. Böhm & Jacopini (1966)

02 · IF / ELSE

if / else: choosing a track

An if statement asks a yes-or-no question and picks a track. If the condition is true the token takes the first branch; if not, it goes on to the else. Chain else ifs for more choices: they are tried top to bottom, and the first true one wins. Everything after it is skipped.

On the stage the program runs three times, with t at 30, 20 and 5. Watch the first switch go green for 30, red then green for 20, and red twice for 5. All three tracks meet again before console.log: an if splits the flow, and the flow rejoins after it.

weather.js

Change 30 to 20, then 5, then exactly 25. Is 25 "hot"? Why not?

The condition can be anything that gives true or false. Comparisons are the usual source:

compare.js

Use === and !==. The two-equals version converts types first, which surprises everyone eventually.

03 · && AND ||

Boolean logic: && , || and !

Questions combine. AND (&&) is true only when both sides are true. OR (||) is true when at least one side is. NOT (!) flips true and false. A truth table lists every combination; there are only four for two inputs, and the board at the back of the stage lights the row being tested.

truth-table.js

Now watch the two layouts closely. The AND gate is two switches in a row: the token reaches B only if A was true. The OR gate is two switches side by side: if A is true, there's already a way through, and B is never asked. When a switch is skipped, its sign dims and says so.

That is short-circuit evaluation: JavaScript stops as soon as it knows the answer, and doesn't even run the right-hand side. This is in the language specification, not an optimisation you hope for, so you can rely on it:

short-circuit.js

Count the "checking" lines. B was never checked either time.

The everyday use is a guard: check that something exists before you look inside it.

guard.js

JavaScript is relaxed about what counts as a condition. Any value can go in an if; it is converted to true or false. The values that count as false are called falsy, and there are only a handful. Everything else, including "0" and empty arrays, is truthy:

truthy.js
9falsy values in JavaScript: false, 0, -0, 0n, "", null, undefined, NaN, and the oddity document.all. Every other value is truthy. MDN: Falsy

04 · SWITCH

switch: one question, many tracks

When you compare one value against a list of options, a switch reads better than a long else if chain. It works out the value once, then jumps to the case that matches it with ===, or to default if none does. On the stage a turntable swings to the right lane.

The catch: a case doesn't end on its own. Execution carries on into the next case's code until it hits a break. This is called fall-through. Watch "amber": its lane has no code and no break, so the track runs straight into the red lane and the token does what red does. Used on purpose, that's a neat way to share code.

traffic-light.js

Used by accident, it's a classic bug. Predict the output before you run it:

forgot-break.js

Newer languages learned from this. Go and Swift stop at the end of each case unless you write fallthrough; Python added match in version 3.10 with no fall-through at all:

the same ideas in Python
t = 30
if t > 25:
    msg = "hot"
elif t > 15:
    msg = "mild"
else:
    msg = "cold"
 
for i in range(3):        # 0, 1, 2: the end is excluded
    print(i)
 
match msg:                # Python 3.10+: no fall-through
    case "hot" | "mild":
        print("go outside")
    case _:
        print("stay in")

05 · LOOPS

Loops: tracks that bend back

A loop is a decision at the top of a track that bends back to it. Each time round is called an iteration (or a lap). The for loop puts the three parts on one line: the start (let i = 0), the test asked before every lap (i < 3), and the update after every lap (i++). On the stage, the token passes the start once, then circles: test, body, update, back along the violet track. When the test turns red it leaves on the exit track.

for.js

Press Step through and watch the order: test, body, update, test, body… The test runs 4 times for 3 laps.

A while loop is the same machine with only the test on top. Use it when you don't know in advance how many laps it will take:

while.js

Most of the time you just want each item of a list. for…of hands you the items one at a time, so there's no counter to get wrong. The crates at the back of the stage light up one per lap:

for-of.js

do…while puts the test at the bottom, so the body always runs at least once:

do-while.js

06 · BREAK · CONTINUE

break and continue: leaving early

Two words change a loop's track from inside. break leaves the loop at once, onto the exit (the pink track). continue abandons the rest of this lap and goes straight back to the top for the next one (the short violet track).

On the stage, the loop walks through [3, 8, -1, 7]. 3 is odd, so it's added. 8 is even: continue sends the token back before the addition. -1 is negative: break leaves the loop, so 7 is never even looked at. The sum is 3.

break-continue.js

Step through it and watch which lines run for each x. Then move -1 to the end of the list.

break only leaves the innermost loop. To leave an outer one from inside an inner one, give the outer loop a label:

labelled-break.js

07 · NESTED LOOPS

Nested loops: a loop on every lap

A loop body can contain anything, including another loop. The inner loop then runs all of its laps on every lap of the outer one. On the stage the inner ring spins three times for each trip round the outer ring, and the grid fills in row by row: a times table.

times-table.js

Make it 9 × 9 by changing both 3s.

That multiplication is the thing to remember. An outer loop of n laps around an inner loop of n laps does n × n steps. Ten items is a hundred steps; a thousand items is a million:

how-much-work.js

This is the first taste of Big-O: work that grows like n² is fine for small lists and painful for big ones. Chapter 6 is all about it.

08 · OFF BY ONE

Off by one: the most common loop bug

Arrays count from 0. An array of length 5 has slots 0, 1, 2, 3 and 4, and no slot 5. Write the test as i <= arr.length and the loop takes one lap too many. On the stage the token runs the buggy loop first: the sixth lap reaches for the slot that isn't there and gets undefined. Then it runs again with <, and stops after five.

off-by-one.js

Fix the test and run it again. Then try starting at i = 1: which element goes missing?

Its old name is the fencepost error: a 100 m fence with a post every 10 m has 10 sections but 11 posts. Counting the gaps when you meant the posts, or the other way round, is the same mistake.

fenceposts.js

09 · LOOP VS RECURSION

Iteration vs recursion: a preview

There's a second way to repeat: a function that calls itself with a smaller problem, until the problem is so small the answer is obvious (the base case). That is recursion. On the stage the left token loops round a ring; the right token goes down a staircase. Each step down is a new call with its own copy of n, stacked on top of the one that called it. At countdown(0) it hits the base case, then climbs back up through every waiting call.

countdown.js

Press Step through and watch the call stack: countdown appears four times, one on top of another.

Both print 3, 2, 1. The loop needs one variable, however many laps it runs. The recursion needs a stack frame per waiting call, and the stack has a limit:

too-deep.js

Recursion shines when the data itself is shaped like a tree (folders inside folders, the parse tree from chapter 1), where a loop would need you to manage a stack by hand. Some languages turn a call in "tail position" into a jump, so it costs no stack. The JavaScript standard has required that since 2015, but Safari is the only major browser that does it. Chapter 4 takes the call stack apart properly.

10 · FLOW GRAPH

The control-flow graph

Strip the railway down to its junctions and you get the tool compilers actually use: the control-flow graph. Cut the code into basic blocks, straight runs of statements with one way in (at the top) and one way out (at the bottom). Then draw an arrow for every way control can pass from one block to another. A condition has two arrows out, labelled T and F. A loop shows up as a back edge: an arrow to a block that came earlier.

sum-evens.js
let sum = 0;
for (let i = 1; i <= 4; i++) {
  if (i % 2 === 0) {
    sum += i;
  }
}
console.log(sum);

On the stage this program becomes six blocks. B0 sets up; B1 is the loop test; B2 is the if; B3 adds; B4 is i++, with the back edge to B1; B5 prints. The token takes the F arrow from B2 on odd laps and the T arrow on even ones, and leaves along B1's F arrow when i reaches 5. It prints 6.

sum-evens.js

Step through it and name the block each highlighted line belongs to.

Every layout in this chapter was a control-flow graph in disguise. Compilers use them to find code that can never run, to spot variables used before they're set, and to find loops to speed up.

11 · JUMPS IN THE CPU

Down in the CPU: every branch is a jump

A processor knows nothing about if, while or switch. It has a program counter (PC) holding the address of the next instruction, and by default the PC just moves on to the next one: sequence. Control flow is done with jumps, instructions that load a new address into the PC. A conditional jump does that only if a condition holds; otherwise the PC carries on as usual.

Here is a C loop and roughly what a compiler turns it into on x86-64. The compiler flipped the test: "keep going while i < 3" became "jump out if i >= 3". cmp subtracts and sets flags; jge reads them. On the stage the PC token rides down the instructions, leaps back along jmp loop three times, and finally takes jge done.

loop.c
#include <stdio.h>
 
int main(void) {
    int sum = 0;
    for (int i = 0; i < 3; i++) {
        sum += i;
    }
    printf("%d\n", sum);
    return 0;
}
x86-64, hand-written for clarity
        mov  ecx, 0          ; i = 0
        mov  eax, 0          ; sum = 0
loop:   cmp  ecx, 3          ; compare i with 3 (sets flags)
        jge  done            ; if i >= 3, jump out
        add  eax, ecx        ; sum += i
        inc  ecx             ; i++
        jmp  loop            ; back to the test
done:   ret                  ; the answer is in eax

You can build this in a few lines. Here is chapter 1's stack machine with a JMP and a JZ (jump if zero) added. A loop is nothing more than pc = an earlier number:

vm-with-jumps.js

Press Step through and watch pc: it climbs, then drops back to 1 each lap.

Branch prediction

A modern core works on many instructions at once, in a pipeline, so it has to fetch the instructions after a branch before it knows which way the branch goes. It doesn't wait: it guesses, and carries on along that path. Guess right and nothing is lost. Guess wrong and all the work on the wrong path is thrown away and the pipeline refills: on recent Intel and AMD cores, that costs about 15 to 20 clock cycles.

The simplest good predictor is the 2-bit saturating counter on the right of the board. Each taken branch moves it one step up, each not-taken one step down, and the top two states predict "taken". Watch it with jge: three times not taken (correct guesses), then taken, which it gets wrong: a flush. Real CPUs combine many such counters with the history of recent branches, and get the great majority right.

predictor.js

The same branch, the same numbers, a different order. Random order gets about half the guesses wrong; sorted order gets 2 wrong.

6×faster: the same C++ loop with an if (data[c] >= 128) took 11.54 s on random data and 1.93 s once the data was sorted, purely because the branch became predictable. It's one of the most up-voted questions on Stack Overflow. Stack Overflow question 11227809

Compilers and careful programmers sometimes remove the branch altogether, turning the decision into arithmetic. No jump, nothing to mispredict:

branchless.js

To watch a CPU fetch, decode and execute instructions one clock at a time, open Inside the CPU.

robot.js

Change commands and predict where the robot ends before you run it.

PRACTICE

Now you do it

Type the code yourself rather than pasting it: producing an answer builds memory that reading does not. Use the hints one at a time.

1

FizzBuzz

The classic. For the numbers 1 to 15, push "Fizz" into out if the number divides by 3, "Buzz" if it divides by 5, "FizzBuzz" if both, and otherwise the number itself as text.

exercise-1.js
2

Leap years, in one line of logic

A year is a leap year if it divides by 4, except years that divide by 100, unless they also divide by 400. So 2024 is, 2023 isn't, 1900 isn't, 2000 is. Make isLeap return the right true or false using &&, || and !, without any if.

exercise-2.js
3

Fix the off-by-one

This should add up the five prices and print 150. It prints NaN instead. Find the bug and fix it with the smallest change you can.

exercise-3.js
4

Stop as soon as you know

Set found to the index of the first negative number, and count in looked how many numbers the loop looked at. Use break so it stops at the first one: the answer is found = 3 after looking at 4 numbers.

exercise-4.js
5

switch with fall-through

Make kind(day) return "weekend" for "sat" and "sun", "weekday" for "mon" to "fri", and "not a day" for anything else. Use one switch, and let cases share code by falling through.

exercise-5.js
6

A triangle of stars

Use a loop inside a loop to build four rows: *, **, ***, ****. Push each row into rows. The inner loop has to know which row it is on.

exercise-6.js
7

Turn the recursion into a loop

sumTo adds 1 + 2 + … + n by calling itself. It works for 100 but runs out of stack for 100,000. Write sumLoop(n) that gives the same answers with a loop, so it works for both.

exercise-7.js

RECALL

Check yourself, from memory

Answer each one before you open it. Then come back tomorrow, and again next week, and answer them cold: retrieval spaced over days is the most reliable study method we know of. Why that works.

What are the three building blocks every structured program is made of?

In a && b, when is b never evaluated? And in a || b?

What happens in a switch when a case has no break?

What's the difference between break and continue?

An array has length 5. Which indexes are valid, and what loop test should you write?

What does an if statement become in machine code?

Why can sorting data make a loop with an if in it faster?

GO FURTHER

Resources

ON THIS SITE

See it running

SOURCES

Where the facts come from

  1. MDN: if...else
  2. MDN: Truthy and Falsy The full list of falsy values: false, 0, -0, 0n, "", null, undefined, NaN (and document.all).
  3. MDN: Logical AND (&&) Short-circuit evaluation; returns an operand, not necessarily a boolean.
  4. MDN: Logical OR (||)
  5. MDN: switch Strict equality matching and fall-through.
  6. MDN: for...of
  7. MDN: label
  8. ECMAScript spec: Binary Logical Operators The evaluation rules that make && and || short-circuit.
  9. ECMAScript spec: The switch Statement
  10. ECMAScript spec: Iteration Statements
  11. Böhm & Jacopini (1966), Flow diagrams, Turing machines and languages with only two formation rules Communications of the ACM 9(5). Sequence, selection and iteration are enough.
  12. Dijkstra (1968), Go To Statement Considered Harmful Communications of the ACM 11(3).
  13. Dijkstra (1982), Why numbering should start at zero (EWD831) The case for half-open ranges.
  14. Allen (1970), Control flow analysis SIGPLAN Notices 5(7): basic blocks and control-flow graphs for optimising compilers.
  15. Abelson & Sussman, SICP §1.2.1: Linear Recursion and Iteration
  16. PEP 634: Structural Pattern Matching Python's match statement, added in 3.10.
  17. Go spec: Switch statements and fallthrough
  18. Langley (2014), Apple's SSL/TLS bug The "goto fail" bug: an if without braces.
  19. NVD: CVE-2014-1266
  20. x86 reference: Jcc (jump if condition is met)
  21. x86 reference: CMP
  22. Agner Fog, The microarchitecture of Intel, AMD and VIA CPUs §3.1 the 2-bit saturating counter; §3.8 misprediction penalty of 15–20 clock cycles on Haswell to Skylake-class Intel cores; 15–18 on Zen 4.
  23. Yeh & Patt (1991), Two-level adaptive training branch prediction MICRO-24: the pattern-history predictors modern CPUs grew from.
  24. Stack Overflow: Why is processing a sorted array faster than an unsorted array? 11.54 s unsorted vs 1.93 s sorted for the same loop; one of the site's most up-voted questions.
  25. WebKit: ECMAScript 6 Proper Tail Calls in WebKit