38. Control flow
Drip learns to choose and to repeat. Two of the three new statements are a few lines each, borrowed from TypeScript, and the third never reaches the interpreter at all.
- Implement if and while in a tree-walking interpreter, using truthiness.
- Desugar a for loop into a block, a while loop and another block, and explain each wrapper.
- Explain why and and or are not ordinary binary operators, and predict the value they produce.
A program that can’t change its mind
Every Drip program so far runs straight down the page. Each statement
runs exactly once, in order, and then the program ends. You can’t print
the numbers 1 to 100 without writing 100 print statements, and you
can’t do something only sometimes.
This chapter adds the three statements that fix that: if, while and
for. Plus two operators, and and or, which turn out to be control
flow in disguise.
Choosing: if
The interpreter is a reader working through a recipe. An if is a step
that says “if the oven is hot, do this; otherwise, do that”. The reader
checks once, follows one branch, and never even reads the other.
The parser already turns if (c) A else B into a node with three
children: condition, then and an optional else. Running it takes
three lines of the companion’s interpreter.ts:
case 'if': {
const c = this.evaluate(s.condition);
if (isTruthy(c)) this.execute(s.then);
else if (s.else) this.execute(s.else);
return;
}
Evaluate the condition, ask whether it’s truthy with chapter 35’s
isTruthy (only nil and false are falsey), and execute one of the
branches. The other branch’s tree is never visited, so a runtime error
hiding in it can’t happen.
Notice the trick: Drip’s if is implemented with TypeScript’s if. The
interpreter makes a decision in its own language whenever the program
it’s running makes one.
One parsing detail: in if (a) if (b) x; else y; the else belongs to
the inner if. The inner ifStatement call parses its then-branch and
then greedily takes any else that follows, before the outer one gets a
look.
Repeating: while
while has the same shape, and borrows TypeScript’s while
(chapter 5)[2]:
case 'while':
while (isTruthy(
this.evaluate(s.condition))) {
this.execute(s.body);
}
return;
The condition is re-evaluated before every trip round the loop, so the
body must change something the condition reads, or the loop never stops.
(Drip has no break, so the condition is the only way out, for now.)
Control flow is the order in which a program’s statements run.
if and while make that order depend on runtime values: which
statement runs next is decided by evaluating a condition and testing
its truthiness. A tree-walking interpreter implements each of
them with the matching control flow of its host language.
The loop that isn’t there: for
A for loop packs four things into one line:
for (let i = 0; i < 3; i = i + 1)
print i;
You could add a fifth kind of statement and a new case to the
interpreter. But look at what it means: run the initializer once, then
while the condition holds, run the body and then the increment. That is
a while loop you already know how to run. So the parser rewrites it
into one, and the interpreter never hears the word for:
for (let i = 0; i < 3; i = i + 1)print i;
Here is the end of forStatement in parser.ts, lightly simplified
(block and exprStmt stand for the plain node objects the real code
spells out):
let body = statement();
if (increment) {
body = block([body,
exprStmt(increment)]);
}
body = { kind: 'while',
condition, body };
if (initializer) {
body = block([initializer, body]);
}
return body;
Each part is optional. With no increment, the body is left alone; with
no initializer, there’s no outer block. A missing condition, as in
for (;;), becomes the literal true: a loop that runs forever.
The outer block matters. Because it gets its own environment (chapter
37), the loop’s i lives only as long as the loop, and it can shadow an
i outside:
let i = "before";
for (let i = 0; i < 3; i = i + 1) {
print i;
}
print i; // before
Desugaring means rewriting a construct in terms of simpler ones
the language already has. The convenient form is syntactic sugar.
Drip’s parser desugars for into a block containing the initializer
and a while loop whose body is a block of the original body and the
increment. Every pass that walks the tree afterwards (the interpreter,
and the resolver of chapter 42) gets for loops for free.
Desugaring is only correct if the rewrite means exactly the same
thing. Drip has no continue. If it did, this rewrite would break: a
continue jumping to the end of the body would also skip the increment,
which lives in the same block. Real languages with continue have to
handle the increment more carefully.
and, or: operators that skip
and and or look like binary operators, and they sit in the Pratt
table with binding powers like the rest (and binds tighter than or).
But the binary case evaluates both sides before it looks at the
operator. For these two, that’s wrong:
let ok = false and (calls = calls + 1);
Once the left side is false, the answer is known, and the assignment
on the right must not happen. So the parser makes a separate logical
node, and the interpreter treats it like a tiny if:
case 'logical': {
const l = this.evaluate(e.left);
if (e.operator.type === 'or') {
return isTruthy(l)
? l : this.evaluate(e.right);
}
return isTruthy(l)
? this.evaluate(e.right) : l;
}
Look at what comes back: not true or false, but one of the operand
values. nil or "default" is "default", and 1 and 2 is 2.
JavaScript’s || and && behave the same way[3].
let calls = 0; let ok = false and (calls = calls + 1); print ok; print calls; print nil or "default"; print 1 and 2 and 3; print 0 or "zero is truthy";
Short-circuit evaluation means an operator skips evaluating its
right operand when the left one already decides the result. In Drip,
a or b returns a if it is truthy, and otherwise evaluates and
returns b. a and b returns a if it is falsey, and otherwise
evaluates and returns b. The skipped side’s side effects never
happen.
The tree-walker gets its control flow for free by borrowing
TypeScript’s if and while. The bytecode VM in Part IV can’t: it
runs a flat list of instructions with no nesting at all. In chapter 55
its compiler turns if, while, for, and and or into
jumps: instructions that say “if the value on top of the stack is
falsey, continue at instruction 42”. Short-circuiting is then just a
jump over the right operand’s code. The approach here, including
desugaring for in the parser, follows Crafting
Interpreters[1].
Your turn
The core exercise is the desugaring itself, in plain JavaScript.
Write desugarFor(f), the rewrite Drip's parser does. f has four parts: init (a statement or null), cond (an expression or null), incr (an expression or null) and body (a statement). Return the equivalent tree:
- If there is an
incr, the body becomes{ kind: 'block', statements: [body, { kind: 'expression', expression: incr }] }. - Wrap that in
{ kind: 'while', condition, body }. A missingcondbecomes{ kind: 'literal', value: true }. - If there is an
init, wrap everything in{ kind: 'block', statements: [init, theWhile] }.
function desugarFor(f) {
// f has init, cond, incr, body.
// Return a block / while tree.
return null;
}Write a Drip for loop that prints every power of two below 100, one per line, starting from 1: 1, 2, 4, ... 64.
// One for loop. Its increment // doesn't have to add 1.
What you learned
- Control flow is the order statements run in.
ifruns one branch andwhilere-checks its condition before every trip, both using truthiness, both borrowed from TypeScript. - Desugaring rewrites a construct into simpler ones. The parser turns
forinto{ init; while (cond) { body; incr } }, and a missing condition becomestrue. - The outer block keeps the loop variable local to the loop.
andandorshort-circuit: they skip the right side when the left decides, and they return an operand’s value, not a boolean.
Next: functions, the last thing Drip needs before it can run real programs.
Check your understanding
0/6- 1.
What does this print?
if (0) print "yes"; else print "no"; - 2.
Which tree does Drip’s parser build for
for (let i = 0; i < 3; i = i + 1) print i;? - 3.
What does this print?
let i = "out"; for (let i = 0; i < 2; i = i + 1) {} print i; - 4.
print nil or false;What is printed? - 5.
After this runs, what is
n?let n = 0; true or (n = 1); false and (n = 2); - 6.
Why does the tree-walker need hardly any code for
ifandwhile?
References
- [1] Robert Nystrom (2021). Crafting Interpreters. Free web edition at craftinginterpreters.com (© 2015–2021); source, text and test suite at github.com/munificent/craftinginterpreters. github.com/munificent/craftinginterpreters (checked 2026-10-31)
- [2] MDN contributors. while. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/while (checked 2026-10-31)
- [3] MDN contributors. Logical OR (||). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR (checked 2026-10-31)