Step 1 of 3 · Reading · ~2 min
From Expressions to Statements
Statements & State
Expressions Produce Values; Statements Produce Effects
Everything you've built so far — the scanner, the precedence ladder, the AST, the evaluator — deals with expressions: things that reduce to a value. But a real program is a sequence of statements: things that do something (print output, declare a variable, run a loop) and don't themselves have a value. This lesson introduces the statement/expression split that the rest of the language is built on.
Two Statement Forms, One New Grammar Layer
program → statement* EOF
statement → exprStmt | printStmt
exprStmt → expression ";"
printStmt → "print" expression ";"
A program is just a flat list of statements, each terminated by ;. There are two kinds so far:
- Expression statement: an expression followed by
;, evaluated purely for its side effects (which, right now, means "none" — but this shape becomes essential once function calls and assignments exist, sincefoo();is a statement that discards its return value). - Print statement: the keyword
printfollowed by an expression and;— evaluates the expression and writes the result to output.
Parsing Statements: One Level Above Expressions
The parser gets a new entry point above expression():
Note the shape: statement() looks at the first token to decide which statement rule applies (here, just checking for the print keyword), then delegates. This "peek at the front, dispatch, delegate" pattern is exactly how you'll add every future statement type (var, if, while, for, blocks) — each one gets a leading keyword or token that statement() checks for, with a fallback to expression_statement() when nothing matches.
Two New AST Node Types
These look nearly identical, but they mean different things to the interpreter: executing a Print node evaluates its expression and writes the result; executing an ExpressionStmt evaluates its expression and discards the result. The distinction matters even though both currently just call evaluate() — it's the hook where "does this produce visible output or not" gets decided.
The Top-Level Loop
The interpreter's entry point becomes: parse the whole token stream into a list of statements (not a single expression), then execute them one at a time in order:
Watch for the stringify detail: Lox-style languages print nil for None, drop the .0 from whole-number floats (3 not 3.0), and print booleans as true/false rather than Python's True/False. Getting this output formatting exactly right is what separates "the interpreter works" from "the interpreter's test output matches."
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…