A Clojure dialect that compiles to Emacs Lisp — like ClojureScript targets JavaScript, ClojureElisp targets Emacs.
Write .cljel files using Clojure syntax, compile them to .el files that run natively in Emacs 28.1+.
Published on Clojars. The Clojars badge above carries the current version, and the Clojars page has ready-to-paste coordinates for deps.edn, Leiningen and Maven.
;; deps.edn
io.github.buddhilw/clojure-elisp {:mvn/version "<latest>"}
;; Leiningen / project.clj
[io.github.buddhilw/clojure-elisp "<latest>"]
The standalone CLI uberjar (clel-<version>.jar) is attached to each GitHub release.
(require '[clojure-elisp.core :as clel])
;; Compile a single form
(clel/emit '(defn greet [name] (str "Hello, " name "!")))
;; => "(defun greet (name)\n (clel-str \"Hello, \" name \"!\"))"
;; Compile a string of code
(clel/compile-string "(defn inc2 [x] (+ x 2))")
;; => "(defun inc2 (x)\n (+ x 2))"
;; Compile a .cljel file to .el
(clel/compile-file "src/my_package.cljel" "out/my-package.el")
;; Compile an entire project in dependency order
(clel/compile-project ["src"] "out")
;; my-package.cljel
(ns my.package
(:require [clojure.string :as str]))
(defn greet [name]
(let [msg (str "Hello, " name "!")]
(message msg)))
(defn process-buffer []
(-> (buffer-string)
str/upper-case
insert))
Compiles to:
;;; my-package.el --- -*- lexical-binding: t; -*-
;; Generated by ClojureElisp
(require 'clel)
;;; Code:
(defun my-package-greet (name)
(let* ((msg (clel-str "Hello, " name "!")))
(message msg)))
(defun my-package-process-buffer ()
(insert (upcase (buffer-string))))
(provide 'my-package)
;;; my-package.el ends here
No build step stands between you and your editor. C-c C-c compiles the form
at point and evaluates the resulting Elisp in the Emacs you are using. The
function is redefined in the running image, nothing is written to disk, and
M-x finds it immediately. It is the elisp REPL loop, with Clojure syntax.
examples/bb-demo is a complete ClojureElisp project that
runs on Babashka alone. It compiles a .cljel source, loads it into a real
Emacs, and calls the functions:
git clone https://github.com/BuddhiLW/clojure-elisp
cd clojure-elisp/examples/bb-demo && bb demo
Compiled src/demo/greeter.cljel -> out/demo-greeter.el (963 chars)
greet: Hello, world!
shout: HELLO, CLJEL!
region: Hello, alpha!
M-x ready: t
bb nrepl in that directory starts the server below. See its
README for the task list.
Install the CLI once:
bbin install io.github.BuddhiLW/clojure-elisp
Then, with no JVM, no .nrepl.edn and no jack-in:
clel nrepl # ClojureElisp nREPL server on port 7888
M-x cider-connect-clj RET localhost RET 7888 RET
M-x cider-cljel-mode
Compilation is active from the first message, so cider-cljel-mode is only
there for the keybindings.
Already inside a Clojure project and want the JVM server instead? Put the
middleware in .nrepl.edn and jack in as usual:
;; .nrepl.edn
{:middleware [clojure-elisp.nrepl/wrap-cljel]}
M-x cider-jack-in (with the :dev alias)
M-x cider-cljel-mode
Both servers run the same compiler and speak the same protocol.
| Key | Does |
|---|---|
C-c C-e | Compile and eval the form before point |
C-c C-c | Compile and eval the top-level form at point |
C-c C-k | Compile and eval the whole buffer |
C-c C-e and C-c C-c send the buffer's (ns ...) form along with the code,
so a definition evaluated at point gets the same Elisp name that C-c C-k and
clel compile give it. Evaluate (defn greet ...) inside (ns my.pkg) and you
get my-pkg-greet every way you compile it.
Building a command that wraps the region in a Markdown code fence. Open
fence.cljel, then evaluate as you go.
(ns my.fence)
C-c C-c on the ns form. Now a helper:
(defn fence-text [lang text]
(str "```" lang "\n" text "\n```"))
C-c C-c. It is defined right now. Try it without leaving the buffer, with
C-c C-e after the closing paren:
(fence-text "clojure" "(+ 1 2)")
;; => "```clojure\n(+ 1 2)\n```"
The result appears inline, at point. Now the interactive command:
(defn fence-region [start end]
(interactive "r")
(let [text (buffer-substring-no-properties start end)]
(delete-region start end)
(insert (fence-text "clojure" text))))
C-c C-c, then select a region and run M-x my-fence-fence-region. It works,
because it is a real defun in your live Emacs.
Wrong language hardcoded. Fix it in place:
(defn fence-region [start end lang]
(interactive "r\nsLanguage: ")
(let [text (buffer-substring-no-properties start end)]
(delete-region start end)
(insert (fence-text lang text))))
C-c C-c again. The old definition is replaced. No recompile, no reload, no
restart. When the buffer is right, ship it:
clel compile fence.cljel -o fence.el
The emitted fence.el defines the same my-fence-fence-region you have been
calling all along.
Compiled code calls runtime functions such as clel-str, so
the runtime library clel.el must be loadable. cider-cljel-mode loads it from
load-path and tells you if it cannot find it; point
cider-cljel-runtime-file at the file to be explicit.
defn, fn (lambda), multi-arity, variadic (& rest), destructuring in paramslet with sequential bindings, vector/map destructuring, :keys, :as, :orif, when, cond, case, do, and, orloop/recur, letfn with mutual recursiondefmacro (compile-time only), syntax-quote/unquote, macroexpand-1, macroexpandtry/catch/finally, throw, ex-infons with :require, :as, :refer; namespace-prefixed definitionsdefprotocol, defrecord, deftype with ^:mutable fields, set!defmulti/defmethod via cl-defgeneric/cl-defmethodlazy-seq, realized?, doall, dorunatom, deref/@, reset!, swap!, add-watch, remove-watch.method dot-notation, elisp/fn namespace, .-property accessClojure core functions mapped to Elisp equivalents:
| Category | Functions |
|---|---|
| Arithmetic | +, -, *, /, mod, inc, dec |
| Comparison | =, <, >, <=, >=, not= |
| Predicates | nil?, string?, number?, zero?, pos?, neg?, even?, odd?, coll?, some? |
| Collections | first, rest, next, cons, conj, count, nth, get, assoc, dissoc, keys, vals, into, seq, empty? |
| Sequences | map, filter, remove, reduce, take, drop, concat, mapcat, sort, group-by, frequencies |
| Seq predicates | every?, some, not-every?, not-any? |
| Strings | str, subs, format, pr-str, println |
| Higher-order | apply, identity, constantly, partial, comp |
;;; L<line>:C<col> comments.clel-cache/manifest.edn)valid? → valid-p, reset! → reset-bang, my.ns/foo → my-ns-foo┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Reader │───▶│ Analyzer │───▶│ Emitter │───▶│ Elisp Code │
│ (portable) │ │ (AST + env) │ │ (codegen) │ │ (.el) │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
| Component | File | Role |
|---|---|---|
| Reader | src/clojure_elisp/reader.clj | Source text → forms with :line/:column; the same forms on every host |
| Analyzer | src/clojure_elisp/analyzer.clj | Parse forms → AST nodes, macro expansion, destructuring, env tracking |
| Emitter | src/clojure_elisp/emitter.clj | AST nodes → Elisp source strings |
| Core | src/clojure_elisp/core.clj | Public API, file/project compilation, dependency resolution |
| Runtime | resources/clojure-elisp/clel.el (the Emacs package clel) | 55+ Elisp functions implementing Clojure semantics |
| MCP Server | src/clojure_elisp/mcp.clj | MCP stdio server exposing compiler as AI tools |
| CLI | src/clojure_elisp/cli.clj | JVM uberjar entry point (compile, mcp, version) |
| Portable CLI | src/clojure_elisp/main.clj | compile/version on the JVM, Babashka and cljw |
| BB CLI | bb/clel/main.clj | Babashka CLI frontend (delegates to uberjar) |
| nREPL kernel | src/clojure_elisp/nrepl_kernel.clj | Session registry, compile modes, op semantics (transport-free) |
| nREPL middleware | src/clojure_elisp/nrepl.clj | JVM transport: wrap-cljel for a CIDER jack-in |
| nREPL server | bb/clel/nrepl_server.clj | Standalone transport: clel nrepl, no JVM |
| Emacs mode | resources/clojure-elisp/clojure-elisp-mode.el | Major mode for .cljel files |
| CIDER | resources/clojure-elisp/cider-clojure-elisp.el | CIDER client: sends forms, evals the compiled Elisp locally |
Install the clel CLI with bbin:
bbin install io.github.BuddhiLW/clojure-elisp
This gives you the clel command globally, including clel nrepl, which
compiles in the Babashka process and needs no JVM. Requires
Babashka. Only clel mcp and the
compile fallback path need the uberjar (see below).
The compiler and the nREPL server both load under Babashka, so a bb.edn is
the whole setup. examples/bb-demo is a working project
built this way:
{:deps {io.github.buddhilw/clojure-elisp {:mvn/version "<latest>"}}
:tasks
{compile {:requires ([clojure-elisp.core :as clel])
:task (clel/compile-file "src/my/pkg.cljel" "out/my-pkg.el")}
runtime {:requires ([clojure-elisp.core :as clel])
:task (clel/bundle-runtime! "out")}
nrepl {:requires ([clel.nrepl-server :as server])
:task (server/start-server! 7888)}}}
bundle-runtime! writes the runtime, clel.el, out of the dependency, so
nothing has to name a path into the ClojureElisp checkout.
ClojureWasm starts in about
20 ms, so it is the quickest way to compile a file from the command line. The
compiler runs on it unchanged, through the portable entry point
clojure-elisp.main. cljw does not fetch Maven artifacts; the :cljw alias in
deps.edn takes the compile path's dependencies from Git instead (cloned once
into ~/.cljw/gitlibs):
# from a clojure-elisp checkout
cljw -A:cljw -m clojure-elisp.main compile src/my_app.cljel -o out/my-app.el
cljw -A:cljw -m clojure-elisp.main compile src/ -o out/
cljw -A:cljw -m clojure-elisp.main compile # the project in ./clel.edn
The same namespace is the CLI on every host (bb -m clojure-elisp.main ...,
clojure -M -m clojure-elisp.main ...), and all of them emit the same bytes:
make parity compiles every .cljel in examples/ and test/, plus the
runtime, on the JVM, Babashka and cljw, and diffs the results.
Measured wall time, median, compiling examples/demo.cljel (14 lines) and the
1,973-line runtime.cljel (cljw 1.14.7, Babashka 1.13.224, OpenJDK 25 uberjar):
| Command | small file | runtime.cljel |
|---|---|---|
cljw -A:cljw -m clojure-elisp.main compile | 143 ms | 597 ms |
bb -m clojure-elisp.main compile | 239 ms | 440 ms |
java -jar target/clel-<version>.jar compile | 662 ms | 850 ms |
clel compile (Babashka CLI delegating to the uberjar) | 692 ms | |
clojure -M -m clojure-elisp.main compile | 2,439 ms |
cljw wins wherever startup dominates, which is the usual case of one file per
save; Babashka's interpreter is quicker on large inputs. On cljw the project
build recompiles every file each time, because its java.io.File has no
modification times to compare. cljw build cannot yet produce a
self-contained binary of the compiler: see CHANGELOG.md for the upstream
gaps that block it.
A file with defmacro can crash cljw 1.14.7. The compiler evals each
macro, and eval of an fn form corrupts cljw's heap, so the process may
segfault a little later, depending on how much it allocates after the macro.
Compile such files with Babashka or the JVM until cljw fixes it.
The CLI delegates compilation to a JVM uberjar. Download from GitHub Releases or build from source:
# Build and install
make build install
# => ~/.local/lib/clel.jar
The CLI auto-detects the jar at ~/.local/lib/clel.jar or via $CLEL_JAR. If no jar is found, it falls back to clojure -M -e.
clel compile # Compile project from clel.edn
clel compile src/my_app.cljel -o out/ # Compile a single file
clel compile src/ -o out/ # Compile a directory
clel watch src/ -o out/ # Watch and recompile on changes
clel nrepl --port 7888 # Start the nREPL server for CIDER
clel mcp # Start MCP stdio server
clel version # Print version
Compiled .el files require the runtime library, the Emacs package clel
(resources/clojure-elisp/clel.el, compiled from runtime.cljel by
make runtime). Install it via MELPA (once available):
(package-install 'clel)
Or manually copy it from the repo:
cp resources/clojure-elisp/clel.el ~/.emacs.d/site-lisp/
Before 0.8.0 the runtime was clojure-elisp-runtime.el, providing the feature
clojure-elisp-runtime. It was renamed because MELPA requires every definition
in a package to start with the package's name, and the runtime's all start
with clel. Code compiled by 0.8.0 requires clel; recompile code compiled
by an earlier version.
Give the package's main namespace an :elisp/package attr-map and the compiled
file gets the library headers package.el and MELPA read:
(ns my.pkg
"One-line summary.
Commentary paragraphs."
{:elisp/package {:author "Jane Doe <jane@example.org>"
:url "https://example.org/my-pkg"
:version "0.1.0"
:package-requires [[emacs "28.1"]]
:keywords ["convenience"]
:license "GPL-3.0-or-later"}})
Package-Requires gains (clel "<minimum>") automatically.
Other keys: :maintainer, :copyright, :commentary; :author and
:maintainer also take a vector. :assisted-by ("Agent:model", or a
vector of them) writes the ;; Assisted-by: line MELPA's CONTRIBUTING asks
for under Author when an AI assistant helped write the code.
A multi-file package states this once, in clel.edn (or in its main
namespace's attr-map), and compile-project gives every file a header:
;; clel.edn
{:source-paths ["src"]
:output-dir "."
:package {:name "my-pkg" :author "Jane Doe <jane@example.org>"
:url "https://example.org/my-pkg" :version "0.1.0"
:package-requires [[emacs "28.1"]] :keywords ["convenience"]
:license "GPL-3.0-or-later"}}
my-pkg.el, the file named after the package, gets the full header. Every
other my-pkg-*.el gets the header package-lint and melpazoid expect of a
secondary file: its ns docstring as summary and Commentary, Author and
SPDX-License-Identifier, and no Package-Requires. ^:autoload on a
defn, define-minor-mode or defcustom name emits ;;;###autoload:
(define-minor-mode ^:autoload my-pkg-mode
"Toggle my-pkg."
:global true)
# Build uberjar
clojure -T:build uber
# => target/clel-<version>.jar
# Start REPL with dev dependencies (nREPL, CIDER)
clojure -M:dev
# Run tests (Kaocha)
clojure -M:test
# Check that the JVM, Babashka and cljw emit identical output
make parity
# Build uberjar
clojure -T:build uber
MIT
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |