What Is This?
A complete Lisp interpreter built from scratch in plain JavaScript with zero external dependencies. It parses and evaluates a subset of Lisp/Scheme, supports lambdas, closures, recursion, higher-order functions, and comes with an interactive REPL and a comprehensive test suite.
Supported Features
- Arithmetic & comparisons:
(+ 1 2 3),(> 5 3),(equal? x y) - Variables:
(define x 10),(set! x 20) - Conditionals:
(if condition then else) - Lambda & closures:
(lambda (x) (* x x)) - Higher-order functions:
((repeat twice) 10) - Recursion:
(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1)))))) - List operations:
list,car,cdr - Quoting, booleans, strings, sequencing with
begin
How It Works
Recursive Descent Parser
The interpreter has no separate tokenizer/lexer phase. It's a pure recursive descent parser that operates character-by-character on raw strings. Each sub-parser tries to match its pattern and returns [value, remainingInput] or null on failure:
parser → expressionParser | numberParser | booleanParser | valueParser | stringParser
Special Forms
S-expressions are intercepted for special Lisp keywords before being treated as function calls — define, begin, if, lambda, quote, and set! each have their own parser that handles their unique evaluation semantics.
Lambda & Closures
The lambdaParser implements lexical scoping by shallow-cloning the environment at definition time. This means closures correctly capture their surrounding scope:
(define repeat (lambda (f) (lambda (x) (f (f x)))))
((repeat twice) 10) ; => 40
Lambdas create actual JavaScript functions stored in the environment, making higher-order functions and recursion work naturally.
Global Environment
A built-in standard library maps Lisp symbols to JavaScript functions — arithmetic, comparison, math builtins (pi, abs, sqrt, pow), and list operations (car, cdr, list).
Interactive REPL
A readline-based REPL with the prompt lisp>>. The environment persists across inputs, so variables defined in one line are available in the next. Type exit to quit.
Key Design Decisions
- No tokenizer — the recursive descent parser works directly on raw strings, keeping the implementation simple and self-contained
- Environment as a plain JS object — enables lexical scoping via object spread (
{ ...env }) - Functions are first-class JS closures —
lambdaproduces real JavaScript functions, so higher-order patterns work naturally - Multi-expression input — the
main()evaluator handles multiple top-level expressions in a single input string - 38 test cases covering arithmetic, recursion, closures, higher-order functions, quoting, mutation, and syntax error detection
What I Learned
Building a language interpreter from scratch deepened my understanding of how programming languages work at a fundamental level — from parsing and evaluation to scope chains and closure semantics. Implementing features like lambda, set!, and recursive functions revealed the elegance behind Lisp's minimal syntax and powerful abstractions.