femtoLisp


femtoLisp is an elegant Lisp implementation. Its goal is to be a reasonably efficient and capable interpreter with the shortest, simplest code possible. As its name implies, it is small (10-15). Right now it is just 1000 lines of C (give or take). It would make a great teaching example, or a useful system anywhere a very small Lisp is wanted. It is also a useful basis for developing other interpreters or related languages.

The language implemented

femtoLisp tries to be a generic, simple Lisp dialect, influenced by McCarthy's original. Builtin special forms:
quote, cond, if, and, or, lambda, macro, label, while, progn, prog1

Builtin functions:
eq, atom, not, symbolp, numberp, boundp, cons, car, cdr, read, eval, print, load, set, +, -, *, /, <, apply, rplaca, rplacd

Included library functions and macros:
setq, setf, defmacro, defun, define, let, let*, labels, dotimes, macroexpand-1, macroexpand, backquote, null, consp, builtinp, self-evaluating-p, listp, eql, equal, every, any, when, unless, =, !=, >, <=, >=, compare, mod, abs, identity, list, list*, length, last, nthcdr, lastcdr, list-ref, reverse, nreverse, assoc, member, append, nconc, copy-list, copy-tree, revappend, nreconc, mapcar, filter, reduce, map-int, symbol-plist, set-symbol-plist, put, get

system.lsp

The implementation

lisp.c

femtoLisp2

This version includes robust reading and printing capabilities for circular structures and escaped symbol names. It adds read and print support for the Common Lisp read-macros #., #n#, and #n=. This allows builtins to be printed in a readable fashion as e.g. "#.eq".

The net result is that the interpreter achieves a highly satisfying property of closure under I/O. In other words, every representable Lisp value can be read and printed.

The traditional builtin label provides a purely-functional, non-circular way to write an anonymous recursive function. In femtoLisp2 you can achieve the same effect "manually" using nothing more than the reader:
#0=(lambda (x) (if (<= x 0) 1 (* x (#0# (- x 1)))))

femtoLisp2 has the following extra features and optimizations:

Those two optimizations are a Big Deal.

lisp2.c (uses flutils.c)

Performance

femtoLisp's performance is surprising. It is faster than most interpreters, and it is usually within a factor of 2-5 of compiled CLISP.
solve 5 queens problem 100x
interpretedcompiled
CLISP 4.02 sec 0.68 sec
femtoLisp22.62 sec 2.03 sec**
femtoLisp 6.02 sec 5.64 sec**
recursive fib(34)
interpretedcompiled
CLISP 23.12 sec 4.04 sec
femtoLisp24.71 sec n/a
femtoLisp 7.25 sec n/a
** femtoLisp is not a compiler; in this context "compiled" means macros were pre-expanded.

"Installation"

Here is a Makefile. Type make to build femtoLisp, make NAME=lisp2 to build femtoLisp2.

Tail recursion

The femtoLisp evaluator is tail-recursive, following the idea in Lambda: The Ultimate Declarative (should be required reading for all schoolchildren).

The femtoLisp source provides a simple concrete example showing why a function call is best viewed as a "renaming plus goto" rather than as a set of stack operations.

Here is the non-tail-recursive evaluator code to evaluate the body of a lambda (function), from lisp-nontail.c:

        PUSH(*lenv);    // preserve environment on stack
        lenv = &Stack[SP-1];
        v = eval(*body, lenv);
        POP();
        return v;
(Note that because of the copying garbage collector, values are referenced through relocatable handles.)

Superficially, the call to eval is not a tail call, because work remains after it returns—namely, popping the environment off the stack. In other words, the control stack must be saved and restored to allow us to eventually restore the environment stack. However, restoring the environment stack is the only work to be done. Yet after this point the old environment is not used! So restoring the environment stack isn't necessary, therefore restoring the control stack isn't either.

This perspective makes proper tail recursion seem like more than an alternate design or optimization. It seems more correct.

Here is the corrected, tail-recursive version of the code:

        SP = saveSP;    // restore stack completely
        e = *body;      // reassign arguments
        *penv = *lenv;
        goto eval_top;
penv is a pointer to the old environment, which we overwrite. (Notice that the variable penv does not even appear in the first code example.) So where is the environment saved and restored, if not here? The answer is that the burden is shifted to the caller; a caller to eval must expect that its environment might be overwritten, and take steps to save it if it will be needed further after the call. In practice, this means the environment is saved and restored around the evaluation of arguments, rather than around function applications. Hence (f x) might be a tail call to f, but (+ y (f x)) is not.