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.
No JVM, no .nrepl.edn, 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. Requires Babashka and the uberjar (see below).
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 |