Changelog
August 22, 2026 · View on GitHub
All notable changes to zwasm are documented here. Format follows Keep a Changelog; versioning will follow SemVer from the first tag.
zwasm v2 is a ground-up redesign of v1. v1 ABI compatibility is out of
scope — see docs/migration_v1_to_v2.md.
SemVer compatibility guarantees start at the first stable v2.0.0 tag.
[Unreleased]
Changed
- Building zwasm no longer fetches anything (#235).
build.zigimported the zlinter dev-dep at the top level, so every step — includingstatic-lib, the one C and Rust consumers build — had to resolve the lint tool first, pulling zlinter, zls, diffz, known_folders and lsp_kit from GitHub. Packaged consumers (thezwasm-syscrate),cargo vendor --offline, sandboxed builders and offline CI all failed on it. The lint wiring moved totools/lint/build.zigwith its own manifest (ADR-0214); the root package now declares no dependencies at all.zig build lintis unchanged in rules, coverage and exit code — it is the only step that still needs the network on a cold package store.
[2.5.0] - 2026-08-11
Added
- Full WASI 0.3 coverage (ADR-0205, opt-in
-Dwasi=p3). zwasm now serves all six WASI 0.3.0 proposals —cli/clocks/random/filesystem/sockets/http— over the Component-Model async substrate. Conformance is the officialwasm32-wasip3corpus, 45/45 green on all three supported OSes (macOS aarch64 / Linux x86_64 / Windows x86_64;@unstable-gated interfaces excluded, matching upstream release gating). Sockets and http run their real data planes: TCP/UDP connect / listen / send / receive / echo,ip-name-lookupreal DNS,wasi:httpfields / request / response / request-options resources, the exportedhandler.handle(service world), andclient.sendoverstd.http.Client. Platform substance behind the claim: on Linux, TCP listen composes a raw SO_REUSEADDR-only bind (the stdlib couples SO_REUSEPORT, whosefastreuseportbind-bucket cache breaks the address-in-use contract); on Windows, the runtime composes its own NT/AFD socket control plane (UNIQUE-share bind, dgram/bound connect, getsockname) and NT hardlinks (FILE_LINK_INFORMATION) where the pinned stdlib has gaps. Adoc-truthCI guard (check_wasi03_coverage_claims.sh) keeps the coverage prose honest.
Fixed
-
"the interpreter is the default engine" was still claimed in five places (#163, reported by @jtakakura). The default has been
auto(prefers the JIT, interpreter fallback) since the D-496 ch6 flip; v2.4.1 correctedzwasm --helpbut notdocs/benchmarks.md(engine table + methodology),.github/ISSUE_TEMPLATE/bug_report.yml(the Execution-mode dropdown, which had no way to say "I did not pass--engine"),.dev/ROADMAP.md§10.2, ordocs/handoff_cw_v2_zig_api.md. The same sweep retired two adjacent stale claims found alongside them: ROADMAP §10.2 still said the JIT was compute-only and rejected--dir(D-244 closed that), and the cljw handoff still listed JIT fuel / memory-cap / table-cap as a gap (they ship on both engines; only the D-314(a) epoch-counter upgrade remains). Published benchmark numbers are unaffected —scripts/run_bench.sh --engines=passes--engineexplicitly for every row.scripts/check_engine_default_claims.shnow gates the claim: it anchors on the CLI usage text and sweeps tracked files for the stale phrasings, and it runs ingate_commit.shand in a new always-on CIdoc-truthjob — doc-only PRs skip the 3-host matrix, which is how a prose claim rotted past three releases in the first place. -
81 declared C-API symbols were missing from
libzwasm.a(#161). Zig only emitsexport fnsymbols from files the analysis pass visits, and foursrc/api/files (ref_base.zig,config.zig,host_info.zig,module_serialize.zig) were absent from thesrc/zwasm.zigcomptime force-analyse block — their lazypub constre-exports throughapi/wasm.zignever trigger analysis. Everywasm_X_copy/wasm_X_same/wasm_X_as_ref/wasm_ref_as_X/wasm_X_{get,set}_host_info/wasm_config_*/wasm_module_serialize-family call declared ininclude/wasm.hfailed at link time.ref_base.zigadditionally did not compile (wasm_extern_copynulled a fieldExterndoes not have) — unnoticed precisely because the file was never analysed; its 6 tests (plus 5 more from the other three files) now run underzig build test. Three guards close the class: commit-timescripts/check_api_export_analysis.sh(everypub export fnfile listed in the comptime block), plus two archive assertions inscripts/test_extlink.sh(everysrc/api/pub export fn, and every installed-header declaration, present inlibzwasm.a). Verified end-to-end from C, Rust, and Go (cgo) consumers over the system linker.
[2.4.1] - 2026-08-04
Consumer-driven patch release. Both fixes were found from ClojureWasm, and both are things zwasm's own fixtures could not have surfaced: every component fixture in this repo exported exactly one function, and no test captured guest output from a guest that wanted to produce a lot of it.
Added
Limits.max_output_bytes/Host.max_capture_bytes— a cap on how many bytes a captured run may buffer. Nothing else bounded this: fuel bounds instructions, and bytes-per-instruction is the guest's choice. Measured from ClojureWasm, a guest looping on a 64-bytefd_writebuffered 64,000,000 bytes for 1,000,000 fuel — ~64 GB under zwasm's own 1e9 default before the fuel trap fires.nullstays the default, so every existing caller is unchanged; a capped buffer keeps the prefix that fits and returns.nospc(#158).
Fixed
A component with two or more exports failed validation. Every component fixture in this repo and in every known downstream exported exactly one function, which is why it survived to a tagged release.
- Component export index-space accounting. A component-level
exportof a func ADDS an entry to the component func index space (Binary.md) — it does not merely name an existing one. Exports were never appended, so from the second export onward everycanon lift's sortidx read out of bounds and the component was rejected withInvalidSort.wasm-tools validate --features allaccepts every case this rejected, i.e. zwasm was refusing spec-valid components rather than being strict..instanceexports had the same omission (#157). - An export could satisfy its own sortidx bound. Once exports began
extending their index spaces, the export bound check read the space
size AFTER the export's own entry, so
(export "a" (instance 0))with no instances validated — the export was the instance it named. The bound is now the space size at the export's definition point, which is what a sortidx referring only to earlier definitions actually means. Caught by the official corpus (types_02).
Found from the consumer side: ClojureWasm could not load a typed 16-export component fixture built to verify its WIT marshalling table. Its headline feature — a Wasm component becoming a Clojure namespace — could only ever load single-function components.
[2.4.0] - 2026-08-03
External-consumer release: static-library linking from non-Zig toolchains now works without a workaround, and sub-3.0 builds shed the GC code that had been leaking into them.
Added
-Dcompiler-rt=trueforzig build static-lib— bundles Zig's compiler-rt intolibzwasm.aso an external non-Zig linker (rustc, gcc, clang) can resolve__zig_probe_stack(x86_64-macos) and the__divti3-class builtins. Same spelling as the v1 option. Default stays off, and the default archive is byte-identical to before. Thanks to @jtakakura for the report and the fix (#153, #154).
Fixed
-
Docs corrected:
docs/migration_v1_to_v2.mdclaimed nocompiler-rtflag was needed because "Zig bundles it into the archive". That is false — Zig's implicit default covers executables and dynamic libraries only, never a static library. The external link line now builds with-Dcompiler-rt=true, andscripts/test_extlink.shassertscompiler_rt.ois in the archive and runs on the CI extended leg so the claim can no longer rot. -
Sub-3.0 builds no longer carry the WasmGC code path. The GC/subtyping JIT helpers are held as
JitRuntimefunction-pointer fields whose default is the real helper (ADR-0203 D1), and a field default takes the address unconditionally — so the whole GC cohort stayed live even in a-Dwasm=1.0build. Each helper body is now comptime-guarded, letting DCE reclaim it:-Dwasm=1.0 -Dwasi=p1.textdrops 2,957,749 → 2,614,800 bytes (−11.6%). Wasm 3.0 builds are unaffected (#150).
[2.3.0] - 2026-07-17
Inventory sweep against the officially released WASI 0.3.0 (2026-06-11): the first slice of the official interface set, plus a docs truth-sweep and Homebrew packaging.
Added
- Official WASI 0.3.0 clocks surface —
wasi:clocks/system-clock(0.3.0's renamedwall-clock;instant{seconds: s64, nanoseconds: u32}, pre-1970 instants representable with the floored split) andget-resolutionon both clocks, served on the component host. The asyncwait-until/wait-forneed scheduler-wired timer waitables and are tracked as debt (D-524). - Homebrew:
brew install clojurewasm/tap/zwasm(macOS arm64, Linux x86_64 / aarch64), packaging the release binaries.
Changed
- wasip3 fixture toolchain repinned nightly-2026-06-14 →
nightly-2026-06-24 (the newest nightly that can still
-Z build-stdwasm32-wasip3 — 2026-07-08/-16 hit an upstream std regression) and the conformance fixtures regenerated. Measured: the emitted imports are unchanged (wasi 0.2.6) because they come from the borrowed wasip2 wasi-libc, not the nightly's std bindings — the official-WIT fixture gap remains open as D-523.
Fixed
zwasm --helpengine wording: the usage text claimedinterp (default); the real default (since 2.0.0) isauto— prefers the JIT with transparent interpreter fallback. The help now matchesdocs/reference/cli.mdand the README, and also lists the--cache/--cache-clearflags shipped in 2.2.0.
Documentation
- WASI 0.3 documented: README gains a WASI 0.3 row (Component-Model
native-async core, opt-in
-Dwasi=p3; official WASI 0.3.0 released 2026-06-11 —wasi:filesystem/wasi:socketsdata-plane andwasi:httppending), and the migration guide's "until it settles" framing is replaced accordingly. - GC build default corrected in the migration guide: WasmGC is
compiled in and executes by default (part of
-Dwasm=v3_0); the guide previously described it as opt-in via-Dgc, which is inert (tracked as debt). - README gains an Install section (Homebrew + prebuilt release binaries).
[2.2.1] - 2026-07-16
Binary-size campaign (ADR-0204, PRs #144-#146), triggered by downstream embedder measurement (cljw): the zwasm binary shrank ~21% (ReleaseSafe arm64 CLI 5,282,584 → 4,173,736 B) with no API, behaviour, or JIT-output change.
Changed
- JIT host-callback FP thunks share their bridge bodies (D-522
stage 1): the
f32/f64-arg host-call thunks previously monomorphized the full marshalling body per (arg-kinds × result × slot) — ~300 B × 3,840 instantiations. The bodies now live in 60 sharednoinlinebridges; each per-slot thunk is a ~23 B tail-forwarder.api.jit_host_bridgecode: 1,311 KB → 232 KB (−82%). Thunk C-ABI signatures, trap semantics, and the embedder-facing API are unchanged.
Internal
- Binary-size baseline + per-stage rows recorded in
bench/results/size_history.yaml; campaign record in ADR-0204 (including the measured refutation of the "table-driven dispatch shrinks the emitter" hypothesis — D-521 discharged).
[2.2.0] - 2026-07-09
AOT-full-fidelity campaign (ADR-0203, PRs #136-#142): .cwasm is now a
real deployment-grade artifact, and compilation is transparently cacheable.
Added
- Transparent compilation cache (
zwasm run --cache[=DIR], D-508): modules are keyed by content hash and the.cwasmartifact of a previous run is reused — parse/validate/codegen skipped (measured 2.2x cold start on a 3 MB Go module). Deploy artifact stays.wasm; the cache lives in the platform user-cache dir under a versioned subdirectory. Any cache defect (corrupt entry, unserializable module, I/O failure) degrades to a miss or bypass — the cache can never makerunfail.--cache-cleardeletes this build's cache subdirectory. .cwasmformat v0.5: embeds the original module bytes plus per-func frame/EH/oob metadata, so an artifact loads back into the FULL runtime.
Changed
zwasm run x.cwasmnow runs through the full runtime — identical WASI, sandbox limits (--fuel/--timeout/--max-memory/--max-table-elements),--invoke NAME=ARGS, and start-function behaviour to running the source.wasm(cache-hit == cache-miss by construction). The former compute-only AOT mini-runtime is retired, and the.cwasmsandbox-flag refusal is gone.- Bounds-check-elided artifacts serialize (guard-page hosts):
zwasm compileoutput now carries the elision bit and re-registers trap entries at load; non-guarded hosts refuse the artifact loudly. --engine interpwith a.cwasminput is now a loud exit-2 refusal (the artifact is precompiled JIT code); with--cacheit bypasses the cache and runs the interpreter as asked.
Fixed
- JIT helper addresses are no longer baked into emitted code (D-516):
a
.cwasmproduced by one process crashed (or worse) in another under ASLR — all 36 helper call sites now route through position-independent runtime slots. A cross-process differential gate (zig build test-aot-diff, 63 fixtures) pins the fix. (start)function now runs on the lenient JIT path (Wasm §4.5.4) — a pre-existing--engine jitspec bug the campaign's differential harness caught.- CI: the
ci-requiredaggregator no longer reports green when its change-detection job fails.
[2.1.0] - 2026-07-06
Added
- table64 compiles natively in the JIT (D-475) — i64-indexed tables (the
memory64 proposal's table extension) no longer fall back to the interpreter:
the JIT table descriptors widened to u64 (
TableSlice.len/max,table_size), and every table op (table.get/set/size/grow/fill/copy/init),call_indirect, andreturn_call_indirectnow emits the index width declared by the table's type on both arm64 and x86_64, with wrap-safe 64-bit bounds sums. i32 tables keep the byte-identical fast path.
Fixed
- table64 element segments under the JIT engine: an active elem segment
with an
i64.constoffset failed instantiation on the JIT path (masked by the interp fallback) — offsets now evaluate at u64 width, matching the interpreter. - Instantiate-time bounds hardening: a guest-chosen 64-bit element offset can no longer wrap the table bounds check.
- AOT: a table64 whose minimum size exceeds the
.cwasmu32 field is now rejected loudly instead of silently saturated.
[2.0.0] - 2026-07-01
First stable release. Carries the complete feature set of v2.0.0-rc.1
(below), promoted to stable after the final hardening pass.
Added
- JIT
table.growfor no-max tables — a table declared without an upper bound now grows under the JIT up to a synthesized cap (max(min*2, 1024), matching WAMR), where it previously returned the spec-1(D-501).
Fixed
- Docs / reference / examples corrected to the code truth: the default
engine is
.auto(JIT-preferring, interp fallback);include/zwasm.hcarries the sandboxing + engine-selection +zwasm_instance_get_funcsurface;Linker.instantiatetakes(module, opts); the build flag is-Dwasm=v3_0. Thedocs/examples/zig_depexternal consumer builds and runs again. - Repo layout decluttered:
CLAUDE.md→.claude/,THIRD_PARTY.md→legal/,examples/→docs/examples/, community-health files →.github/. - Test harness: guest std streams no longer leak to the real process fd 1/2
in test builds (removed a phantom
failed command: … --listen=-that appeared even when every test passed).
[2.0.0-rc.1] - 2026-07-01
The first tagged release candidate for v2.0.0. The v2 redesign is
feature-complete and verified on the 3-host gate (Mac aarch64 + Linux x86_64 +
Windows x86_64). Earlier pre-releases were tagged v2.0.0-alpha.*.
Added
- WebAssembly 3.0 — all 9 proposals: GC, exception handling, tail
calls, memory64, multi-memory, typed function references,
extended-const, relaxed-SIMD, custom annotations. Plus full Wasm 1.0 + 2.0 (multi-value,
SIMD-128, bulk-memory, reference-types, non-trapping FP→int conversion,
sign-extension, mutable globals). Spec testsuite green,
skip-impl == 0. - Execution backends — interpreter (full WASI), JIT for ARM64
(AAPCS64) + x86_64 SysV + x86_64 Win64 (MSVC ABI), and AOT (
.cwasmcompile + load + run).interp == jitdifferential testing. - Memory-safe GC-on-JIT — a conservative native-stack-scan collector roots live references across collections; verified by an adversarial use-after-free test on aarch64 + x86_64.
- WASI preview1 — args, environment, preopened directories, clock, random, fd I/O (under the interpreter).
- C API —
include/wasm.hbyte-identical to the upstream wasm-c-api standard (the interface wasmtime/wasmer follow), with full coverage of the standard surface, pluswasi.h+zwasm.hextensions. - Zig embedding API — native
Engine/Module/Instance/Linker/Caller/Memory/Global/Table/TypedFunc/Trap/Valuefacade, consumable as an externalbuild.zig.zondependency. - CLI —
zwasm run(WASI exec,--invoke/--engine/--dir/--env) andzwasm compile(.cwasmAOT), plus--version/--help. - Sandboxing — cooperative interruption (cancel/timeout),
deterministic fuel metering, and a host memory-growth cap, on BOTH
engines (the JIT polls at function entry + every loop back-edge):
Zig facade setters, C
zwasm_instance_*setters +zwasm_trap_kind(zwasm.h), and CLI--fuel/--timeout/--max-memory.
Changed (from v1)
- Breaking redesign of the C / Zig / CLI surfaces to the first-principles,
industry-standard shape (not v1 parity). The CLI drops v1's
validate/inspect/features/wat/wasmsubcommands and capability-flag sprawl — validation is programmatic; conversion and introspection delegate towasm-tools/wabt.
Known limitations
- WASI 0.1 (preview1)
sock_*calls have no host socket layer (they validate the fd and returnnotsock; preview1 has no socket-open). Real socket support — including TCP listeners — is available via WASI 0.2 (wasi:sockets/tcp), which is default-ON (Component Model functional, not deferred). - Table funcref slots surface as opaque handles (not yet directly callable from the host).