SLip — a Lisp system in your browser
I've put some love into my browser-based Lisp system. This project is so old that I could actually say it's new and nobody would know. I just let it rot for something like 12 years, but since I revamped Ymacs recently, I wanted to fix the “IDE”, which was broken; and then I got carried away.
Before reading further, if you are familiar with Common Lisp and Emacs/SLIME, check out this fine REPL! There you'll also find an info file with some information about the environment. And here's the old “crazy clock demo”, which I showcased in a video when I first announced this project.
This is a Lisp implementation (Common Lisp wannabe, but oh well, we're still far from that) that runs in a JavaScript VM. See the project page for more information about existing features. Recent work includes:
-
I modernized the JS code to use ES6 modules and classes. I updated the object file format (.fasl) to a more compact representation (using numeric opcodes). The “executables” are quite small now and compress very well after gzip.
-
Efficient support for “ordinary lambda lists” in all functions. Rather than using
destructuring-bind
(which, previously, was the only way to get fancy argument list), we now have compiler support for parsing lambda lists, and there's a new VM instruction to bind arguments at runtime. -
We now have a
loop
macro. It's small and written in almost portable Common Lisp (uses custom primitives for iterating through hash tables and package symbols). -
Added a pretty printer, which comes in handy for testing macroexpansion (C-c Enter and C-c M-m).
-
return-from
can now be used in all functions (they are automatically wrapped in ablock
). The compiler will avoid the block overhead if return is not used. -
The “IDE” got some polish as well. Most importantly, you can save files (even in my demo above). There's more info in the info.md buffer that shows up along with the REPL. Just open it! :)
-
Many less visible changes for stability and performance.
I think performance is OK, for interpreted “bytecode”. It can never get on par with JS code, but a lot can be achieved
by making the VM more powerful. For example, the form (pop x)
used to compile to 9 instructions (consing an
environment frame in between). That felt like a good opportunity to add a dedicated instruction POPLIST, which can do
the job without needing a new environment frame; besides a significant reduction in the object file size, it also
executes much faster simply because it's a single instruction instead of 9 (and it doesn't cons!).
I've done a similar optimization for (or ...)
. It was previously a macro, which used let
to save
the current value (because OR has to return the first non-false value). I thought I could use a VM jump instruction
which jumps if the top of the stack is non-NIL, but without popping the stack — so the value would just
remain available for whatever code follows, and we don't need to cons an environment frame to save it. To make it
work, OR is now a special case in the compiler, rather than just a macro — but it's worth the trouble!
On the other hand, even more could be achieved by making the compiler smarter (and the cases above could partly be improved by a smarter compiler, without the need for special instructions). But I don't feel smart enough to do it 😂. Here's one pattern that occurs a lot in macros:
(defmacro foo (thing ...)
(let ((val (gensym)))
`(let ((,val ,thing)) ;; conses a new env frame
;; ... do stuff with ,val
)))
Many times “thing” will be a constant, or a symbol (a variable defined elsewhere), so we could optimize this by using “thing” directly, instead of giving it a new local name (of course, only if it's not reassigned). My compiler currently doesn't do this. Whether it would be faster, I can't be sure without testing — variable access is expensive too, because the VM needs to look up the right environment frame; the closer it is, the faster it is. So a nested environment is not necessarily a bad thing (it does add pressure on the garbage collector though). A VM with registers might work better, but again, I don't feel smart enough for this yet.
All in all, performance is good enough for having fun, which I hope you will! Feedback would be nice — please comment here or drop me an email if you do anything interesting with this!