{"id":13647570,"url":"https://github.com/anko/eslisp","last_synced_at":"2025-04-04T16:12:33.493Z","repository":{"id":32652080,"uuid":"36239262","full_name":"anko/eslisp","owner":"anko","description":"un-opinionated S-expression syntax and macro system for JavaScript","archived":false,"fork":false,"pushed_at":"2020-11-22T23:55:16.000Z","size":519,"stargazers_count":533,"open_issues_count":20,"forks_count":32,"subscribers_count":25,"default_branch":"master","last_synced_at":"2025-03-28T15:05:30.634Z","etag":null,"topics":["compiler","javascript","lisp","macro"],"latest_commit_sha":null,"homepage":"","language":"LiveScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anko.png","metadata":{"files":{"readme":"readme.markdown","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-05-25T15:39:21.000Z","updated_at":"2025-02-16T16:44:19.000Z","dependencies_parsed_at":"2022-09-12T15:02:28.887Z","dependency_job_id":null,"html_url":"https://github.com/anko/eslisp","commit_stats":null,"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Feslisp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Feslisp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Feslisp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Feslisp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anko","download_url":"https://codeload.github.com/anko/eslisp/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247208142,"owners_count":20901570,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compiler","javascript","lisp","macro"],"created_at":"2024-08-02T01:03:39.219Z","updated_at":"2025-04-04T16:12:33.474Z","avatar_url":"https://github.com/anko.png","language":"LiveScript","readme":"# eslisp [![](https://img.shields.io/npm/v/eslisp.svg?style=flat-square)][1] [![](https://img.shields.io/travis/anko/eslisp.svg?style=flat-square)][2] [![](https://img.shields.io/badge/chat-gitter_%E2%86%92-blue.svg?style=flat-square)][3]\n\nAn [S-expression][4] syntax for [ECMAScript][5]/JavaScript, with [Lisp-like\nmacros][6].  Minimal core, maximally customisable.\n\nThis is not magic:  It's just an S-expression encoding of the [estree][7] AST\nformat.  The macros are ordinary JS functions that return objects, which just\nexist at compile-time.  This means macros can be put on [npm][8] to distribute\nyour own language features, [like this][9].\n\n\u003e :warning: **Note the 0.x.x [semver][10].**  The API may shift under your\n\u003e feet.\n\n\u003e :warning: **Only ES5 is supported right now.**  Unless you [write the macros\n\u003e yourself](https://github.com/anko/eslisp/issues/60#issuecomment-718196427).\n\n## Philosophy\n\n-   **Small core, close to JS**.  This core eslisp corresponds closely with the\n    [estree][11] abstract syntax tree format, and hence matches output JS\n    clearly.  It's purely a syntax adapter unless you use macros.\n\n-   **Maximum user control**.  Users must be able to easily extend the language\n    to their needs, and to publish their features independently of the core\n    language.\n\n    User-defined macros must be treated like built-in ones, and are just\n    ordinary JS functions.  This means you can write them in anything that\n    compiles to JavaScript, put them on [npm][12], and `require` them.\n\n## Motivating example\n\nHere's an example of implementing conditional compilation in eslisp:\n\n\u003c!-- !test program DEBUG=1 ./bin/eslc | head -c -1 --\u003e\n\n\u003c!-- !test in fib --\u003e\n\n    ; Macros are functions bound to names, which operate on code.  This one\n    ; checks whether the `$DEBUG` environment variable is set, and if so,\n    ; returns a call to `console.log` that also includes a string of the code\n    ; that was passed in.\n    (macro debug\n     (lambda (expression)\n      (if (. process env DEBUG)\n        (return `((. console log)\n                  ; Compile the input expression to JavaScript, and convert\n                  ; that to a string.\n                  ,((. this string)\n                    ((. this compileToJs)\n                     ((. this compile) expression)))\n                  \"=\"\n                  ,expression))\n       (return null))))\n\n    (var fib ; Fibonacci number sequence\n     (lambda (x)\n\n      ; Conditionally compile logging code\n      (debug x)\n\n      ; Basic Fibonacci algorithm\n      (switch x\n       (0 (return 0))\n       (1 (return 1))\n       (default (return (+ (fib (- x 1)) (fib (- x 2))))))))\n\nCompiled with `DEBUG=1 eslc file.esl`, that compiles to this JavaScript:\n\n\u003c!-- !test out fib --\u003e\n\n    var fib = function (x) {\n        console.log('x', '=', x);\n        switch (x) {\n        case 0:\n            return 0;\n        case 1:\n            return 1;\n        default:\n            return fib(x - 1) + fib(x - 2);\n        }\n    };\n\nNote how the generated `console.log` also has *the name of the variable `x` as\na string*.  Try changing the `debug` call to `(debug ((. Math pow) (+ x 1) 2)`\nand watch the logging code change to say `Math.pow(x + 1, 2)` also inside the\nfirst string.  (You can edit it in your browser [on runkit\nhere](https://runkit.com/anko/590b2059e9af030012b47bb8).)\n\nCompiled with just `eslc file.esl`, the logging code disappears:\n\n    var fib = function (x) {\n        switch (x) {\n        case 0:\n            return 0;\n        case 1:\n            return 1;\n        default:\n            return fib(x - 1) + fib(x - 2);\n        }\n    };\n\nDoing it this way has a few advantages:\n\n - Your output code is smaller, compared to the usual technique of hiding your\n   debug code behind a boolean flag.\n - This actually logs the expression that produced the result.  Can't do that\n   in JS without writing it manually every time, because you can't invoke the\n   compiler at compile-time.\n\n\u003c!-- !test program ./bin/eslc | head -c -1 --\u003e\n\n## Why?\n\nI wanted JavaScript to be [homoiconic][13] and have modular macros written in\nthe same language.  I feel like this is the [adjacent possible][14] in that\ndirection.  [Sweet.js][15] exists for macros, but theyre awkward to write and\naren't JavaScript.  [Various JavaScript lisps][16] exist, but most have\nfeaturitis from trying too hard to be Lisp (rather than just being a JS\nsyntax), and none have macros that are just JS functions.\n\nI want a language that I can adapt.  When I need [anaphoric conditionals][17],\nor [conditional compilation][18] or file content inlining (like [brfs][19]), or\na [domain-specific language][20] for my favourite library, or something insane\nthat hacks NASA and runs all my while-loops through `grep` during compilation\nfor some freak reason, I want to be able to create that language feature myself\nor `require` it from npm if it exists, and hence make the language better for\nthat job, and for others doing it in the future.\n\nThat's the dream anyway.\n\nS-expressions are also quite conceptually beautiful; they're just nested lists,\nminimally representing the [abstract syntax tree][21], and it's widely known\nthat [they rock][22], so let's use what works.\n\nThis has great [hack value][23] too of course.  [Lisp macros][24] are the\ncoolest thing since mint ice cream.  Do I even need to say that?\n\nFurther documentation in [`doc/`][25]:\n\n-   [Language basics reference][26]\n-   [Macro-writing tutorial][27]\n-   [Module packaging and distribution tutorial][28]\n-   [Comparison against other JS-lisps][29]\n-   [Using source maps][30]\n-   [Using with client-side bundling tools][31]\n\n## Brief tutorial\n\nThis is a quick overview of the core language.  See [the basics reference][32]\nor the [test suite][33] for a more complete document.\n\n### Building blocks\n\nEslisp code consists of comments, atoms, strings and lists.\n\n    ; Everything from a semicolon to the end of a line is a comment.\n\n    hello           ; This is an atom.\n    \"hello\"         ; This is a string.\n    (hello \"hello\") ; This is a list containing an atom and a string.\n    ()              ; This is an empty list.\n\nLists describe the code structure.  Whitespace is insignificant.\n\n    (these mean (the same) thing)\n\n    (these\n    mean (the\n    same) thing)\n\n    (these    mean (the\n                    same) thing)\n\nAll eslisp code is constructed by calling macros at compile-time.  There are\nbuilt-in macros to generate JavaScript operators, loop structures, expressions,\nstatements… everything needed to write arbitrary JavaScript.\n\n### Some simple built-in macros\n\nA macro is called by writing a list with its name as the first element and its\narguments as the rest:\n\n\u003c!-- !test in simple macros --\u003e\n\n    ; The \".\" macro compiles to property access.\n    (. a b)\n    (. a b 5 c \"yo\")\n\n    ; The \"+\" macro compiles to addition.\n    (+ 1 2)\n\n    ; ... and similarly for \"-\", \"*\", \"/\" and \"%\" as you'd expect from JS.\n\n\u003c!-- !test out simple macros --\u003e\n\n    a.b;\n    a.b[5].c['yo'];\n    1 + 2;\n\nIf the `(. a b)` syntax feels tedious, you might like the [eslisp-propertify][34] transform macro, which lets you write `a.b` instead.\n\nIf the first element of a list isn't the name of a macro which is in scope, it\ncompiles to a function call:\n\n\u003c!-- !test in function call --\u003e\n\n    (a 1)\n    (a 1 2)\n    (a)\n\n\u003c!-- !test out function call --\u003e\n\n    a(1);\n    a(1, 2);\n    a();\n\nThese can of course be nested:\n\n\u003c!-- !test in nested macros --\u003e\n\n    ; The \"=\" macro compiles to a variable declaration.\n    (var x (+ 1 (* 2 3)))\n\n    ; Calling the result of a property access expression\n    ((. console log) \"hi\")\n\n\u003c!-- !test out nested macros --\u003e\n\n    var x = 1 + 2 * 3;\n    console.log('hi');\n\n### More complex built-in macros\n\nConditionals are built with the `if` macro:\n\n\u003c!-- !test in special form --\u003e\n\n    ; The \"if\" macro compiles to an if-statement\n    (if lunchtime                 ; argument 1 becomes the conditional\n        (block\n          (var lunch (find food)) ; argument 2 the consequent\n          (lunch))\n        (writeMoreCode))          ; argument 3 (optional) the alternate\n\n\u003c!-- !test out special form --\u003e\n\n    if (lunchtime) {\n        var lunch = find(food);\n        lunch();\n    } else\n        writeMoreCode();\n\nNote how the block statement (`(block ...)`) has to be made explicit.  Because\nit's so common, other macros that accept a block statement as their last\nargument have sugar for this: they just assume you meant the rest to be in a\nblock.\n\nFor example. the `lambda` macro (which creates function expressions) treats its\nfirst argument as a list of the function's argument names, and the rest as\nstatements in the body.\n\n\u003c!-- !test in func and call --\u003e\n\n    (var f (lambda (x)\n          (a x)\n          (return (+ x 2))))\n    (f 40)\n\n\u003c!-- !test out func and call --\u003e\n\n    var f = function (x) {\n        a(x);\n        return x + 2;\n    };\n    f(40);\n\nWhile-loops similarly.\n\n\u003c!-- !test in while loop --\u003e\n\n    (var n 10)\n    (while (-- n)   ; first argument is loop conditional\n     (hello n)      ; the rest are loop-body statements\n     (hello (- n 1)))\n\n\u003c!-- !test out while loop --\u003e\n\n    var n = 10;\n    while (--n) {\n        hello(n);\n        hello(n - 1);\n    }\n\nYou *can* use an explicit block statements (`(block ...)`) wherever implicit\nones are allowed, if you want to.\n\n\u003c!-- !test in while loop with explicit block --\u003e\n\n    (var n 10)\n    (while (-- n)\n     (block (hello n)\n            (hello (- n 1))))\n\n\u003c!-- !test out while loop with explicit block --\u003e\n\n    var n = 10;\n    while (--n) {\n        hello(n);\n        hello(n - 1);\n    }\n\n### Writing your own macros\n\nThis is what eslisp is really for.\n\nMacros are functions that run at compile-time.  Whatever they return becomes\npart of the compiled code.  User-defined macros and pre-defined compiler ones\nare treated equivalently.\n\n**There's a [fuller tutorial to eslisp macros in the `doc/` directory][35].**\nThese are just some highlights.\n\nYou can alias macros to names you find convenient, or mask any you don't want\nto use.\n\n\u003c!-- !test in macro aliasing --\u003e\n\n    (macro a array)\n    (a 1)\n    (array 1)     ; The original still works though...\n\n    (macro array) ; ...unless we deliberately mask it\n    (array 1)\n\n\u003c!-- !test out macro aliasing --\u003e\n\n    [1];\n    [1];\n    array(1);\n\nMacros can use [`quasiquote`][36] (`` ` ``), `unquote` (`,`) and\n`unquote-splicing` (`,@`) to construct their outputs neatly.\n\n\u003c!-- !test in macro and call --\u003e\n\n    (macro m (lambda (x) (return `(+ ,x 2))))\n    ((. console log) (m 40))\n\n\u003c!-- !test out macro and call --\u003e\n\n    console.log(40 + 2);\n\nThe macro function is called with a `this` context containing methods handy for\nworking with macro arguments, such as `this.evaluate`, which compiles and runs\nthe argument and returns the result, and `this.atom` which creates a new\nS-expression atom.\n\n\u003c!-- !test in evaluate in macro --\u003e\n\n    (macro add2 (lambda (x)\n                 (var xPlusTwo (+ ((. this evaluate) x) 2))\n                 (return ((. this atom) xPlusTwo))))\n    ((. console log) (add2 40))\n\n\u003c!-- !test out evaluate in macro --\u003e\n\n    console.log(42);\n\nYou can return multiple statements from a macro by returning an array.\n\n\u003c!-- !test in multiple-return macro --\u003e\n\n    (macro log-and-delete (lambda (varName)\n     (return (array\n              `((. console log) ((. JSON stringify) ,varName))\n              `(delete ,varName)))))\n\n    (log-and-delete someVariable)\n\n\u003c!-- !test out multiple-return macro --\u003e\n\n    console.log(JSON.stringify(someVariable));\n    delete someVariable;\n\nReturning `null` from a macro just means nothing.  This is handy for\ncompilation side-effects and conditional compilation.\n\n\u003c!-- !test in nothing-returning macro --\u003e\n\n    ; Only include statement if `$DEBUG` environment variable is set\n    (macro debug (lambda (statement)\n     (return (?: (. process env DEBUG) statement null))))\n\n    (debug ((. console log) \"debug output\"))\n    (yep)\n\n\u003c!-- !test out nothing-returning macro --\u003e\n\n    yep();\n\nBecause macros are JS functions and JS functions can be closures, you can even\nmake macros that share state.  One way is to put them in an\n[immediately-invoked function expression (IIFE)][37], return them in an object,\nand pass that to `macro`.  Each property of the object is imported as a macro,\nand the variables in the IIFE are shared between them.\n\n\u003c!-- !test in macros block --\u003e\n\n    (macro ((lambda ()\n            (var x 0) ; visible to all of the macro functions\n            (return\n             (object increment (lambda () (return ((. this atom) (++ x))))\n                     decrement (lambda () (return ((. this atom) (-- x))))\n                     get       (lambda () (return ((. this atom) x))))))))\n\n    (increment)\n    (increment)\n    (increment)\n\n    (decrement)\n\n    (get)\n\n\u003c!-- !test out macros block --\u003e\n\n    1;\n    2;\n    3;\n    2;\n    2;\n\n### Macros as modules\n\nThe second argument to `macro` needs to evaluate to a function, but it can be\nwhatever, so you can put the macro function in a separate file and do—\n\n    (macro someName (require \"./file.js\"))\n\n—to use it.\n\nOr if you'd like to also be able to load macros from `.esl` eslisp files\nwithout first compiling them to JavaScript, you can do that with\n\n    (macroRequire someName \"./file.esl\")\n\nThis means you can publish eslisp macros on [npm][38].  The name prefix\n`eslisp-` and keyword `eslisp-macro` are recommended.  [Some exist\nalready.][39]\n\n### Transformation macros\n\nWhen running `eslc` from the command line, to apply a transformation macro to\nan eslisp file during compilation, supply the `--transform \u003cmacro-name\u003e`\nargument (`-t` for short). For example,\n\n    eslc --transform eslisp-propertify myprogram.esl\n\nuses [eslisp-propertify][40] to convert all atoms containg dots into member\nexpressions.  The flag can be specified multiple times.\n\n## Try it\n\n### Global install\n\nIf you want `eslc` in your [`$PATH`][41], `npm install --global eslisp`.  (You\nmight need `sudo`.)  Then `eslc` program takes eslisp code as input and outputs\nJavaScript.\n\nIf run interactively without arguments, the compiler loads a [REPL][42] that\nyou can type commands into to test them.\n\nYou can also just pipe data to it to compile it if you want.\n\n    echo '((. console log) \"Yo!\")' | eslc\n\nOr pass a filename, like `eslc myprogram.esl`.\n\nTo remove it cleanly, `npm uninstall --global eslisp`.\n\n### Local install\n\nIf you want the compiler in `node_modules/.bin/eslc`, do `npm install eslisp`.\n\nYou can also use eslisp as a module: it exports a function that takes a string\nof eslisp code as input and outputs a string of JavaScript, throwing errors if\nit sees them.\n\n## How does it work\n\nIn brief:  A table of predefined macros is used to turn S-expressions into\n[SpiderMonkey AST][43], which is fed to [escodegen][44], which outputs JS.\nSome of those macros allow defining further macros, which get added to the\ntable and work from then on like the predefined ones.\n\nFor more, read [the source][45]. Ask questions!\n\n## Bugs, discussion \u0026 contributing\n\nCreate a [github issue][46], or say hi [in gitter chat][47].\n\nI'll assume your contributions to also be under the [ISC license][48].\n\n## License\n\n[ISC][49].\n\n[1]: https://www.npmjs.com/package/eslisp\n[2]: https://travis-ci.org/anko/eslisp\n[3]: https://gitter.im/anko/eslisp\n[4]: https://en.wikipedia.org/wiki/S-expression\n[5]: http://en.wikipedia.org/wiki/ECMAScript\n[6]: http://stackoverflow.com/questions/267862/what-makes-lisp-macros-so-special\n[7]: https://github.com/estree/estree\n[8]: https://www.npmjs.com/\n[9]: https://www.npmjs.com/search?q=eslisp-\n[10]: http://semver.org/spec/v2.0.0.html\n[11]: https://www.npmjs.com/\n[12]: https://www.npmjs.com/\n[13]: http://en.wikipedia.org/wiki/Homoiconicity\n[14]: http://www.wsj.com/articles/SB10001424052748703989304575503730101860838\n[15]: http://sweetjs.org/\n[16]: doc/comparison-to-other-js-lisps.markdown\n[17]: https://en.wikipedia.org/wiki/Anaphoric_macro\n[18]: http://en.wikipedia.org/wiki/Conditional_compilation\n[19]: https://github.com/substack/brfs\n[20]: http://en.wikipedia.org/wiki/Domain-specific_language\n[21]: http://en.wikipedia.org/wiki/Abstract_syntax_tree\n[22]: http://blog.rongarret.info/2015/05/why-lisp.html\n[23]: http://www.catb.org/jargon/html/H/hack-value.html\n[24]: http://c2.com/cgi/wiki?LispMacro\n[25]: doc/\n[26]: doc/basics-reference.markdown\n[27]: doc/how-macros-work.markdown\n[28]: doc/ditributing-modules.markdown\n[29]: doc/comparison-to-other-js-lisps.markdown\n[30]: doc/source-maps.markdown\n[31]: doc/bundling.markdown\n[32]: doc/basics-reference.markdown\n[33]: test.ls\n[34]: https://www.npmjs.com/package/eslisp-propertify\n[35]: doc/how-macros-work.markdown\n[36]: http://axisofeval.blogspot.co.uk/2013/04/a-quasiquote-i-can-understand.html\n[37]: https://en.wikipedia.org/wiki/Immediately-invoked_function_expression\n[38]: https://www.npmjs.com/\n[39]: https://www.npmjs.com/search?q=eslisp-\n[40]: https://www.npmjs.com/package/eslisp-propertify\n[41]: http://en.wikipedia.org/wiki/PATH_(variable)\n[42]: https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop\n[43]: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API\n[44]: https://github.com/estools/escodegen\n[45]: src/\n[46]: https://github.com/anko/eslisp/issues/new\n[47]: https://gitter.im/anko/eslisp\n[48]: http://opensource.org/licenses/ISC\n[49]: http://opensource.org/licenses/ISC\n","funding_links":[],"categories":["LiveScript","Languages"],"sub_categories":["JavaScript"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanko%2Feslisp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanko%2Feslisp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanko%2Feslisp/lists"}