Copyright (c) 2021, 2022 by Manuel J. Simoni
License: MIT
Status: Work in progress, but mostly stable. Using it successfully in a bunch of personal projects.
LispX is a new Lisp I am developing to make web programming bearable.
It has three main parts:
-
LispX’s raison d’être is to solve JavaScript’s async problem. It does this with delimited continuations. It faithfully implements the delimcc API as well as delimited dynamic binding by Oleg et al. and can run the full test suite from the delimcc distribution. With continuations, all async JS APIs (promise- and callback-based) can be used as if they were synchronous.
-
As the language core, LispX uses vau calculus developed by John Shutt for his Kernel language. Vau calculus is essentially a lambda calculus without implicit evaluation of arguments (sometimes called "call-by-text"). Vau calculus turns the usual programming/metaprogramming dichotomy on its head: metaprogramming is the default, and ordinary programming the boring special case.
-
As the "user interface", LispX uses the same classic Lisp syntax, naming conventions, and overall organisation as Common and Emacs Lisp. It is a Lisp-2, is fully object oriented with single inheritance (
defclass
), has single-dispatching generic functions (defgeneric
,defmethod
), the usual simple control forms (catch
,throw
,block
,return-from
,unwind-protect
,loop
,dotimes
,dolist
, …), and implements large parts of the CL condition system (handler-bind
,handler-case
,restart-case
,signal
,error
,invoke-restart
,invoke-restart-interactively
,compute-restarts
, …). The condition system is of course written in LispX itself and is a good demonstration of LispX programming in practice: src/cond-sys.lispx.
-
The implementation is about 2000 lines of JS and 1000 lines of Lisp (as measured by
cloc
). -
Clean and well-documented code.
-
The build is < 20 KB (minified and gzipped).
-
There are more than thousand unit tests.
-
The architecture is a tree-walking interpreter so it’s not a speed demon, but it’s fast enough for many apps, especially GUIs.
-
Readable Lisp stack traces.
-
Easy to interface with JS.
-
Works practically everywhere (browsers, Node, Deno, QuickJS, Duktape, …)
-
src/vm.mjs — Core VM data structures like objects, classes, symbols, environments, etc.
-
src/eval.mjs — Evaluation of core forms like
def
,vau
,progn
,if
, etc. -
src/control.mjs — Evaluation of delimited and simple control forms.
-
src/read.mjs — Reader code.
-
src/boot.lispx — Language bootstrap. This also documents all built-ins until I put together a reference manual.
-
src/cond-sys.lispx — Condition system.
-
src/js.lispx — JS interface.
$ ./scripts/node-repl Welcome to LispX! * (+ 100 x) Debugger invoked on condition: #<unbound-symbol-error :environment #<environment> :message "Unbound variable: x" :symbol x> Available restarts -- use (invoke-restart 'name ...) to invoke: continue use-value store-value abort Backtrace: x (#<function> 100 x) (%eval form environment) (eval (read) repl:+environment+) (#<function> (eval (read) repl:+environment+)) (print (eval (read) repl:+environment+)) [1] (invoke-restart 'use-value 42) 142 *
We use options throughout instead of nil-punning.
An option is either nil or a one-element list.
Functions returning options end in ?
.
(get? '(:bar 1 :foo 2) :quux) => () (get? '(:bar 1 :foo 2) :foo) => (2)
Forms like if-option
are used for destructuring:
(if-option (value (get? '(:bar 1 :foo 2) :foo)) value 3) => 2
There are three namespaces: variable (no particular read syntax), function (sharpsign single-quote), and class (sharpsign caret).
Unlike in CL, function (and class) symbols can also be used on the left-hand side of definitions and as parameters:
(def #'foo (lambda ()))
has the same effect as (defun foo ())
.
LispX combines the advantages of Lisp-1 and Lisp-2.
We can call functions received as arguments without the need for funcall
by using
function symbols as parameters:
(defun compose (#'f #'g) (lambda (x) (g (f x))))
If it’s not a symbol, the operator position of a form is evaluated normally, as in Lisp-1:
((compose (lambda (x) (+ 1 x)) (lambda (x (* 3 x))) 10) => 33
The left hand side of definitions and parameter forms can be not only symbols
but also nested lists. This provides a uniform solution for destructuring
and multiple values without any special forms such as multiple-value-bind
.
(def (x y) (list 1 2)) x => 1 y => 2
#ignore
is used to ignore unneeded data:
(let (((((# 535A ignore . rest))) '(((1 2 3))))) rest) => (2 3)
If you think that let
has too many parentheses, LispX might not be for you.