Tier 5 — Expert

Expression Interpreter

Build a calculator that respects operator precedence and parentheses — tokenize, parse with recursive descent, and evaluate. The heart of every language.

🐍 Python 🟨 JavaScript

1. The Problem

Build an interpreter for arithmetic expressions like 2 + 3 * 4 that respects precedence (multiplication before addition) and parentheses. Three stages: tokenize the text, parse it into structure, and evaluate the result — here fused into a recursive-descent evaluator.

Why this project? Tokenize → parse → evaluate is how every programming language works. A calculator is the smallest complete example, and recursive descent is the parsing technique used in real compilers.

2. How to Think About This

Tokenizing turns "2 + 3" into [NUM 2, +, NUM 3] with a regex. Precedence falls out of the grammar structure: expr handles +/- (lowest), calls term for *// (higher), which calls factor for numbers and parenthesised sub-expressions (highest). Because factor can call expr again for a (, parentheses and nesting just work.

3. The Build

interpret.mjs
// ---- 1. TOKENIZE: text -> list of tokens ----
export function tokenize(text) {
  const tokens = [];
  for (const part of text.match(/\d+|[+\-*/()]/g) || []) {
    if (/^\d+$/.test(part)) tokens.push(["NUM", parseInt(part, 10)]);
    else tokens.push([part, part]);
  }
  return tokens;
}

// ---- 2 & 3. PARSE + EVALUATE (recursive descent) ----
class Parser {
  constructor(tokens) { this.tokens = tokens; this.pos = 0; }
  peek() { return this.pos < this.tokens.length ? this.tokens[this.pos] : [null, null]; }
  eat()  { return this.tokens[this.pos++]; }

  expr() {                       // + and - (lowest precedence)
    let value = this.term();
    while (this.peek()[0] === "+" || this.peek()[0] === "-") {
      const op = this.eat()[0];
      const right = this.term();
      value = op === "+" ? value + right : value - right;
    }
    return value;
  }

  term() {                       // * and / (higher precedence)
    let value = this.factor();
    while (this.peek()[0] === "*" || this.peek()[0] === "/") {
      const op = this.eat()[0];
      const right = this.factor();
      value = op === "*" ? value * right : value / right;
    }
    return value;
  }

  factor() {                     // numbers and parentheses (highest)
    const [type, value] = this.eat();
    if (type === "NUM") return value;
    if (type === "(") {
      const inner = this.expr();
      this.eat();                // consume ")"
      return inner;
    }
  }
}

export function evaluate(text) {
  return new Parser(tokenize(text)).expr();
}

// Demo
console.log(evaluate("2 + 3 * 4"));     // 14, not 20
console.log(evaluate("(2 + 3) * 4"));   // 20

4. Test & Prove

interpret.test.mjs
import { evaluate, tokenize } from "./interpret.mjs";

let pass = 0, fail = 0;
function test(desc, fn) {
  try { fn(); console.log("  \u2713", desc); pass++; }
  catch (e) { console.log("  \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }

test("adds two numbers", () => assert(evaluate("2 + 3") === 5));
test("respects precedence (* before +)", () => assert(evaluate("2 + 3 * 4") === 14));
test("parentheses override precedence", () => assert(evaluate("(2 + 3) * 4") === 20));
test("handles subtraction and division", () => assert(evaluate("20 - 8 / 2") === 16));
test("nested parentheses work", () => assert(evaluate("((1 + 2) * (3 + 4))") === 21));
test("tokenize splits numbers and operators", () => {
  assert(JSON.stringify(tokenize("2+3")) === JSON.stringify([["NUM",2],["+","+"],["NUM",3]]));
});

console.log(`\n${pass} passed, ${fail} failed`);

5. The Interface

node interpret.mjs

14
20

6. Run It

node interpret.mjs
node interpret.test.mjs

🎯 Try this next

  1. Add support for negative numbers and unary minus
  2. Add variables: let x = 5, then evaluate expressions using x
  3. Return the parse tree (AST) instead of a number, then evaluate it separately
  4. Add exponentiation (^) with right-associative precedence

What you practised

The tokenize → parse → evaluate pipeline, recursive-descent parsing, and how grammar structure encodes operator precedence — the foundation of every interpreter and compiler.

Reference: Recursion · Classes · Strings

Try It Yourself