A small scripting language with a tree-walking interpreter, written in pure Python — no dependencies, no build step, no C extensions. Roughly 1,100 lines of Python, readable end to end.
It exists because reading about how interpreters work never stuck for me; writing one did. If you've wanted to understand how a language goes from source text to running code, this is a complete, working example you can read in an afternoon.
$ python -m lithe
Lithe 0.1.0 -- type Ctrl-D (Ctrl-Z on Windows) then Enter to exit
lithe> let greet = fn(name) { return "hello, " + name; };
lithe> greet("world")
"hello, world"
lithe> for i in range(1, 4) { print(i * i); }
1
4
9
git clone https://github.com/yangming-zhang/lithe
cd lithe
python -m lithe # start the REPL
python -m lithe examples/fib.li # run a file
# optional: install so you get a `lithe` command
pip install -e .
lithe examples/quicksort.liPython 3.9 or newer. Nothing to install beyond that.
// Variables. Dynamic typing: numbers, strings, booleans, nil, lists, functions.
let name = "Ada";
let year = 1843;
let active = true;
// Functions are values. This one is named...
fn square(x) { return x * x; }
// ...and this one is anonymous, assigned to a variable.
let double = fn(x) { return x * 2; };
print(square(8)); // 64
print(double(21)); // 42
// Closures capture their surrounding scope.
fn adder(n) {
return fn(x) { return x + n; };
}
let add10 = adder(10);
print(add10(5)); // 15
// Control flow.
for n in range(1, 6) {
if (n % 2 == 0) {
print(str(n) + " is even");
} else {
print(str(n) + " is odd");
}
}
// Lists are mutable, support indexing (including negative), and concatenate with +.
let xs = [3, 1, 2];
xs[0] = 30;
push(xs, 99);
print(xs); // [30, 1, 2, 99]
print(xs[-1]); // 99
print([1, 2] + [3]);// [1, 2, 3]
| function | what it does |
|---|---|
print(...) |
print arguments separated by spaces |
len(x) |
length of a string or list |
str(x) / num(x) |
convert to string / parse a number |
type(x) |
"number", "string", "bool", "nil", "list", "function" |
range(a, b, step?) |
list of numbers (1–3 args, like Python's) |
push(list, x) / pop(list) |
append / remove-last (mutating) |
abs(x) |
absolute value |
clock() |
wall-clock seconds, for quick benchmarks |
input(prompt?) |
read a line from stdin |
A few deliberate rules worth knowing:
- Only
nilandfalseare falsy.0,""and[]are all truthy. ==never crosses types:1 == "1"isfalse, andtrue == 1isfalse.+works on two numbers, two strings, or two lists — mixing types is an error, not a silent coercion.
Source text flows through three stages, one module each:
source ──► lexer.py ──► tokens ──► parser.py ──► AST ──► interpreter.py ──► result
lexer.py— a hand-written scanner. Walks the source character by character producing tokens, tracking line numbers, skipping//and nested/* */comments, and handling string escapes.parser.py— recursive descent for statements, Pratt parsing (precedence climbing) for expressions. The grammar is written out at the top of the file. This is where operator precedence and associativity live.ast.py— the node types, plain dataclasses. Each carries its source line.interpreter.py— a tree-walking evaluator. It dispatches on node class name (eval_Binary,exec_WhileStmt, …), so the structure mirrors the AST one-to-one. Scopes areEnvironmentobjects chained by parent pointer; closures capture the environment they were defined in;returnunwinds the Python stack via a small internal exception.
If you want to add a feature, the path is short: add a token in lexer.py, a node in ast.py, a parse rule in parser.py, and an eval_/exec_ method in interpreter.py.
The examples/ directory has runnable programs, each kept short:
fib.li— recursionfizzbuzz.li—if/else ifchains and%closures.li— a counter factory; each counter is independentquicksort.li— recursion + list concatenationhigher_order.li—mapandfilterwritten in Lithe
python -m unittest discover -s tests66 tests cover the lexer, parser, interpreter semantics, and every example program's output. They're a good map of what the language guarantees.
This is a learning-grade interpreter, not a production runtime. It's a tree-walker, so it's not fast, and the language is intentionally small — no modules, no classes, no exceptions. What's here works and is tested. If you build something on it or extend it, I'd love to see it.
MIT