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 'clojure-elisp-runtime)
;;; 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
clojure-elisp-runtime.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 │
│ (Clojure's) │ │ (AST + env) │ │ (codegen) │ │ (.el) │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
| Component | File | Role |
|---|---|---|
| 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/clojure-elisp-runtime.el | 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) |
| 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 clojure-elisp-runtime.el out of the dependency, so
nothing has to name a path into the ClojureElisp checkout.
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 ClojureElisp runtime. Install via MELPA (once available):
(package-install 'clojure-elisp)
Or manually copy from the repo:
cp resources/clojure-elisp/clojure-elisp-runtime.el ~/.emacs.d/site-lisp/
# Build uberjar
clojure -T:build uber
# => target/clel-<version>.jar
# Start REPL with dev dependencies (nREPL, CIDER)
clojure -M:dev
# Run tests (Kaocha, 598 tests, 3021 assertions)
clojure -M:test
# 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 |