# eslisp: an S-expression syntax for JavaScript with macros as plain functions

> eslisp is an experimental S-expression front end for ECMAScript that compiles to the estree AST, so macros are ordinary JavaScript functions that return objects at compile time. It is a 0.x project that supports only ES5, and its author is explicit that the API may shift.

**anko/eslisp** — un-opinionated S-expression syntax and macro system for JavaScript

- Repository: https://github.com/anko/eslisp
- Stars: 539 · Forks: 34
- Language: LiveScript
- License: ISC
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/anko-eslisp

## What eslisp is, in the author's own terms

eslisp is an S-expression syntax for ECMAScript/JavaScript, with Lisp-like macros. The tagline is Minimal core, maximally customisable.

The README is unusually direct about what the thing is not doing. It is not magic: it's just an S-expression encoding of the estree AST format. The macros are ordinary JS functions that return objects, which just exist at compile-time.

That single sentence tells you the entire architecture. There is no separate macro language and no runtime library. A macro is a JavaScript function that receives the parsed representation of a form and returns a new one, and the compiler runs those functions before emitting code. Because the output is an estree AST, the code that comes out is ordinary JavaScript that any runtime accepts.

The philosophy section states two commitments. Small core, close to JS means the core eslisp corresponds closely with the estree abstract syntax tree format, and hence matches output JS clearly. It is purely a syntax adapter unless you use macros, which is an honest way to say that without macros you have gained parentheses and lost semicolons.

Maximum user control means 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 must be treated like built-in ones, and are just ordinary JS functions, so you can write them in anything that compiles to JavaScript, put them on npm, and require them.

The primary language in the repository metadata is LiveScript, and the package.json confirms it with a livescript dependency and test files named test.ls and test-browser.ls. The licence is ISC, the default branch is master, and there are no GitHub releases.

Two warnings sit at the top of the README and both matter. The API may shift under your feet because of the 0.x.x semver, and only ES5 is supported right now, unless you write the macros yourself.

## The debug macro, and what compile-time code generation buys you

The README's motivating example is a conditional compilation macro, and it is worth reading in full because it demonstrates the thing the project is actually for.

Here is the eslisp source.

```js
    (macro debug
     (lambda (expression)
      (if (. process env DEBUG)
        (return `((. console log)
                  ; Compile the input expression to JavaScript, and convert
                  ; that to a string.
                  ,((. this string)
                    ((. this compileToJs)
                     ((. this compile) expression)))
                  "="
                  ,expression))
       (return null))))
```

A macro is bound to a name and operates on code. This one checks whether the $DEBUG environment variable is set, and if so returns a call to console.log that also includes a string of the code that was passed in.

Compiled with DEBUG=1 eslc file.esl, the output is this.

```js
    var fib = function (x) {
        console.log('x', '=', x);
        switch (x) {
        case 0:
            return 0;
        case 1:
            return 1;
        default:
            return fib(x - 1) + fib(x - 2);
        }
    };
```

The detail that makes this more than a logging helper is that the generated console.log includes the name of the variable x as a string, and the string reflects the expression that was actually passed. Changing the debug call to something like (debug ((. Math pow) (+ x 1) 2) changes the logged text to Math.pow(x + 1, 2) inside the first string.

Compiled without the environment variable, the logging code disappears entirely.

```js
    var fib = function (x) {
        switch (x) {
        case 0:
            return 0;
        case 1:
            return 1;
        default:
            return fib(x - 1) + fib(x - 2);
        }
    };
```

The README lists the advantages of doing it this way. Your output code is smaller, compared to the usual technique of hiding your debug code behind a boolean flag. And this actually logs the expression that produced the result, which the README notes you cannot do in JS without writing it manually every time, because you cannot invoke the compiler at compile-time.

That last point is the real argument for the design. A macro that inspects and rewrites code needs a compiler to run inside, and once you have one, the language becomes extensible in a way a runtime flag never is.

## The core language and its built-in macros

The brief tutorial in the README describes the core language, and the building blocks are minimal by design.

Eslisp code consists of comments, atoms, strings and lists. Everything from a semicolon to the end of a line is a comment. An atom is hello, a string is "hello", a list containing an atom and a string is (hello "hello"), and an empty list is ().

Lists describe the code structure and whitespace is insignificant, which means the layout of a form carries no meaning at all.

```js
    (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, and there are built-in macros to generate JavaScript operators, loop structures, expressions and statements, everything needed to write arbitrary JavaScript.

The macro calling convention is ordinary Lisp: a macro is called by writing a list with its name as the first element and its arguments as the rest.

```js
    ; 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.
```

The generated JavaScript for that is property access and addition, written the way a JavaScript programmer expects.

```js
    a.b;
    a.b[5].c['yo'];
    1 +
```

That mapping is the clearest statement of the design. The dot macro handles both property access and index access, and the arithmetic macros map one to one onto the JavaScript operators. You are not learning a new expression model; you are learning a new surface syntax for one you already know, and the compiler is the thing that translates.

The README points at the basics reference and the test suite for a more complete document than the tutorial provides.

## Building and running the compiler

The build is driven by a makefile, and the npm scripts call into it.

```bash
make -j all
```

That is what the prepare script runs, and it is how the package is built when installed from npm, since the prepare script is make -j all.

The test targets are separate, and there are several of them.

```bash
make -j test
make -j test-readme
make -j test-docs
make -j test-browser
```

The readme and docs tests are worth noticing. The README contains inline test directives in HTML comments, such as a !test program DEBUG=1 ./bin/eslc command and !test in fib and !test out fib markers around the code examples. That means the examples in this document are executed and their output checked against what is written here. The documentation is therefore verified rather than aspirational, which is a good sign for a project where the README is the main tutorial.

The browser test uses puppeteer, and the dev dependencies include browserify and ecstatic, a static file server, which together suggest the browser test serves the built output locally and drives a headless browser against it. tape is the test runner and tests-ex-markdown runs the markdown examples.

The published package contains only lib and bin, and the bin entry is eslc pointing at ./bin/eslc. That binary is the compiler, and it is what the README invokes as eslc file.esl.

The runtime dependencies are the interesting part for anyone reading the source. sexpr-plus is the S-expression parser, escodegen generates the JavaScript output from the AST, esutils provides the AST utilities and esvalid validates it, prelude-ls supplies the functional helpers the macros are written in, and convert-source-map plus source-map handle source mapping. chalk is for terminal output and nopt for option parsing.

The compiler is written in LiveScript, which compiles to JavaScript, and the whole thing builds with make.

## What the author wanted, and how it compares to other approaches

The Why section explains the motivation in a way that is useful for judging fit.

The author wanted JavaScript to be homoiconic and to have modular macros written in the same language, and describes this as the adjacent possible in that direction.

Two named alternatives are rejected, and the reasons are specific. Sweet.js exists for macros, but they're 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.

That is a specific and defensible position. The complaint about JavaScript lisps is not that they are Lisps, it is that they add language features rather than adding a syntax, and eslisp's answer is to add almost nothing and let macros carry the weight.

The list of things the author wants to be able to build is where the project's real ambition shows: anaphoric conditionals, conditional compilation, file content inlining like brfs, a domain-specific language for a favourite library, or something insane that hacks NASA and runs all the while-loops through grep during compilation. The goal is that you can create the language feature yourself, or require it from npm if it exists, and hence make the language better for that job.

The further documentation in doc/ is listed in the README as a language basics reference, a macro-writing tutorial, a module packaging and distribution tutorial, a comparison against other JS-lisps, and guides on using source maps and on using it with client-side bundling tools.

The comparison document in particular is the one to read if you are choosing between JavaScript Lisp implementations, and the source maps and bundling guides are the ones that decide whether the tool works inside an existing build.

The primary language is LiveScript rather than JavaScript, which is itself a statement about the project's priorities: the implementation language is chosen for what it makes the compiler convenient to write, and the output is plain JavaScript.

## Where eslisp is the wrong choice

There are three limits, and the first one is stated at the top of the README.

The first is that only ES5 is supported right now, and the only way around that is to write the macros yourself. There is an issue reference in the README pointing at the discussion. So the language is a syntax over a decade-old ECMAScript, and extending it to modern syntax means writing the macros for the newer constructs yourself rather than using built-ins. For a language whose entire purpose is to be a syntax adapter close to the AST, that is a substantial gap rather than a cosmetic one.

The second is the 0.x.x semver warning. The API may shift under your feet. That matters more here than for an ordinary library, because the things you depend on are macros and their calling convention. A macro you wrote against the current API may not survive the next release, and there is no release stream to track because the repository has no GitHub releases at all. The npm version is 0.9.0, so the pre-1.0 boundary has not been crossed.

The third is the S-expression tax on the reader. The core is close to JavaScript, but it is not JavaScript, and the cost shows up in ordinary code rather than in the clever parts. Every operator becomes a function-like form, property access becomes the dot macro, and the resulting source is markedly more verbose than the JavaScript it compiles to. A file of eslisp is harder for a JavaScript developer to read than the equivalent JavaScript, and for a team that already reads JavaScript that is a permanent tax on every file in the repository.

There is a related point about who the tool is for. eslisp is a research and language-design project, and the author says as much by listing what they want to build. The people who benefit are the ones who want to author a language extension and publish it. Everyone else pays the syntax cost for a feature set they could get from a library.

The honest alternative depends on what you actually want. If you want macros in JavaScript without changing the syntax, Sweet.js is the established option, and the author's own comparison says as much even while arguing it is awkward and not JavaScript. If you want a Lisp that compiles to JavaScript but with a smaller core and a smaller learning curve, the various JavaScript Lisps the README dismisses are the comparison point, and their extra features are the price of not writing macros yourself. If you want to extend JavaScript with compile-time behaviour in the language you already use, then Babel plugins and TypeScript transformers are the mainstream answer, and they are far better documented and far better supported than a 0.x compiler with an ISC licence.

What eslisp offers that the mainstream tools do not is a single, small, uniform mechanism where a macro is a plain JavaScript function that returns an AST node. For a language designer, that is a clean idea worth studying even if you would not use the tool.

## Conclusion

Adopt eslisp only if you specifically want to extend JavaScript with your own compile-time macros, since that is the one thing it does that ordinary JavaScript cannot, and the macros can be published to npm as ordinary functions. Do not adopt it for production JavaScript today, because the README states that only ES5 is supported and that the API may shift under your feet in the 0.x line. Before you invest, read the language basics reference and the macro-writing tutorial in doc/, and note the last push was on 2026-05-28.

## FAQ

### What is eslisp and how does it relate to JavaScript?

eslisp is an S-expression syntax for ECMAScript/JavaScript with Lisp-like macros, described as a minimal core that is maximally customisable. It is an S-expression encoding of the estree AST format, so it compiles to ordinary JavaScript, and it is purely a syntax adapter unless you use macros.

### How are macros written in eslisp?

They are ordinary JS functions that operate on code and exist only at compile-time, so you can write them in anything that compiles to JavaScript, publish them to npm and require them. User-defined macros are treated like built-in ones, and the macro call convention is the usual Lisp one, a list with the macro name first and its arguments after.

### Does eslisp support modern JavaScript syntax?

No. The README states that only ES5 is supported right now, and points to a discussion about writing the macros yourself as the alternative. The package version is 0.9.0 and the README warns that the 0.x.x semver means the API may shift under your feet.

### How do I build eslisp and run its tests?

The build is driven by a makefile and the npm prepare script runs make -j all. The test targets are make -j test, make -j test-readme, make -j test-docs and make -j test-browser, and the README and docs examples are executed and checked against the documented output.

### What compiler binary does eslisp ship?

The package exposes a bin entry named eslc pointing at ./bin/eslc, and the README invokes it as eslc file.esl. The published package contains only the lib and bin directories, and the compiler is written in LiveScript, compiling to JavaScript.

## Sources

- [anko/eslisp on GitHub](https://github.com/anko/eslisp)
- [Issues](https://github.com/anko/eslisp/issues)
- [License: ISC](https://github.com/anko/eslisp/blob/master/LICENSE)
- [README](https://github.com/anko/eslisp/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/anko-eslisp
