A Clojure compiler implemented on top of Chez Scheme https://jolt-lang.github.io/
  • Scheme 78.1%
  • Clojure 17.7%
  • Shell 3.6%
  • Makefile 0.4%
  • C 0.1%
Find a file
2026-09-06 01:11:19 -04:00
.github/workflows test: avoid PowerShell native quote stripping 2026-09-04 22:34:59 -07:00
bench Merge pull request #864 from jolt-lang/unboxed-prim-arrays 2026-09-06 12:19:41 +08:00
bin Nightly: replace the release after the gates, one version definition 2026-09-02 09:55:58 -04:00
ci Build reactive-dashboard, don't test it 2026-09-05 14:34:04 -04:00
host Merge pull request #865 from jolt-lang/fix/infer-through-coerce 2026-09-06 12:32:58 +08:00
img ci: run jpm bootstrap from inside the jpm checkout 2026-06-05 18:30:27 -04:00
jolt-core Merge pull request #865 from jolt-lang/fix/infer-through-coerce 2026-09-06 12:32:58 +08:00
stdlib Publish the confined arena's state with a compare-and-set! 2026-09-05 23:36:15 -04:00
test Merge pull request #867 from jolt-lang/test/nil-method-call-npe 2026-09-06 12:19:47 +08:00
tools Write the changelog for the error-reporting epic 2026-09-05 13:31:27 -04:00
vendor Update Grenadine to 0.1.7 2026-08-18 15:07:48 -04:00
.gitattributes Parse repo text files the same on CRLF checkouts 2026-08-13 22:48:34 -04:00
.gitignore cpcache review fixes: natives are descriptors, guard the whole hit path 2026-08-13 15:55:44 -04:00
.gitmodules Timed deref on a process honours the timeout 2026-08-15 00:19:33 -04:00
CHANGELOG.md Changelog for 0.8.3 2026-09-06 01:11:19 -04:00
CONTRIBUTING.md Write the changelog for the error-reporting epic 2026-09-05 13:31:27 -04:00
flake.lock Pin makeplus/makes as a flake input 2026-08-21 12:24:33 +03:00
flake.nix Nix flake: aarch64-darwin support 2026-08-21 09:31:21 -04:00
install Bake lz4 into the binary instead of borrowing Homebrew's 2026-09-04 21:18:14 -04:00
LICENSE Relicense under EPL-2.0 2026-06-27 13:17:19 -04:00
llms.txt Split the README, add llms.txt 2026-08-16 10:05:56 -04:00
Makefile Pin the array backings, and certify what they may not change 2026-09-05 21:23:43 -04:00
README.md -M with nothing to run is a REPL; a caught load error no longer pins the source position 2026-09-02 15:13:21 -04:00
remint.log Merge main into the type-hint bridges 2026-09-05 17:33:32 -04:00

Jolt

tests

A Clojure implementation on Scheme. Jolt reads Clojure source, analyzes it to a host-neutral IR, emits Scheme, and runs it — on Chez by default, or on Gambit compiled to JavaScript for the browser. The compiler is self-hosted: it is written in Clojure (jolt-core/) and compiles itself. It ships a Clojure-compatible standard library.

This is not the JVM

Most portable Clojure runs unchanged, but there is no JVM underneath and JVM reasoning does not carry over. The four that bite first:

  • No Java interop. No reflection, no gen-class/proxy. Interop syntax (Class., Class/static, .method) resolves against a shimmed subset of java.* written in Scheme; a class token is a name, not a loaded class. The shims reimplement their JVM counterparts' API — they are not the JVM, so classloaders, JVM GC behaviour, and JVM thread lifetime rules do not apply. To call C libraries, use the jolt.ffi foreign-function interface.
  • Codepoint strings. (count "😀") is 1, not 2. No UTF-16 surrogate pairs.
  • A different regex engine. Patterns compile through irregex, not java.util.regex.
  • Partial clojure.core coverage. Broad but not total; a namespace can load with most functions working and a few not yet implemented.

Differences from Clojure below is the full list. Read it before assuming a JVM behaviour holds.

Contents

Machine-readable index for coding agents: llms.txt.

Install

Prebuilt binaries are self-contained — runtime, compiler, and stdlib in one executable — and need only the base system libraries: Linux x86_64 wants glibc 2.35 or newer (Ubuntu 22.04+, Debian 12+, RHEL 9+), macOS arm64 wants macOS 14+. Anything else (Intel Mac, musl/Alpine, older glibc) is not supported by the prebuilt binaries — build from source.

With Homebrew:

brew install jolt-lang/jolt/jolt

Or with the install script (installs to ~/.local/bin, or /usr/local/bin as root; --dir <dir> and --version <v> — or nightly, the daily build of main — override that):

curl -sL https://raw.githubusercontent.com/jolt-lang/jolt/main/install | bash

Or download the binary archive for your platform from the releases page (jolt-<ver>-<platform>.tar.gz, or the .zip on Windows). The "Source code" archives GitHub attaches to a release are not binaries and omit the submodules, so they can neither run nor build — clone the repo instead.

Then jolt -e '(+ 1 2)'.

Running from source has no build step. The bootstrap seed (host/chez/seed/{prelude,image}.ss) is checked in, so a fresh clone runs immediately:

git clone --recurse-submodules https://github.com/jolt-lang/jolt.git
cd jolt
bin/jolt -e '(+ 1 2)'        # => 3

The --recurse-submodules matters: jolt vendors its regex engine, its Maven resolver, and its test suites as git submodules. In a checkout that's missing them (a plain git clone, or after pulling a commit that adds one), fetch them with:

git submodule update --init --recursive

bin/jolt needs a threaded Chez Scheme 10.x. It first honors JOLT_CHEZ, then reuses a 10.x Chez already provisioned under .cache/local by make, and finally searches PATH for chez or chezscheme. make provisions its own 10.4.1 when PATH has a different version and exports JOLT_CHEZ so both halves of a build agree.

Note that GitHub's auto-generated "Source code (zip/tar.gz)" archives on the releases page do not contain submodules, so they can't run or build — clone the repo instead (or grab a prebuilt binary from the same page).

After changing a compiler source — the reader (host/chez/reader.ss), the analyzer/IR/backend (jolt-core/jolt/*.clj), or the clojure.core overlay (jolt-core/clojure/core/*.clj) — re-mint the seed:

make remint                   # iterates host/chez/bootstrap.ss to a byte-fixpoint

Resolving a project's deps.edn needs git for git deps, and OpenSSL (libssl/libcrypto, loaded via FFI) plus unzip for Maven deps — jolt downloads and resolves those itself, with no curl and no Java. A dependency that can't be fetched is skipped, never fatal. See Getting Started for the per-platform packages and deps.edn internals for how resolution works.

Run

jolt -e EXPR                 # evaluate a Clojure expression and print the result
$ jolt -e '(->> (range 10) (filter even?) (map (fn [x] (* x x))) (reduce +))'
120
$ jolt -e '(/ 1 2)'
1/2

When the current directory has a deps.edn, -e resolves it first, so the expression can require the project's own namespaces and its dependencies. -Sdeps and -A compose with it for a one-off evaluation, and -M takes the same main options on the command line when the selected aliases declare none (none at all starts a REPL, like clj -M:dev):

jolt -Sdeps '{:paths ["src" "test"]}' -e "(require 'my.app-test 'clojure.test)
                                          (clojure.test/run-tests 'my.app-test)"
jolt -A:test -M -e "(println :hi)"

The rest of the clj option surface works the same way — each takes the aliases around it and runs no program:

jolt -Spath                  # the classpath (what an editor asks for before connecting)
jolt -Stree                  # the dependency tree, tools.deps format
jolt -Strace                 # write the expansion decisions to trace.edn
jolt -Sdescribe              # version, deps.edn chain, and caches, as edn
jolt -P                      # fetch every dependency, then stop (CI, images)
jolt -Srepro …               # ignore ~/.clojure/deps.edn for this run
jolt -Sverbose …             # say where deps are read from and fetched into
jolt -Scp "$(cat cp.txt)"# run against a recorded classpath, expanding nothing

An alias the project doesn't declare is skipped with a warning rather than failing the query, so jolt -A:test:dev -Spath and jolt -Spath -M:test both answer. Under -Scp the deps.edn is still read — aliases, :main-opts and tasks work — but nothing is expanded, so a shared library declared by a dependency is not loaded (the project's own :jolt/native still is). -Sforce, -Sthreads, and -Jopt are accepted and ignored: no classpath cache to force, serial fetching, no JVM to pass options to.

Differences from Clojure

Jolt targets Clojure semantics but runs on Chez, not the JVM. Most portable Clojure runs unchanged — persistent collections (32-way-trie vectors, HAMT maps/sets, RRB vectors), the numeric tower (exact integers, bignums, ratios, doubles, BigDecimal with M literals and with-precision), lazy and infinite sequences, transducers, destructuring, multimethods with hierarchies, protocols/records (deftype/defrecord/reify/extend-protocol), metadata, namespaces, atoms, refs/STM (ref/dosync/alter/commute), future/promise/agent/pmap, clojure.core.async (and .flow), runtime eval/load-string/defmacro, and the full reader (#(), #_, #?, tagged literals, #"…") all behave as on the JVM. = is category-aware ((= 3 3.0)false) and == is value-equality, as in Clojure. The genuine divergences:

  • No JVM, no Java interop. No reflection, no gen-class/proxy. Interop syntax (Class., Class/static, .method) resolves only against a shimmed subset of the java.* standard library; a class token is a name, not a loaded class. See Host Interop. To call C libraries directly, use the jolt.ffi foreign-function interface (how the db and http-client libraries bind SQLite/libpq and sockets/OpenSSL/zlib).
  • The java.* shims are not the JVM. A shimmed class implements its JVM counterpart's API on Scheme, so it can look convincing while the surrounding runtime is not the JVM. Process and memory semantics in particular are Chez's: a thread does not keep the process alive after the main thread returns (.setDaemon is accepted and ignored), and there is no classloader, no JVM heap tuning, and no JVM GC behaviour to reason about.
  • Codepoint strings. Strings are Chez strings — codepoint-indexed, no UTF-16 surrogate pairs. (count "😀") is 1 (JVM: 2) and subs never splits a character; only code doing UTF-16 unit arithmetic notices.
  • Regex engine. Patterns compile through irregex (vendored), not java.util.regex; common patterns work, Java-specific features can differ.
  • Coverage. clojure.core is implemented function by function against the JVM-sourced conformance corpus — broad but not total; a namespace can load with most functions working and a few not yet implemented.
  • A .jolt extension. A namespace's source can be foo.jolt as well as foo.clj or foo.cljc, and the three are the same language: the reader, analyzer, and emitter never look at the extension. .jolt is a marker for readers and tooling, saying the file uses jolt-specific interop and is not portable Clojure. It resolves first, so a library can ship a portable foo.cljc next to a foo.jolt that wins on jolt, the way .clj wins over .cljc on the JVM. data_readers.jolt works like data_readers.clj too.
  • Digit separators in numbers. 1_000_000, 0xFF_FF and 36rR_Z read as numbers; the JVM raises Invalid number on all three. The rule is Java's — an underscore must sit between two digits, never against a sign, radix marker, decimal point, exponent marker or N/M suffix — so 1_ and 0x_52 still raise. A leading underscore is still an ordinary symbol. clojure.edn refuses separators: edn's grammar has none, and a config that read only here would fail in every other edn reader. Additive — nothing that reads on the JVM changes meaning.
  • Reader macros. The # dispatch table is open for punctuation: jolt.reader/set-dispatch-macro! puts a reader on a character. jolt ships #$"a ~{x}" interpolation (clojure.core.strint's grammar) on it. Additive — #<punct> is a read error on the JVM.
  • Clojure is a terminal dependency. jolt is Clojure, so org.clojure/clojure in a deps.edn contributes neither an artifact nor children. On the JVM that artifact pulls in org.clojure/spec.alpha, so a project declaring only Clojure still gets clojure.spec.alpha; here it has to be declared. See Runtime dependencies.

The tracked, gated list of value-level divergences is test/conformance/known-divergences.edn; the prose version is Differences from Clojure on the docs site.

Runtime dependencies

Jolt supplies org.clojure/clojure and org.clojure/clojurescript itself, so those libraries are terminal when encountered transitively: their artifacts and dependency trees are not acquired. Explicitly declared org.clojure/spec.alpha and org.clojure/core.specs.alpha dependencies remain ordinary dependencies.

Code can acquire and import dependencies while it runs with the portable clojurestar.deps/require-deps macro:

(require '[clojurestar.deps :refer [require-deps]])

(require-deps
 ["mvn:dev.weavejester/medley@1.10.0/medley.core" :as medley])

Literal dependency vectors need no quote; quoted vectors remain supported for compatibility. Maven, Gist, and GitHub source-file coordinates support :as and explicit :refer imports. An optional leading map accepts :mvn/local-repo and :gitlibs/dir; :cache-dir remains a compatibility alias for the source-file cache root. A pinned Gist file accepts either gist:<owner>/<id>/<file>@<revision> or gist:<owner>/<id>/<revision>/<file>; both forms use the same cache entry. A GitHub source file accepts either github:<owner>/<repo>/<ref>/<path.clj|cljc> or the equivalent github:<owner>/<repo>/blob/<ref>/<path.clj|cljc> form. Refs occupy one path segment; full commit SHAs reuse persistent cache while named refs refresh in a new process. Selected files must be self-contained and begin with an ns form. The explicit Maven option takes precedence over JOLT_MAVEN_REPOSITORY, which takes precedence over GRENADINE_MAVEN_REPOSITORY. For Gist and GitHub source dependencies, JOLT_GITLIBS_DIR takes precedence over GRENADINE_GITLIBS_DIR, then GITLIBS; source lives under gist/ or github/ in that effective root.

Diagnostics

  • "Did you mean?" — when a bare symbol doesn't resolve, the compile error lists the closest in-scope names by edit distance (current-namespace vars, clojure.core publics, and lexical locals):
    $ jolt -e '(prinltn 1)'
    Unable to resolve symbol: prinltn in this context (did you mean print, printf, println?)
    
  • JOLT_DIAG=edn — emit an uncaught error as a single line of valid EDN to stderr (:message plus source :line/:column/:file; an unresolved symbol also carries :type/:symbol/:suggestions/:ns) so an editor or tool can read it back. Default output is unchanged.
  • JOLT_CHECK — opt-in success-type lint (RFC 0006): each runtime-compiled form is run through the checker and findings print as located warnings, e.g. 1:10: warning: `+` requires a number, but argument 2 is a keyword. Off by default (zero cost); a checker error never breaks a compile.
  • JOLT_DEBUG — verbose dependency resolution (the fetching / using-cache / skipping lines that are otherwise quiet) and the host static-shim drift warning.

REPL and editor integration

jolt repl                    # a line REPL with the project's deps loaded
jolt nrepl-server [port]     # an nREPL server (default 7888) for editors

Both resolve the deps.edn in the current directory first, so the project's source roots and native libraries are loaded — (require '[my.ns]) works live. nrepl-server writes a .nrepl-port file in the project dir, so CIDER / Calva / Cursive auto-detect the port; override it with the argument or JOLT_NREPL_PORT.

The server runs in dev mode — calls deref their var, so redefining a function takes effect on the next call without restarting the process. The built-in handler speaks clone/describe/eval/load-file/close; everything past that is nREPL middleware, listed in deps.edn under :nrepl/middleware. jolt-lang/nrepl supplies both layers — sessions and interruptible eval, plus the cider-nrepl ops an editor expects (info, complete, the namespace browser, tests, error analysis):

{:deps {jolt-lang/nrepl {:git/url "https://github.com/jolt-lang/nrepl"
                         :git/sha "<full-sha>"}}
 :nrepl/middleware [nrepl.middleware/default-middleware
                    cider.nrepl/cider-middleware]}

See REPL-Driven Development.

Compile a binary

jolt build ahead-of-time compiles a project into a single self-contained executable — the runtime, clojure.core, the standard library, the app, and its deps.edn dependencies are linked in, so the result needs no Chez install, no JVM, and no source on disk to run.

jolt build -m myapp.core -o myapp   # compile myapp.core's -main into ./myapp
./myapp arg1 arg2                   # runs anywhere; args reach -main

Modes trade dynamism for speed: the default (release) build uses the proven code generator; --opt also runs the inference + inlining + scalar-replacement passes over the closed-world program; --dev is unoptimized. Numeric code unboxes to raw flonum/fixnum machine ops when types are proven — by whole-program inference, by JVM-style ^double/^long hints, or by (double x)/(long x) casts where inference can't see. See Building & Running.

Two opt-in closed-world flags cut dispatch cost and binary size:

jolt build -m myapp.core --direct-link   # app->app calls bind directly (no var lookup)
jolt build -m myapp.core --tree-shake    # ship only code reachable from -main

--tree-shake walks the call graph across your app, its libraries, and clojure.core, drops everything unreachable from -main, and typically removes 12 MB. It stays sound by bailing out — keeping everything, and naming the library responsible — when reachable code resolves vars by name at runtime (eval/resolve/ns-resolve/…). See RFC 0007.

Built executables carry an optional startup profiler: launch one with JOLT_STARTUP_PROFILE=1 to get per-stage wall time, process CPU time, collection counts, reclaimed bytes, and heap size on stderr, marked at the native boot loader, the runtime files, each application namespace, and -main. Normal launches leave it disabled and silent.

Linking a binary needs Chez's kernel development files (libkernel.a, scheme.h) and a C compiler. They come with a from-source Chez install and with the prebuilt jolt binary; a distro chezscheme package ships only the runtime, so build won't link there.

Compile a library

jolt build --library compiles a project into a shared object (.so/.dylib/.dll) that a C/C++/Rust host links or dlopens and calls through a small C ABI. Like build, the whole runtime is embedded — the result is a managed-runtime library: it carries its own GC and must be entered through jolt_library_init before any call.

The Jolt side publishes entry points with jolt.ffi/export!:

(ns libadd.core
  (:require [jolt.ffi :as ffi]))

(defn add [x y] (+ x y))
(ffi/export! "add" add [:int :int] :int)
jolt build --library -m libadd.core -o libadd   # => libadd.so / libadd.dylib

The C side dlopens it, calls jolt_library_init once, then resolves each entry by name with jolt_lookup and casts to its type; Native Interop has the full example, the type keywords (the same ones foreign-fn uses), and the threading limits. The same --opt/--dev/--direct-link/--tree-shake flags apply, and the same Chez kernel development files + C compiler are required to link.

Documentation

Full documentation is at jolt-lang.github.ioGetting Started, Differences from Clojure, Host Interop, Native Interop (FFI), Writing Libraries, the language specification, and the RFCs. Every page is listed in llms.txt as well.

Contributing

Building from source, the seed and re-minting, the architecture, the Scheme backends, and the test gates are in CONTRIBUTING.md.

License

Eclipse Public License 2.0