This repository contains slisp a simple Lisp compiler which generates static/standalone binaries for Linux AMD64 systems.
The repository also contains a lisp interpreter, inception, which gives you the ability to write code in a lisp REPL interactively.
Quick links:
- INTRODUCTION.md
- Brief high-level overview of the facilities.
- INCEPTION.md
- The lisp interpreter which can interpret itself, hence the name.
- GC
- Notes on our garbage collector, and how to retrieve stats from it or tweak the behaviour at run-time.
- PRIMITIVES.md
- Detailed list of all available functions and special-forms.
This is a simple example of what a program might look like:
(defun fact (n)
"Calculate, and return, the value of N!"
(if (<= n 1) 1 (* n (fact (- n 1)))))
;; entry-point
(defun main (args)
"Command line arguments are available in the list ARGS."
(println "factorial demonstration, 10!:" (fact 10))
;; exit code - use "(exit 0)" if you prefer
0)You can find bigger examples beneath examples/, and our test/ directory also contains a large number of programs which are used for validation purposes (they are compiled and executed, their output compared to known-good results stored alongside them).
-
Notable examples
- examples/brainfuck.lisp contains a useful/working brainfuck interpreter.
- examples/example.lisp has other misc. snippets.
- examples/life.lisp - Game of Life.
- examples/globals.lisp - Explicit demonstration of scopes, showing that local variables always take precedence over global ones.
- examples/nqueens.lisp is a solver for the N Queens problem, defaults to solving the 8x8 grid but you may specify different sizes via a CLI argument.
- examples/wc.lisp is a clone of the standard
wcutility, which demonstrates our included argument-parser packages/.
-
Notable tests:
- test/entries.lisp - Read all the files in a directory, filter them, sort them, and print their names.
- Standard programs: test/factorial.lisp, test/fibonacci.lisp, test/fizzbuzz.lisp.
- File I/O: test/fread.lisp and test/fwrite.lisp.
- test/sort3.lisp - A mergesort implementation.
- test/vararg.lisp - Demonstration of a function accepting a variable number of arguments.
It should be noted that we prepend a "standard library" of functions to all user programs unless -stdlib=false is added to the compiler command line. That library itself is a useful reference/demonstration of functionality:
- stdlib.slisp
- Our "standard library" written in
slispitself. - Has
map,lengthand similar general-purpose functions.
- Our "standard library" written in
- Support for bindings, functions, floating-point numbers, integers, strings, lambdas, lists, etc.
- The lambdas have support for closures.
- Run-time type detection via functions such as
int?, andcons?. - A rough and ready bump-allocator for memory-allocation.
- Floats, Lambdas, Lists, and Strings all live on the heap.
- This is supported by a stop© garbage collector, using Cheney's algorithm, named after it's inventor Chris J. Cheney.
- See the Garbage Collection file for further details on that..
- Mathematical operations
+,-,*, and/.- These work against integers, floating point numbers, or combination of the two.
- File I/O operations:
fopen,fclose,fread, andfwrite.
- Filesystem primitive:
dir?,entries,exists?,file?,mkdir,mkdirs,rmdir,stat,unlinkandwhich.
- Comparison operations:
=,<,<=,>=,>, and!to invert a result.
- Special forms (only some of which are valid at the top-level, those are marked with
*):(alias! ..)-*- Alias/overwrite a function.(defmacro ..)-*- declare a macro.(defun ..)-*- declare a function.(defconst ..)-*- declare a global constant.(defvar ..)-*- declare a global variable.(do ..)(if ..)(lambda ..)(let ..)(require ..)-*- Include other source files.(set! ..)(while ..)
- Support for simple macros.
- For example our standard functions
and,cond,list,or,unless, andwhenare implemented as macros.
- For example our standard functions
- Tail call optimization.
You can see a complete list of our primitives, and their details in PRIMITIVES.md. The primitives are grouped by their implementation location (some things are implemented in assembly, and some things are implemented in our own slisp language, as part of the embedded stdlib.slisp.)
NOTE: Our lisp interpreter, inception, allows all forms at the top-level and has more complete macro support.
- Macros (
defmacro) in our compiler are a deliberately restricted, non-hygienic, compile-time expansion mechanism.- A macro body may use bound parameters, literals,
quote/quasiquotetemplates, a compile-timeif, andcar/cdr/nil?(for recursing over a variadic parameter) to construct its expansion - but not arbitrary compile-time computation (e.g. calling+directly against a parameter) - A macro can't substitute into "raw name" slots - the target of
set!,let-binding names, orlambda/defunparameter names - since those are parsed as literal tokens, not expressions. - So you cannot write a decent
dolistmacro that inserts a named variable in the callee scope, however you can use an anaphoric approach.
- A macro body may use bound parameters, literals,
- We don't have "symbols" exposed to the language.
- You may prefix a variable with "
:" to make it visually distinct. - Quoting a bare symbol, e.g.
'foo, produces the same kind of string. - So both
:fooand'fooare treated as the string"foo".
- You may prefix a variable with "
Build the compiler and interpreter by running make. If you just want the compiler you can build that in the standard fashion:
go build .
Using the compiler you can then compile, assemble and link a program like so:
./slisp -compile example/example.lisp
If you prefer to run the commands manually you may do it this way:
./slisp example/example.lisp > example.s
nasm -f elf64 example.s
ld -o example --gc-sections example.o
Finally you may execute your compiled program:
./example
There are some functional test programs beneath test/, which compile fixed programs and compare their output to known-good results. You can run these tests by executing:
cd test && make testRunning make clean at the top-level will remove the test artifacts, and compiled programs.
In addition to the functional tests there are also golang tests of the internal implementation packages, these can be executed in the standard fashion:
$ go test ./...
ok github.com/skx/slisp 0.004s
ok github.com/skx/slisp/compiler 0.009s
ok github.com/skx/slisp/env (cached)
ok github.com/skx/slisp/lexer 0.008s
ok github.com/skx/slisp/parser 0.006sThere is also support for the fuzz-testing that golang provides, you can run five minutes of fuzz-testing by executing the following (remove the -fuzztime=300s to run forever, and remove -parallel=1 to run more than a single instance at a time):
$ go test -fuzztime=300s -parallel=1 -fuzz=FuzzProject -vI've spent a few weeks writing a compiler for a home-made language, s-lang. Initially that language only used integers, but later I added floats/strings/pointers with appropriate type-markers in the lower bits of the values.
I found the overhead of dealing with typing and syntax a bit complex, and kinda backed myself into a corner with it - I wrote a reasonably complete standard-library with File I/O, getenv, and other things.
However adding more types, and dynamic things felt like it would be too complex as it would involve ripping out so much of what I'd done. The compiler, the standard library, and the interface between the two.
So this repository was born:
- Implement a compiler.
- With proper typing from the ground-up. Using macros for readability and to minimize the chances of making mistakes.
- Use the well-known SysV ABI, rather than my home-grown alternative.
- Use lisp because the syntax is trivial to parse.
- And I've written interpreters for it in the past so there are dragons, but somewhat friendly ones.
I guess at the end of the day both of these languages are toys, and both are here for my own personal learning.