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 hereNo 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 demoCompiled 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-elispThen, with no JVM, no .nrepl.edn and no jack-in:
clel nrepl # ClojureElisp nREPL server on port 7888M-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.elThe 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.
- Functions:
defn,fn(lambda), multi-arity, variadic (& rest), destructuring in params - Bindings:
letwith sequential bindings, vector/map destructuring,:keys,:as,:or - Control flow:
if,when,cond,case,do,and,or - Looping:
loop/recur,letfnwith mutual recursion - Macros:
defmacro(compile-time only), syntax-quote/unquote,macroexpand-1,macroexpand - Error handling:
try/catch/finally,throw,ex-info - Namespaces:
nswith:require,:as,:refer; namespace-prefixed definitions - Protocols & types:
defprotocol,defrecord,deftypewith^:mutablefields,set! - Multimethods:
defmulti/defmethodviacl-defgeneric/cl-defmethod - Lazy sequences:
lazy-seq,realized?,doall,dorun - Atoms:
atom,deref/@,reset!,swap!,add-watch,remove-watch - Elisp interop:
.methoddot-notation,elisp/fnnamespace,.-propertyaccess
Clojure 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 |
- 3-stage pipeline: Reader (Clojure's) → Analyzer (AST + env) → Emitter (codegen)
- Source location tracking with optional
;;; L<line>:C<col>comments - Incremental compilation with mtime tracking (
.clel-cache/manifest.edn) - Cross-file symbol table with compile-time warnings for missing symbols
- Dependency-aware project compilation with topological sort
- Name mangling:
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-elispThis 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.jarThe 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 versionCompiled .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- Clojure 1.12+
- Java 21+ (for building/running the uberjar)
- Emacs 28.1+ (for compiled output)
MIT