A small, dynamically-typed programming language with a tree-walking interpreter,
written from scratch in JavaScript — zero dependencies.
Lumen is a complete little programming language I built to understand how programming languages actually work — from raw source text all the way to execution. It implements the classic three-stage pipeline:
source code ──▶ Lexer ──▶ tokens ──▶ Parser ──▶ AST ──▶ Interpreter ──▶ result
No parser generators, no libraries — every stage is hand-written and unit-tested.
# Recursive Fibonacci
fn fib(n) {
if (n < 2) { return n; }
return fib(n - 1) + fib(n - 2);
}
let i = 0;
while (i <= 10) {
print("fib(" + str(i) + ") = " + str(fib(i)));
i = i + 1;
}$ lumen examples/fib.lum
fib(0) = 0
fib(1) = 1
...
fib(10) = 55
- Values: numbers, strings, booleans,
null - Variables:
let x = 5;with lexical scoping - Operators:
+ - * / %, comparisons,and/or(short-circuit),! - Control flow:
if / else if / else,while - Functions: first-class, recursive, with closures that capture their environment
- Built-ins:
print,len,str,num,type,clock - Comments:
# like this - An interactive REPL and a file runner
- Clear lexer / parser / runtime error messages with line numbers
| Stage | File | Responsibility |
|---|---|---|
| Lexer | src/lexer.js |
Scans characters into tokens (numbers, strings, identifiers, operators) |
| Parser | src/parser.js |
Recursive-descent parser that builds an AST and enforces operator precedence |
| Environment | src/environment.js |
Lexical scopes — the mechanism behind closures |
| Interpreter | src/interpreter.js |
Tree-walking evaluator: executes the AST, manages scope and return |
The parser climbs precedence levels (assignment → or → and → equality → comparison → term → factor → unary → call → primary), and closures work by having each function keep a reference to the environment it was defined in.
git clone https://github.com/Dere752/lumen-lang.git
cd lumen-lang
# run a program
node src/index.js examples/fizzbuzz.lum
# start the REPL
node src/index.js# variables and arithmetic
let name = "Lumen";
let answer = 6 * 7;
# functions are values; closures capture state
fn makeCounter() {
let count = 0;
return fn() { count = count + 1; return count; };
}
let next = makeCounter();
print(next()); # 1
print(next()); # 2
# control flow
let n = 15;
if (n % 15 == 0) { print("FizzBuzz"); }
else if (n % 3 == 0) { print("Fizz"); }
else { print(str(n)); }More in examples/: fib.lum, fizzbuzz.lum, closures.lum.
Unit tests run on Node's built-in test runner (zero dependencies):
npm testThey cover the lexer, parser and interpreter — operator precedence, control flow, recursion, closures, built-ins, and both runtime and parse errors. CI runs the suite on Node 20 & 22 via GitHub Actions.
src/
token.js token types & keywords
lexer.js source text -> tokens
parser.js tokens -> AST
environment.js lexical scopes (closures)
interpreter.js AST -> execution
run.js lex + parse + interpret a string
index.js CLI: REPL and file runner
examples/ sample Lumen programs
test/ unit tests
Building Lumen taught me how a language is layered: tokenizing, parsing with correct precedence and associativity, representing programs as trees, and evaluating them with proper scoping. Implementing closures and return (via stack unwinding) made abstract concepts from compiler theory concrete.
MIT © Ali Dere