Technopedia

The technologist’s encyclopedia

Control Flow and Semantics

Control flow and semantics define what a program means: how statements execute, how variables are bound, how functions compose, and how expressions are evaluated. They are relevant whenever a program’s behavior must be predicted, reasoned about, or verified. This page covers the control-flow constructs (conditionals, loops, exceptions), the binding and scoping rules that give variables meaning, the evaluation strategies that determine when and how expressions are reduced, and the formal semantics frameworks that make all of this precise.

Authoritative sources

Sequencing, state, and assignment

Every imperative language rests on two primitive rules. Skip (skip\text{skip}) does nothing: it is the identity on state. Sequencing (S1;S2S_1; S_2) composes two statements: evaluate S1S_1 to an intermediate state σ′\sigma', then evaluate S2S_2 in σ′\sigma'. A state σ\sigma is a mapping from variables to values (σ:Var→Val\sigma : \text{Var} \to \text{Val}), and assignment x:=ex := e is a state update: it produces σ[x:=v]\sigma[x := v], the state identical to σ\sigma except that xx now maps to the value vv of ee.

Details

These three notions — state, skip, composition — are the foundation on which every other control-flow rule builds: if and while are just different ways of choosing which sequence of assignments to run.

The state: think of σ\sigma as a table with one row per variable currently in scope. There is exactly one cell per binding; assignment overwrites the cell’s content, it never creates a second cell. (Where those cells live physically — stack frame or heap — is covered in Memory Management.)

Assignment: σ[x:=v]\sigma[x := v] is defined pointwise: it agrees with σ\sigma on every variable except xx, where it maps x↦vx \mapsto v. All other bindings are untouched — this is what makes assignment local.

Skip: skip\text{skip} leaves the state unchanged: σ⊢skip→σ\sigma \vdash \text{skip} \to \sigma. It is the identity element of sequencing: S;skipS; \text{skip} and skip;S\text{skip}; S behave exactly like SS. This is why a for loop with an empty body is legal and means “repeat the condition test only”.

Composition: S1;S2S_1; S_2 is evaluated left to right:

σ⊢S1→σ′σ′⊢S2→σ′′σ⊢S1;S2→σ′′\frac{\sigma \vdash S_1 \to \sigma' \quad \sigma' \vdash S_2 \to \sigma''}{\sigma \vdash S_1; S_2 \to \sigma''}

Specific example: take the program x:=3;  y:=x+1x := 3;\; y := x + 1 starting from the empty state σ0={}\sigma_0 = \{\}:

  1. Evaluate x:=3x := 3: the state becomes σ1=σ0[x:=3]={x↦3}\sigma_1 = \sigma_0[x := 3] = \{x \mapsto 3\}.
  2. Evaluate y:=x+1y := x + 1 in σ1\sigma_1: look up xx (yields 3), compute 4, and the state becomes σ2=σ1[y:=4]={x↦3,  y↦4}\sigma_2 = \sigma_1[y := 4] = \{x \mapsto 3,\; y \mapsto 4\}.

The order matters: had the program been y:=x+1;  x:=3y := x + 1;\; x := 3, the lookup of xx in step 2 would fail (or yield a default), which is why sequencing is not commutative.

σ = { x ↦ 2, y ↦ 5 } x: 2 y: 5 x := 7 only the x cell changes σ[x := 7] = { x ↦ 7, y ↦ 5 } x: 7 y: 5 skip: σ[skip] = σ (identity) seq: S1;S2 = run S1 to σ′, then S2 in σ′

Authoritative source: The Formal Semantics of Programming Languages (Winskel, 1993) — chapters 1–3 build everything from exactly these rules.

Example problem

Starting from the empty state, what is the final state of x:=2;  x:=x⋅x;  y:=x+xx := 2;\; x := x \cdot x;\; y := x + x? Which single statement could you replace with skip without changing the result?

Solution
  • x:=2x := 2 gives σ1={x↦2}\sigma_1 = \{x \mapsto 2\}.
  • x:=x⋅xx := x \cdot x looks up 2, computes 4, gives σ2={x↦4}\sigma_2 = \{x \mapsto 4\}.
  • y:=x+xy := x + x looks up 4 twice, gives σ3={x↦4,  y↦8}\sigma_3 = \{x \mapsto 4,\; y \mapsto 8\}.

The first statement x:=2x := 2 can be replaced by skip only if xx already holds 2 in the initial state; from the empty state, no statement can be removed — each assignment feeds the next. This shows sequencing is genuinely order-dependent: removing or reordering any SiS_i changes the intermediate states that later statements read.

Conditionals and loops

if: if b then S1 else S2\text{if } b \text{ then } S_1 \text{ else } S_2 executes S1S_1 if bb is true, else S2S_2. while: while b do S\text{while } b \text{ do } S repeats SS while bb is true. for: a bounded loop with three components — initialization, condition, and update — that desugars directly to a while loop.

Details

Conditionals and loops are derived rules: each one selects which sequence of assignments (the primitive from the previous section) to run, and how many times.

Short-circuit evaluation: a&&ba \&\& b evaluates bb only if aa is true; a∥∥ba \|\| b evaluates bb only if aa is false. This is a semantic choice, not a syntactic one.

The for loop desugared: the C-style for (init; cond; update) body is syntactic sugar for:

init;
while (cond) {
  body;
  update;
}

“Bounded” means the loop’s lifetime is controlled by three explicit parts: init runs once before the first test, cond is checked before every iteration, and update runs after each iteration. This contrasts with an unbounded while where the programmer must manage the counter manually.

Loop invariants: a predicate PP that holds before and after each iteration. Proving PP holds at loop exit (when bb is false) establishes the loop’s postcondition.

Tail position and tail recursion: a call is in tail position when no operation is pending after it — the caller’s frame has nothing left to do, so it can be discarded before the call executes. A function whose recursive calls are all in tail position is tail-recursive and can be compiled to a loop. Whether a runtime actually performs this tail-call optimization is a property of the implementation, not the language semantics; the mechanism (stack frames, stack overflow) is covered in Memory Management.

Example problem

In f(n) { if (n == 0) return 0; return 1 + f(n - 1); }, is the recursive call in tail position? What minimal rewrite makes it so?

Solution

No: the call’s result is used by the pending operation 1 + …, so the caller’s frame must survive the call. Moving the pending addition into an accumulator parameter — f(n, acc) { if (n == 0) return acc; return f(n - 1, acc + 1); } — leaves nothing pending after the call, putting it in tail position. See Memory Management for how this eliminates stack growth.

Variables, scope, and closures

A variable is a named binding in an environment. Scope determines where a binding is visible. Closures capture the environment in which a function is defined, allowing the function to access variables that are no longer in scope. The physical layout of environments and stack frames is covered in Memory Management.

Details

Lexical scope (static): a variable’s scope is determined by its textual position. letx=1;letf=()=>x;letx=2;f()//returns1let x = 1; let f = () => x; let x = 2; f() // returns 1.

Dynamic scope: a variable’s scope is determined by the call stack. Rare in modern languages (Scheme’s dynamic-wind, R5RS+; Common Lisp uses unwind-protect instead).

The difference is easiest to see when the same program is resolved both ways:

let x = 1; function outer() { let x = 2; function inner() { return x; } return inner; } function caller() { let x = 3; return outer()(); // inner called here } outer's x = 2 caller's x = 3 lexical: inner's x resolves where inner is WRITTEN → 2 dynamic: resolves where inner is CALLED → 3

Closures: letmakeCounter=()=>letn=0;return()=>n++;returnn;;;letc=makeCounter();c();//1,c();//2let makeCounter = () => { let n = 0; return () => { n++; return n; }; }; let c = makeCounter(); c(); // 1, c(); // 2. The inner function captures the mutable cell for nn. The environment cell is shared: every call to c() mutates the same cell.

inc () dec () n = 0 ← ONE shared cell, captured by both closures inc() → 1, inc() → 2, dec() → 1: both closures read and write the same cell.

Hoisting: in JavaScript, variable declarations (var) and function declarations are moved to the top of their scope before execution. let and const are hoisted but not initialized (temporal dead zone).

Connection to the formal model: this is the state-and-assignment model of the sequencing section with one addition — an environment layer mapping names to cells, so that two names may share one cell (closures) or one name may denote different cells in different scopes (let per iteration).

Authoritative source: ECMAScript Language Specification — Lexical Environments.

Example problem

Under dynamic scoping, what does the scope-nesting program above print when caller() runs? Why do most modern languages reject dynamic scoping?

Solution

It returns 3: inner is called from caller, so dynamic resolution picks caller‘s x. Dynamic scoping is rejected because a function’s meaning then depends on its callers — you cannot reason about a function in isolation, and renaming a variable in any caller can silently change the callee’s behavior. Lexical scope makes every function’s meaning fixed at its definition site, which is what enables local reasoning and separate compilation.

Functions, composition, and currying

A function maps inputs to outputs. Recursion is a function that calls itself. Higher-order functions take functions as arguments or return functions: map, filter, reduce, fold. The type system that constrains which functions can be composed is studied in Language Theory.

Details

Composition: (g∘f)(x)=g(f(x))(g \circ f)(x) = g(f(x)), type (A→B)→(B→C)→(A→C)(A \to B) \to (B \to C) \to (A \to C). Composition is associative — h∘(g∘f)=(h∘g)∘fh \circ (g \circ f) = (h \circ g) \circ f — which is why pipelines of transformations can be regrouped freely.

Partial application: fixing one argument of f:A×B→Cf: A \times B \to C yields f′:A→(B→C)f': A \to (B \to C).

Currying: f:A×B→Cf: A \times B \to C is equivalent to f′:A→(B→C)f': A \to (B \to C). f(a,b)=f′(a)(b)f(a, b) = f'(a)(b). Currying is what makes partial application uniform: every function of several arguments is secretly a function of one argument returning a function.

Specific example: with add = a => b => a + b, the partially applied add(3) is the function b↦3+bb \mapsto 3 + b, and map(add(3), [1, 2, 3]) yields [4, 5, 6] — no new machinery needed, just composition of an existing function with map.

Recursion schemes: catamorphisms (folds) and anamorphisms (unfolds) generalize recursion over inductive data types. A left fold (the default reduce in Python, JavaScript, and Haskell’s foldl):

foldl(f,z,[a1,…,an])=f(…f(f(z,a1),a2)…,an)\text{foldl}(f, z, [a_1, \ldots, a_n]) = f(\ldots f(f(z, a_1), a_2) \ldots, a_n)

A right fold (Haskell’s foldr):

foldr(f,z,[a1,…,an])=f(a1,f(a2,…f(an,z)…))\text{foldr}(f, z, [a_1, \ldots, a_n]) = f(a_1, f(a_2, \ldots f(a_n, z) \ldots))

The left fold is strict in the accumulator; the right fold can be lazy and may terminate on infinite lists.

Authoritative source: Haskell Report, section on folds.

Example problem

Why does foldr terminate on the infinite list [1, 2, 3, ...] for foldr (\x acc -> if x > 2 then acc else x : acc) [] while foldl with the same arguments never produces a result?

Solution

foldr is defined as f(a1,foldr(f,z,rest))f(a_1, \text{foldr}(f, z, \text{rest})): it produces the outermost application f(1,…)f(1, \ldots) before forcing the rest of the list. Here ff returns x : acc without inspecting acc when x>2x > 2 is false… more precisely, for each x≤2x \leq 2 it conses and only the tail remains unevaluated; once x>2x > 2, ff returns acc without forcing the tail, so the infinite remainder is never demanded. foldl is f(foldl(f,z′,rest),a)f(\text{foldl}(f, z', \text{rest}), a): the recursive call on the rest of the list is the first argument, so any result requires the whole infinite list to be consumed first. This strictness asymmetry is exactly the call-by-need behavior discussed in the next section.

Functors, applicatives, and monads

Functors, applicatives, and monads generalize plain function composition to computations wrapped in a context (optionality, lists, effects). They are the category-theory layer above the composition and currying of the previous section.

The progression: a plain function maps A→BA \to B; a functor maps that function over a wrapper FF; an applicative applies a wrapped function to a wrapped value; a monad chains when the next step depends on the previous result.

Details

Global picture: each layer adds one degree of dependency. Functor: the function is total and independent per element. Applicative: the shape of the computation is fixed in advance (all arguments known up front). Monad: the shape may depend on intermediate results — which is why monads can model effects like exceptions (stop early) and state (thread σ\sigma through).

Specific example: parsing two integers where the second parse depends on the first requires a monad (>>=); parsing two independent integers needs only an applicative (<*>), which is why applicative parsers can be parallelized and monadic ones generally cannot.

Where to go next: the practical, pattern-level uses of these abstractions (Option chaining, Result propagation, promise chaining as a monad) with worked code are treated in Design Patterns; this page fixes only the type signatures and the progression.

Authoritative source: Applicative Programming with Effects (McBride & Paterson, 2008) and Monads for Functional Programming (Wadler, 1995).

Example problem

Given safeDiv : Int -> Int -> Maybe Int, why can safeDiv 6 3 be written with fmap but divAll : Int -> [Int] -> Maybe [Int] (divide n by every element, failing if any division fails) needs a monad?

Solution

safeDiv 6 3 applies a known function to known arguments; only the result is wrapped, so fmap suffices. In divAll, whether to continue with the next element depends on the result of the previous division — the structure of the computation depends on intermediate values. That dependency is precisely what >>= adds over <*>: applicative computations have a fixed shape, monadic ones do not.

Evaluation strategies

An evaluation strategy specifies the order in which sub-expressions are reduced and when function arguments are evaluated. This is a semantic choice that affects both performance and expressiveness.

Details

Call-by-value (strict / eager): arguments are fully evaluated before the function body executes. This is the strategy of C, Java, Python, and most imperative languages. Side effects in arguments are visible to the callee.

Call-by-name (lazy): arguments are substituted unevaluated into the body and evaluated only when needed. This can avoid unnecessary computation but may re-evaluate the same expression multiple times.

Call-by-need (lazy with memoization): like call-by-name, but each argument is evaluated at most once and the result is cached. Haskell’s default strategy. This gives the efficiency of strict evaluation with the laziness of call-by-name.

A concrete comparison: take f(x)=x+xf(x) = x + x and call it with a side-effecting argument — a counter increment:

let counter = 0;
function inc() { counter++; return counter; }
function f(x) { return x + x; }
f(inc());

The comparison, rendered as raw HTML (LaTeXMarkdown does not render pipe tables):

StrategyWhat happensResultFinal counter
Call-by-valueinc() evaluated once, value 1 passed to the body21
Call-by-nameinc() substituted, giving inc() + inc(); each occurrence evaluated when needed1 + 2 = 32
Call-by-needinc() evaluated once (value 1), result cached and reused21

Call-by-name is the only strategy where the argument’s side effect happens twice — and it happens interleaved with the body’s execution rather than before it, which is why call-by-name programs with effects are notoriously hard to reason about. Call-by-need restores the “evaluated once” guarantee while keeping laziness.

Evaluation order: even in strict languages, the order in which arguments are evaluated is often unspecified (C) or left-to-right (Java, Python). This matters when arguments have side effects.

Short-circuit vs. full evaluation: a&&ba \&\& b and a∥∥ba \|\| b are inherently non-strict in their second operand — a special case of call-by-name embedded in an otherwise strict language.

The choice of strategy interacts with the type system (see Language Theory) and with the compiler’s code generation (see Compilation): a lazy language requires the runtime to allocate thunks (unevaluated expressions) on the heap.

Authoritative source: Report on the Algorithmic Language Haskell 2010 — semantics of lazy evaluation.

Example problem

Under call-by-name, f(inc()) above returns 3 and leaves counter = 2. What does g(x) = 1 return under each strategy when called as g(inc()), and what is counter afterwards?

Solution

All three strategies return 1, but the side effects differ. Call-by-value evaluates inc() before the call: counter = 1. Call-by-name substitutes the argument into the body 1 — the argument never appears, so it is never evaluated: counter = 0. Call-by-need also defers evaluation, and since the thunk is never demanded, counter = 0. This is the key expressiveness difference: non-strict strategies can discard unused arguments entirely, which strict evaluation cannot.

Exceptions and non-local exits

Exceptions are a control-flow mechanism that transfers control from a throwing site to the nearest enclosing handler, bypassing intermediate frames. They generalize the break/return pattern to arbitrary nesting depths.

Details

try / catch / finally:

try {
  S;            // may throw
} catch (e: T) {
  H;            // handler for type T
} finally {
  C;            // cleanup, runs whether or not an exception was thrown
}

The finally block runs on normal completion, on exception, and on non-local exit (e.g., return inside try). This makes it the canonical place for resource cleanup.

Non-local exits: return, break, continue, and exceptions all transfer control out of the current frame without executing the remaining statements. The key semantic question is: which cleanup code runs? In most languages, finally blocks and destructors run in reverse order of entry as the stack unwinds.

Stack before the throw main [has catch] readFile [finally] parse [throws] throw! unwind: parse's frame popped, its finally runs first Cleanup order during unwinding 1. parse: finally { close(file) } ← runs 2. readFile: frame popped, locals freed 3. main: catch (e) { handle } ← lands Cleanup runs in reverse order of entry, like popping nested parentheses. No handler anywhere → the program terminates after running all finallys. The exception does not "break" scope: each unwound frame runs the same cleanup (finally blocks, destructors, variable deallocation) that a normal return would — only the control-transfer target differs.

Interaction with scope: an exception does not “break” scope — it triggers the same cleanup (variable deallocation, finally blocks) that a normal return would, but the control transfer target is the handler, not the caller.

Scheme’s dynamic-wind (R5RS+) and Common Lisp’s unwind-protect provide explicit before/after guards that run on any exit (normal or exceptional), making the cleanup contract explicit rather than implicit.

Checked vs. unchecked exceptions: Java distinguishes checked (must be declared or caught) from unchecked (runtime) exceptions. C++ uses noexcept specifications. Python has no checked exceptions. The choice affects the type system and the compiler’s code generation.

Authoritative source: R5RS Scheme — control features (dynamic-wind).

Example problem

In the stack-unwinding diagram above, suppose readFile’s finally block itself throws. Which handler catches that new exception, and what happens to the original exception from parse?

Solution

The new exception propagates from readFile’s frame upward — it is not caught by main’s catch in the sense of resuming the original flow; rather, the original exception from parse is discarded (in most languages: Java, JavaScript, Python), and the new exception continues unwinding from readFile. main‘s handler receives the second exception. This “exception during cleanup” problem is why some languages (e.g., Python’s raise ... from, Java’s suppressed exceptions in try-with-resources) provide a way to chain or suppress the original exception instead of losing it.

Operational and denotational semantics

Formal semantics gives a mathematical definition of what a program means, independent of any particular implementation. The primitives it must define first are exactly the ones from the sequencing section: the state σ\sigma, assignment σ[x:=v]\sigma[x := v], skip, and composition S1;S2S_1; S_2.

Details

Operational semantics defines meaning as a sequence of state transitions:

Denotational semantics maps each syntactic construct to a mathematical object (usually a function between domains):

Axiomatic semantics (Hoare logic) specifies meaning as pre/postcondition triples: {P}  S  {Q}\{P\} \; S \; \{Q\} means “if PP holds before SS and SS terminates, then QQ holds after.” This is the basis for formal verification.

The two primitive rules, formally:

σ⊢skip→σσ⊢S1→σ′σ′⊢S2→σ′′σ⊢S1;S2→σ′′\sigma \vdash \text{skip} \to \sigma \qquad \frac{\sigma \vdash S_1 \to \sigma' \quad \sigma' \vdash S_2 \to \sigma''}{\sigma \vdash S_1; S_2 \to \sigma''}

skip is the identity on state; S1;S2S_1; S_2 composes by evaluating S1S_1 to a state σ′\sigma', then S2S_2 in σ′\sigma'. Every other rule (if, while, exceptions) is defined on top of these.

Worked example: take the tiny program x:=3;  y:=x+1x := 3;\; y := x + 1 from the empty state σ0={}\sigma_0 = \{\}.

(a) Big-step derivation — one judgment per compound command, bottom-up:

σ0⊢x:=3⇓σ1σ1⊢y:=x+1⇓σ2σ0⊢x:=3;  y:=x+1⇓σ2\frac{\sigma_0 \vdash x := 3 \Downarrow \sigma_1 \qquad \sigma_1 \vdash y := x + 1 \Downarrow \sigma_2}{\sigma_0 \vdash x := 3;\; y := x + 1 \Downarrow \sigma_2}

where σ1={x↦3}\sigma_1 = \{x \mapsto 3\} and σ2={x↦3,  y↦4}\sigma_2 = \{x \mapsto 3,\; y \mapsto 4\}. The premise on the left is discharged by the assignment axiom σ⊢x:=3⇓σ[x:=3]\sigma \vdash x := 3 \Downarrow \sigma[x := 3]; the right premise needs the expression sub-derivation σ1⊢x+1⇓4\sigma_1 \vdash x + 1 \Downarrow 4 before the assignment axiom applies.

(b) Small-step derivation — a linear trace of atomic transitions on configurations ⟨S,σ⟩\langle S, \sigma \rangle:

⟨x:=3;  y:=x+1,  σ0⟩\langle x := 3;\; y := x + 1,\; \sigma_0 \rangle
→⟨y:=x+1,  {x↦3}⟩\to \langle y := x + 1,\; \{x \mapsto 3\} \rangle
→⟨skip,  {x↦3,  y↦4}⟩\to \langle \text{skip},\; \{x \mapsto 3,\; y \mapsto 4\} \rangle
→{x↦3,  y↦4}\to \{x \mapsto 3,\; y \mapsto 4\}

Each arrow is one rule application: the sequencing rule exposes x:=3x := 3 first, the assignment axiom fires, then the same happens for y:=x+1y := x + 1, and finally the skip rule terminates the trace.

(c) Denotational mapping — no states are “reached”; the program is a function:

⟦x:=3⟧=λσ.  σ[x:=3]\llbracket x := 3 \rrbracket = \lambda \sigma.\; \sigma[x := 3]
⟦S1;S2⟧=⟦S2⟧∘⟦S1⟧\llbracket S_1; S_2 \rrbracket = \llbracket S_2 \rrbracket \circ \llbracket S_1 \rrbracket
⟦x:=3;  y:=x+1⟧ σ0=σ0[x:=3][y:=4]={x↦3,  y↦4}\llbracket x := 3;\; y := x + 1 \rrbracket\, \sigma_0 = \sigma_0[x := 3][y := 4] = \{x \mapsto 3,\; y \mapsto 4\}

The three frameworks agree on the outcome (σ2\sigma_2), but they expose different information: the big-step derivation shows why the result holds, the small-step trace shows each intermediate state (essential for concurrency), and the denotation shows the program as a composable function, enabling algebraic laws such as ⟦S;skip⟧=⟦S⟧\llbracket S; \text{skip} \rrbracket = \llbracket S \rrbracket.

Small-step: a chain of atomic transitions ⟨ S1;S2 , σ ⟩ ⟨ S2 , σ′ ⟩ σ″ each → is one rule; intermediate states σ′ are visible Big-step: one judgment for the whole evaluation σ ⊢ S1;S2 ⇓ σ″ no intermediate states — result only Small-step suits concurrency and nontermination; big-step suits termination proofs.

The three approaches are equivalent in expressive power for sequential programs but differ in what they make easy to reason about: operational → execution traces; denotational → algebraic laws; axiomatic → correctness proofs.

Authoritative source: The Formal Semantics of Programming Languages (Winskel, 1993).

Example problem

Write the small-step trace of x:=0;  while (x<2) do x:=x+1x := 0;\; \text{while } (x < 2) \text{ do } x := x + 1 from the empty state, down to the first three transitions and the final state.

Solution

⟨x:=0;  while x<2 do x:=x+1,  {}⟩\langle x := 0;\; \text{while } x < 2 \text{ do } x := x + 1,\; \{\} \rangle
→⟨while x<2 do x:=x+1,  {x↦0}⟩\to \langle \text{while } x < 2 \text{ do } x := x + 1,\; \{x \mapsto 0\} \rangle (assignment axiom)
→⟨x:=x+1;  while x<2 do x:=x+1,  {x↦0}⟩\to \langle x := x + 1;\; \text{while } x < 2 \text{ do } x := x + 1,\; \{x \mapsto 0\} \rangle (while unrolls when the guard holds)
→⟨while x<2 do x:=x+1,  {x↦1}⟩\to \langle \text{while } x < 2 \text{ do } x := x + 1,\; \{x \mapsto 1\} \rangle (assignment; the skip after the unrolled body is elided)
→⟨x:=x+1;  while …,  {x↦1}⟩→⟨while …,  {x↦2}⟩\to \langle x := x + 1;\; \text{while } \ldots,\; \{x \mapsto 1\} \rangle \to \langle \text{while } \ldots,\; \{x \mapsto 2\} \rangle
Now the guard x<2x < 2 is false, so the while rule yields skip, and ⟨skip,{x↦2}⟩→{x↦2}\langle \text{skip}, \{x \mapsto 2\} \rangle \to \{x \mapsto 2\}. Final state: {x↦2}\{x \mapsto 2\}.

The interactive simulation below steps through a for loop’s phases (init, condition, body, update) and shows how two closures share one environment cell — the concrete counterpart of the sequencing and closure sections above.

Quiz

Quiz

Problem: Explain why the following JavaScript code prints 3, 3, 3 and not 0, 1, 2:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
Solution

The code prints 3, 3, 3 because var is function-scoped (not block-scoped). All three closures capture the same binding of i. By the time the setTimeout callbacks execute (after 100 ms), the loop has already finished and i === 3.

In the terms of the Variables, scope, and closures section: the var binding creates one environment cell for the entire function scope, so all three closures capture that same cell; let creates a fresh cell per block iteration, so each closure captures its own. The loop’s update statement (i++i{+}{+}) is an assignment σ[i:=v]\sigma[i := v] on that shared cell — exactly the state-update semantics of the sequencing section — and each callback reads the cell only after the loop has written 3 into it.

To print 0, 1, 2, use let (block-scoped): each iteration creates a new binding for i, and each closure captures its own copy:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// prints 0, 1, 2

Or use an IIFE to create a new scope per iteration:

for (var i = 0; i < 3; i++) {
  (function(j) { setTimeout(() => console.log(j), 100); })(i);
}
// prints 0, 1, 2