This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ClojureElisp is a Clojure dialect that compiles to Emacs Lisp, similar to how ClojureScript targets JavaScript. Write Clojure code in .cljel files and compile to .el files that run in Emacs.
Status: Early development - core compiler infrastructure in progress.
# Start REPL with dev dependencies (nrepl, cider-nrepl)
clojure -M:dev
# Run tests (Kaocha)
clojure -M:test
# Build (tools.build)
clojure -T:buildReader (Clojure's) → Analyzer (AST + env) → Emitter (codegen) → Elisp (.el)
-
analyzer.clj (
src/clojure_elisp/analyzer.clj)- Transforms Clojure forms into AST nodes
- Uses dynamic
*env*for tracking locals and namespace context - Handles special forms:
def,defn,fn,let,if,when,cond,do,ns,quote,loop,recur analyzemultimethod dispatches on form type
-
emitter.clj (
src/clojure_elisp/emitter.clj)- Transforms AST nodes to Elisp source strings
emit-nodemultimethod dispatches on:opkeycore-fn-mapping: Maps Clojure core functions to Elisp equivalents (e.g.,first→car,str→clel-str)mangle-name: Converts Clojure symbols to valid Elisp identifiers (?→-p,!→-bang)
-
core.clj (
src/clojure_elisp/core.clj)- Public API:
emit,emit-forms,compile-string,compile-file,compile-ns - Entry point for compilation
- Public API:
-
Runtime (
resources/clojure-elisp/clel.el)- Elisp runtime library required by compiled code
- Provides Clojure-like functions:
clel-conj,clel-get,clel-assoc,clel-str,clel-atom, etc. - Requires Emacs 28.1+
(require '[clojure-elisp.core :as clel])
;; Compile a form
(clel/emit '(defn foo [x] (+ x 1)))
;; => "(defun foo (x)\n (+ x 1))"
;; Compile a file
(clel/compile-file "src/my_package.cljel" "out/my-package.el")Trigger: public feedback said cljel "needs an external transpiler process which imo isn't really ergonomic while you're writing elisp functions, unless if you have way to invoke the transpiler, but this comes with a lot of tooling while elisp already has a good REPL-model".
The loop already existed and was CIDER-native. The README showed it in one table
row, so the reader could not find it, and three defects made it feel
second-class. See CHANGELOG.md for the full discussion.
nREPL split into a kernel and two transports:
clojure-elisp.nrepl-kernel- session registry, compile modes,handle-op. No transport, no nREPL dependency.handle-opreturns a vector of response maps and writes nothing.clojure-elisp.nrepl- JVM transport,wrap-cljelover the kernel. Public surface unchanged.clel.nrepl-server- bencode socket loop over the same kernel.clel nreplstarts it under Babashka in ~275ms with no JVM, no.nrepl.edn, no jack-in. Sessions start active because there is no Clojure evaluator to fall through to.
Namespace parity (the worst defect): handle-eval hardcoded :expr while
handle-load-file used :file, so C-c C-c defined greet and C-c C-k
defined my-pkg-greet. New compile-string-in-ns compiles in the buffer's ns
context, emitting forms only. The client sends the (ns ...) form as
cljel-ns.
Elisp client: cider-cljel-ensure-runtime (no more void-function clel-str on first eval), cider-cljel-buffer-ns-form, inline overlays via
cider--display-interactive-eval-result instead of message.
Compile path no longer carries test.check: ast/gen-node resolves
malli.generator through requiring-resolve. It is test-only, and requiring it
blocked every lightweight host.
cljw evaluated, blocked upstream: 36ms vs bb's 275ms and cljw nrepl ships
already, but malli.core/-memoize reaches java.util.concurrent.atomic.AtomicReference,
absent from ClojureWasm. Transport is ready; card 20260905132610-70d34b63.
Test stats: 598 tests, 3021 assertions, 0 failures. Neutralizing
compile-string-in-ns turns the new suite red (6 failures, 1 error).
Files modified:
src/clojure_elisp/nrepl_kernel.clj- new, the transport-free corebb/clel/nrepl_server.clj- new, standalone bencode servertest/clojure_elisp/nrepl_ns_parity_test.clj- new, 9 testssrc/clojure_elisp/nrepl.clj- reduced to the JVM transportsrc/clojure_elisp/compile.clj,core.clj-compile-string-in-nssrc/clojure_elisp/ast.clj- lazymalli.generatorresources/clojure-elisp/cider-clojure-elisp.el- ns form, runtime, overlaysbb/clel/main.clj,bb.edn-clel nreplsubcommand and classpathREADME.md,CHANGELOG.md- Interactive Development section, changelog
Added comprehensive Emacs buffer/process interop for writing Emacs extensions in ClojureElisp:
Buffer operations (direct mappings):
- Content:
buffer-string,buffer-substring,buffer-substring-no-properties,erase-buffer - Point/movement:
point,point-min,point-max,goto-char,forward-char,backward-char,forward-line,beginning-of-line,end-of-line - Editing:
insert,insert-buffer-substring,delete-char,delete-region - Buffer management:
current-buffer,set-buffer,get-buffer,get-buffer-create,kill-buffer,buffer-name,buffer-live-p,buffer-modified-p - Region:
mark,set-mark,region-beginning,region-end,use-region-p - Narrowing:
narrow-to-region,widen,buffer-narrowed-p - Text properties:
put-text-property,get-text-property,remove-text-properties,propertize
Process operations:
start-process,call-process,process-send-string,process-send-region,process-send-eofset-process-filter,set-process-sentinel,process-buffer,process-status,process-live-p,delete-process
File operations:
find-file,find-file-noselect,write-file,save-buffer,buffer-file-namefile-exists-p,file-readable-p,file-writable-p,file-directory-p,expand-file-name
Window operations:
selected-window,select-window,window-buffer,set-window-buffer,split-window,delete-window,other-window
Special forms (analyzer + emitter):
save-excursion— Save point/mark, execute body, restoresave-restriction— Save narrowing state, execute body, restorewith-current-buffer— Execute body in specified bufferwith-temp-buffer— Execute body in temporary buffersave-current-buffer— Save current buffer, execute body, restorewith-output-to-string— Capture output as string
Test stats: 211 tests, 1278 assertions, 2 pre-existing failures
Files modified:
src/clojure_elisp/analyzer.clj— Added 6 special form analyzerssrc/clojure_elisp/emitter.clj— Added 60+ function mappings and 6 emit-node methodsexamples/buffer-demo.cljel— New comprehensive example showing buffer/process interoptest/clojure_elisp/emitter_test.clj— Added 10 test functions (56 new assertions)
Added extend-type, extend-protocol, satisfies?, and reify:
extend-type— Extend multiple protocols to an existing type, emitscl-defmethodwith type specializerextend-protocol— Extend a single protocol to multiple typessatisfies?— Runtime check if value satisfies a protocol (clel-satisfies-p)reify— Anonymous protocol implementations with closure capture
Type mapping: Clojure types map to Elisp type specializers:
String→string,Number→number,Boolean→boolean, etc.- Custom record types use mangled names directly
Test stats: 201 tests, 1222 assertions, 2 pre-existing failures
Files modified:
src/clojure_elisp/analyzer.clj— Added analyzers for extend-type, extend-protocol, satisfies?, reifysrc/clojure_elisp/emitter.clj— Added emitters with type mapping and reify counterresources/clojure-elisp/clel.el— Added protocol registry andclel-satisfies-ptest/clojure_elisp/analyzer_test.clj— Added 4 test functions for new formstest/clojure_elisp/emitter_test.clj— Added 4 test functions for emissiontest/clojure_elisp/runtime_test.clj— Added 4 test functions for end-to-end compilation
Implemented source location tracking through the compiler pipeline:
- Fixed
read-all-formsto useLineNumberingPushbackReader(captures line/column from reader) - Forms read from files now carry
:lineand:columnmetadata - AST nodes propagate source locations via
*source-context*dynamic var analysis-errorincludes file:line:col in error messages*emit-source-comments*emits;;; L<line>:C<col>comments for debugging
Existing infrastructure leveraged:
extract-source-locationfunction in analyzerast-nodeconstructor merges source contextsource-commentfunction in emitter
Test stats: 200 tests, 1211 assertions, 0 failures
Files modified:
src/clojure_elisp/core.clj- UseLineNumberingPushbackReaderinread-all-formstest/clojure_elisp/core_test.clj- Added 4 source location test functionstest/clojure_elisp/nrepl_test.clj- Fixed to avoid loading nrepl deps when not available
Implemented add-watch/remove-watch for atoms via swarm (2 parallel slaves):
- Updated atom structure:
(list 'clel-atom val watchers-alist) - Runtime functions:
clel-add-watch,clel-remove-watch,clel--notify-watchers - Updated
clel-reset!,clel-swap!to notify watchers on state change - Emitter mappings:
atom,deref,clojure.core/deref(for@),reset!,swap!,add-watch,remove-watch
Test stats: 94 tests, 595 assertions, 0 failures
Files modified:
resources/clojure-elisp/clel.el- Updated atom structure, added watch functionssrc/clojure_elisp/emitter.clj- Added 7 atom-related core-fn-mapping entriestest/clojure_elisp/runtime_test.clj- Added 4 atom watch test functions (28 assertions)
Implemented 11 predicate functions via swarm (3 parallel slaves):
- Numeric:
zero?,pos?,neg?,even?,odd?→ direct Elisp cl-lib mapping - Boolean:
some?,true?,false?→clel-some-p,clel-true-p,clel-false-p - Collection:
coll?,sequential?,associative?→clel-coll-p,clel-sequential-p,clel-associative-p
Test stats: 90 tests, 556 assertions, 0 failures
Files modified:
resources/clojure-elisp/clel.el- Added 6 runtime functionssrc/clojure_elisp/emitter.clj- Added 11 core-fn-mapping entriestest/clojure_elisp/runtime_test.clj- Added numeric, boolean, collection predicate tests
Implemented via emacs-mcp swarm (3 parallel slaves):
- Test infrastructure: 71 tests, 395 assertions (Kaocha)
- Destructuring in let: vector
[a b], map{:keys [x]},:as,:or - Destructuring in fn/defn params:
& rest→(&rest args)withnthcdr - try/catch/finally:
condition-case+unwind-protect - throw:
(signal 'error ...)for ex-info, Exception., rethrow - Multi-arity:
cl-casedispatch on(length args)
Files modified:
src/clojure_elisp/analyzer.clj- Added special forms, destructuring expansionsrc/clojure_elisp/emitter.clj- Added emit-node methods for new formstest/- Created analyzer_test.clj, emitter_test.clj, core_test.cljkanban.org- Task tracking
Workflow notes added to project memory:
- Swarm conventions (monitoring, unsticking slaves, broadcasts)
- Session startup/wrap conventions (/catchup, /wrap, git)
The core-fn-mapping in emitter.clj defines Clojure→Elisp translations:
- Arithmetic:
+,-,*,/,inc→1+,dec→1- - Collections:
first→car,rest→cdr,cons→cons,count→length - Runtime functions (prefix
clel-):str,conj,get,assoc,keys,vals