ClojureWasm

April 26, 2026 · View on GitHub

Status: Pre-Alpha License: EPL-1.0 Zig 0.16.0 GitHub Sponsors

Status: Pre-Alpha / Experimental

ClojureWasm is under active development. APIs may change, and there are behavioral differences from reference Clojure. Bugs and rough edges are expected. See DIFFERENCES.md for details.

Verified on: macOS (Apple Silicon / aarch64) and Linux (x86_64). Cross-compiles to aarch64-linux. All test suites pass on both platforms.

A Clojure runtime written from scratch in Zig. No JVM, no transpilation — a native implementation targeting behavioral compatibility with Clojure.

Highlights

  • Fast startup — ~4ms to evaluate an expression (ReleaseSafe)
  • Small binary — ~4MB single executable (ReleaseSafe)
  • Single binary distributioncljw build app.clj -o app, runs without cljw installed (temporarily disabled during the Zig 0.16 migration; see note below)
  • Wasm FFI — call WebAssembly modules from Clojure (523 opcodes including SIMD + GC)
  • Dual backend — bytecode VM (default) + TreeWalk interpreter (reference)
  • deps.edn compatible — Clojure CLI subset (-A/-M/-X/-P, git deps, local deps)
  • 1130+ vars across 30+ namespaces (651/706 clojure.core)

Getting Started

Prerequisites

  • Zig 0.16.0 (or nix develop for a pinned environment)

Build

zig build                     # Debug build
zig build -Doptimize=ReleaseSafe  # Optimized build

Run

./zig-out/bin/cljw -e '(println "Hello, world!")'   # Evaluate expression
./zig-out/bin/cljw script.clj                        # Run a file
./zig-out/bin/cljw                                   # Interactive REPL

deps.edn Projects

# Download dependencies (git deps require explicit -P)
./zig-out/bin/cljw -P

# Run with aliases
./zig-out/bin/cljw -M:run                # Main opts
./zig-out/bin/cljw -X:build              # Exec function
./zig-out/bin/cljw -A:dev src/app.clj    # Extra paths
./zig-out/bin/cljw -Spath                # Show classpath

# Run tests
./zig-out/bin/cljw test                  # Auto-discover test/
./zig-out/bin/cljw test -A:test          # With alias

Supports :local/root, :git/url+:git/sha, :deps/root, transitive deps. No Maven/Clojars support (git deps and local deps only).

Build a Standalone Binary

./zig-out/bin/cljw build app.clj -o myapp
./myapp                         # Runs without cljw

Note (Zig 0.16 migration): cljw build, the nREPL server, and the built-in HTTP server/client are temporarily disabled while their backing stdlib APIs (std.fs.selfExePath, std.net.Server, std.http.Client) migrate to the new std.Io model. Each prints a clear runtime error when invoked. Tracked in .dev/checklist.md F140-F144 (target: next minor release after this one). Use zig build && cljw <file.clj> to run apps in the meantime.

nREPL / CIDER

./zig-out/bin/cljw --nrepl-server --port=7888 app.clj

Connect from Emacs CIDER or any nREPL client. 14 ops supported (eval, complete, info, stacktrace, eldoc, etc.). (See the build note above — temporarily unavailable while the std.net migration lands.)

Features

Namespaces

Each namespace targets behavioral equivalence with its Clojure JVM counterpart. Known divergences are documented in DIFFERENCES.md.

Core Language

NamespaceVarsDescription
clojure.core651/706Core language functions
clojure.core.protocols10/11CollReduce, IKVReduce, Datafiable
clojure.core.reducers22/22Parallel fold, monoid, reducers

Standard Library

NamespaceVarsDescription
clojure.string21/21String manipulation
clojure.math45/45Math functions
clojure.set12/12Set operations
clojure.walk10/10Tree walking
clojure.zip28/28Zipper data structure
clojure.data5/5Data diff
clojure.edn2/2EDN reader
clojure.template2/2Code templates
clojure.xml7/9XML parsing (pure Clojure)
clojure.datafy2/2datafy/nav protocols
clojure.instant3/5#inst reader, RFC3339 parser
clojure.uuid#uuid data reader (reader only)

Spec

NamespaceVarsDescription
clojure.spec.alpha87/87Spec validation, s/def, s/valid?
clojure.spec.gen.alpha54/54Spec generators
clojure.core.specs.alpha1/1Spec for core macros

Dev & Test

NamespaceVarsDescription
clojure.test38/39Test framework
clojure.test.tap7/7TAP output formatter
clojure.repl11/13doc, dir, apropos, source, pst
clojure.pprint26/26Pretty printing, print-table
clojure.stacktrace6/6Stack trace utilities
clojure.main16/20REPL, script loading, ex-triage

IO & System

NamespaceVarsDescription
clojure.java.io19/19File I/O (Zig-native)
clojure.java.shell5/5Shell commands (sh)
clojure.java.browse2/2Open URL in browser
clojure.java.process5/9Process API (Clojure 1.12)

Infrastructure (stubs — requireable, API surface for compatibility)

NamespaceVarsDescription
clojure.core.server7/11Socket REPL, prepl (partial)
clojure.repl.deps3/3Dynamic lib addition (stub)

ClojureWasm Extensions

NamespaceVarsDescription
cljw.wasm17/17WebAssembly FFI
cljw.http6/6HTTP server/client

Not implemented (JVM-only): clojure.reflect, clojure.inspector, clojure.java.javadoc, clojure.test.junit

Wasm FFI

Call WebAssembly modules directly from Clojure:

(require '[cljw.wasm :as wasm])

(def mod (wasm/load "add.wasm"))
(def add (wasm/fn mod "add" [:i32 :i32] :i32))
(add 1 2)  ;=> 3
  • 523 opcodes (236 core + 256 SIMD + 31 GC)
  • All Wasm 3.0 proposals (9/9 including GC, function references, exception handling)
  • WASI support (file I/O, clock, random, args, environ)
  • Multi-module linking with cross-module imports
  • Predecoded IR with superinstructions for optimized dispatch

Performance note: The Wasm runtime (zwasm) uses Register IR with ARM64/x86_64 JIT. Full Wasm 3.0 support (all 9 proposals including GC, function references, SIMD, exception handling). zwasm wins 14/23 benchmarks vs wasmtime, with ~43x smaller binary.

Server & Networking

(require '[cljw.http :as http])

(defn handler [req]
  (case (:uri req)
    "/hello" {:status 200 :body "Hello!"}
    {:status 404 :body "Not Found"}))

(http/run-server handler {:port 8080})
  • Ring-compatible handler model
  • HTTP client: http/get, http/post, http/put, http/delete
  • nREPL in built binaries (./myapp --nrepl 7888)
  • SIGINT/SIGTERM graceful shutdown with hooks

Temporarily disabled during the Zig 0.16 migration — see the build note earlier in this README. Tracked as F140/F141/F142 in .dev/checklist.md.

Internals

  • NaN-boxed Value — 8-byte tagged representation (float pass-through, i48 integer, 40-bit heap pointer)
  • MarkSweep GC — allocation tracking, free-pool recycling, safe points
  • Bytecode VM — 75 opcodes, superinstructions, fused branch ops
  • ARM64 JIT — hot integer loop detection with native code generation
  • Bootstrap cache — core.clj pre-compiled at build time (~5ms restore)
  • deps.edn projects — Clojure CLI compatible config (git deps, local deps, aliases)

Project Structure

src/
├── main.zig                    CLI entry point
├── root.zig                    Library root
├── clj/clojure/                Clojure source files
│   ├── core.clj                Core library (~2400 lines)
│   └── string.clj, set.clj... Standard library namespaces

├── runtime/                    Layer 0: Value, collections, GC, environment
├── engine/                     Layer 1: Reader, Analyzer, Compiler, VM, TreeWalk
│   ├── reader/                   Source → Form
│   ├── analyzer/                 Form → Node
│   ├── compiler/                 Node → Bytecode
│   ├── vm/                       Bytecode execution (+ ARM64 JIT)
│   └── evaluator/                TreeWalk interpreter
├── lang/                       Layer 2: Built-in functions, interop, lib namespaces
├── app/                        Layer 3: CLI, REPL, deps.edn, Wasm bridge
└── regex/                      Regex engine

bench/                          31 benchmarks, multi-language
test/                           83 Clojure test namespaces (54 upstream ports)

Strict 4-zone layered architecture: lower layers never import from higher layers. Zone dependencies enforced by CI gate (0 violations).

The .dev/ directory contains design decisions, optimization logs, and development notes.

Benchmarks

The benchmark suite is in bench/ with 31 programs covering computation, collections, higher-order functions, GC pressure, and Wasm.

# Requires hyperfine
bash bench/run_bench.sh                  # All benchmarks (ReleaseSafe)
bash bench/run_bench.sh --quick          # Quick check (1 run)
bash bench/run_bench.sh --bench=fib_recursive  # Single benchmark

Testing

zig build test                  # 1,300+ Zig test blocks
bash test/e2e/run_e2e.sh       # End-to-end tests (6 wasm)
bash test/e2e/deps/run_deps_e2e.sh  # deps.edn E2E tests (14)

83 Clojure test namespaces including 54 upstream test ports with 600+ deftests. All tests verified on both VM and TreeWalk backends.

Future Plans

  • JIT expansion — float operations, function calls, broader loop patterns
  • Generational GC — nursery/tenured generations for throughput
  • Persistent data structures — RRB-Tree for vectors (HAMT for maps: done)
  • wasm_rt — compile Clojure to run inside WebAssembly

Potential Use Cases

Once production-ready, ClojureWasm could enable workloads where the JVM is too heavy:

  • Serverless functions — ~4MB image + ~4ms cold start for AWS Lambda or Fly.io, eliminating JVM warm-up penalties
  • Wasm plugin host — embed user-supplied .wasm modules as extensibility points (e.g., Cloudflare Workers-style logic, game scripting)
  • Edge / IoT — run Clojure on Raspberry Pi or resource-constrained devices where a JVM runtime is impractical

Acknowledgments

Built on Clojure by Rich Hickey and Zig by Andrew Kelley. Includes adapted Clojure standard library code and ported test cases (EPL-1.0). See NOTICE for attribution details.

License

Eclipse Public License 1.0 (EPL-1.0)

Copyright (c) 2026 chaploud

Support

Developed in spare time alongside a day job. If you'd like to support continued development, sponsorship is welcome via GitHub Sponsors.