Viscosity
Review
Part III · A tree-walking interpreter · Day 9

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.

About 17 minutes, including the exercises and quiz
By the end you will be able to
  • 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

Intuition

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:

Step throughEvaluating the tree for 1 + 2 * 3 - 4
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);
}
}
}
The tree for 1 + 2 * 3 - 4 being evaluated. Each node is evaluated after its children, and its value is written beside it.-+41*23thick ring: being evaluated · dashed: waiting · badge: its value
evaluate() starts at the root, a binary - node. It cannot subtract yet: it needs the values of both children first.
1/10

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.

Definition

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.

Definition

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.

Try itValues and truthiness
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.

Definition

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:

Try itA runtime error
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.

Definition

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.

Under the hood

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.

ExerciseAn evaluator with Drip’s rules

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;
    }
  }
}
Extra practiceHow print shows a value

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 case per kind of node.
  • Drip’s runtime values are JavaScript values: nil is null, and booleans, numbers and strings are themselves.
  • Truthiness in Drip: only nil and false are false. 0 and "" 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. 1.

    In what order does the tree-walker finish the nodes of (+ 1 (* 2 3))?

  2. 2.

    Which line prints true in Drip?

  3. 3.

    What happens when Drip runs print "a" + 1;?

  4. 4.

    How is the Drip value "hi" represented while the interpreter runs?

  5. 5.

    A program prints three lines and then hits a runtime error on line 4. What does the reader see?

  6. 6.

    Why does a grouping node exist in the tree at all, if evaluate just evaluates its inner expression?

References

  1. [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)