Skip to content

Repository files navigation

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.

Try it in 30 seconds

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.

Start the server

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.

Keys

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.

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:

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

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)      │
└─────────────┘    └──────────────┘    └─────────────┘    └──────────────┘
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

Installation

Via bbin (recommended)

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

As a Babashka dependency

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.

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

About

A Clojure dialect that compiles to Emacs Lisp

Resources

Stars

29 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages