Making a Language: Using Spark
Tying everything together — the run function, complete Spark examples, and exercises for extending the language.
In the previous part, we built the evaluator. Now let's tie everything together.
The Public API
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.
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:
1const KEYWORDS = {
2 // ... existing keywords ...
3 loop: TokenType.Loop,
4};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:
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:
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.