Spark code in a playground

Making a Language: Using Spark

Tying everything together — the run function, complete Spark examples, and exercises for extending the language.

May 18, 2026#typescript#compilers

In the previous part, we built the evaluator. Now let's tie everything together.

The Public API

index.ts
1export function run(source: string): { output: string[]; error?: string } {
2  try {
3    const tokens = tokenizeFn(source);
4    setSource(source);
5    const program = parseFn(tokens);
6    return evaluateFn(program);
7  } catch (e) {
8    if (e instanceof ParseError || e instanceof RuntimeError) {
9      return { output: [], error: e.format(source) };
10    }
11    throw e;
12  }
13}

run chains lexer to parser to evaluator. If a ParseError or RuntimeError is thrown, run catches it and formats the error with source context, showing the line and a pointer to the exact position.

index.ts
1export function tokenize(source: string) {
2  return tokenizeFn(source);
3}
4
5export function parse(tokens: ReturnType<typeof tokenizeFn>): Program {
6  return parseFn(tokens);
7}
8
9export function evaluate(program: Program) {
10  return evaluateFn(program);
11}
12
13export { formatValue };
14export type { Program };

Each stage is also exported separately so the playground can use them independently for debugging or step-by-step execution.

Examples

Grade Calculator

1func average(nums) {
2  val sum = nums[0] + nums[1] + nums[2]
3  return sum / 3
4}
5
6val scores = [85, 92, 78]
7say("average:", average(scores))
8
9when (average(scores) >= 90) {
10  say("grade: A")
11} else when (average(scores) >= 80) {
12  say("grade: B")
13} else {
14  say("grade: C")
15}

Demonstrates arrays, function calls, and else when chaining.

String Utilities

1func greet(name) {
2  return "hello " + name + "!"
3}
4
5say(greet("alice"))
6
7func repeat(s, n) {
8  when (n <= 1) {
9    return s
10  }
11  return s + repeat(s, n - 1)
12}
13
14say(repeat("ha", 3))

Shows string concatenation and recursion. repeat calls itself until n reaches 1.

Todo Tracker

1val todos = []
2
3func add(text) {
4  todos = todos + [text]
5}
6
7add("write lexer")
8add("write parser")
9add("write evaluator")
10
11say("todos:")
12say(todos[0])
13say(todos[1])
14say(todos[2])

Uses the array + operator to append, and assignment to mutate a top-level variable.

Fibonacci

1func fib(n) {
2  when (n <= 1) {
3    return n
4  }
5  return fib(n - 1) + fib(n - 2)
6}
7
8say("fib(10) =", fib(10))

Two-branch recursion: each call spawns two more until the base case.

FizzBuzz

1func fizzbuzz(n) {
2  when (n == 0) {
3    return
4  }
5  when (n % 15 == 0) {
6    say("fizzbuzz")
7  } else when (n % 3 == 0) {
8    say("fizz")
9  } else when (n % 5 == 0) {
10    say("buzz")
11  } else {
12    say(n)
13  }
14  fizzbuzz(n - 1)
15}
16
17say(fizzbuzz(100))

Uses modulo and else when to check conditions in priority order.

Your Turn: Add Loops

I left loops out of Spark on purpose. Adding them is the best way to understand how all three layers fit together. You need one small change in each file:

Lexer. Add loop to the keywords map and Loop to the token enum:

lexer.ts
1const KEYWORDS = {
2  // ... existing keywords ...
3  loop: TokenType.Loop,
4};
types.ts
1export enum TokenType {
2  // ... existing tokens ...
3  Loop,
4}

Parser. Add a WhileStatement case. Check for the loop token, then parse the condition and body:

parser.ts
1// In parseStatement(), add before the atEnd check:
2if (this.match(TokenType.Loop)) return this.parseWhile();
3
4// New method:
5private parseWhile(): Statement {
6  this.consume(TokenType.LParen, "Expected '(' after loop");
7  const condition = this.parseExpression(0);
8  this.consume(TokenType.RParen, "Expected ')' after loop condition");
9  this.consume(TokenType.LBrace, "Expected '{' before loop body");
10  const body = this.parseBlock();
11  return { kind: "WhileStatement", condition, body };
12}

Also add WhileStatement to the Statement type union in types.ts.

Evaluator. One small case in the statement switch:

evaluator.ts
1case "WhileStatement":
2  while (isTruthy(evaluateExpression(stmt.condition, env))) {
3    evaluateBlock(stmt.body, env);
4  }
5  return { type: "null" };

Put it together and now Spark has loops:

1val i = 0
2loop (i < 5) {
3  say(i)
4  i = i + 1
5}

That's all it takes. Every feature follows the same recipe: a token in the lexer, a node in the parser, and a handler in the evaluator. Try it yourself. Break things. Change the syntax. Add features. Now that you know how the pieces fit together, you're not just a language user. You're a language designer.

The full source code for Spark is available on GitHub.

Spark Playground
Output
Press Run or ⌘⏎ to execute