All notable changes to ClojureWasm are documented here. The format follows
Keep a Changelog; versioning follows
SemVer. SemVer compatibility guarantees start at the
first stable 1.0.0 tag; pre-1.0 alpha / rc tags may still change surfaces.
The last release. v1.10.0 was meant to be it; this one exists because two
things about the project's surroundings changed in the days after, and leaving
them only on main would mean the final tag documented a world that no longer
exists. No runtime behaviour changes — cljw itself is byte-for-byte the same
program.
- The embedded Wasm engine is fetched from
zwasm/zwasm. zwasm moved out of theclojurewasmorg into its own, where it continues under separate maintainership.build.zig.zonnow names that location directly instead of relying on GitHub's transfer redirect, which is only an alias until something else claims the vacated name. The pin itself is unchanged — same tag (v2.5.0), same commit, same content hash, verified by fetching both URLs. - The two hosted demos are shut down and their repositories archived.
cw-playgroundandcw-serverless-demoran on Fly until August 2026. The README anddocs/works/demos.mdnow describe them in the past tense and point at the source rather than at URLs that no longer answer. The source still builds and remains the most complete worked example of an end-to-endcljwdeployment. - CI runs one configuration everywhere. Pull requests, pushes and manual
dispatches previously ran three different suites, all called "the gate", so
"green" meant three different things depending on where it was said. They now
all run the same
test/run_all.sh --serial-e2ethat a localscripts/run_gate.shruns, andcheck_gate_parity.shfails if a tier comes back. The nightly schedule is retired with it (ADR-0185).
- The nREPL end-to-end tests leaked a server process each.
( cd DIR && cmd ) &cannot be exec-optimised by bash, so$!was the subshell rather than the runtime, andkill $!reaped the shell while orphaning itscljwchild. Each survivor held its memory for the rest of the run, and the accumulated pressure was enough for macOS to SIGKILL an unrelated, freshly-built binary — a leak wearing the costume of a runtime crash. All four nREPL suites now reap by a port-scoped pattern and end with zero surviving processes. - The entry-point parity test aborted instead of reporting. Its whole job is
to notice when one entry point behaves differently from the others, but under
set -ea killed child's exit status propagated through command substitution and took the step down with no diagnosis. It now captures status and output per entry point, so a dying entry point is reported as a named divergence.
The final release. ClojureWasm is no longer maintained — a from-scratch Clojure runtime turned out to be more than one person can sustain, and this release exists so it stops at a working, fully-tested state rather than mid-stream. The code stays up under EPL-2.0; fork it and build on it, no permission needed. See the README for the full notice.
Everything here is a fix, and three of the four were found by @BuddhiLW,
who reported them with diagnoses and working branches — thank you. Each
one's class was then swept, which is where the rest of the fixes came
from. MINOR rather than PATCH because repl and nrepl gain a -cp /
-A:alias surface, and cljw repl now rejects unknown arguments it used
to ignore.
-
A top-level
(do …)that requires before it uses now works in every entry point.(do (require '[clojure.string :as s]) (s/upper-case "ok"))returned"OK"fromcljw -ebut raisedNo namespace: 's'over nREPL and incljw repl, and failed to compile undercljw build: the top-level-dounroll, which analyses and evaluates each child in sequence so an earlierrequireordefmacrois visible to a later child, had only ever been wired into the script path. Reported, diagnosed and fixed for the REPL engine by @BuddhiLW (Discussion #14; commit cherry-picked with authorship preserved); thecljw buildhalf was found by auditing the other callers. A new differential oracle (test/e2e/entrypoint_eval_parity.sh) now runs each program through-e, a script file, stdin,repl,nrepl, andbuild+run and requires all six to agree, so this class of entry-point divergence fails the gate instead of reaching users. -
A set literal of 9+ elements no longer crashes macroexpansion (segfault / index-out-of-bounds / bogus "cannot be re-analysed" — three faces of one misread). The analyzer decoded the set's backing map as if it were always array-backed; past the 8-element promotion boundary it read hash-map fields as array fields. Reported with a model bisection and fixed by @BuddhiLW (Discussion #12; commit cherry-picked with authorship preserved).
-
The same representation misread was audited tree-wide and three more live instances fixed, all at the same 8→9 boundary: multimethod hierarchy (
derive/isa?) dispatch silently vanished at the 9thdefmethod;cljw.http.clientrejected a 9-entry:headersmap;cljw.http.serverdropped all custom response headers past 8 entries. A structured-error-log context map of 9+ entries was also silently omitted from the EDN event. A new per-commit gate (scripts/check_repr_decode.sh) forbids the whole class: collection backing representations are now private tosrc/runtime/collection/. -
cljw nreplresolves the filesystem classpath — it previously rejected-cp, ignored$CLJW_PATH, and never installed the filesystem require chain, so an editor session could notrequirea single namespace from the project it was started in. It now takes[-cp <dirs>] [-A:alias…]alongside--port, exactly like every other entry point. Diagnosed and fixed by @BuddhiLW (Discussion #13; commit cherry-picked with authorship preserved). -
cljw replno longer silently ignores trailing arguments — it takes the same[-cp <dirs>] [-A:alias…]surface and rejects unknown args (cljw repl -cp srcused to drop the-cpwithout a diagnostic). A new per-commit gate (scripts/check_entrypoint_surface.sh) requires every CLI entry point to be classified and reach the shared classpath resolution, so a future subcommand cannot repeat the nREPL omission.
- The embedded Wasm runtime is pinned to zwasm v2.5.0 (was v2.4.1),
which brings full WASI 0.3 coverage and completes
libzwasm.a's C-API exports. Neither reaches cljw's embedding surface — cljw drives Engine/Module/Instance through Zig on the JIT-default.autoengine — so this lands the final release on zwasm's current stable rather than changing behaviour. zwasm continues under separate maintainership.
Upgrade if you run untrusted code under cljw.eval/with-budget, parse
untrusted JSON/EDN, use long-running programs (the GC got a lot faster on
them), or want named groups / lookbehind / \A \z \Z / class algebra in
regexes. MINOR rather than PATCH because behaviour changes: code that
relied on a blocking call outliving its budget now raises.
- The eval budget's wall-clock deadline now bounds blocking calls — all of
them.
(Thread/sleep 25000)under a 3 s deadline used to return normally after 25 s;@(promise)under a deadline used to park forever. Every blocking primitive (future/promisederef, timed deref,Thread/sleep,await) now waits on a latch bounded bymin(caller timeout, budget deadline). - The budget follows the work, not the thread. A
Thread.,futureor agent action spawned inside awith-budgetextent inherits the budget — same absolute deadline, same shared (atomic) step ceiling — and keeps it even if it outlives the extent. A spawned 60 s sleep under a 500 ms deadline used to hold the process for the full 60 s; it now dies at the deadline. A compute loop cannot escape the step ceiling by moving onto a spawned thread. - A rejection caused by bad DATA is now catchable. Malformed JSON/EDN
input, wrong-typed arguments (
clojure.string/replace,deliver, walk callbacks,*out*bindings), malformed form shapes undereval/read-string(libspecs, destructuring,nsdirectives, reader conditionals) and malformed regex patterns all raise catchable errors, as JVM Clojure does — ~60 sites reclassified, each verified against the oracle. Genuinely unsupported features stay deliberately uncatchable. Messages improve with it: "deliver: expected a promise, got integer" instead of "… is not supported in ClojureWasm".
- Regex: named groups
(?<name>e)(with(.group m "name")), lookbehind(?<=)/(?<!),\A\z\Zanchors, nested character-class unions[a[b]]and&&intersection ([a-z&&[^m-p]]), and empty alternatives/groups ((),a|). Backreferences are declined by design — the matcher guarantees linear-time matching (the RE2 posture), and a backreference would force backtracking. - Multi-dimensional
aget/asetand the 8 typedaset-*multidim arms:(aget a i j),(aset a i j v)on nested arrays, exactly clj's variadic recursion.
- Long-running programs stopped paying a 2-4x GC tax. The collection trigger is steered by the measured GC time share (the same control variable SubstrateVM sizes its heap with), and live-set accounting now counts what an object owns (string payloads, array elements, bignum limbs) instead of only its 32-byte record. A breadth-first search went 58.9 s → 17.7 s (1624 → 114 collections); a 200k-string workload went 263 → 55 collections. Short scripts and benchmarks are unchanged.
@(future (Thread/sleep 2000))ran 17% long (2321-2358 ms) with no budget armed at all — the cancel poll sliced every worker sleep. Waits are latch-based now: 2000-2001 ms, andfuture-cancelwakes a sleeping worker in 0 ms instead of up to 20 ms.(Thread/sleep <huge long>)aborted the process with an integer overflow; it now sleeps, as clj does.
Upgrade if you use Wasm components, or if you use doc / find-doc.
A component with more than one export could not be loaded at all, and doc on
most of clojure.core printed nothing.
clojure.core.reducers. The full sequential surface —r/map,r/filter,r/remove,r/mapcat,r/flatten,r/take,r/take-while,r/drop,r/reduce,r/fold,r/foldcat,r/cat,r/monoid,r/append!— with the curried one-arity forms. 31 of 32 probed expressions are byte-identical to JVM Clojure; the one difference is set print order.foldcomputes the same value as upstream's but never in parallel: there is no ForkJoinPool here, so it takes the same single-reduce arm upstream itself takes for lists, ranges and sets. Its docstring says so.wasm/runtakes:max-output-bytesand:timeout-ms, alongside the existing:fueland:max-memory-pages. Both default to finite values (16 MiB per captured stream, 60 s);0or a negative value opts out.
- A Wasm component with two or more exports failed to load. Every fixture
in this project and in zwasm exported exactly one function, so
(:require ["x.wasm" :as x])— the headline feature — had only ever been exercised on single-function components. Fixed in zwasm v2.4.1, which this release pins. wasm/runcould be made to exhaust host memory. It buffered the guest's stdout and stderr without limit, and the fuel budget does not bound that: fuel counts instructions, and how many bytes an instruction writes is the guest's choice. Measured, a guest could force ~64 GB before its fuel ran out. Also had no wall-clock bound — the same guest could run for roughly 18 hours inside the default fuel budget.- A resource handle from a one-shot
wasm/component-invokewas already invalid when you received it, because the component was torn down before the call returned. Handle numbers are per-instance table indices starting at 1, so passing it on could silently operate on a different resource. The component now lives as long as the handle does, and a handle is bound to the component that minted it. resultandvariantcould not be passed INTO a component. They lifted out correctly, so the round-trip the mapping table describes was one-way.(doc reduce)printed nothing — and so didassoc,conj,first,apply,=and 320 others. The generated metadata table covered 246 of 685clojure.corepublics and none of the ~290 implemented as native primitives, sofind-docandaproposwere searching a third of core while answering as though they had searched all of it. Now 572 of 684.(clojure.set/intersection)returnednilwhere Clojure throwsArityException, and no var inclojure.setreported:arglists.
clojure.pprintno longer publishes 49 internal formatter helpers (cl-tens,cl-roman,cl-under-1000, …). Upstream keeps them private; they were public here only because they were written asdefn.cl-formatand the other six documented names are unaffected. Same for fiveclojure.datainternals and three contrib parser implementation details.clojure.mathgained docstrings, written for this runtime rather than imported — upstream's all end in ajava.lang.MathJavadoc URL.
Upgrade if you load Wasm components. An untrusted component could kill the
host process, and result errors were reaching you as generic Wasm traps with
the error value discarded.
- An untrusted Wasm component could kill the host process. The component
marshaller used bare integer casts, which in the shipped ReleaseSafe build
are process-killing safety panics rather than catchable errors. Two paths
were reachable: lowering an out-of-range argument (
(c/f 300)against a WITu8), and — worse, because the data is the guest's — lifting au64abovei64's maximum. Both now go through the same range check the core-modulewasm/callsurface has always used. Au64beyondi64and ans64beyond 48 bits now lift exactly via BigInt instead of panicking or silently becoming a lossy float.
resultandvariantlift as tagged vectors.resultis now[:ok v]/[:err e]andvariantis[:case-name payload], with the payload slot omitted when the arm carries none — one destructuring idiom for every WIT sum type. This replaces anerr→throw mapping that discarded the error value: the old path raised a generic Wasm trap, so a component returningerr("not found")reached you as "WebAssembly module trapped" with the message gone. Throwing also had no meaning off the return position — aresultnested in alistorrecordaborted the whole conversion. (ADR-0135 amendment 2.)
clojure.core/*file*— the source being evaluated, matching JVM Clojure: the script path when a file runs, and"NO_SOURCE_PATH"otherwise. Still^:dynamic, so tooling can rebind it.
Upgrade if you use threads, futures, promises, or wasm/run. This
release carries four fixes for ways cljw could hang or abort, two of them
outstanding since 1.5.1 (2026-07-17).
- A live worker at process exit could abort (glibc
abort). Afuture/promisewhose worker was still running while the runtime tore down could crash the process instead of exiting.exitBarrierplus deinit-chokepoint guards make a live worker force a daemon-style hard exit rather than a teardown under its own feet. Measured: 6 failures in 32 runs before, 0 in 96 after. (ADR-0176, AD-056) - Thread registration was not a GC safepoint (SIGABRT on x86_64 Linux).
The window between spawning a worker and registering it with the collector
was not a safepoint, so a collection landing in that window could abort.
Registration is now a safepoint, and the
TooManyThreadsrun-unregistered fallthrough is closed. (ADR-0175) (wasm/run …)ran guests unmetered — an infinite loop in a guest hung the host forever.wasm/loadhas always defaulted to a finite fuel and memory budget;wasm/runpassed no budget at all, so the two sibling surfaces had opposite postures.wasm/runnow takes the same finite default. Measured: a spinning guest ran past 15s before, trapsout_of_fuelin about 1s after.cljw.http.servercould be wedged by one unfinished request. The accept loop had no read deadline, so a client that opened a connection and never completed the request head held the whole server — which binds 0.0.0.0 — indefinitely. A request-head deadline now bounds it. Measured: a half-open connection blocked the next client for 12s before, 1s after. (Note: a serial server still handles one connection at a time; this bounds the wedge, it does not add concurrency.)- Namespace names with an empty segment resolved.
(require (symbol "..a.b"))could loada.bbecause the munge collapsed the doubled separator. JVM Clojure refuses such names; so does cljw now. These names cannot be written through the reader, so only computed namespace names are affected.
run-serveraccepts:header-timeout-ms— how long a connection may take to deliver its request head. Default 10000;0disables the deadline.wasm/runaccepts:fueland:max-memory-pages— the same budget keyswasm/loadalready took.0or negative means unmetered, which is how you opt a trusted command out of the new default.
- zwasm pinned to v2.4.0 (its external-consumer release:
-Dcompiler-rtfor linkinglibzwasm.afrom a non-Zig toolchain, and a DCE fix that keeps the WasmGC cohort out of sub-3.0 builds). Neither reaches cljw — cljw links zwasm through Zig, which supplies its own compiler-rt, and it builds at zwasm's default Wasm 3.0 level where the DCE guard is inert. Pin hygiene, not a behaviour follow;-Dwasmbuild + the phase16 wasm e2e suite verified green on the new pin. - Issues and Pull Requests are open. Reports of Clojure code that behaves differently here than on the JVM are the most useful thing you can file; there is an issue template for exactly that. Outside contributors are explicitly not asked to follow the internal commit / ledger conventions — see CONTRIBUTING.
Two behaviour changes can surface in working code:
(wasm/run …)is now fuel-bounded by default. A legitimately long-running guest that exceeds roughly 1e9 instructions will trap; pass{:fuel 0}to restore the previous unbounded behaviour.(require (symbol s))with a computedscontaining a leading, trailing, or doubled dot no longer resolves. Well-formed names are unaffected.
Patch release: dependency follow.
- zwasm pinned to v2.3.0 (the WASI-0.3.0-official inventory-sweep
release: docs truth-sweep against the officially released WASI 0.3.0,
wasi:clocks/system-clockcomponent-host support, Homebrew packaging). No embedding-API, behaviour, or JIT-output change — a pure engine follow; cljw's Wasm FFI surface is unchanged.
Minor release: the host-class identity & member-surface campaign (ADR-0174) — class symbols, class identity, and the Java member surface become uniformly resolvable, precisely diagnosed, and machine-audited; plus the user-requested Thread lifecycle.
- Class symbols resolve as values everywhere: bare
System/Math/Thread/StringBuilder(java.lang auto-import), fully-qualifiedjava.util.Date/java.time.Instant/ …, and(:import …)ed names — in value position,instance?,resolve,group-by class, and across AOT round-trips.(= java.util.Date (class d))is identity on ONE canonical descriptor per class. Classis first-class:(class Long)→Class(was a raw internal tag leak),(instance? Class (class 5))→ true.- Thread lifecycle (user-requested):
(Thread. f)/(Thread. f name),.start,.join(+ ms),.isAlive,.getName/.setName,.setDaemon/.isDaemon,Thread/yield,Thread/onSpinWait, priority constants. JVM-faithful non-daemon default: main waits for live non-daemon threads at exit;Thread/currentThreadreturns the real per-thread object. Uncaught thunk errors print the JVM-style "Exception in thread" stderr line. - System close-out:
getProperties(map), 0-arggetenv(env map),clearProperty,identityHashCode,gc, and real stdio streamsSystem/in/System/out/System/err(PrintStream-classed;(.println System/err "…")works;System/inreads stdin incrementally;(instance? java.io.OutputStream System/out)→ true). - Constants sweep:
Math/TAU;Long/Integer/DoubleBYTES/SIZE/MIN_NORMAL;Fileseparator/pathSeparator (+Char) +createTempFile/listRoots;Pattern's 9 flag constants + 2-argPattern/compile(CASE_INSENSITIVE/MULTILINE/DOTALL/COMMENTS);BigDecimalZERO/ONE/TWO/TEN;Instant/EPOCH,Duration/ZERO,LocalTimeMIDNIGHT/NOON/MIN/MAX,LocalDate/LocalDateTimeMIN/MAX/EPOCH; host-enumvalues/valueOf(+Month/of,DayOfWeek/of). - java.time fill:
Duration/parse(full ISO-8601 grammar) +Duration/of,LocalDate/ofEpochDay,LocalTime/ofSecondOfDay+ofNanoOfDay. scripts/check_compat_members.sh— the member-truth gate: compat_tiers member lists are now machine-verified against the registered descriptors both ways (over-claims and silent omissions both fail); deliberately-skipped members are explicitopaque_members:rows.
- Host-class names are their JVM FQCNs:
(class (java.util.Date. 0))→java.util.Date(wasDate), StringBuilder/Thread etc. no longer leak the internalcljw.prefix. cljw-native types, user records, and exceptions keep simple names (AD-003, clarified). - Member misses are precise diagnostics:
(System/getProperties)on a class without that member now says "No matching static method: … in class java.lang.System" instead of the misleading "No namespace: 'System'" — every deliberately-skipped Java member renders this. - fix(time): pre-year-0 civil date conversion was off by one era
(
(.getYear (java.time.LocalDate/of -1 12 31))was wrong); negative / 5-digit years now print in JVM sign form.
cljw buildartifacts from ≤ 1.4.0 (envelope ≤ v7) are rejected by a 1.5.0 runtime — rebuild with the newcljw build(baked class constants changed spelling with the FQCN unification).(class …)output for Java-surface-backed values changed spelling to the JVM FQCN (see Changed) — string-matching on the old simple names ("Date","Instant") needs updating;instance?/=code is unaffected (it got strictly more capable).
Minor release: the binary-size campaign — the shipped binary shrinks 9,469,816 → 6,974,584 bytes (−26.3%, now under 7 MB) with no feature loss and no safety-check loss (still ReleaseSafe), plus the envelope format v7 it rides on.
- Binary size: 9.5 MB → 6.97 MB (macOS arm64; Linux similar). The
levers, all measured: unwind tables dropped from release builds
(−739 KB — a stripped binary printed no native trace anyway); the
AOT bootstrap gains an interned-name constant pool (42% of the blob
was duplicate encoding) and flate-compressed lazy namespace regions +
.cljsources (decompressed on demand); the embedded zwasm engine updated to v2.2.1 (its JIT host-callback thunk collapse, −1.08 MB as seen from cljw); shared sort instantiation. Full ledger:.dev/decisions/0172_binary_size_budget_and_ledger.md, public comparison:docs/works/binary_size.md. - Bytecode envelope format v7 (
.dev/decisions/0173_*.md): 4-byte aligned wire instructions readable in place from the binary, per-blob constant pool, chunksource_file/has_handlersnow serialized (AOT-restored fns can frame-flatten; error frames in bundled namespaces keep their real file labels), headerless nested fn chunks. Startup is unchanged within noise; RSS slightly lower. - zwasm pinned to v2.2.1 (binary-size campaign release; no API or JIT-output change).
- Cross-language benchmark records re-measured 2026-07-16
(
bench/cross-lang-latest.yaml,bench/RELEASE_METRICS.md: cold start ~6 ms on the shipped-Dwasmbuild).
cljw buildartifacts made by 1.3.x (envelope ≤ v6) are rejected by a 1.4.0 runtime with a version error — rebuild the app with the newcljw build. (Pre-2.0 format policy: the on-disk spec is archived per version indocs/spec/formats/; decoders are not kept.)
scripts/binary_size_report.sh— size report / attribution tool + thesize_claimsgate (README size claim and the budget ceiling are now CI-enforced against the built binary).- Guard e2e for VM error line:col fidelity across the new instruction
encoding (
phase16_vm_error_loc_sidecar).
Patch release: Clojure 1.12 method values ((every? Character/isWhitespace s) — the brew-user report), and defrecords gain their mainline
ns-qualified identity (#user.Pt{…} printing, reader round-trip, portable
record hash).
- Method values (Clojure 1.12, static form) — a bare
Class/methodin value position is the static method as a first-class fn:(every? Character/isWhitespace s),(map Character/toUpperCase "ab"),(sort-by Math/abs …)all work as in mainline. (The 1.12Class/.instanceMethod/Class/newforms are tracked for a future release.) - Record literals read back —
#user.Pt{:x 1, :y 2}constructs the record through the reader (mainline parity; extra keys land in the extmap), so the printed form round-trips. scripts/nrepl_send.py— a dependency-free nREPL debug client (eval / describe / completions against a running server).
- Records print ns-qualified —
#user.Pt{:x 1, :y 2}(was#Pt{…}), matching mainline and making the print form readable. - Record hash values are portable —
(hash (->Pt 1 2))returns real Clojure's value (the qualified-name type-hash XOR the map hash), completing the 1.3.0 portable-hash work for records.
Minor release: mainline-parity deep-dive — the rt namespace is gone
((resolve '+) is #'clojure.core/+), CIDER completion reaches the
built-in nREPL completion's fidelity (classes and Class/ static members
complete without require), java.lang.Character is complete, and
(hash x) values are now portable Clojure hash values.
java.lang.Charactercomplete — the full static surface (47 methods- 70 static fields incl. the category/directionality constants), with
classification (
isLetter,isDigit,getType, …) evaluated over generated UCD 16.0.0 tables — full Unicode, matching the JVM (previously ASCII-only). Includes the JDK 21isEmoji*family,getDirectionality, the char/int overload pairs ((Character/toUpperCase 97)→65), and.charValue/.compareToinstance methods. The one member out:getName(explicit unsupported — the Unicode name table's size/value trade is tracked).
- 70 static fields incl. the category/directionality constants), with
classification (
- CIDER completion parity — nREPL
completionsnow serves every source the built-in serves: special forms + literals, vars with dash-fuzzy matching (ma-i→map-indexed), namespaces/aliases, classes,Class/static members with camelCase matching (Character/isD→isDigit), and interned keywords — by-name sorted, with(clojure.core)-correct namespace annotations. Java-interop completion works withoutrequire, from the closed class registry (no JVM-internal classpath leak). defmacroresolves as a Var (#'clojure.core/defmacro) anddefinlineis available (defn-equivalent surface).- nREPL
describeadvertisesversions.clojurewasm(the babashka-style key a CIDER REPL banner can render).
- The internal
rtnamespace is GONE — Zig builtins and bootstrap macros intern directly intoclojure.core, so(resolve '+),(meta #'when),(ns-publics 'clojure.core), doc/eldoc/completion namespaces, and printed values all match mainline. Kernel helpers live in the documentedcljw.internalnamespace. Compiled.cljwcarchives from earlier versions need a rebuild (format v6). - Portable hash values —
(hash x)now returns real Clojure's value for strings, keywords, symbols, doubles, booleans, chars, UUIDs, ratios, BigDecimals, and every collection built from them ((hash "abc")→74834163everywhere). Records and identity objects remain cljw-specific. comparereturns mainline's magnitudes for strings / chars / keywords / symbols ((compare "a" "c")→-2; JavaString.compareTosemantics).(locking 5 …)works (immediates share one monitor;(locking nil …)errors, matching mainline's NPE).Double/parseDoublematches the exact Java grammar — accepts1.5d/1.5Fsuffixes and0x1.8p1hex floats; rejects lowercaseinfinity/nanand exponent-less hex.
(read-string "@x")prints as(clojure.core/deref x)(was the internalrt/deref);clojure.spec.alpha/formshowsclojure.core/int?(wasrt/int?); syntax-quote and macroexpansion namespaces match mainline throughout.
Patch release: the *cider-error* buffer works — CIDER renders numbered
causes with stack frames, phase-aware routing, and Show/Hide frame filters
against cljw, instead of "[no stack trace available]".
- nREPL
cider/analyze-last-stacktraceop (+ the bare alias) — analyzes the session's*eon demand: one cause perex-causechain link with class / message / printed ex-data / error phase / stack frames (innermost-first, withclj/repl/project/dupflags and navigablefile-urlfor on-disk frames). CIDER's rich error buffer and its compile-phase inline overlays activate automatically.
*eis now set for every caught REPL error — Clojure parity: compiler-class errors (an unresolved symbol, a syntax error) previously left*euntouched; now(ex-message *e)works after any error, in both the CLI REPL and nREPL sessions.(throw (ex-info …))carries a stack trace — the live call stack is stamped on the thrown value, so user throws render frames like runtime errors (both backends).- nREPL use-after-free — the 1.2.0 re-architecture fed the per-message scratch-owned request source into evals whose products outlive the message; long editor sessions could see corrupted strings. The source is now copied to the persistent arena.
Minor release: the nREPL server is rebuilt to full base-protocol fidelity — CIDER (and any nREPL editor client) now works end-to-end. Backward-compatible with 1.1.x.
- nREPL
completions/completeops — editor completion (CIDER company/capf) over the live image: vars, macros, namespaces, aliases,ns/varqualified prefixes, with type annotations. - nREPL
lookup/info/eldocops — arglists + docstrings from var metadata; CIDER eldoc andC-c C-ddocumentation work. *1/*2/*3/*eREPL history — interned inclojure.core(upstream shape) and rotated by both the CLI REPL and every nREPL session (per-session isolation: tooling-session evals cannot touch your*1).- nREPL
nsrequest handling — evals honor the request namespace (namespace-not-foundper spec); each session keeps its own current namespace, so anin-nsin one session never leaks into another.
- CIDER REPL buffer was unusable — the read loop blocked with complete requests already buffered, stranding pipelined messages off-by-one; the REPL prompt never rendered and RET did nothing. The transport now drains every buffered message before blocking.
- Messages over 4 KiB reset the connection —
C-c C-k(load-file) on any real file killed the session. The receive buffer now grows (16 MiB frame cap). - Session identity — every
clonereturned the same id and replies ignored the request'ssession; CIDER's two-session model (main + tooling) depends on both. Sessions are now distinct UUIDs and every reply echoes the request'ssession+id. - Error replies — errors now carry the same caret-rendered text the CLI
prints (was: a bare Zig error name like
NameError), as the babashka-style three-message protocol (err→ex/root-ex+eval-error→done; was: bundled in one dict, which nREPL clients mis-route, plus a doubledone). Evaluation stops at the first failing form (JVM parity). *err*output is captured and streamed to the client alongside*out*.describenow derives its ops list from the dispatch table (they can no longer drift) and reports the real version (was: a stale hardcoded0.1.0-pre).
Minor release: new REPL / tooling surface and Java-interop additions, a batch of GC-correctness fixes, and a WebAssembly engine bump. Backward-compatible with 1.0.x.
clojure.replbundled —doc,find-doc,apropos,dir,demunge, plus a bare(doc x)at the interactive prompt (clojure.main parity).:arglists/:docmetadata onclojure.coreand the eager standard library vars — CIDER eldoc now resolves argument lists and docstrings.- Regex lookbehind —
(?<=…)/(?<!…)andPattern.split. formatdate/time — the%t/%Tconversion family (UTC, English).- Namespace metadata —
alter-meta!/reset-meta!on namespaces, and a namespace docstring / attribute map merged into the namespace metadata. - Java-interop surface — common
java.util.Arraysandjava.util.Collectionsstatics,Stringchar[]forms, and JVM-bit-parity Murmur3 hashing. slurp/spitaccept open streams (the remaining IOFactory arms).- WebAssembly FFI on zwasm v2.2.0 — up from v2.0.0 (table64 JIT + AOT full-fidelity); hot loops inside a module keep running as native code.
- GC-correctness batch — several use-after-free / corruption classes under
frequent collection: unrooted analysis-time constants, tree-walk
native-stack intermediates,
recurreentrancy inside lazyfor, and arest-of-chunked-seq self-allocation hole that could corrupt a growing BFS-style queue. All now hold their roots across the collection. - Value semantics — sorted collections work as hash keys / set elements;
hash-map full-hash collision buckets match Clojure;
counton aCharSequencedeftype;(var alias/x)resolves namespace aliases;read-linereads process stdin; exceptionstr/prand*print-readably*shadowing match Clojure.
Patch release: memory-safety fixes found by a post-release audit. No API changes. Users on 1.0.0 should upgrade — the first item is reachable from ordinary code.
- Stack overflow on deep lazy chains — realizing or counting a lazy
sequence / cons chain longer than ~400k elements (e.g.
(count (repeat 1000000 1))) crashed the process: the GC mark phase descended the object graph recursively on the native stack. Marking now uses an explicit gray worklist (O(1) stack for any depth). - Use-after-free family during analysis/compilation — GC values created
while a form is analyzed, compiled, or AOT-deserialized (literal strings,
quoted data, chunk constants, macro-expansion intermediates) were not GC
roots until execution, so a collection mid-analysis (large file loads,
user-macro expansion) could recycle them — surfacing as per-run-varying
Unable to resolve symbol: '<garbage>'errors, index-out-of-bounds panics, or spurious "host-marker method not yet wired" errors when loading bigger libraries (instaparse, math.combinatorics). A per-thread analysis-roots frame now keeps them alive;deftype/reifymethod tables are also traced (their method functions could be collected out from under a live type). - Namespace reflection errors are now catchable —
ns-resolve,ns-map,ns-name,intern,create-ns,alias,find-varand friends raised an uncatchable "not supported" error on a missing namespace or wrong-typed argument; Clojure raises a plain catchable exception there, and libraries rely on that for capability probes ((try (ns-resolve …) (catch Exception e nil))). They now match Clojure's exception classes.
The first stable release. ClojureWasm is a JVM-free Clojure runtime written
in Zig, feature-complete for everyday Clojure with a WebAssembly FFI as its
headline capability. The WebAssembly FFI runs on the embedded zwasm v2.0.0
engine. Earlier pre-releases were tagged 1.0.0-alpha.*. SemVer compatibility
guarantees start here.
- Clojure language core — reader, macros, destructuring, the sequence
library, transducers, protocols, multimethods,
deftype/reify/defrecord, metadata, namespaces with lazy loading, and the numeric tower (long / double / ratio / BigInteger / BigDecimal, JVM-surface semantics). - Standard library —
clojure.coreplusclojure.string/set/walk/zip/edn/data.json/data.csv/math/pprint/test/tools.cliand more, reimplemented for the runtime. - WebAssembly FFI —
(wasm/load "mod.wasm")then(wasm/call m "fn" …): load a sandboxed module compiled from any language (Rust, Go, Zig, C) and call it like an ordinary function. The FFI is JIT-compiled by default via the embedded zwasm v2.0.0 engine, so a hot loop inside a module runs as native code. - WebAssembly components as namespaces —
(:require ["comp.wasm" :as c])pulls a WIT-typed component in like a library; its exports become ordinary Vars, with arguments and results as plain Clojure data. - CIDER-compatible nREPL —
cljw nreplfor live editor-connected eval. - Single-binary builds —
cljw build script.clj -o appcompiles a program and the runtime into one self-contained native executable (arm64 / amd64). - Concurrency — atoms, refs (STM), agents, promises, futures.
- Java-interop surface — a curated, definition-derived subset of common
java.*classes (String / Math / java.time / BigInteger / BigDecimal / Character / …) reimplemented natively; seedata/compat_tiers.yaml.
- Behavioural equivalence with JVM Clojure is the target on the user-observable
surface; intentional divergences are catalogued in
docs/clojure_vs_clojurewasm.md. - Licensed under EPL-2.0; third-party components in
legal/THIRD_PARTY.md.