Step 1 of 3 · Reading · ~2 min
Loops — Repeating Execution
Control Flow & Functions
While & For Loops
Your interpreter can already evaluate expressions and run straight-line statements. This lesson adds the two loop constructs every imperative language needs: while and for. The interesting design decision isn't how to execute a loop — it's how little new machinery you need if you desugar for into while.
The while statement
Add a While node to your statement AST with two fields: condition (an expression) and body (a statement):
Parsing is a direct translation of the grammar rule "while" "(" expression ")" statement. Interpreting it is just as direct — it's the first place your tree-walker actually loops instead of recursing once per node:
Reuse whatever _truthy logic you already wrote for if: in a Lox-style language, nil and false are falsy, everything else (including 0 and "") is truthy. Getting this rule consistent across if, while, and logical operators matters — learners will test edge cases like while (0) ... expecting it to loop, because 0 is truthy here.
Desugaring for into while
A C-style for (init; condition; increment) body doesn't need its own interpreter case at all. Parse it, then rewrite it as an equivalent block using nodes you already have:
{
init;
while (condition) {
body;
increment;
}
}
Concretely, in your parser's for_statement:
- Parse the optional
init(a var declaration, an expression statement, or nothing). - Parse the optional
conditionexpression; if omitted, default to a literaltrueso the loop doesn't terminate immediately. - Parse the optional
incrementexpression. - Parse
body. - If there's an increment, wrap
bodyin aBlock([body, ExpressionStatement(increment)]). - Wrap that in a
While(condition, body). - If there's an init, wrap the whole
Whilein aBlock([init, while_stmt]).
This is the single biggest lesson in "keep the interpreter small": every new piece of surface syntax doesn't need a new evaluation rule if it can be expressed in terms of constructs you already support.
Edge cases to watch
- Scoping: the
initvariable (e.g.var i = 0) must live in a scope that encloses the loop body but isn't visible after the loop — that's exactly what wrapping everything in an outerBlockgives you for free, since blocks already create a new environment. - Missing clauses:
for (;;) { ... }must parse (all three clauses optional) and behave as an infinite loop. - Increment always runs: even if the body contains a
continue-like early exit (if your language has one), the desugared form still executes the increment as the last statement of the body block on every iteration.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…