35. Evaluating expressions
The parser hands over a tree. Now it finally does something: one recursive function walks it from the leaves up and works out what it is worth, and stops with a clear message when the types make no sense.
- Evaluate an expression tree by hand, children before parents.
- Say how each kind of Drip value is represented in the TypeScript interpreter.
- Predict which Drip values count as true, and how that differs from JavaScript.
- Explain when Drip raises a runtime error, and read the message it prints.
A tree is not an answer
After eight chapters of scanning and parsing, 1 + 2 * 3 - 4 has become
a beautiful tree: (- (+ 1 (* 2 3)) 4). But nobody asked for a tree.
They asked for 3. The parser is the front end of the pipeline. This
chapter starts the back end, and in a tree-walking interpreter the back
end is surprisingly small.
Walking the tree
Picture a company asked for its total sales. The boss can’t answer alone, so she asks her two managers. Each manager asks their own team. The people at the bottom just know their number and say it. Then the answers flow back up, each manager adding up what they heard, until the boss has the total. Nobody answers before everyone below them has.
That is the whole algorithm. A node’s value depends only on its children’s values, so ask the children first, then combine:
function evaluate(e) {switch (e.kind) {case 'literal':return e.value;case 'binary': {const l = evaluate(e.left);const r = evaluate(e.right);return apply(e.operator, l, r);}}}
Here is the core of it, simplified from evaluate in
companion/ts/src/interpreter.ts:
function evaluate(e: Expr): Value {
switch (e.kind) {
case 'literal':
return e.value;
case 'grouping':
return evaluate(e.expression);
case 'unary': {
const right = evaluate(e.right);
if (e.operator.type === '!')
return !isTruthy(right);
return -num(e.operator, right);
}
case 'binary': {
const l = evaluate(e.left);
const r = evaluate(e.right);
return binary(e.operator, l, r);
}
}
}
One case per kind of node, and TypeScript’s narrowing on kind knows
which fields each case may touch. A grouping node does nothing of its
own: the parentheses already did their work when the tree was shaped.
To evaluate an expression is to compute the value it stands for. A tree-walking interpreter runs a program by walking its syntax tree directly: a recursive function visits each node, evaluates its children, and combines their values. No other form of the program is produced.
What a value looks like
While a Drip program runs, its values have to live somewhere. The interpreter is written in TypeScript, so the easiest home is a TypeScript value:
// runtime.ts (functions, classes: later)
type Value = null | boolean | number
| string | DripCallable | DripInstance;
Drip in the interpreter
nil null
true true
12.5 the number 12.5
"hi" the string 'hi'
Borrowing the host’s values means + on two Drip numbers is just +
on two JavaScript numbers. print needs one small adjustment, made by
stringify: null must print as nil.
A runtime value is the representation of a Drip value while the
program runs. Drip’s tree-walker reuses JavaScript values: nil is
null, and booleans, numbers and strings are their JavaScript
counterparts. The Drip type of a value is therefore just its JavaScript
type, checked with typeof.
print 7; print 10 / 4; print "drip" + "!"; print nil; print 2 < 3; print !0; print !nil; print !"";
What counts as true
! needs to know whether a value is “true enough”, and so will if
and while in chapter 38. Drip’s rule fits on one line:
const isTruthy = (v: Value) =>
v !== null && v !== false;
Only nil and false are false. Everything else, including 0 and
the empty string "", is true. That is not JavaScript’s rule, where
0 and '' are also falsy. The example above shows it: !0 printed
false, because 0 is truthy in Drip.
Truthiness is the rule that decides which values count as true where
a condition is expected. In Drip, nil and false are falsey and
every other value is truthy.
When the types are wrong
Drip is dynamically typed: nothing checks "1" - 1 before the program
runs. So the interpreter checks while it runs, right before it applies
each operator:
case '+':
if (typeof l === 'number' &&
typeof r === 'number') return l + r;
if (typeof l === 'string' &&
typeof r === 'string') return l + r;
throw new RuntimeError(op,
'Operands must be two numbers or ' +
'two strings.');
case '-':
nums(op, l, r); // throws if not numbers
return l - r;
Try '1' - 1 in JavaScript and you get 0: the string is quietly
converted. Drip refuses. RuntimeError carries the operator’s token,
and the token knows its line, so the message can say where:
print 1 + 2; print "1" - 1; print "never printed";
The first print ran and printed 3. The second stopped the program.
Drip prints the message, then [line 2], and exits with code 70: not
65, because this program was perfectly valid Drip. It went wrong while
running.
A runtime error is an error found while the program runs, such as an operator applied to values of the wrong type. Drip reports the message and the line of the operator’s token, stops the program, and exits with code 70. A syntax error, by contrast, is found before anything runs.
Because Drip numbers are JavaScript numbers, they are 64-bit IEEE 754
doubles, the format from chapters 16 and 22. So print 0.1 + 0.2;
prints 0.30000000000000004 in Drip, exactly as in JavaScript. The
price of walking the tree is just as concrete: every node costs a
function call and a switch, even a lone 4. Chapter 44 measures that
overhead, and Part IV removes most of it with bytecode. Drip’s design
follows Lox from Crafting Interpreters[1].
Your turn
Write the evaluator yourself, in plain JavaScript, with Drip’s type
rules and truthiness. The extra one is stringify.
Finish evaluate(e) for three kinds of node: { kind: 'literal', value }, { kind: 'unary', op, right } with op '!' or '-', and { kind: 'binary', op, left, right } with op one of + - * /. Follow Drip: ! uses Drip truthiness, + takes two numbers or two strings, and the others take numbers only. On a type mismatch, throw an Error with exactly Drip’s message: Operand must be a number. (unary), Operands must be numbers., or Operands must be two numbers or two strings.
function isTruthy(v) {
return v !== null && v !== false;
}
function evaluate(e) {
switch (e.kind) {
case 'literal':
return e.value;
case 'unary': {
// '!' and '-'
return undefined;
}
case 'binary': {
// '+', '-', '*', '/'
return undefined;
}
}
}Write stringify(v), which turns a Drip runtime value into the text print shows. null (Drip’s nil) shows as nil, and negative zero shows as -0. Everything else shows as JavaScript’s String(v) would show it.
function stringify(v) {
return String(v);
}What you learned
- To evaluate a tree, evaluate the children, then combine their values. Values flow from the leaves up to the root.
- A tree-walking interpreter does exactly that, with one recursive
function and one
caseper kind of node. - Drip’s runtime values are JavaScript values:
nilisnull, and booleans, numbers and strings are themselves. - Truthiness in Drip: only
nilandfalseare false.0and""are true. - Operators check their operand types as they run. A mismatch is a runtime error: a message, the line, and exit code 70.
Next: statements, which do things instead of producing values, and the variables they remember.
Check your understanding
0/6- 1.
In what order does the tree-walker finish the nodes of
(+ 1 (* 2 3))? - 2.
Which line prints
truein Drip? - 3.
What happens when Drip runs
print "a" + 1;? - 4.
How is the Drip value
"hi"represented while the interpreter runs? - 5.
A program prints three lines and then hits a runtime error on line 4. What does the reader see?
- 6.
Why does a
groupingnode exist in the tree at all, ifevaluatejust evaluates its inner expression?
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)