The SPAIK Lisp implementation:
plus.lisp
(defun plus (&rest xs) (let ((s 0)) (dolist (x xs) (set s (+ s x))) s))
call.rs
use spaik::spaik::Spaik; use spaik::error::Error;
fn apifunccall() -> Result<(), Error> { let mut vm = Spaik::new()?; vm.load("plus")?; let (x, y, z) = (1, 2, 3); let result: i32 = vm.call("plus", (x, y, z, 4, 5))?; assert_eq!(result, 15); }
The implementation includes the following main parts:
See ~lisp/test.lisp~ and the ~tests/*.lisp~ files for an example of a non-trivial macro, and ~lisp/self.lisp~ for a non-trivial program.
* Accessing GC-memory directly *SPAIK uses a moving garbage-collector, meaning that pointers are not necessarily valid after a call to ~gc.collect()~, even if the pointer is known to be part of the root set. External references to GC-allocated memory need to use indirection (see ~SPV~ type,) but this indirection is not used inside the VM and GC internals because of the overhead.
Instead, the VM and GC maintain the following invariant
A ~PV~ value retrieved from the garbage collector arena may not be used after ~gc.collect()~ runs.
In practice this invariant is very easy to uphold in the internals. The ~unsafe~ blocks themselves are sometimes hidden behind macros for convenience.
// An example of unsafe GC memory access, done ergonomically using a macro withrefmut!(vec, Vector(v) => { let elem = v.pop().unwrapor(PV::Nil); self.mem.push(elem); Ok(()) }).maperr(|e| e.op(Builtin::Pop.sym()))?;
** Accessing VM program memory Program memory needs to be randomly accessed -- fast. But by default, Rust will use bounds-checking on arrays. This bounds-checking is usually cheap enough, but not for this particular use-case. The VM also only supports relative jumps, which would have required two additions when using indexing, but only one when using pointer arithmetic.
This optimization relies on:
* Converting between ~u8~ discriminants and ~enum Thing~ *SPAIK often needs to convert integer discriminants into enums, and vice-versa. For this it uses ~unsafe { mem::transmute(num) }~.