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 originates from Tiny-Lisp by matp (pre 2014), was integrated into Femto by Hugh Barnes (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.
fLisp fancies to converge toward Emacs Lisp. Function descriptions are annotated with a compatibility scale:
Annotation is omitted if the function does not exist in Emacs Lisp.
We use the following notation rule for the fLisp syntax:
name
«name»
.[text]
text
can be given zero or one time.[text..]
text
can be given zero or more times.
Notes:
[
square brackets]
and double-dots ..
as
syntactical elements.
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 object or a function invocation enclosed in parens. Function invocations can be infinitely nested.
The following characters are special to the reader:
(
)
'
:
(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. Exponent notation is not supported.
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.
t
true, 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 sexp 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.
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.
(lambda params body)
(lambda ([param ..]) body)
(lambda (param[ param..] . opt) body)
(macro params body)
(macro ([param ..]) body)
(macro (param[ param..] . opt) body)
(quote expr)
(signal type list)
(signal 'error 'nil)
is probably the
simplest signal.(fopen path mode)
(fclose stream)
(fread
stream
[ eof-value
])
nil
. 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
.(symbolp object)
t
if object is of type symbol, otherwise nil
.(symbol-name object)
(numberp object)
t
if object is of type number, otherwise nil
.(stringp object)
t
if object is of type string, otherwise nil
.(consp object)
t
if object is of type cons, otherwise nil
.(cons car cdr)
(car cons)
(cdr cons)
(eq a b)
t
if a and b evaluate to the same object, nil
otherwise.(+[ arg..])
0
if none is given.(*[ arg..])
1
if none given.(-[ arg..])
(/ arg[ 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.
(% arg[ div..])
1
if no div is given, arg%div[%div..] if one or
more divs are given. If one of the divs is 0
, the program exits with an
arithmetic exception.
(= arg[ arg..])
(< arg[ arg..])
(> arg[ arg..])
(<= arg[ arg..])
(>= arg[ arg..])
t
or nil
. If only one arg is given they all
return t
.
(string.length string)
(string.substring string start end)
(string.append string1 string2)
(string-to-number string)
(number-to-string number)
(ascii number)
(ascii->number string)
Whenever fLisp encounters an error an exception is thrown. Exceptions have a non-zero result code and a human readable error message. fLisp does not implement stack backtracking. exceptions are only caught on the top level of an evaluation.
In the flisp
interpreter the error message is 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 and message is the error
message.
When called from C-code, the object
field of the interpreter is set to the object causing the error
instead of the evaluation result.
Exceptions can be thrown from within in Lisp code via the signal
function.
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:
/usr/local/share/femto
, FEMTOLIB
/usr/local/share/flisp
, FLISPLIB
femto.rc
, FEMTORC
flisp.rc
, FLISPRC
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 a set of libraries, some of them are required by the editor
This library is built into the startup file.
(list
[element
..])
(defmacro name params body)
(defun name params body)
(string arg)
(concat
[arg
..])
(memq arg list)
nil
.
(fload
stream)
(load
path)
(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
.
This library implements commonly excpected Lisp idioms, which are used in the editor libraries.
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:
In the following, all mentions of these variables refer to the current buffers properties.
(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)
(find-file string)
(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. Two extensions are provided: the Femto extension which provides the editor functionality and the file
extension which provides access to the low level stream I/O functions and adds some more.
lisp_new()
lisp_destroy()
lisp_eval()
lisp_eval_string()
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 it through until end of
file.
After processing the given input, the interpreter puts a pointer to the object which is the result of the last
evaluation into the object
field of the interpreter structure. The result
field is set
to FLISP_OK
, which has the integer value 0
. The message
field is set to
the empty string.
fLisp sends all output to the default output stream. If NULL
is given on initialization, output is
suppressed altogether.
If an exception is thrown inside the Lisp interpreter an error message is formatted and copied to
the message
buffer of the interpreter, A pointer to the object causing the error is set to
the object
field. The result
field is set to an error specific code:
FLISP_ERROR
FLISP_USER
FLISP_READ_INCOMPLETE
FLISP_READ_INVALID
FLISP_READ_RANGE
FLISP_WRONG_TYPE
FLISP_INVALID_VALUE
FLISP_PARAMETER_ERROR
FLISP_IO_ERROR
FLISP_OOM
FLISP_GC
In this error state of the interpreter, the function lisp_write_error()
can be used to
write a standardized error message including the error object to a file descriptor of choice
Interpreter *lisp_new(int size, char **argv, char
*library_path, FILE *input, FILE *output, FILE* debug)
lisp_new()
creates and initializes an fLisp interpreter. The initial environment contains the
following symbols:
*argv[0]
, if anyargv
library_path
A pointer to an Interpreter struct is returned, which is used to operate the interpreter.
The other arguments to lisp_new()
are:
NULL
, 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)
ResultCode lisp_eval(Interpreter *interp, Object *gcRoots)
Evaluates the input file set in the input field of the fLisp interpreter interp until
end of file. gcRoots must be set to a global variable with initial value nil
. If no
input file is set, interp
is set to a respective error state.
ResultCode lisp_eval_string(Interpreter *interp, char *string, Object *gcRoots)
nil
void lisp_write_object(Interpreter *interp, FILE *fd, Object *object,
bool readably, Object *gcRoots)
nil
void lisp_write_error(Interpreter *interp, FILE *fd,
Object *gcRoots)
true
. gcRoots must be set to a global variable with initial
value nil
Note: currently only one interpreter can be created.
An extensions has to create C functions with the
signature: Object *primitive(Object **args, GCPARAM)
, 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, primitive}
. Here
name is a distinct name in Lisp space.
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.
CG_PARAM
is a CPP macro which carries on the interpreter root node for the garbage collector. Its
companion are GC_ROOTS
which is used in the place of GC_PARAM
when calling a primitive
and GC_TRACE(name, value
which creates an object variable name, sets
it to value and adds it to the root node.
Some CPP macros are provided to simplify argument validation in primitives, all of them receive the name of the primitive as a parameter:
TWO_STRING_ARGS(name)
Object *
variables first and second.
ONE_STRING_ARG(name)
Object *
variable arg.
ONE_NUMBER_ARG(name)
Object *
variable num.
ONE_STREAM_ARG(name)
Object *
variable stream.
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.
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_ROOTS
and GC_PARAM
are used to pass the list from function to
function.
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.
Some compile time adjustable limits in lisp.h
:
INPUT_FMT_BUFSIZ
, size of the formatting buffer for lisp_eval()
.WRITE_FMT_BUFSIZ
, size of the output and message formatting buffer.
fLisp can live with as little as 300k object memory. The Femto editor requires 16M since the OXO
game requires a lot of memory.
The two memory pages should be separated and the second one should be allocated only during garbage collection. When memory runs out, the garbage collection should be restarted with an increased capacity of the new page.
It should be able to trap exceptions within Lisp code. This, together with an (eval)
primitive would
allow to write the repl directly in Lisp.
Integer arithmetic would be sufficient for all current purposes and increase portability and speed while reducing size.
Exception handling returns differentiated error codes. One could implement an interactive repl
with stdin
/stdout
streams, by reading and eval'ing until no more incomplete
input
result codes are returned.
The file extension only contains (fflush)
, (ftell)
and (fgetc)
and could
easily be extended into something useful. (fstat)
would be most pressing for
improving femto.rc
and flisp.rc
.