Liking cljdoc? Tell your friends :D

ClojureElisp

Clojars Project cljdoc Tests GitHub release License: MIT Emacs

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+.

Installation

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.

Quick Start

(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")

Example

;; 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

Interactive Development

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.

Start the server

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.

Keys

KeyDoes
C-c C-eCompile and eval the form before point
C-c C-cCompile and eval the top-level form at point
C-c C-kCompile 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.

A session

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.

Requirements

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.

Features

Language

  • Functions: defn, fn (lambda), multi-arity, variadic (& rest), destructuring in params
  • Bindings: let with sequential bindings, vector/map destructuring, :keys, :as, :or
  • Control flow: if, when, cond, case, do, and, or
  • Looping: loop/recur, letfn with mutual recursion
  • Macros: defmacro (compile-time only), syntax-quote/unquote, macroexpand-1, macroexpand
  • Error handling: try/catch/finally, throw, ex-info
  • Namespaces: ns with :require, :as, :refer; namespace-prefixed definitions
  • Protocols & types: defprotocol, defrecord, deftype with ^:mutable fields, set!
  • Multimethods: defmulti/defmethod via cl-defgeneric/cl-defmethod
  • Lazy sequences: lazy-seq, realized?, doall, dorun
  • Atoms: atom, deref/@, reset!, swap!, add-watch, remove-watch
  • Elisp interop: .method dot-notation, elisp/fn namespace, .-property access

Core Functions (100+)

Clojure core functions mapped to Elisp equivalents:

CategoryFunctions
Arithmetic+, -, *, /, mod, inc, dec
Comparison=, <, >, <=, >=, not=
Predicatesnil?, string?, number?, zero?, pos?, neg?, even?, odd?, coll?, some?
Collectionsfirst, rest, next, cons, conj, count, nth, get, assoc, dissoc, keys, vals, into, seq, empty?
Sequencesmap, filter, remove, reduce, take, drop, concat, mapcat, sort, group-by, frequencies
Seq predicatesevery?, some, not-every?, not-any?
Stringsstr, subs, format, pr-str, println
Higher-orderapply, identity, constantly, partial, comp

Compiler

  • 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/foomy-ns-foo

Architecture

┌─────────────┐    ┌──────────────┐    ┌─────────────┐    ┌──────────────┐
│   Reader    │───▶│   Analyzer   │───▶│   Emitter   │───▶│  Elisp Code  │
│ (Clojure's) │    │ (AST + env)  │    │ (codegen)   │    │   (.el)      │
└─────────────┘    └──────────────┘    └─────────────┘    └──────────────┘
ComponentFileRole
Analyzersrc/clojure_elisp/analyzer.cljParse forms → AST nodes, macro expansion, destructuring, env tracking
Emittersrc/clojure_elisp/emitter.cljAST nodes → Elisp source strings
Coresrc/clojure_elisp/core.cljPublic API, file/project compilation, dependency resolution
Runtimeresources/clojure-elisp/clojure-elisp-runtime.el55+ Elisp functions implementing Clojure semantics
MCP Serversrc/clojure_elisp/mcp.cljMCP stdio server exposing compiler as AI tools
CLIsrc/clojure_elisp/cli.cljJVM uberjar entry point (compile, mcp, version)
BB CLIbb/clel/main.cljBabashka CLI frontend (delegates to uberjar)
nREPL kernelsrc/clojure_elisp/nrepl_kernel.cljSession registry, compile modes, op semantics (transport-free)
nREPL middlewaresrc/clojure_elisp/nrepl.cljJVM transport: wrap-cljel for a CIDER jack-in
nREPL serverbb/clel/nrepl_server.cljStandalone transport: clel nrepl, no JVM
Emacs moderesources/clojure-elisp/clojure-elisp-mode.elMajor mode for .cljel files
CIDERresources/clojure-elisp/cider-clojure-elisp.elCIDER client: sends forms, evals the compiled Elisp locally

Installation

Via bbin (recommended)

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).

Uberjar

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.

CLI Usage

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

Runtime (Emacs Package)

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/

Building from Source

# Build uberjar
clojure -T:build uber
# => target/clel-<version>.jar

Development

# 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

Requirements

  • Clojure 1.12+
  • Java 21+ (for building/running the uberjar)
  • Emacs 28.1+ (for compiled output)

License

MIT

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close