Type SystemΒΆ
As previous pages have mentions, lisp objects rely on a mark and sweep garbage collection system to manage their lifetimes. This page describes how the type system is implemented, which includes the garbage collection implementation.
In Java, everything is an object. In this language, everything is a
lisp_value
. This means two things:
- Every object contains a
type
pointer. - Every object has a variable to store its
mark
. - Every object has a pointer to the object allocated after it, forming a linked list.
Every type declares these using the LISP_VALUE_HEAD
macro, like so:
typedef struct {
LISP_VALUE_HEAD;
int x;
} lisp_integer;
You can cast pointers to these objects to lisp_value*
and still access the
type object. All objects are passed around as lisp_value*
.
In order to allow objects to be treated differently based on their type (but in a generic way to calling code), we use the type object.
A type object is just another object, with type lisp_type
. However, it is
NOT managed by the garbage collector. It contains the string name of a type,
along with pointers to implementations for the following functions: print, new,
free, eval, call, and expand. Therefore, if you have lisp_value *object
and
you want to print it, you can do:
object->type->print(stdout, object);
Unfortunately that’s very verbose. To simplify, each of the functions implemented by a type object has an associated helper function. So you can instead do:
lisp_print(stdout, object);
But there’s no magic or switch statements involved here–we’re simply using the type object.
This means that it’s not too difficult to add a type to the language! All you need to do is declare a struct for your type, implement the basic functions, and create a type object for it. Here is an example for a regex type built on libstephen’s regex implementation:
// HEADER
typedef struct {
LISP_VALUE_HEAD;
Regex r;
} lisp_regex;
extern lisp_type *type_regex;
// CODE
static void regex_print(FILE *f, lisp_value *v);
static lisp_value *regex_new(void);
static void lisp_value_free(void *v);
static lisp_value *regex_eval(lisp_runtime *rt, lisp_scope *s, lisp_value *re);
static lisp_value *regex_call(lisp_runtime *rt, lisp_scope *s, lisp_value *c, lisp_value *a);
static smb_iter regex_expand(lisp_value *v);
static lisp_type *type_regex_obj = {
.type=type_type,
.name="regex",
.print=regex_print,
.new=regex_new,
.free=regex_free,
.call=regex_call,
.expand=regex_expand,
};
lisp_type *type_regex = &type_regex_obj;
// function implementations
Many function implementations in the type object are trivial. However, a few are rather important to get right:
- The print function should not print a newline.
- The new function should take no arguments. It does not need to bother to
populate the
lisp_value
fields such astype
ornext
. Instead, these will be called by thelisp_new()
wrapper function. It may be wise to write alisp_new_TYPENAME(args...)
function so users can easily create an instance of a type from some arguments. - The eval function is tricky. Many things just don’t get evaluated. For instance, a regex object would probably be returned by a builtin function, but never produced as raw code to be evaluated. So its eval function should probably return an error.
- Generally the call function should return an error. However it may not be the worst thing to make creative use of function call syntax. For instance, it would be interesting to have a regex type that, when called, matches on a string and returns the match object.
- The most critical function to implement correctly is expand. This must return
a generator, which yields pointers to other
lisp_value
objects contained by this object. For simple objects, this will be trivial, but for data structures it is incredibly important to get right.