Skip to content

Repository files navigation

clojurust

crates.io docs main

A Rust-hosted dialect of the Clojure programming language.


What is this?

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 .cljrs and .cljc source files via cljrs 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 compilationcljrs 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.cljc files 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.asyncgo/chan/<!/>!/alts!/timeout via 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 servercljrs lsp: parse diagnostics + document-symbol outline (cljrs-lsp)
  • nREPL servercljrs nrepl: bencode-over-TCP for CIDER/Calva/Conjure (cljrs-nrepl)
  • IR visualizercljrs ir viz: HTML view of optimized IR + region allocation
  • Dependenciescljrs deps fetch/status: git-hosted deps from cljrs.edn (cljrs-project)
  • WASM REPL — browser REPL compiled to wasm32-unknown-unknown (cljrs-wasm)

Status

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.


Crates

Core pipeline

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

Compilation

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

Interop

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

Async, I/O & networking

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

Project & tooling

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

Binary

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.


Building

cargo build               # build all crates
cargo test                 # run all tests
cargo clippy -- -D warnings # lint
cargo fmt --check          # format check

Usage

cljrs 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 server

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


Environment variables

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.

Debug logging

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 output

Format: -X <level>:<feature1>,<feature2>,... where level is debug or trace. Use --jit-stats <path> to dump JIT specialization / inline-cache / deopt counters on exit.


Architecture

Execution pipeline

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.

IR lowering

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

Dependency graph (core path)

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


Repository layout

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

About

Clojure and Rust

Resources

Stars

59 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages