Skip to content

yangming-zhang/lithe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lithe

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

Quick start

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.li

Python 3.9 or newer. Nothing to install beyond that.

The language in five minutes

// 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]

Built-in functions

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 nil and false are falsy. 0, "" and [] are all truthy.
  • == never crosses types: 1 == "1" is false, and true == 1 is false.
  • + works on two numbers, two strings, or two lists — mixing types is an error, not a silent coercion.

How it works

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 are Environment objects chained by parent pointer; closures capture the environment they were defined in; return unwinds 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.

Examples

The examples/ directory has runnable programs, each kept short:

Tests

python -m unittest discover -s tests

66 tests cover the lexer, parser, interpreter semantics, and every example program's output. They're a good map of what the language guarantees.

Status & non-goals

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.

License

MIT

About

A small scripting language with a tree-walking interpreter, in pure Python. Lexer, Pratt parser, closures, REPL — zero dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages