cljrs (Clojurust CLI)
The cljrs binary — command-line interface for running, compiling, and
interactively exploring clojurust programs.
File layout
src/
main.rs — CLI entry point: Clap structs, miette error hook, subcommand
dispatch, REPL loop, test harness, GC-stats reporter
Subcommands
| Subcommand | Purpose |
|---|---|
run |
Interpret a .cljrs / .cljc source file |
repl |
Start an interactive REPL |
compile |
AOT-compile a source file or project (via cljrs.edn) to a native binary, or a .wasm module with --target wasm |
eval |
Evaluate a single Clojure expression and print the result |
ir build |
Pre-lower namespaces to IR and write a serialized bundle |
ir dump |
Print a human-readable dump of a serialized IR bundle |
ir viz |
Render the optimized IR + source as a self-contained HTML visualizer |
test |
Run clojure.test namespaces (named on the CLI or auto-discovered) |
deps fetch |
Clone / update git dependencies declared in cljrs.edn |
deps status |
Show which dependencies are cached and which are missing |
-main entry point
After all top-level forms in the source file are evaluated, cljrs run looks
up -main in the current namespace. If the var exists it is called with the
arguments that follow -- on the command line, each as an individual string:
The same convention applies to AOT binaries produced by cljrs compile: the
compiled binary calls -main after __cljrs_main finishes, passing all
argv entries (skipping the program name) as individual string arguments.
If -main is not defined the program exits normally without error.
An ^:async -main is supported: calling it returns a Future immediately,
so cljrs run awaits that future on the shared async LocalSet (see
implementation notes) before exiting, ensuring the body and anything it spawns
run to completion. A synchronous -main is awaited as a no-op pass-through.
Per-subcommand flags
run, repl, compile, test accept:
--src-path <DIR>— repeatable; directories searched byrequire--gc-soft-limit-mb <MB>— soft GC threshold--gc-hard-limit-mb <MB>— hard GC threshold
run additionally accepts:
[-- ARGS…]— positional arguments forwarded verbatim to-main
compile additionally accepts:
-o, --out <PATH>— output path (required): a native binary, or a.wasmmodule with--target wasm--target <native|wasm>— code-generation target (defaultnative).wasmemits a WebAssembly module via the AOT wasm backend (the entry namespace's functions; the"rt"imports are satisfied by the runtime built forwasm32-unknown-unknown).--testis not yet supported withwasm.--main <NS>— namespace containing-main; overrides:mainincljrs.ednand auto-detection--test— compile a test harness that runs every test in the given file/directory--require-fully-compiled- fail the build if the binary would embed readable Clojure source text (interpreted preambles, bundled namespaces). The audit runs incompile_file, so the flag is an error with--testand with--target wasm, whose paths do not audit; it is rejected there rather than accepted and ignored.
ir build accepts:
-n, --ns <NS>— repeatable; namespaces to lower (defaultclojure.core)-o, --output <PATH>— output bundle path (defaultir_bundle.bin)--src-path <DIR>— repeatable; source directories forrequire-ing non-clojure.corenamespaces-v, --verbose— print per-arity lowering progress
ir dump takes a single positional bundle path and prints the IR of every function it contains.
ir viz accepts:
-o, --out <PATH>— output HTML path (defaults to<file>.ir.html)--src-path <DIR>— repeatable--quiet— suppress the[aot] …progress output
test additionally accepts:
[namespaces…]— positional list; if empty, namespaces are auto-discovered under--src-path-v, --verbose— print each passing assertion (helps isolate hangs)
eval takes a single positional expression string.
deps fetch accepts an optional positional dependency name; without it all git
deps are fetched. deps status takes no arguments.
cljrs.edn auto-discovery
When any command that runs code (run, repl, eval, test, compile)
starts, it walks up the directory tree from the current working directory
looking for a cljrs.edn file. If found, its :paths entries are appended
to the source search path (after any --src-path CLI flags), and the parsed
DepsConfig is stored in GlobalEnv.deps_config so that versioned symbol
resolution can use it without a second parse.
compile and cljrs.edn
cljrs compile reads cljrs.edn to determine:
- Source paths —
:pathsentries are added to--src-path(CLI flags come first). - Dependency source roots — each dep's source directories are resolved and appended so
requireresolves correctly during compilation. - Entry-point namespace — determined by the following priority:
--main <NS>CLI flag (highest priority):mainkey incljrs.edn(e.g.:main my.app.core)- Auto-detection: scans
:pathsfor a unique-mainfunction; errors if zero or multiple are found
When cljrs.edn is present and the entry-point namespace is known, the
file positional argument may be omitted; the compiler finds the source file
for the main namespace automatically.
Each declared dependency's own source roots are also appended to the search
path, so a plain (require '[dep.ns :as …]) resolves namespaces provided by a
dependency:
- Local deps (
:local/root) contribute theircljrs.edn:paths(orsrc/) from the directory on disk. - Git deps are materialized from the local bare cache at their pinned
:git/sha(no network — runcljrs deps fetchfirst; a missing cache warns and is skipped), and contribute the checkout's:paths(orsrc/). - Native deps (
:rust/load :dylib) carry no Clojure source; they are built and registered on demand by the native-requireloader (cljrs-dylib) when their namespace is firstrequired.
Global flags
These appear before the subcommand and apply to every command:
-
--stack-size-mb <MB>— thread stack size (default 64). Raise if you hit stack overflows in deeply recursive code. -
--debug— enable debug logging -
--trace— enable trace logging (implies--debug)Codegen crates (
cranelift_*,regalloc2) are pinned towarnat all verbosity levels —cranelift-jit/cranelift-objectlog every compiled function's whole CLIF body atinfo, which otherwise buries real output. SetRUST_LOG(tracingtarget=level syntax) to replace the defaults and get them back, e.g.RUST_LOG=info,cranelift_jit=info cljrs run app.cljrs. -
-X <LEVEL:FEATURES>— feature-level logging, repeatable. Format:<level>:<feat1>,<feat2>,…. Levels:debug,trace. Example:-X debug:gc,jit. -
--gc-stats [FILE]— print acljrs_gc::GC_STATSsnapshot at program exit (allocations, region/bump usage, GC pause count + total duration, freed objects/bytes). No value → stdout; with a path → that file. Honoured byrun,eval, andtest. -
--jit-stats [FILE]— print a JIT specialization / inline-cache counter snapshot at program exit (boxed arithmetic bridge calls, entry-guard deopts, keyword IC fills, protocol IC hits/misses; Phase 10.6,cljrs_compiler::rt_abi::jit_stats). No value → stdout; with a path → that file. Honoured byrun,eval, andtest.
Examples
# Interpret a file
# REPL
# AOT compile to a native binary
# One-shot expression
# Render IR visualizer (writes samples/graph.cljrs.ir.html, open in any browser)
# Pre-lower namespaces to IR for fast startup loading (cljrs_eval::load_prebuilt_ir)
# Tests
# GC stats
# Bigger stack + tracing for one feature
# Dependency management (reads cljrs.edn from the current directory tree)
Build features
| Feature | Effect |
|---|---|
async (default on) |
Pulls in cljrs-async and cljrs-io and builds the Tokio runtime that drives top-level async evaluation (see implementation notes). Without it, ^:async/core.async/clojure.rust.io.async are unavailable and evaluation is purely synchronous. |
no-gc (default off) |
Propagated to cljrs-gc/cljrs-value/cljrs-eval/cljrs-compiler/cljrs-runtime/cljrs-stdlib. Disables the tracing GC; only region-allocated and stack values are permitted. Compiles fail (AotError::NoGcBlacklist) if the program contains allocations the optimizer can't lift onto regions. |
enable-rustyline |
Pulls in rustyline for a line-editing REPL. Without it, cljrs repl falls back to a plain BufRead loop. |
Build with e.g. cargo build --release --features enable-rustyline,no-gc.
Implementation notes
- Argument parsing uses Clap derive macros (
Parser,Subcommand). - The miette error hook is installed at startup so
CljxErrorpropagated tomainrenders with terminal-linked source snippets. - A worker thread is spawned with the configured stack size to run the actual command; the main thread only handles signal/exit setup.
- The REPL prints results, paginates errors via
miette, and persists multi-line input across blank prompts. - Top-level async (with the
asyncfeature).mainbuilds a single-threaded Tokio runtime +LocalSetand stashes it in a thread-localAsyncDriverrather than wrapping the whole session in oneblock_on. Each top-level form is then evaluated throughcljrs_async::eval_asyncviaLocalSet::block_onineval_form, so spawned tasks (core.async producers,^:asynccalls,clojure.rust.io.asyncreaders/writers) make progress and a top-levelawaitresolves. Tasks that outlive a form — e.g. a channeldefd at one REPL prompt and consumed at the next — stay queued on the sharedLocalSetand continue on the next form's drive. Note: blocking ops (<!!/>!!) still park the single executor thread and so are not usable at the top level; use(await (take! ch))/goinstead. ir vizruns the AOT pipeline through region optimization (viacljrs_compiler::aot::lower_file_to_ir) and hands the resultingIrFunctiontocljrs_ir_viz::render_html.
Dependencies
| Crate | Role |
|---|---|
cljrs-types (workspace) |
CljxError for miette::Result propagation; Span |
cljrs-gc (workspace) |
GC root, configuration, GC_STATS snapshot |
cljrs-reader (workspace) |
Lexer + parser |
cljrs-value (workspace) |
Value and persistent collections |
cljrs-eval (workspace) |
Tree-walking interpreter, Env |
cljrs-stdlib (workspace) |
Bootstrapped standard library (standard_env*) |
cljrs-runtime (workspace) |
Concurrency primitives consumed by stdlib |
cljrs-compiler (workspace) |
AOT pipeline (compile_file, compile_test_harness, lower_file_to_ir) |
cljrs-ir-viz (workspace) |
HTML IR visualizer used by ir viz |
cljrs-ir (workspace) |
IrBundle, deserialize_bundle — used by ir dump |
cljrs-ir-prebuild (workspace) |
run_prebuild — used by ir build |
cljrs-interop (workspace) |
Rust ↔ Clojure FFI |
cljrs-async (workspace, optional) |
clojure.core.async runtime + eval_async; enabled by async |
cljrs-io (workspace, optional) |
clojure.rust.io.async async file I/O; enabled by async |
tokio (workspace, optional) |
Single-threaded runtime + LocalSet driving async; enabled by async |
cljrs-logging (workspace) |
--debug / --trace / -X flag handling |
cljrs-deps (workspace) |
cljrs.edn parser; DepsConfig / Dependency types |
cljrs-vcs (workspace) |
Pure-Rust (gitoxide) git helpers: fetch_remote, cache_path_for_url, native signature verification |
clap (workspace) |
CLI argument parsing |
miette (workspace) |
Rich terminal error rendering |
tracing / tracing-subscriber |
Structured logging output |
rustyline (workspace, optional) |
Line-editing REPL when enable-rustyline is on |