An embedded lisp implementation designed for use scripting games.
This repository provides an implementation of RT Lisp, a simple Lisp dialect
similar to Clojure. The emphasis is on a small memory footprint, simple language
definition, and easy usage from C.
RT Lisp is being developed with game scripting in mind as its primary use case.
Note: the name RT Lisp is terrible, it stands for “Runtime Lisp”, because it’s
meant to be used in an old game project of mine at runtime… It does NOT
stand for “Real Time Lisp”, the garbage collector likely prevents this system
from being used in true realtime systems.Who knows, maybe I’ll rename it …
It’s been a while since I ran this on a platform other than Linux, so a little
debugging may be in order on other systems.
In order to build, you need to ensure that all submodules are checked out
cd rt-lisp.git
git submodule init
git submodule update
After that you can simply build it by running
make
Alternatively, if you are on a system with the development libraries
for libexplain installed, you can setUSE_LIBEXPLAIN=true
for the make command in order to get better
error messages.
USE_LIBEXPLAIN=true make
RT Lisp is built as a shared library in the current directory, so you’ll need
to tell the dynamic linker where to find it when you run the REPL binary
LD_LIBRARY_PATH=. ./crtl
That’s it! After some loading text, you should get a prompt like this
[CRTL]
You can hit Ctrl
+ D
to quit. Maybe twice if you’re in the middle
of an expression…
Here is an example session:
[ CRTL ] (defun even? (n) (zero? (% n 2)))
Compiled function 'repl:even?'
=> repl:even?
[ CRTL ] (even? 2)
=> T
[ CRTL ] (even? 3)
=> nil
[ CRTL ] <Ctrl+D>
=> .io:EOF
At this point, RT Lisp is feature complete except for the standard
library. Notable features include:
RT Lisp uses a stack-based virtual machine with a pretty nicely pipelined
interpreter loop. Profiling is still needed, but anecdotally the performance is
good enough for relatively complex game scripting with hundreds of C to VM calls
per frame at this point.
Instructions for the VM are generated by a straightforward syntax-directed
multi-pass compiler. It currently doesn’t do much optimizing, but it does support
the tail call optimization.
RT Lisp uses a very responsive and cache-friendly generational garbage
collector. It uses compact rank/select bitmaps to perform efficient mark/sweep
passes.
Unlike many other Lisp package systems, packages in RT Lisp are lexically
scoped, allowing for imports only within a block of code. This also allows for
macros that implicitly switch packages.
Is this a good idea? I don’t know. But in the worst case just don’t use it :).
(in-package foo
(defun hello ()
'hello-foo)
(export hello))
=> T
(foo:hello)
=> hello-foo
(use-package foo
(hello))
=> hello-foo
Right now the implementation is incomplete, but I’m putting consistent effort
into getting the basic features done, improving usability, and cleaning up the
code.
Symbols are resolved in a manner similar to Common Lisp (except that the current
package is determined lexically). This allows for practically conflict-free
macro implementations with little effort.
That said, macros in RT Lisp are simply functions on S-Expressions, there are no
beaurocratic hygiene rules or other limits imposed.
Unlike common Lisp keywords (but a bit like Clojure keywords), RT Lisp uses
selectors. They are like self-evaluating symbols that begin with a dot
(e.g. .x
, .foo
). Selectors may be part of a package, as in .foo:something
.
Unlike symbols, selectors need to be explicitly annotated with a package, no
resolution is performed; .foo
is the same value no matter what package you are
in.
Like Clojure, RT Lisp supports immutable HAMT-based maps. Maps may be “called”
as if they were functions, for example:
(let ((m { .x 1 .y 2 }))
(m .x))
=> 1
Similarly, a selector may be “called” on a map as if it were a function.
(let ((m { .x 1 .y 2 }))
(.y m))
=> 2
Both of these forms are convenient for use with higher-order functions.
Obviously, RT Lisp supports standard Lisp cons lists.
Tuples are fixed-size zero-indexed vectors. Similar to maps, they can be called
as a unary function of an integer argument.
(let ((t [7 8 9]))
(t 1))
=> 8
A string in RT Lisp is simply a tuple of characters. They are pretty-printed in
double quote syntax.
['L' 'I' 'S' 'P']
=> "LISP"
This syntax is also accepted by the reader.
("LISP" 2)
=> 'S'
RT Lisp supports modern lexically scoped closures
(let ((c (let ((x 1)
(y 2)
(z 3))
(lambda ()
(list x y z)))))
(c))
=> (1 2 3)
RT Lisp supports modern nesting quasi-quote syntax.
(let ((x 1)
(y 2))
`(~x y `(~x ~(~y))))
=> (1 y `(~x ~(2)))
RT Lisp throws exceptions and prints stack traces for unhandled exceptions.
(+ 1 '2')
Exception .expected-int28:
Wrong type, expected 'Int28', got 'Char'.
{ .object '2', .type .expected-int28, .message "Wrong type, expected \'Int28\', got \'Char\'.", }
Stack Trace (most recent at bottom):
0: repl:code-page #0302
@0004 iadd
ERROR!
Exceptions can be propagated easily through C code and back into Lisp for
arbitrary nesting of C and Lisp function calls.
Exception handling is possible, but messy at the moment.
TODO: document exception handler macros.
The Reader actually doesn’t fully support UTF-8 yet (I’m working on it…), so
currently you can only read strings as UTF-8. The VM has full UTF-8 support
though, it’s just a matter of allowing multibyte characters in the reader.
("カタカナ" 2)
=> 'カ'
A single C program can have multiple VM instances with seperate heaps, stacks,
etc. These VMs may share bytecode, to avoid replicating the same functions over
and over in memory.
Currently shared code isn’t thread safe (though it is if you avoid defining
functions or macros in seperate threads), but I’m working on it.
Soon to be implemented/added are (in rough order of priority):
rt-lisp.c
.Long term plans include:
The VM’s performance is currently pretty good, but there are two big
issues I’m aware of.
Large data structures (e.g. maps with thousands of entries) can slow
things down, but that hasn’t been an issue for me working on game
scripting.
Vectors with more than 100 or so entries will crash the VM! This is
problematic for working with strings, obviously… The same bug can arise when
creating a (labels ...)
form with more than 20 or so functions.
Of these two problems, 1 is unlikely to be fixed (by me…) unless it becomes an
issue. 2 is a bigger problem, but it might require some large
performance-affecting changes to the garbage collector to fix. There are easy
work-arounds for working with large strings.
TODO: Document use of erlang-style i/o strings.
The error handling system has been improved massively, and at this point there
are only a few calls to abort()
left in the VM. They are there for dealing with
some unlikely and catastrophic scenarios (e.g. VM Out of Memory).
There is still room to handle these errors more gracefully, and it’s on my TODO
list.
RT Lisp is free software: you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
RT Lisp is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program. If not, see https://www.gnu.org/licenses.
All git submodules in this repository (listed in the .gitmodules file) have
their own licenses. The LGPLv3 license does not necessarily apply to those
submodules, and the only license which applies to the main RT Lisp repository
(excluding submodules) is the LGPLv3.