An S-expression syntax for ECMAScript/JavaScript, with Lisp-like hygienic macros. Minimal core, maximally customisable.
This is not magic: It's just an S-expression encoding of the estree AST format. The macros are ordinary JS functions that return arrays but just exist at compile-time. Macros can be put on npm to distribute your own language features, like this.
⚠️ Note the 0.x.x semver. The API may shift under your feet.
; Only include given statement if `$DEBUG` environment variable is set
(macro debug
(function (statement)
(return (?: (. process env DEBUG)
statement
null))))
(var fib ; Fibonacci number sequence
(function (x)
(debug ((. console log) (+ "resolving number " x)))
(switch x
(0 (return 0))
(1 (return 1))
(default (return (+ (fib (- x 1)) (fib (- x 2))))))))
var fib = function (x) {
switch (x) {
case 0:
return 0;
case 1:
return 1;
default:
return fib(x - 1) + fib(x - 2);
}
};
-
Small core, close to JS. This core eslisp corresponds closely with the estree abstract syntax tree format, and hence matches output JS clearly. It's purely a syntax adapter unless you use macros.
-
Maximum user control. Users must be able to easily extend the language to their needs, and to publish their features independently of the core language.
User-defined macros are treated like built-in ones, and are just ordinary JS functions. This means you can write them in anything that compiles to JavaScript, put them on npm, and
require
them.
I wanted JavaScript to be homoiconic and have modular macros written in the same language. I feel like this is the adjacent possible in that direction. Sweet.js exists for macros, but theyre awkward to write and aren't JavaScript. Various JavaScript lisps exist, but most have featuritis from trying too hard to be Lisp (rather than just being a JS syntax), and none have macros that are just JS functions.
I want a language that I can adapt. When I need anaphoric conditionals,
or conditional compilation or file content inlining (like brfs), or
a domain-specific language for my favourite library, or something insane
that hacks NASA and runs all my while-loops through grep
during compilation
for some freak reason, I want to be able to create that language feature myself
or require
it from npm if it exists, and hence make the language better for
that job, and for others doing it in the future.
That's the dream anyway.
S-expressions are also quite conceptually beautiful; they're just nested lists, minimally representing the abstract syntax tree, and it's widely known that they rock, so let's use what works.
This has great hack value too of course. Lisp macros are the coolest thing since mint ice cream. Do I even need to say that?
Further documentation in doc/
:
- Language basics reference
- Macro-writing tutorial
- Module packaging and distribution tutorial
- Comparison against other JS-lisps
This is a quick overview of the core language. See the basics reference or the test suite for a more complete document.
Eslisp code consists of comments, atoms, strings and lists.
; Everything from a semicolon to the end of a line is a comment.
hello ; This is an atom.
"hello" ; This is a string.
(hello "hello") ; This is a list containing an atom and a string.
() ; This is an empty list.
Lists describe the code structure. Whitespace is insignificant.
(these mean (the same) thing)
(these
mean (the
same) thing)
(these mean (the
same) thing)
All eslisp code is constructed by calling macros at compile-time. There are built-in macros to generate JavaScript operators, loop structures, expressions, statements… everything needed to write arbitrary JavaScript.
A macro is called by writing a list with its name as the first element and its arguments as the rest:
; The "." macro compiles to property access.
(. a b)
(. a b 5 c "yo")
; The "+" macro compiles to addition.
(+ 1 2)
; ... and similarly for "-", "*", "/" and "%" as you'd expect from JS.
a.b;
a.b[5].c['yo'];
1 + 2;
If the (. a b)
syntax feels tedious, you might like the eslisp-camelify transform macro, which lets you write a.b
instead.
If the first element of a list isn't the name of a macro which is in scope, it compiles to a function call:
(a 1)
(a 1 2)
(a)
a(1);
a(1, 2);
a();
These can of course be nested:
; The "=" macro compiles to a variable declaration.
(var x (+ 1 (* 2 3)))
; Calling the result of a property access expression
((. console log) "hi")
var x = 1 + 2 * 3;
console.log('hi');
Conditionals are built with the if
macro:
; The "if" macro compiles to an if-statement
(if lunchtime ; argument 1 becomes the conditional
(block
(var lunch (find food)) ; argument 2 the consequent
(lunch))
(writeMoreCode)) ; argument 3 (optional) the alternate
if (lunchtime) {
var lunch = find(food);
lunch();
} else
writeMoreCode();
Note how the block statement ((block ...)
) has to be made explicit. Because
it's so common, other macros that accept a block statement as their last
argument have sugar for this: they just assume you meant the rest to be in a
block.
For example. the function
macro treats its first argument as a list of the
function's argument names, and the rest as statements in the function body.
(var f (function (x)
(a x)
(return (+ x 2))))
(f 40)
var f = function (x) {
a(x);
return x + 2;
};
f(40);
While-loops similarly.
(var n 10)
(while (-- n) ; first argument is loop conditional
(hello n) ; the rest are loop-body statements
(hello (- n 1)))
var n = 10;
while (--n) {
hello(n);
hello(n - 1);
}
You can use an explicit block statements ((block ...)
) wherever implicit
ones are allowed, if you want to.
(var n 10)
(while (-- n)
(block (hello n)
(hello (- n 1))))
var n = 10;
while (--n) {
hello(n);
hello(n - 1);
}
This is what eslisp is really for.
Macros are functions that run at compile-time. Whatever they return becomes part of the compiled code. User-defined macros and pre-defined compiler ones are treated equivalently.
There's a fuller tutorial to eslisp macros in the doc/
directory.
These are just some highlights.
You can alias macros to names you find convenient, or mask any you don't want to use.
(macro a array)
(a 1)
(array 1) ; The original still works though...
(macro array) ; ...unless we deliberately mask it
(array 1)
[1];
[1];
array(1);
Macros can use quasiquote
(`
), unquote
(,
) and
unquote-splicing
(,@
) to construct their outputs and to perform arbitrary
computations.
(macro m (function (x) (return `(+ ,x 2))))
((. console log) (m 40))
console.log(40 + 2);
The macro function is called with a this
context containing methods handy for
working with macro arguments, such as this.evaluate
, which compiles and runs
the argument and returns the result.
(macro add2 (function (x) (return `,(+ ((. this evaluate) x) 2))))
((. console log) (add2 40))
console.log(42);
You can return multiple statements from a macro with this.multi
.
(macro log-and-delete (function (varName)
(return ((. this multi)
`((. console log) ((. JSON stringify) ,varName))
`(delete ,varName)))))
(log-and-delete someVariable)
console.log(JSON.stringify(someVariable));
delete someVariable;
Returning null
from a macro just means nothing. This is handy for
compilation side-effects or conditional compilation.
; Only include statement if `$DEBUG` environment variable is set
(macro debug (function (statement)
(return (?: (. process env DEBUG) statement null))))
(debug ((. console log) "debug output"))
(yep)
yep();
You can even make macros that share state: just pass an immediately-invoked
function expression (IIFE) to macro
and return an object. Each property
of the object will become a macro. The variables in the IIFE closure are
shared between them.
(macro ((function ()
(var x 0) ; this is visible to all of the macro functions
(return (object increment (function () (return (++ x)))
decrement (function () (return (-- x)))
get (function () (return x)))))))
(increment)
(increment)
(increment)
(decrement)
(get)
1;
2;
3;
2;
2;
The second argument to macro
needs to evaluate to a function, but it can be
whatever. so you can put the macro function in a separate file and do—
(macro someName (require "./file.js"))
—to use it.
This means you can publish eslisp macros on npm. The name prefix
eslisp-
is recommended. Some exist already.
If you want eslc
in your $PATH
, npm install --global eslisp
. (You
might need sudo
.) Then eslc
program takes eslisp code as input and outputs
JavaScript.
The compiler runs as a REPL if given no arguments, though it doesn't (yet) support macros in that mode.
You can also just pipe data to it to compile it if you want.
echo '((. console log) "Yo!")' | eslc
Or pass a filename, like eslc myprogram.esl
.
To remove it cleanly, npm uninstall --global eslisp
.
If you want the compiler in node_modules/.bin/eslc
, do npm install eslisp
.
You can also use eslisp as a module: it exports a function that takes a string of eslisp code as input and outputs a string of JavaScript, throwing errors if it sees them.
In brief: A table of predefined macros is used to turn S-expressions into SpiderMonkey AST, which is fed to escodegen, which outputs JS. Some of those macros allow defining further macros, which get added to the table and work from then on like the predefined ones.
For more, read the source and ask questions.
Create a github issue, or say hi in gitter chat.
I'll assume your contributions to also be under the ISC license.
ISC.