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
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:
interpreted | compiled | |
CLISP | 4.02 sec | 0.68 sec |
femtoLisp2 | 2.62 sec | 2.03 sec** |
femtoLisp | 6.02 sec | 5.64 sec** |
interpreted | compiled | |
CLISP | 23.12 sec | 4.04 sec |
femtoLisp2 | 4.71 sec | n/a |
femtoLisp | 7.25 sec | n/a |
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.