engineeringtooling 4 min read

Emoji AST Interpreter Walker Stage - Part 3

May 21, 2026 ยท 17:36 UTC

Emoji AST Interpreter Walker Stage - Part 3

Now the payoff finally arrives, a parsed tree is a nice structure, but a tree alone computes nothing, and this part builds the evaluator, the stage that walks the abstract syntax tree from Part 2 and collapses it into one number the caller can use.

Walking the tree is the entire job, and a tree-walking interpreter is really a plain recursive function that visits each node, asks what type it is, and returns a value, and because the tree itself is recursive the walking function is recursive too.

Before math runs, one decision comes first, what number each fruit represents, since that mapping is the only arbitrary choice.

Note

This is a five-part series building one emoji language end to end.

  1. Part 1 - Lexer
  2. Part 2 - Parser
  3. Part 3 - Walker (you are here)
  4. Part 4 - Scope
  5. Part 5 - REPL

Mapping Fruit to Numbers

That mapping is a plain lookup table, and the values are entirely free to pick, so a sensible choice is to let each fruit stand for a small counting number, an apple is one, a banana is two, a cherry is three, and so on, which keeps mental math easy during testing and makes any wrong answers obvious the very instant they first show up on the screen in plain view.

A number node evaluates by reading its fruit value from that table, and a missing fruit throws a clear error, not a vague result.

This lookup lives beside the evaluator because it is purely a runtime concern, the parser never needs to know a banana's worth.

// src/eval.ts
import type { Node } from './types.ts'

const FRUIT_VALUE: Record<string, number> = {
  '๐ŸŽ': 1,
  '๐ŸŒ': 2,
  '๐Ÿ’': 3,
  '๐Ÿ‡': 4,
  '๐Ÿ“': 5
}

Walking the Tree

Evaluation itself is refreshingly short, it takes a node and switches on its type, a number node returns its mapped value straight away, and a binary node first evaluates its left child and its right child by calling itself recursively, then applies the operator to those two results, and that single recursive step is the entire heart of interpreting the language.

Because each binary node evaluates its children before it evaluates itself, the deepest and tightest subtrees resolve first, which is exactly the order precedence demanded when the parser built the tree, so correctness simply falls out naturally.

Operators are applied with a tiny lookup from symbol to a small function, keeping the switch statement clean and easy to extend.

const APPLY: Record<string, (a: number, b: number) => number> = {
  '+': (a, b) => a + b,
  '-': (a, b) => a - b,
  '*': (a, b) => a * b,
  '/': (a, b) => a / b
}

export function evaluate(node: Node): number {
  if (node.type === 'number') {
    const value = FRUIT_VALUE[node.value]
    if (value === undefined) {
      throw new Error(`Unknown fruit: ${node.value}`)
    }
    return value
  }
  const left = evaluate(node.left)
  const right = evaluate(node.right)
  return APPLY[node.op](left, right)
}

Running the Full Pipeline

Wiring all three stages together in the main file finally yields a real working pipeline at last, a source string flows into the lexer and becomes tokens, those tokens flow into the parser and become a tree, and that tree flows into the evaluator and becomes a number, so an apple plus a banana times a cherry quietly returns seven, respecting precedence from end to end.

Here is the first moment the language feels genuinely usable, an expression typed in gets the correct answer back every single time, which already feels like a small victory worth pausing on for a second before anything much more ambitious arrives.

So far every expression is a one-off calculation, and the next part adds variables so results can be named, stored, and reused.

// main.ts
import { tokenize } from './src/lexer.ts'
import { parse } from './src/parser.ts'
import { evaluate } from './src/eval.ts'

function run(source: string): number {
  return evaluate(parse(tokenize(source)))
}

console.log(run('๐ŸŽ + ๐ŸŒ * ๐Ÿ’')) // 1 + (2 * 3) = 7
console.log(run('(๐ŸŽ + ๐ŸŒ) * ๐Ÿ’')) // (1 + 2) * 3 = 9
Share
On this page
No results found.