A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away.
— Antoine de Saint-Exupery
fLisp is a tiny yet practical interpreter for a dialect of the Lisp programming language. It is used as extension language for the Femto text editor.
fLisp is hosted in the Femto Github repository, it is released to the public domain.
fLisp is a Lisp-1 interpreter with Scheme like lexical scoping, tailcall optimization and other Scheme influences.
fLisp originates from Tiny-Lisp by matp (pre 2014), was integrated into Femto by Hugh Barney (pre 2016) and compacted by Georg Lehner in 2023.
This is a reference manual. If you want to learn about Lisp programming use other resources eg.
We use the following notation rule to describe the fLisp syntax:
name«name».[text]text can be given zero or one time.[text..]text can be given zero or more times.
fLisp does not use [square brackets] and double-dots .. as
syntactical elements.
Variables names convey the following context:
fLisp fancies to converge towards Emacs and Common Lisp, but includes also Scheme functions. Function descriptions are annotated according to their compatibility:
By default compatibility with Common Lisp is annotated. The suffix e is used to indicate reference to Emacs Lisp, s for Scheme. fLisp specific function are annotated with f.
When fLisp is invoked it follows a three step process:
Core functions of the language operate on built-in objects only. fLisp can be extended with additional functions. With respect to the interpreter, extension functions behave the same as core functions.
Program text is written as a sequence of symbolic expressions - sexp's - in parenthesized form. A sexp is either a single symbol or a sequence of symbols or sexp's enclosed in parenthesis.
The following characters are special to the reader:
()' and :(quote sexp) before it is evaluated.
. (a . b) evaluates to a cons object, holding the
objects a and b.
"\;Numbers are read and written in decimal notation. Number notation and formatting conventions are the same as in the C language. Exponent notation is not supported by the reader.
A list of objects has the form:
([element ..])
A function invocation has the form:
(name [param ..])
There are two predefined objects. Their symbols are:
nil(), the end of a list marker or the false value in logical operations.
ttrue, a predefined, non-false value.
fLisp objects have one of the following data types:
[A-Z][0-9][a-z]!#$%&*+-./:<=>?@^_~Objects are immutable, functions either create new objects or return existing ones.
Characters do not have their own type. A single character is represented by a string with length one.
All operations of the interpreter take place in an environment. An environment is a collection of named objects. The object names are of type symbol. An object in an environment is said to be bound to its name. Environments can have a parent. Each fLisp interpreter starts with a root environment without a parent.
lambda and macro objects are functions. They have a parameter list and a sequence of sexp's as body. When functions are invoked a new environment is created as child of the current environment. Functions receive zero or more objects from the caller. These are bound one by one to the symbols in the parameter list in the new environment.
lambdas return the result of evaluating the body in the new environment.
macros first evaluate the body in the calling environment. The resulting sexp is evaluated in the new environment and that result is returned. macro bodies are typically crafted to return new sexp's in terms of the parameters.
When a sexp is evaluated and encounters a symbol it looks it up in the current environment, and then recursively in the environments from which the lambda or macro was invoked. The symbol of the first found binding is then replaced by its object.
Whenever fLisp encounters an error an exception is thrown. Exceptions have an error type symbol a human
readable error message and the object in error, which is nil with generic errors. fLisp does
not implement stack backtracking. Exceptions are either caught on the top level of an evaluation or by
a catch statement.
The following error type symbols are defined and used internally:
end-of-fileread-incompleteinvalid-read-syntaxrange-errorwrong-type-argumentinvalid-valuewrong-num-of-argumentsarith-errorio-errorout-of-memorygc-error
Exceptions can be thrown via the throw function. As long as applicable use
one of the existing error codes with throw.
fLisp outputs an error message formated as error: message if the error object
is nil otherwise as error: 'object', message,
where object is the serialization of the object causing the error. message is the error
message.
fLisp counts with a set of built-in functions called primitives. They are grouped in the manual by the type of objects they operate on. The primitives are bound in the global environment to the names under which they are described.
(progn[ expr..])progn returns nil.
(cond[ clause..])(pred[ action ..]). cond
evaluates each clause in turn. If pred evaluates to nil, the
next clause is tested. If pred evaluates not to nil and if there is
no action the value of pred is returned, otherwise (progn action ..)
is returned and no more clauses are evaluated.
(setq symbol value[ symbol value..])setq returns the last value.
(define symbol value[ symbol value..]) Ss: define, letdefine returns the last value.
define cannot be used to define functions, its features rather resemble let.
(lambda params body)(lambda ([param ..]) body) s(lambda (param[ param..] . opt) body) s(macro params body)(macro ([param ..]) body) s(macro (param[ param..] . opt) body) s(quote expr)(catch expr) DEvaluates expr and returns a list with three elements:
nil on success or an error type symbol.(throw result message[ object]) D(open path[ mode]) S: open"r"ead only by default.
open can open or create files, file descriptors and memory based streams.
r, w, a, r+, w+, a+ plus an
optional b modifier.
<n for reading, >n for
writing. n is the number of the file descriptor. Omit mode.
<. The
name of the opened file is set to <STRING.
>. The
name of the opened file is set to >STRING.
(close stream) S: close(file-info stream) f(path buf fd) for stream. buf is
either nil or the text buffer of a memory stream. fd is either the integer
representation of the file descriptor or nil when stream is already closed.(read stream[ eof-value]) S: readnil. In that case eof-value is returned.
(write object[ keys..]]
keys::stream stream:readably flag:stream output is written to the given stream. With key :readable
not nil output is formatted in a way which which gives the same object when read again.
write returns the object.
(eval object)(system string)(os.getenv string) (null object)t if object is nil, otherwise nil.(type-of object)(consp object)t if object is of type cons, otherwise nil.(symbol-name object)t if object is of type cons, otherwise nil.(cons car cdr)(car cons)(cdr cons)(same a b)t if a and b are the same object, nil otherwise.(i+ i j)(i* i j)(i- i j)(i/ i j)arith-error if j is zero.
(i% i j)arith-error if j is zero.
(i= i j)(i< i j)(i> i j)(i<= i j)(i>= i j)(& i j)(| i j)(^ i j)(<< i j)(>> i j)(~ i)(string-length string)(string-append s1 s2)(substring string[ start [end]])(string-search needle haystack) Cnil.
(string-to-number string)(ascii integer)(ascii->number string)Tbd. carry over comprehensive documentation from file.c
(fflush[ stream])(fseek stream offset[ relativep])(ftell[ stream])(feof[ stream])end-of-file if stream or input are exhausted, else nil(fgetc[ stream])(fungetc i[ stream])ungetc() integer i as char to stream or input.(fgets[ stream])INPUT_FMT_BUFSIZ from stream or input.(fstat path[ linkp])(fmkdir path[ mode])(popen line[ mode])(pclose stream)(popen)(d+ x y)(d* x y)(d- x y)(d/ arg[ div..])inf if y is zero.
(d% x y)-nan if y is zero.
(d= x y)(d< x y)(d> x y)(d<= x y)(d>= x y)
On startup, both femto and flisp try to load a single Lisp file. The default location
and name of this startup file are hardcoded in the binary and can be overwritten with environment
variables:
femto.rc, FEMTORCflisp.rc, FLISPRC/usr/local/share/femto, FEMTOLIB/usr/local/share/flisp, FLISPLIB
The library path is exposed to the Lisp interpreter as the variable script_dir.
The provided startup files implement a minimal library loader, which allows to load Lisp files from the library
path conveniently and without repetition. The command to load the file example.lsp from the
library is (require 'example).
Femto provides the following set of libraries:
.rc files, always loaded. The core library implements the minimum required Lisp
features for loading libraries.
femto and flisp.
femto editor specific functions.femto editor utilitiesfemto editor modules(list [element ..]) C(defmacro name params body) C(defun name params body) C(curry (func a))(func a b).
(typep (type object)) C(integerp object) C(stringp object) C(symbolp object) C(lamdap object) C(macrop object) C(streamp object) Ct if object is of the respective type, otherwise nil.(numberp object) C(cadr list) C(car (cdr list)).(cddr list) C(cdr (cdr list)).(caddr list) C(car (cdr (cdr list))).(number-to-string number) C(eq a b)t if a and b evaluate to the same object, number or
string, nil otherwise.
integerp.(not object) Cnull(fold-left func init list) Ss: fold-left(length obj) C(string arg) C(concat [arg..]) Ce(memq arg list)nil.
(map1 func list) S: mapcarmap1 is a specialized form of mapcar restricted to one list only.nfoldcoercecoercecfold-leftp(let ((name value)[ (name value)..]) body) C(let label((name value)[ (name value)..]) body) Csnamed
let: define a local function label with body and
all names as parameters bound to the values.
(prog1 sexp[sexp..]) C(fload stream) f(load path) C(provide feature)(require feature)feature.lsp is loaded from
the library path and registers the feature if loading was successful. The register is the
variable features.
The following arithmethic functions coerce their arguments to double if any of them is double, then they use double arithmetic operators. If all arguments are integer they use integer arthmetic.
(+[ num..])0 if none is given.(*[ num..])1 if none given.(-[ num..])(/ num[ div..])inf if one of the divs is 0 and the sum
of the signs of all operands is even, -inf if it is odd.
(% num[ div..])1 if no div is given, num%div[%div..] if one or
more divs are given. If one of the divs is 0, the program exits with an
arithmetic exception.
(= num[ num..])(< num[ num..])(> num[ num..])(<= num[ num..])(>= num[ num..])t or nil. If only one num is given they all
return t.
This library implements commonly excpected Lisp idioms. fLisp implements a carefully selected minimum set of commonly used functions.
(listp o) Dnil or a cons.(and[ o..])t or the last object o if none is given or all evaluate to non
nil, nil otherwise.
(or[ o..])nil if all objects o are nil, otherwise returns the first object
which evaluates to non nil.
(reduce func list start) Dreduce applies the binary func to the first element of list
and start and then recursively to the first element of the rest of the list and the result
of the previous invocation: it is right binding.
reduce is right associative and start is not optional, it differs significantly
both from Common Lisp and Scheme.
(fold-right func end list) Cs(unfold func init pred) Cs(iota count[ start[ step]]) Cs(flip func) f(reverse l)To be integrated into the flisp library
This library implements some Common Lisp functions, which are not used in the editor libraries. They are provided for reference.
This library implements helper function required by the Femto editor. It is written only in Lisp idioms provided by fLisp itself plus the fLisp Library.
The editor extensions introduces several types of objects/functionality:
This section describes the buffer related functions added by Femto to fLisp. The description is separated in function related to buffer management and text manipulation. Text manipulation always operates on the current buffer. Buffer management creates, deletes buffers, or selects one of the existing buffers as the current buffer.
Buffers store text and allow to manipulate it. A buffer has the following properties:
special, modified.
cmode and lispmode are
available. If none is set text mode is used for syntax hightlighting.In the following, any mention to one of them refers to the respective current buffers property.
(insert-string string)(insert-file-contents-literally string [flag])
Note: Currently the flag is forced to nil. The function should
return (filename count) but it returns a flag indicating if the operation
succeeded.
(erase-buffer)(delete)(backspace)(get-char)(copy-region)(kill-region)(yank)(set-mark)(get-mark)(get-point)(get-point-max)(set-clipboard variable)Sets clipboard to the contents of variable. S: gui-set-selection(get-clipboard)(set-point number)(goto-line number)(search-forward string)(search-backward string)(beginning-of-buffer)(end-of-buffer)(beginning-of-line)(end-of-line)(forward-word)(backward-word)(forward-char)(backward-char)(forward-page)(backward-page)(next-line)(previous-line)(list-buffers)(get-buffer-count)(select-buffer string)set-buffer in Elisp.(rename-buffer string)(kill-buffer string)(get-buffer-name)buffer-name in Elisp.(add-mode-global string)(add-mode string)(delete-mode string)(find-file string)(read-hook string) is called. C
(save-buffer string)This section lists function related to window and message line manipulation, keyboard input and system interaction.
(delete-other-windows)(split-window)(other-window)Note: Elisp other-window has a required parameter count, which specifies the number
of windows to move down or up.
(update-display)(refresh)update-display.(message string)(clear-message-line)(prompt prompt default)(show-prompt prompt default)t.
(prompt-filename prompt)(set-key key-name lisp-func)(get-key-name)(get-key-funcname)(execute-key)(describe-bindings)(describe-functions)(getch)get-key-name, get-key-funcname and execute-key.
(exit)(eval-block)eval-block evaluates
this region and inserts the result at point. If point is
before mark eval-block does nothing but returning t.
(log-message string)(log-debug string)debug.out.(get-version-string)
fLisp can be embedded into a C application. Two examples of embedding are the femto editor and the
simplistic flisp command line Lisp interpreter.
Currently embedding can only be done by extending the build system. Application specific binary Lisp extensions
are stored in separated C files and the interface code is conditionally included into the lisp.c
file. Three extensions are provided: the Femto extension which provides the editor functionality, the file
extension which provides access to the low level stream I/O functions and others and the double extensions which
provides double float arithmetic.
lisp_new()lisp_destroy()lisp_eval()lisp_write_object()lisp_write_error()Different flows of operation can be implemented. The femto editor initializes the interpreter without input/output file descriptors and sends strings of Lisp commands to the interpreter, either when a key is pressed or upon explicit request via the editor interface.
The flisp command line interpreter sets stdout as the default output file descriptors of
the fLisp interpreter and feeds it with strings of lines read from the terminal. If the standard input is
not a terminal stdin is set as the default input file descriptor and fLisp reads through it
until end of file.
After processing the input, the interpreter holds the results corresponding to
a catch result in its internal structure. They can be accessed with the
following C-macros:
FLISP_RESULT_CODE(interpreter)FLISP_RESULT_MESSAGE(interpreter)FLISP_RESULT_OBJECT(interpreter)
Check for (FLISP_RESULT_OBJECT(interpreter) != nil) to find out if the result is an error. Then check
for (FLISP_RESULT_OBJECT(interpreter) == out_of_memory) to see if a fatal condition occured.
On error use lisp_write_error() to write the standard error message to a file descriptor of choice,
or use the above C-macros and FLISP_ERROR_MESSAGE(interpreter)->string for executing a specific
action.
fLisp sends all output to the default output stream. If it is set to NULL on initialization,
output is suppressed altogether.
Interpreter *lisp_new(char **argv, char *library_path, FILE *input,
FILE *output, FILE* debug)
lisp_new() creates and initializes an fLisp interpreter and returns a pointer to
an Interpreter struct to be used in the other functions. The arguments to lisp_new()
are:
*argv[0], if anyargvlibrary_pathNULL, the input stream has to be
specified for each invocation of lisp_eval().
NULL a memory stream is created at the
first invocation of the interpreter and set as the default output stream.
NULL no debug information is generated.void lisp_destroy(Interpreter *interp)void lisp_eval(Interpreter *interp, char *string)NULL evaluates all Lisp expressions in string.
NULL input from the file descriptor in the input field of
the fLisp interpreter interp is evaluated until end of file.
NULL no Lisp
evaluation takes place and FLISP_RESULT_CODE field of the interpreter is set to an io-error.
void lisp_write_object(Interpreter *interp, FILE *fd, Object *object,
bool readably)
void lisp_write_error(Interpreter *interp, FILE *fd)true.
Note: currently only creating one interpreter has been tested.
An extensions has to create C functions with the
signature: Object *primitive(Interpreter *interp, Object **args, Object **env),
where primitive is a distinct name in C space. This function has to be added to the global
variable primitives in the following
format: {"name", argMin, argMax, type_check, primitive}. Here
name is a distinct name in Lisp space.
interp is the fLisp interpreter in which primitive is executed. argMin is the minimum number of arguments, argMax is the maximum number of arguments allowed for the function. If argMax is a negative number, arguments must be given in tuples of argMax and the number of tuples is not restricted.
When type check is set to on of the TYPE_* C-macros the interpreter assures that all arguments are of
the given type and creates a standardized exception otherwise. When type check is set to 0 the
primitive has to take care of type checking by itself. The C-macro CHECK_TYPE helps with this.
When creating more then one new objects within a primitive, care has to be taken to register them with the garbage
collector. Registration is started with the
GC_CHECKPOINT CPP macro. GC_TRACE(name, value creates an object
variable name, sets it to value and registers it with the garbage collector. The
macro GC_RELEASE must be called to finalize the registration. The convenience
macro GC_RETURN(object) calls GC_RELEASE and returns object.
Some CPP macros are provided to simplify argument access and validation in primitives:
FLISP_HAS_ARGSFLISP_HAS_ARG_TWOFLISP_HAS_ARG_THREEFLISP_ARG_ONEFLISP_ARG_TWOFLISP_ARG_THREECHECK_TYPE(argument, type, signature)type_string. signature is the signature of the primitive followed
by - and the name of the argument to be type checked. This is used to form a
standardized wrong-type-argument error message.
fLisp implements Cheney's copying garbage collector, with which memory is divided into two equal halves (semi spaces): from- and to-space. From-space is where new objects are allocated, whereas to-space is used during garbage collection. The from-space part of the memory is also called the Lisp object space.
When garbage collection is performed, objects that are still in use (live) are copied from from-space to to-space. To-space then becomes the new from-space and vice versa, thereby discarding all objects that have not been copied.
Our garbage collector takes as input a list of root objects. Objects that can be reached by recursively traversing this list are considered live and will be moved to to-space. When we move an object, we must also update its pointer within the list to point to the objects new location in memory.
However, this implies that our interpreter cannot use raw pointers to objects in any function that might trigger garbage collection (or risk causing a SEGV when accessing an object that has been moved). Instead, objects must be added to the list and then only accessed through the pointer inside the list.
Thus, whenever we would have used a raw pointer to an object, we use a pointer to the pointer inside the list instead:
function: pointer to pointer inside list (Object **)
|
v
list of root objects: pointer to object (Object *)
|
v
semi space: object in memory
GC_TRACE adds an object to the list and declares a variable which points to the objects
pointer inside the list.
GC_TRACE(gcX, X): add object X to the list and
declare Object **gcX to point to the pointer to X inside the list.
Information about the garbage collection process and memory status is written to the debug file descriptor.
Object allocation adjusts the size of the Lisp object space on demand: If after garbage collection the free space
is less then the required memory plus some reserved space for exception reporting, the memory is increased by a
multiple of the amount specified in the C-macro FLISP_MEMORY_INC, defined in lisp.h. The
multiple is calculated to hold at least the additional requested space.
lisp_new() allocates FLISP_MIN_MEMORY, defined in lisp.h, and then
allocates all initial objects without taking care of garbage collection. Then it prints out the amount of Lisp
object space consumed to the debug file descriptor. For fLisp this is currently about 21 kB,
for femto about 34 kB.
In order to reduce garbage collection frequency, especially during startup, one can
set FLISP_INITIAL_MEMORY to a desired additional amount of memory to allocate on startup.
Some other compile time adjustable limits in lisp.h:
INPUT_FMT_BUFSIZ, size of the formatting buffer for lisp_eval() and for the
input buffer of (fgets).
WRITE_FMT_BUFSIZ, size of the output and message formatting buffer.
fLisp can live with as little as 50k object memory up to startup. The Femto editor requires much more
memory because of the needs of the OXO
game.
It is now possible to catch exceptions within Lisp code and exceptions return differentiated error codes and use
POSIX stream I/O. This, together with the (eval) primitive would allow to write the repl directly in
Lisp, and reading and eval'ing until no more incomplete
input
result codes are returned.
Loops are availble via the labelled let macro and supported by iota. It could made easier, by any
combination of:
Implement backquote and friends.
Pluggable extensions.
Take away more things.