A Rust-hosted dialect of the Clojure programming language.
clojurust runs a Clojure dialect natively on Rust. Source files use the
.cljrs extension (native) or .cljc (cross-platform, with reader
conditionals). The runtime platform key is :rust.
A program flows through a tiered execution pipeline: the reader and tree-walking interpreter give immediate startup, hot functions are lowered to an SSA/A-normal-form IR and interpreted faster, and the hottest arities are JIT-compiled to native code via Cranelift while the program runs. The same backend powers ahead-of-time compilation to a standalone binary.
Current capabilities:
- Interpret
.cljrsand.cljcsource files viacljrs run <file> - REPL — interactive read-eval-print loop via
cljrs repl - Eval — evaluate expressions from the shell via
cljrs eval '<expr>' - Test — run clojure.test suites via
cljrs test --src-path <dir> <ns> - JIT compilation — hot function arities are compiled to native code via Cranelift while the program runs, with type specialization, inline caches, on-stack replacement (OSR) of hot loops, code unloading on redefinition, and deoptimization back to the interpreter when type assumptions break
- AOT compilation —
cljrs compile <file> -o <bin>produces a standalone native binary; end-to-end for multi-file programs (variadic fns, protocols, escape-analysis region allocation, HOFs, sequence/collection ops) - Reader conditionals —
.cljcfiles use#?(:rust ... :clj ... :default ...) - Persistent collections — HAMT-backed maps/sets, RRB vectors, sorted maps/sets (via rpds)
- Tracing GC — non-moving mark-and-sweep garbage collector with
GcPtr<T>, conservative scanning of JIT frames, and escape-analysis scratch regions - Clojure-compatible hashing — Murmur3-based hashing matching Clojure/JVM semantics
- Lazy sequences — lazy-seq, cons cells, and full lazy evaluation
- Protocols & multimethods — defprotocol, extend-type, defmulti, defmethod, defrecord, reify
- Concurrency — atom, volatile, delay, promise, future, agent (with send/await)
- core.async —
go/chan/<!/>!/alts!/timeoutvia a Tokio executor (cljrs-async) - Dynamic variables — binding, thread-local bindings, future conveyance
- Namespaces — require with :as/:refer, load-file, alias resolution
- Metadata — with-meta/meta on all collection types, preserved through assoc/conj/dissoc
- Transducers — map, filter, take, drop, partition-all, partition-by, distinct, dedupe, etc.
- Rust interop —
#[cljrs_interop::export]proc-macro, NativeObject, FromValue/IntoValue marshalling, protocol dispatch, and dynamic loading of native.so/.dylib - Standard library — clojure.string, clojure.set, clojure.test, clojure.walk, clojure.edn, clojure.zip, clojure.data, clojure.template
- IR acceleration — hot function arities are lowered to IR on a background worker and dispatched from an IR cache
Tooling:
- LSP server —
cljrs lsp: parse diagnostics + document-symbol outline (cljrs-lsp) - nREPL server —
cljrs nrepl: bencode-over-TCP for CIDER/Calva/Conjure (cljrs-nrepl) - IR visualizer —
cljrs ir viz: HTML view of optimized IR + region allocation - Dependencies —
cljrs deps fetch/status: git-hosted deps fromcljrs.edn(cljrs-project) - WASM REPL — browser REPL compiled to
wasm32-unknown-unknown(cljrs-wasm)
| Phase | Description | Status |
|---|---|---|
| 1 | Project infrastructure | complete |
| 2 | Lexer + parser (Form AST) |
complete |
| 3 | Core data types & persistent collections | complete |
| 4 | Evaluator & special forms | complete |
| 5 | Core standard library | complete |
| 6 | Protocols & multimethods | complete |
| 6-ext | defrecord, reify, built-in protocols | complete |
| 7 | Concurrency primitives | complete |
| 8 | Garbage collector | complete |
| 8.1 | Program optimization (ANF/SSA, escape analysis, regions) | complete |
| 8-ext-* | require, dynamic vars, ns, stdlib registry, transducers, … | complete |
| 9 | Rust interop (NativeObject, FromValue/IntoValue, #[export], dylib) |
mostly complete |
| 9-ir | IR pipeline (run-time ANF lowering, IR cache, tiering) | complete |
| 10 | JIT compiler (Cranelift) | working |
| 11 | AOT compiler | working (end-to-end for multi-file programs) |
| 12 | REPL & tooling (REPL, LSP, nREPL) | working |
| async | core.async, async I/O, networking, charset | implemented |
1,162 Rust tests across the workspace, plus 5,486 assertions in the
clojure-test-suite, which passes identically under the interpreter and
AOT-compiled.
See TODO.md for the full itemised roadmap.
| Crate | Description | Status |
|---|---|---|
cljrs-types |
Shared foundational types: Span, CljxError, CljxResult |
complete |
cljrs-reader |
Lexer + recursive-descent parser; produces Form AST with source spans |
complete |
cljrs-value |
Value enum; persistent collections (rpds-backed); Clojure-compatible hashing |
complete |
cljrs-gc |
Non-moving mark-and-sweep GC; GcPtr<T> smart pointer; Trace trait; scratch regions |
complete |
cljrs-runtime |
The runtime: env (GlobalEnv, Env, dynamic bindings, namespace loader, GC roots), builtins (~300 native core functions, transients, regex, bitops), interp (tree-walking interpreter: eval, special forms, macros, destructuring), tiered (IR interpreter, IR cache, tiering/JIT state, background lower worker, load_prebuilt_ir bundle replay) |
complete |
cljrs-tx |
Pure tree-walked transaction functions in a bounded, invocation-lifetime no-GC arena | initial |
cljrs-ir |
IR types (ANF/SSA) with serialization (postcard); ANF lowering, escape analysis, OSR | complete |
cljrs-stdlib |
Embedded stdlib: clojure.string, clojure.set, clojure.test, clojure.walk, clojure.edn, clojure.zip, clojure.data | complete |
| Crate | Description | Status |
|---|---|---|
cljrs-compiler |
Cranelift codegen (generic over Module), type inference, C-ABI runtime bridge, AOT object/binary emission, and the in-process JIT (jit/): hot-arity native compilation, type specialization + inline caches, OSR, code unloading, region threading |
working |
| Crate | Description | Status |
|---|---|---|
cljrs-interop |
Rust ↔ Clojure FFI: NativeObject, FromValue/IntoValue, error bridging, Registry | mostly complete |
cljrs-export-macro |
Proc-macro backing #[cljrs_interop::export] (re-exported by cljrs-interop) |
complete |
cljrs-base64 |
Base64 encode/decode exposed as Clojure native functions (interop example) | implemented |
cljrs-blake3 |
BLAKE3 hashing exposed as Clojure native functions (interop example) | implemented |
| Crate | Description | Status |
|---|---|---|
cljrs-async |
clojure.core.async via a Tokio current_thread + LocalSet executor; per-isolate heaps |
implemented |
cljrs-io |
Non-blocking file I/O delivered over core.async channels | implemented |
cljrs-net |
TCP/UDP/Unix/TLS sockets as core.async channels | implemented |
cljrs-charset |
Charset encode/decode with stream support (encoding_rs) |
implemented |
| Crate | Description | Status |
|---|---|---|
cljrs-project |
Project layer: config parses cljrs.edn (DepsConfig; git/local/Rust deps), vcs is the pure-Rust (gitoxide) git layer for versioned symbol resolution + the dep cache |
implemented |
cljrs-lsp |
LSP server (cljrs lsp): parse diagnostics + document symbols (tower-lsp) |
implemented (syntactic) |
cljrs-nrepl |
nREPL server (cljrs nrepl): bencode over TCP for CIDER/Calva/Conjure |
implemented |
cljrs-wasm |
Browser REPL compiled to wasm32-unknown-unknown (wasm-bindgen) |
implemented |
| Crate | Description | Status |
|---|---|---|
cljrs |
cljrs CLI: run, repl, eval, test, compile, ir (build/dump/viz), deps, lsp, nrepl, build-native (clap-based) |
functional |
Each crate has its own README.md with purpose, status, file layout, and public API.
cargo build # build all crates
cargo test # run all tests
cargo clippy -- -D warnings # lint
cargo fmt --check # format checkcljrs run <file.cljrs> # interpret a source file (JIT-accelerated)
cljrs run --src-path lib/ <file> # with additional source paths
cljrs repl # start interactive REPL
cljrs eval '(+ 1 2 3)' # evaluate expression from shell
cljrs test --src-path test/ <ns> # run clojure.test namespaces
cljrs compile app.cljrs -o app # AOT-compile to a standalone binary
cljrs ir viz <file> -o ir.html # render optimized IR + source to HTML
cljrs ir build -o bundle.bin --ns clojure.core # pre-lower namespaces to a serialized IR bundle
cljrs deps fetch # fetch git deps declared in cljrs.edn
cljrs lsp # start an LSP server (stdio)
cljrs nrepl --port 7888 # start an nREPL serverThe JIT runs by default. --jit-threshold N sets the per-arity invocation
count before native compilation (default 1000; 0 disables the JIT, falling
back to the IR interpreter and tree-walker). It can also be set via
CLJRS_JIT_THRESHOLD.
| Variable | Default | Description |
|---|---|---|
CLJRS_NO_IR |
unset | Disable all IR functionality. The IR cache is not consulted and nothing is lowered; all evaluation falls back to the tree-walking interpreter. Useful for debugging semantic differences between the IR interpreter and the tree-walker. |
CLJRS_EAGER_LOWER |
unset | Lower every fn* body to IR at definition time instead of lazily after warm-up. Expensive; primarily for testing the IR pipeline. No effect when CLJRS_NO_IR is set. |
CLJRS_IR_THRESHOLD |
50 | Tier-0 → Tier-1 warm threshold: tree-walked calls per arity before the body is lowered to IR (0 disables). |
CLJRS_JIT_THRESHOLD |
1000 | Tier-1 → JIT-native threshold: IR-interpreted calls per arity before native compilation (0 disables the JIT). |
CLJRS_OSR_THRESHOLD |
(JIT threshold) | Loop back-edge count within a single call before on-stack replacement promotes the loop to native code. |
CLJRS_JIT_NO_SPEC |
unset | Disable type specialization; the JIT compiles generic boxed entries only. |
CLJRS_JIT_DEOPT_LIMIT |
10 | Deopt failures per arity before its specialized code is unpublished and the arity is banned from re-specialization. |
CLJRS_IR_CACHE_TTL |
600 (s) | Idle time before a cold IR cache entry is evicted at the stop-the-world reclaim pass. |
Feature-level debug logging is available via the -X CLI flag:
cljrs -X debug:ir eval '(+ 1 2)' # show IR loading/dispatch diagnostics
cljrs -X debug:jit eval '(+ 1 2)' # show JIT compilation/dispatch diagnostics
cljrs -X debug:gc eval '(range 100)' # show GC collection diagnostics
cljrs -X trace:reader eval '(+ 1 2)' # trace-level reader outputFormat: -X <level>:<feature1>,<feature2>,... where level is debug or trace.
Use --jit-stats <path> to dump JIT specialization / inline-cache / deopt
counters on exit.
Source code
|
v
Reader (cljrs-reader) lexer + parser -> Form AST
|
v
Macroexpansion (runtime::interp) expand macros, syntax-quote
|
v
Tier 0: tree-walk (runtime::interp) immediate execution; counts calls per arity
|
v
Tier 1: IR interp (runtime::tiered) hot arities lowered to ANF/SSA IR
| (background lower worker), interpreted
v faster; OSR counters on hot loops
Tier 2: JIT native (compiler::jit) hottest arities compiled to native code
| via Cranelift: type specialization,
v inline caches, OSR, region threading;
Result (Value) deopt back to Tier 1 on guard failure
The same Cranelift backend (cljrs-compiler, generic over
cranelift_module::Module) drives both the in-process JITModule and the
AOT ObjectModule, so cljrs compile reuses the JIT's codegen.
Lowering is pure Rust and happens at run time. cljrs_ir::lower turns a
function arity into ANF/SSA IrFunctions; there is no build-time step, no
bootstrap through Clojure-hosted compiler namespaces, and no embedded IR
bundle. A function is lowered when its tree-walked call count crosses
--ir-threshold (default 50), on a background worker, and the result is cached
per ir_arity_id.
cljrs ir build lowers whole namespaces ahead of time and serializes them to a
bundle with postcard; cljrs ir dump prints one back. Those are diagnostics for
the lowerer — no runtime path loads a bundle today. cljrs_runtime::tiered::load_prebuilt_ir
is the public API an embedder would call to replay one into a live environment,
which matters for targets with no background lowering worker (a wasm32 build,
for instance).
cljrs-types
|
cljrs-gc -----------> cljrs-types
|
cljrs-reader -------> cljrs-types
|
cljrs-value ---------> cljrs-gc, cljrs-reader, cljrs-types
|
cljrs-ir ------------> cljrs-reader, cljrs-types
|
cljrs-runtime -------> cljrs-value, cljrs-gc, cljrs-reader, cljrs-ir,
| cljrs-project
| modules: env, builtins, interp, tiered, logging
|
cljrs-stdlib --------> cljrs-runtime, cljrs-ir
|
cljrs-compiler ------> cljrs-runtime, cljrs-ir, cljrs-stdlib
| (Cranelift JIT + AOT)
| + cljrs-async — the state-machine poll ABI its codegen
| implements. I/O, net, charset and base64 are *not*
| dependencies: the host passes an ExtensionSet
|
cljrs (binary+lib) --> cljrs-stdlib, cljrs-compiler, cljrs-lsp,
cljrs-nrepl, cljrs-project, cljrs-interop
(+ async/net/charset/base64 behind features)
modules: cli, commands (incl. ir::viz), session, native
The CLI is the only place that chooses optional extensions: cljrs-compiler
does not depend on cljrs-io, cljrs-net, cljrs-charset, or cljrs-base64,
and takes an ExtensionSet from its host instead (stage 4 of
docs/crate-consolidation-plan.md).
Cargo.toml # workspace manifest (resolver=2)
crates/
# core pipeline
cljrs-types/ # foundational types
cljrs-reader/ # lexer + parser
cljrs-value/ # Value enum, collections, hashing
cljrs-gc/ # tracing GC + scratch regions
cljrs-runtime/ # env + builtins + interp + tiered (the merged runtime)
cljrs-ir/ # IR types + lowering + serialization
cljrs-stdlib/ # embedded standard library namespaces
# compilation
cljrs-compiler/ # Cranelift codegen + JIT + AOT
# interop
cljrs-interop/ # Rust <-> Clojure FFI
cljrs-export-macro/ # #[export] proc-macro
cljrs-base64/ # base64 interop library
cljrs-blake3/ # BLAKE3 interop library
# async, I/O & networking
cljrs-async/ # clojure.core.async
cljrs-io/ # async file I/O
cljrs-net/ # TCP/UDP/Unix/TLS sockets
cljrs-charset/ # charset encode/decode
# project & tooling
cljrs-project/ # cljrs.edn project config + git layer for versioned deps
cljrs-lsp/ # LSP server
cljrs-nrepl/ # nREPL server
cljrs-wasm/ # browser REPL (wasm)
# binary
cljrs/ # CLI: subcommands, IR visualizer, native package loading
examples/
rust-interop/ # Rust interop example
tests/
fixtures/ # .cljrs / .cljc source files for integration tests
TODO.md # phased implementation roadmap