ellisp is a lisp parser & interpreter made with Rust for fun.
- running the repl
cargo run
- release build, running the binary from cli
cargo build --release
./target/release/ellisp -- "(+ 1 2 3)"
Scroll to end for bigger examples.
> cargo run
ellisp 0.1 REPL
>>> (def a 666)
>>> (def b (lambda (x) (+ x a)))
>>> (b 42)
708
>>> (def fib (lambda (n) (if (< n 2) 1 (+ (fib (- n 1)) (fib (- n 2))))))
>>> (fib 4)
5
>>> (fib 10)
89
The language is a Lisp with s-expressions that must follow our defined special forms or procedure calls.
An s-expression starting with a keyword is a special form, other lists are procedure calls (function calls).
ellisp also supports booleans (true
and false
) and numbers (integers).
Special syntactic forms start with a keyword, everything else is a procedure call.
"def" (def a <expr>) (def a 42)
"if" (if <expr> <then> <else>) (if (= a 5) 1 0)
"set!" (set! a <expr>) (set! a 6)
"quote" (quote <expr>) (quote (hello darkness (my old) friend))
"lambda" (lambda (a b) <expr>) (def fn (lambda (a, b) (+ a b)))
; oh and this would be a comment, it's a total regexp hack in the tokenizer
ellisp provides a small standard library. Users can create their own procedures with (def name (lambda ...))
.
Some functions take N arguments where it makes sense (as in Clojure).
Some examples of what's built in, with native implementations:
; basic math
sum, +, -, *, /,
=, <, <=, >, >=
; lispy things
begin, do, null?,
list, cons, car, cdr,
append, length, len
- Program code (string) is tokenised
fn tokenize(p: &str) -> Vec<Token>
- just some simple string processing
- Tokens are parsed into the AST
fn parser(tokens: &mut Vec<Token>) -> AST
- recursive algorithm that checks for
(
and)
, it's a LISP afterall :-)
- AST is evaluated
fn eval(ast: &AST, denv: &mut Box<DynamicEnv>, pstore: &mut LambdaContextStore) -> Expr
- DynamicEnv contains all runtime definitions like
(def a 5)
. Each procedure (lambda) has it's own environment that extends the top-level env - LambdaContextStore contains data required to execute a procedure (lambda)
If you want to see the code, start with main.rs
-> fn main()
. From there you can follow how the input program goes through tokenization, parsing and finally eval.
- macro system
- prebuilt macros like
defn
(define
+lambda
)
- prebuilt macros like
- performance improvements
DynamicEnv
find has to go N hops up the tree when there are many nested closures or in certain recursion scenarios. Perhaps a better data structure is needed here...
I'm using Scheme syntax highlighting which kinda works :-)
; map
(def map (lambda (data f)
(if
(empty? data) (list)
(cons
(f (car data))
(map (cdr data) f)))))
(map (list 1 2 3) (lambda (x) (* x 2))) ; => (2 4 6)
; filter
(def filter (lambda (data f)
(if
(empty? data) (list)
(if
(f (car data))
(cons
(car data)
(filter (cdr data) f))
(filter (cdr data) f)))))
(filter (list 1 2 3) (lambda (x) (>= x 2))) ; => (2 3)
; flatten
(def flatten (lambda (data)
(do
(def result (list))
(def _rec (lambda (data)
(if (empty? data)
(set! result result) ; we don't yet have better way of saying "do nothing"
(if (list? (car data))
(do
(_rec (car data))
(_rec (cdr data)))
(do
(set! result (append result (car data)))
(_rec (cdr data))
)))))
(_rec data)
result)))
(flatten (list 1 (list 2 (list (list 3))) 4 (list))) ; => (1 2 3 4)
; fold
(def fold (lambda (data f acc)
(if
(empty? data) acc
(fold
(cdr data)
f
(f (car data) acc)))))
(fold (list 1 2 3) + 0) ; => 6