Converting “lisp” to JavaScript in the sense of this article is basically trivial (but then it was done in only 350 lines of Haskell).
Compiling, say, Common Lisp to JavaScript is hard. There are “easy” steps (in the sense that they can be ignored or done a stupid way at first) like dealing with numbers, namespaces, packages, (I.e. the symbol foo can name a function and a variable, amongst other things, and the symbols #:foo and #:foo are not equal).
There are the “boring” steps like doing argument parsing (and harder is working out how to make it efficient)
There are the hard steps like working out how to compile lexical goto and return-from, eval-when
There are the very hard steps necessary for macros like having to reimplement a macroexpander (for macrolet and friends) and then realising that this is insufficient as one also needs to implement environment and such (but this can be done by a macro to slightly modify macros, assuming that this compiler is also implemented in Common Lisp). But this still is insufficient. Consider the following legal Common Lisp:
(defun make-adder (number)
(lambda (x) (+ x number))
(defmacro macro-+ (a b)
`(funcall ,(make-adder a) ,b))
(defun foo (y) (macro-+ 3 y))
So the body of foo expands into:
(funcall #<lambda ...> y)
And one has no way to compile this host-system function to JavaScript and hence to expand macros one needs to be able to interpret Common Lisp and be able to convert any object into JavaScript source code (and keep in mind that the source code needs to ensure that what we’re identical in the CL are identical when the JS is compiled, and that closed-over variables are also properly dealt with in the sense that if two different functions close over the same variable in CL then they need to be compiled into JS functions that close over the same variable, or at least behave the same as the CL functions.
Having to interpret does at least solve problems like having numbers behave the same way in macros as at runtime.
And then there is CLOS which is a bit of a pain to bootstrap (and must be bootstrapped twice of course).
To compile CL to JS one essentially needs three CL compiler (backends), one for macros in the first compile, one for compiling to JS and one for compiling and linking in the code that is compiled at runtime in the JS VM.
Common Lisp is a lot, but compiling a LISP 1.5 is still a useful thing today. For one, LISP 1.5 (without PROG) is a subset (or can be made into one by changing some function names) of Common Lisp, elisp, ISLisp, EuLisp, PicoLisp (which is pretty much LISP 1.5, which is not stopping people from using it for large applications today), and a bunch of others.
Another thing LISP 1.5 is very useful for, and I would like to see more of, is as a simple model for memory management and other runtime algorithms. It makes algorithms both easy to understand and analyze, and obviously as this example shows makes them easy to implement! Henry Baker's amazing List Processing in Real Time on a Serial Computer[1] is probably the best example. If only all papers on garbage collection used a similar style.
I suppose I really mean that one needs to bootstrap at compile time and a way to transfer the compile time objects into runtime (ie into source to make objects at run time). The hierarchy is a bit of a pain to make at runtime as it is quite circular. Eg standard-class is a class, that is an instance of (some subclass of) class, and class is an instance of standard-class.
Cool stuff. I wrote a simple Hindley-Milner type language interpreter in Haskell inspired by Haskell a while back. Consider using Parsec as a parser combinator library.
The original title was "Compiling lisp to JavaScript in 350 lines from Scratch" -- For anyone actually interested in a lisp interpreter written in Scratch, there's https://scratch.mit.edu/projects/159487877/
Compiling, say, Common Lisp to JavaScript is hard. There are “easy” steps (in the sense that they can be ignored or done a stupid way at first) like dealing with numbers, namespaces, packages, (I.e. the symbol foo can name a function and a variable, amongst other things, and the symbols #:foo and #:foo are not equal).
There are the “boring” steps like doing argument parsing (and harder is working out how to make it efficient)
There are the hard steps like working out how to compile lexical goto and return-from, eval-when
There are the very hard steps necessary for macros like having to reimplement a macroexpander (for macrolet and friends) and then realising that this is insufficient as one also needs to implement environment and such (but this can be done by a macro to slightly modify macros, assuming that this compiler is also implemented in Common Lisp). But this still is insufficient. Consider the following legal Common Lisp:
So the body of foo expands into: And one has no way to compile this host-system function to JavaScript and hence to expand macros one needs to be able to interpret Common Lisp and be able to convert any object into JavaScript source code (and keep in mind that the source code needs to ensure that what we’re identical in the CL are identical when the JS is compiled, and that closed-over variables are also properly dealt with in the sense that if two different functions close over the same variable in CL then they need to be compiled into JS functions that close over the same variable, or at least behave the same as the CL functions.Having to interpret does at least solve problems like having numbers behave the same way in macros as at runtime.
And then there is CLOS which is a bit of a pain to bootstrap (and must be bootstrapped twice of course).
To compile CL to JS one essentially needs three CL compiler (backends), one for macros in the first compile, one for compiling to JS and one for compiling and linking in the code that is compiled at runtime in the JS VM.