A small tree-walking interpreter, built from scratch in TypeScript. Lexer → parser → interpreter, with variables, control flow, functions, and closures.
npm install
npm run run examples/demo.lang # run a script file
npm run run 'print 1 + 2;' # run inline source
npm run run # start the REPL
npm test # run the checkpoint test suite// FizzBuzz, closures, and recursion in one file.
fun fizzbuzz(n) {
var i = 1;
while (i <= n) {
if (i % 3 == 0 and i % 5 == 0) {
print "FizzBuzz";
} else if (i % 3 == 0) {
print "Fizz";
} else if (i % 5 == 0) {
print "Buzz";
} else {
print i;
}
i = i + 1;
}
}
fun makeCounter() {
var count = 0;
fun increment() {
count = count + 1;
return count;
}
return increment;
}
fizzbuzz(15);
var counter = makeCounter();
print counter(); // 1
print counter(); // 2
Variables & types — numbers, strings, booleans, nil. nil and false are falsy; everything else (including 0 and "") is truthy.
var x = 5;
var name = "hello";
var flag = true;
var nothing; // implicitly nil
Operators
+ - * / % // arithmetic (+ also concatenates strings)
== != < <= > >= // comparison
and or ! // logical (and/or short-circuit)
Print & comments
print "hi " + name;
// line comment
Control flow — { } blocks create their own scope; shadowing a variable inside a block doesn't touch the outer one.
if (x > 0) { print "pos"; } else { print "non-pos"; }
var i = 0;
while (i < 5) { print i; i = i + 1; }
Functions & closures
fun add(a, b) { return a + b; }
fun makeCounter() {
var count = 0;
fun increment() { count = count + 1; return count; }
return increment;
}
Recursion works normally. Each call gets its own environment, chained to the closure it was defined in — not the caller's — so closures capture correctly.
src/
token.ts # TokenType enum + Token shape
lexer.ts # source string -> tokens
ast.ts # Expr / Stmt discriminated unions
parser.ts # tokens -> AST (recursive descent, panic-mode error recovery)
environment.ts # linked-scope variable storage
interpreter.ts # AST -> executed program
lang-function.ts # user-defined functions + closures
values.ts # runtime value types
errors.ts # ReturnSignal, ParseErrors, RuntimeError
run.ts # CLI entry point (file / inline / REPL)
test/
checkpoints.ts # asserts stdout for every language feature
examples/
demo.lang # FizzBuzz + closures + recursion
Arrays/lists, classes/objects, a standard library beyond print, string methods. Natural next steps from here: a bytecode VM (for speed) or a static type checker (for safety).
Node.js, and typescript pinned to the 6.x line — ts-node isn't yet compatible with TypeScript 7's rewritten compiler internals.