Making a Mini-Lisp: Introduction to Transpilers in JavaScript
angularclass.com
angularclass.com
As someone who has written many compilers before, I'd almost feel it would be a slight disservice to call a transpiler a compiler, mostly because their architecture and goals are often different.
The characteristic that defines a Lisp is that code is not a sequence of characters, it's a data structure -- typically a tree of cons cells -- which can be manipulated within the language itself. The language being defined here doesn't have that property.
It is this property that allows you to write macros, which is what makes Lisp cool. Without it you don't have Lisp, you just have a run-of-the-mill programming language with weird syntax.
{
type: 'function',
value: 'defn',
children: [
{ type: 'function_name', value: 'average' },
{ type: 'arguments', children: [
{ value: 'x' },
{ value: 'y'}
]
},
{ type: 'function', value: '/', children: [...] }
]
}
The parenthesis-based surface syntax is directly translated into the above, which is IMO more verbose and contains no additional information. On the other hand, it is valid Javascript data.So it looks like it is equivalent to manipulate the above form. Now, suppose you want to define a function which returns a form which performs a function call. We won't use macros, but simply a function that builds an AST. We can't write Javascript directly with this parser, so let's say there are primitive Lispy functions to build equivalent javascript trees:
(defn my-funcall ()
(js%hash (js%kv "type" "function")
(js%kv "value" "my-fn")
(js%kv "children"
(js%vector
(js%hash
(js%kv "value" 3))))))
... whereas, a lisp-based approach would be: (defn my-function (list 'my-fun 3))Inspired by Peter Norvig's 'How to write a Lisp Interpreter in Python', I created three variants of it along the same lines as the OP. The first is a Simple Lisp Interpreter in Javascript [1]. The second separates Syntactic Analysis from Execution [2]. And the third supports non deterministic computing [3].
[1] https://bitbucket.org/sainamdar/lisp2js
It's okay if your target language already supports non-local exits, particularly parameterised ones (although even that can be faked with a well-designed global), but if it doesn't then it's a pain (imagine how you'd do it in Go: every single function call will return an additional value, indicating if a non-local exit is intended).
Alternatively, imagine trying to transpile call/cc to JavaScript…
Compiling continuations to a dynamic language is actually really easy.
--
Typically direct CPS conversion are straightforward, but the performance suffers a lot. I'm looking for languages that are using some hybrid approaches.
I'm well aware, but CPS code can be _insanely_ large (essentially a function declaration for every line of code in the original source). That's quite painful.
In my experience getting a BNF grammar right is something of an art which you pick up after having written a few parsers.