All notable changes to zwasm are documented here. Format based on Keep a Changelog.
Developer / CI infrastructure improvements. No public API or runtime behaviour change for embedders.
.github/versions.lock(mirrorsflake.nixpins for Windows + CI YAML) replaces the old.github/tool-versions. Single source of truth for Zig, wasm-tools, wasmtime, WASI SDK, Rust pins; comments must live on their own line per file header (the Python reader inci.ymldoes a plainsplit('=', 1)). (#60, D136).dev/environment.md— developer onboarding doc covering Mac / Linux / Windows native setup, Nix devshell contents, CI ↔ local gate mapping, and the remaining Windows-skipped CI items as a Plan C tracker. (#60)scripts/lib/versions.sh,scripts/sync-versions.sh,scripts/gate-commit.sh,scripts/gate-merge.sh,scripts/run-bench.sh— unified Commit Gate / Merge Gate runners that work identically on macOS, Linux (Nix devshell), and Windows (Git Bash).gate-commit.shauto-clones wasmtimetests/misc_testsuiteinto a gitignored.cache/directory and chainsbuild_all.py && run_compat.pyfor realworld so a fresh checkout works without manual setup. Auto-skipsffion Windows to mirror the existing CI guard. (#61)scripts/windows/install-tools.ps1— provisions Zig + wasm-tools + wasmtime + WASI SDK fromversions.lockinto%LOCALAPPDATA%\zwasm-tools. Updates user PATH +WASI_SDK_PATH. Auto-installs Microsoft Visual C++ Redistributable via winget whenvcruntime140.dllis missing (WASI SDK clang.exe needs it). Idempotent;--Forcere-extracts;-OnlyToolselects one. (#61)versions-lock-syncCI job mechanises Merge Gate item #9 — fails the PR ifflake.nixandversions.lockdisagree on a tool pin. Runs in parallel with the existing test matrix in well under a second. (#62)- Memory usage check on Windows via PowerShell
Process.PeakWorkingSet64— first of the eight Windows-skip CI guards removed. Same 4.5 MB budget as the POSIX path. (#64) zig build shared-libnow runs on Windows in CI. Zig produceszwasm.dll+zwasm.libnatively fromaddLibrary({.linkage = .dynamic})— the old guard was a no-op. Plan C-a. (#68)zig build static-lib -Dpic=true -Dcompiler-rt=trueandtest/c_api/run_static_link_test.shnow run on Windows in CI. The C link tests usezig cc(portable across Mac/Linux/Windows) instead of systemcc. PIE coverage is preserved on Linux. Plan C-d. (#69)examples/rustcargo runnow runs on Windows in CI.build.rsgained a Windows arm: dynamic linking copieszwasm.dllnext to the cargo target binary so the OS finds it via the executable directory at runtime (PE has no-Wl,-rpath); static linking useszwasm.liband skips the POSIX-only-lc/-lm. Plan C-c. (#71)-Dstrip=truebuild option inbuild.zigstrips the CLI binary at link time via LLD. Used by the Binary size check and the size-matrix CI jobs so they no longer depend on a hoststriptool — portable across ELF / Mach-O / PE. The Binary size check now runs onwindows-latest(with a 1.80 MB ceiling reflecting PE overhead; Mac 1.30 MB, Linux 1.60 MB unchanged) andsize-matrixis a 3-OS matrix (Ubuntu / macOS / Windows). Plan C-e + C-f. (#70, D137)test/c_api/test_ffi.cported to Windows: dynamic loading viaLoadLibraryA+GetProcAddress, threading viaCreateThread+WaitForSingleObject, pipes via_pipe(binary mode). Sources are selected by#ifdef _WIN32blocks; POSIX path unchanged. The runner switches from systemgcctozig ccso it does not require a host C compiler. Theif: runner.os != 'Windows'CI guard onRun FFI testsis dropped accordingly.gate-commit.shno longer auto-skipsffion Windows. Plan C-b. (#72)scripts/windows/install-tools.ps1now provisions Rust (viarustup-init.exewithwasm32-wasip1target), Go, and TinyGo in addition to the existing core toolchain — i.e. the same set Linux/macOS get fromflake.nix. Rust is installed into a self-containedrust-<toolchain>/withCARGO_HOME/RUSTUP_HOMEset in user-scope env so the install does not pollute%USERPROFILE%\.cargo.-OnlyToolacceptsrust,go,tinygo. Closes the local-realworld 25/50 → 50/50 gap on Windows. (#74, W52)
- WASI SDK version bumped 25 → 30 to align CI with
flake.nix(which was already at 30). Realworld 50/50 PASS verified locally on macOS aarch64 with the new SDK. (#60) CLAUDE.mdCommit Gate / Merge Gate sections point atbash scripts/gate-commit.sh/gate-merge.shas the one-liner entry points; example commands switched to the.pyrunners that CI exercises. (#61).github/workflows/ci.ymlbenchmark job sourcesHYPERFINE_VERSIONfromversions.lockinstead of hardcoding the version twice. (#65)
- D136 (in
decisions.md): Nix-as-SSoT design recorded with the Plan B / Plan C scope (unified gate scripts, Nix-based CI, Windows native installer, removal of the remaining Windows-skipped CI steps). - D137 (in
decisions.md): cross-platform stripping via Zig-Dstrip=true(LLD--strip-allfor ELF/Mach-O/PE) plus per-OS size ceilings (Mac 1.30 / Linux 1.60 / Windows 1.80 MB). Records the move away from the hoststriptool (which isobjcopy --strip-allELF-only) and from a single global ceiling sized for the largest OS. (#73) - Doc alignment sweep:
ARCHITECTURE.md,CONTRIBUTING.md,.dev/roadmap.md, and the release skill now agree withCLAUDE.mdon Merge-Gate items, runner forms (python3 …py), and per-OS size ceilings. (#75) - Removed obsolete
.dev/checklist-jit-fuel-timeout.mdplanning doc (all items shipped: PR #6 timeout merge, fuel-bypass fix,--timeoutCLI option). (#76) - README front page updated: W52 status (Windows realworld 50/50 via local installer; CI parity is W50) + per-OS size ceilings (Mac 1.30 / Linux 1.60 / Windows 1.80 MB; D137). (#78)
W46 + W48: re-disable link_libc on the core build and trim the release
binary back under the original 1.60 MB ceiling. No public API changes;
embedders upgrading from 1.10.0 should see a smaller binary and identical
behaviour.
- Binary size ceiling: pulled back from the Zig 0.16 transition value 1.80 MB to 1.60 MB. Stripped Mac aarch64 binary is ~1.18 MB; Linux ELF is ~1.56 MB stripped. The original 1.50 MB target remains tracked as W48 Phase 2 (non-blocking).
WasmModule.loadCorenow freesexport_fnsandcached_fns(and the innerparam_types/result_typesslices) on the failure path when the Wasmstartfunction traps. The earliererrdeferchain unwound vm/instance/wasi_ctx/module/store/self but skipped the export caches, leaking the slices for any module with a non-empty export and a trapping start function. The fix mirrorsdeinitso the failure path is symmetric with normal teardown. (Issue #42)- OOM test in
loadLinkednow heap-allocates theFailingAllocatorso its address remains stable while the partially-loaded module is held; the prior stack-allocated form was use-after-free under the test's search loop. (PR #56 by @jtakakura)
- W46 Phase 1+2 (
link_libc=falserestoration):build.zigcore modules flipped to.link_libc = falsewhile C API targets (shared-lib, static-lib, c-test) keeplink_libc=true. WASI path-based ops,cErrnoToWasi,trace.zigstderr writes, andplatform.appCacheDirall routed throughplatform.pfd*helpers so the core no longer pulls in libc on either OS. Linux direct syscalls used where stdlib bindings are missing in 0.16 (std.os.linux.statx, etc.). - W48 Phase 1: release binary trimmed via panic handler tightening,
segfault handler disabled in ReleaseSafe, and
mainreturn type narrowed tou8. Combined effect restores the ceiling back below the original 1.60 MB target. - Test-site
std.c.{pipe,dup,dup2,read,nanosleep}calls gated for Linux where the bindings are empty (W46 Phase 1c). - Credit @notxorand for the initial Zig 0.16.0 migration draft (PR #41, superseded by the v1.10.0 work).
Toolchain bump from Zig 0.15.2 → Zig 0.16.0 ("I/O as an Interface"). No public API removals or signature changes; downstream source stays compatible but consumers must upgrade to Zig 0.16.0 to build.
- Zig toolchain: 0.15.2 → 0.16.0. Flake pins and all CI workflows updated.
WasmModule.Configgainedio: ?std.Io = nullandVmgainedio: std.Io— whenConfig.iois null,loadCore/loadLinkedstand up a privatestd.Io.Threadedowned by the module (see D135). Existing embedders pass nothing and get the default behaviour.- LEB128 decoding pinned to the pre-0.16 stdlib algorithm. 0.16's
std.Io.Reader.takeLeb128does not enforce WASM's "integer too large" overshoot rule; the zwasm decoder was rewritten inline (40 lines) with the 0.15@shlWithOverflow-based algorithm so spec testbinary-leb128.77/78continue to reject malformed 10-byte i64 encodings.
- Cross-platform fstat: on Linux,
std.c.fstat/std.c.fstatat/std.c.Statare all unavailable (empty bindings) andstd.posix.Statisvoid.path_filestat_getnow dispatches tostd.os.linux.statxon Linux (decoded to a neutralFileStat) andstd.c.fstataton Darwin. Test helpers that only needed the file size moved tolseek(SEEK_END). build.zigmodules explicitly setlink_libc = true. Mac's Zig toolchain was lenient aboutextern "c"without explicit libc linkage; Ubuntu 0.16 rejects it hard.
Vmstruct:io: std.Io = undefinedfield (set by loader).WasmModule.owned_io: holds the auto-constructedstd.Io.Threadedwhen the embedder did not supply one.main(init: std.process.Init)on entry points (CLI, e2e_runner) so they can grabinit.io/init.gpa/ args from the runtime's start.zig.- WASI handlers: use
std.c.*withfile.handlefor the POSIX ops thatstd.posixdropped (fsync/mkdirat/unlinkat/renameat/ftruncate/futimens/ pread/pwrite/dup/readlinkat/symlinkat/linkat/close/pipe/getenv). Errno →Errnovia a single localcErrnoToWasi()helper. @Vectorruntime indexing was rejected by 0.16's compiler; SIMD extract/replace_lane and lane-memory ops rewritten to use[N]Tarrays with@bitCastat push time.- Closes PR #41 (notxorand's migration draft) as superseded.
invoke/invokeInterpreterOnlynow returnerror.ModuleNotFullyLoadedif the underlying VM is uninitialized (e.g., after OOM inloadLinked). This is a new error variant in the public API. Embedders matching on specific errors should handle this case. See API docs for details. (PR #40 by @jtakakura, closes #39)
WasmModule.cancel()no longer segfaults on a partially-loaded module (vm == nullafter OOM inloadLinked). Matches the C API contract that documents cancel as a no-op on idle modules.
- Asynchronous execution cancellation (PR #28 by @jtakakura, closes #27). A host thread can now abort a running invocation:
- Zig API:
WasmModule.cancel()/Vm.cancel()— thread-safe, returnserror.Canceledfrominvoke()at the next ~1024-instruction checkpoint (or JIT fuel interval). - C API:
void zwasm_module_cancel(zwasm_module_t *)andvoid zwasm_config_set_cancellable(zwasm_config_t *, bool). - CLI: reports
execution canceledwhen the runtime returnserror.Canceled.
- Zig API:
WasmModule.Config.cancellable: ?bool— opt-out of periodic cancellation checks for peak JIT throughput when the host never cancels.
- By default, JIT-compiled loops now fire the fuel-check helper every
DEADLINE_JIT_INTERVALiterations even when no fuel/deadline is set, so cancellation takes effect without host instrumentation. Passcancellable = falseto restore the pre-v1.9.0 unconditionaljit_fuel = maxInt(i64)behaviour.
WasmModule.Config— unified configuration struct for module loading (PR #30 by @jtakakura)WasmModule.loadWithOptions(allocator, wasm_bytes, config)— new primary entry point that consolidates all load variants- C API configuration setters:
zwasm_config_set_fuel,zwasm_config_set_timeout,zwasm_config_set_max_memory,zwasm_config_set_force_interpreter
- Resource limits (fuel, timeout, max_memory_bytes, force_interpreter) now apply during the start function. Previously the CLI applied them post-load, leaving the start function unconstrained.
- Fuel budget is now decremented across successive
invoke()calls, matching the pre-existing/// Persistent fuel budgetdoc comment. Previously each invoke reset fuel to the originally-configured value. - CLI
--linkfallback retry is now scoped toerror.ImportNotFound. Previously any error on the imports-enabled load attempt triggered a retry without imports. loadWithImports/loadWasiWithImportsaccept?[]const ImportEntry(source-compatible with existing[]const ImportEntrycallers via Zig optional coercion).
loadLinked:store.deinit()is now called on decode/instantiate failure paths (resource leak fix).loadCore:errdefer allocator.destroy(self.vm)protects against VM leak on post-allocation failures.
- All existing
load*helpers (load,loadWithFuel,loadFromWat,loadWasi,loadWasiWithOptions,loadWithImports,loadWasiWithImports) are retained as thin wrappers overloadWithOptions, so embedders do not need to update call sites.
- ARM64 JIT:
i32.rem_s/uandi64.rem_s/uproduced wrong remainders when the destination register aliased the divisor (rd == rs2). UDIV/SDIV clobbered the divisor before MSUB could read it, so MSUB computeddividend - quotient * quotientinstead ofdividend - quotient * divisor. Triggered by TinyGo-compiledgcd(IR:r3 = r0 % r3), causing infinite loops after JIT compilation (HOT_THRESHOLD). The prior fix in v1.7.1 only coveredrd == rs1. This commit preserves whichever operandrdaliases before the divide.
-Dpicand-Dcompiler-rtbuild options for static library consumers (PR #24)
- Preserve caller-set
vm.*settings (fuel, deadline, max_memory_bytes, force_interpreter) acrossinvoke()andinvokeInterpreterOnly()— previously reset to defaults between invocations (PR #31, #32) setup-orbstack.md: broken Zig download URL (PR #35)
- Spec testsuite bumped to
f9c743a(from072bd0d)
- SIMD JIT: ARM64 NEON (253/256 native) and x86_64 SSE (244/256 native) (Phase 13)
- SIMD JIT optimizations: v128 base address cache (W43), Q-reg/XMM register class (W44), loop persistence (W45), guard page bounds check elimination (W46), FMLA/FMLS instruction fusion (W47)
- C API: FD-based WASI stdio and preopen configuration (
zwasm_wasi_config_set_stdin_fdetc.) (D133) - C API:
zwasm_module_invokeargs markedconst(PR #16) - JIT fuel check at back-edges: enables timeout support for JIT-compiled code (Phase 19.2)
- Epoch-based JIT timeout (D131): cooperative timeout via shared fuel counter
- Multi-value return support in RegIR (
OP_RETURN_MULTI) --interpCLI flag for interpreter-only execution (debugging/differential testing)- Wide-arithmetic validation support (i64.add128, i64.sub128, i64.mul_wide_s/u)
- 5 real-world SIMD benchmarks (C -msimd128): grayscale, box_blur, sum_reduce, byte_freq, nbody
- 3 additional SIMD benchmarks: mandelbrot, matmul (C, wasi-sdk), blake2b_simd (Rust), simd_chain
- SIMD benchmark comparison infrastructure (
bench/run_simd_bench.sh,bench/simd_comparison.yaml) - Rust FFI example using zwasm C API
- FFI tests in CI and commit/merge gate checklists
- memory64: u64 offset decoding and i64 address handling across decoder, validator, predecoder, and VM — fixes wasm-tools 1.246.1 compatibility
- JIT correctness sweep (Phase 20): 8 real-world compat programs fixed (45→50/50)
- ARM64 fuel check clobbering vreg 20 (x0) at loop back-edges
written_vregspre-scan: stale reload in loops after call sites- Void self-call result clobber (
n_resultsvsresult_count) - Void-call
reloadVregclobbering live local variables - ARM64 ABI register clobbering in
emitMemFill/emitMemCopy/emitMemGrow - Remainder register aliasing when rd == rs1 (MSUB on ARM64)
- Stale scratch cache in signed division overflow check
- x86 i32 div/rem signed edge case: 32-bit CMP for -1 check
- JIT memory64 bounds check and custom-page-sizes (CI failure since 3/24)
- JIT
memory_grow64truncation and cross-module call instance - JIT
extract_laneencoding and back-edge poisoning - ARM64 JIT
simd_v128sync in MOV/CONST for SIMD correctness - Q-cache stale read in
extract_laneupper-half fallback - ARM64 JIT
emitGlobalSetABI clobber: vreg r21 overwritten before reading value (W35) - x86_64 wide-arithmetic E2E: 19 tests fixed (validator + Debug i128 build mode)
i32.store16access size in interpreter--interpflag incomplete fordoCallDirectIRpath- Windows C API:
intptr_tfor host fd, cross-platformFile.closefor stdio handles - Shared library segfault on Linux x86_64 (PR #11)
- SIMD operations now JIT-compiled (was: stack interpreter only, ~22x slower)
- SIMD microbenchmarks: image_blend 4.7x, matrix_mul 1.6x (beats wasmtime)
- HOT_THRESHOLD lowered from 10 to 3 for earlier JIT compilation
- Contiguous v128 storage layout for SIMD JIT (W37)
- Real-world compat: Mac 50/50, Ubuntu 50/50 (was 42/50)
- E2E tests: 795/795 on Mac (was 773/792 on Ubuntu)
- Spec tests: 62,263/62,263 (100%, 0 skip)
- Binary size: 1.29 MB stripped. Memory: ~3.5 MB RSS.
- wasm-tools bumped to 1.246.1
- E2E runner built with ReleaseSafe (fixes x86_64 Debug i128 issues)
- Module cache for predecoded IR (Phase 1.2): reuse compiled module state across invocations
- Cache-enabled benchmark variants for all bench scripts (
--cacheflag)
- Benchmark suite expanded with cached variants for real-world programs
- CI: pinned wasmtime version to avoid broken install script on macOS
- Release process: migrated Ubuntu testing from SSH to OrbStack
- Project docs reorganized for post-v1.2.0 phase planning
- Real-world compatibility tests: 30 programs (C, C++, Go, Rust) verified against wasmtime
- On-Stack Replacement (OSR) for back-edge JIT: enter at loop body, bypass prologue side effects
- Spec test strict mode:
--strictflag exits non-zero on skips, enforced in CI - 11 new unit tests (521 total)
- E2E tests expanded: 792/792 assertions (from 356)
- Spec validation: 87 previously skipped tests now enforced (GC typed references, subtyping, table types, exception handling catch clauses, rec group boundaries)
- Spec infra: 18
assert_exceptiontests now handled by spec runner - E2E memory leak:
errdeferin void-returning function was a no-op in Zig 0.15 - JIT self-call stack overflow use-after-free (E2E segfault on aarch64)
- Back-edge JIT restart corrupting Go WASI state machines (side-effect detection)
- ARM64 OSR prologue: push FP callee-saved d8-d15 (stack corruption fix)
- x86_64 OSR prologue: load physically-mapped vregs (stale register fix)
- x86_64 select aliasing: val2 clobbered when rd == val2_idx
- JIT IR instruction limit (MAX_JIT_IR_INSTRS=1500) prevents miscompilation of large functions
- Go state machine detection: br_table boundary inclusive of target instruction
- Spec tests: 62,263/62,263 passed (100.0%, 0 skip — up from 62,158 with 105 skips)
- Benchmark results: 20/29 match or beat wasmtime, 27/29 within 1.5x (up from 14/23)
- st_sieve: 0.97x wasmtime (restored from 30.82x regression via OSR)
- GC benchmarks: gc_alloc 0.50x, gc_tree 0.73x wasmtime (JIT for struct ops)
- nbody: 0.97x wasmtime (FP cache D2-D15 expansion)
- Nightly CI: spec tests now run ReleaseSafe (eliminates Debug tail-call timeouts)
- WAT parser spec parity: GC type annotations, memory64 syntax, typed select, NaN payloads
- WAT roundtrip 100%: 62,156/62,156 spec test modules parse and re-encode correctly
- SIMD interpreter fast-path: predecoded IR dispatch for v128 ops (~2x improvement)
- SIMD performance analysis and 3-phase optimization roadmap
- Japanese documentation (book/ja/) with language switcher
- GC benchmarks in runtime comparison recording
- Binary size: 1.31 MB (ReleaseSafe, from 1.28 MB — WAT parser improvements)
- Runtime memory: 3.44 MB RSS (fib benchmark)
- Benchmark results: 14/23 match or beat wasmtime (up from 13/21, at v1.1.0 time)
- Updated all documentation with fresh benchmark data
First stable release. API frozen under SemVer guarantees.
- mdBook documentation site with 12 chapters
- GitHub Pages deployment for book
- CI benchmark regression detection (20% threshold)
- CI binary size check (1.5 MB limit), ReleaseSafe build verification
- E2E tests in CI (wasmtime misc_testsuite, 356 assertions)
- Nightly sanitizer job (Debug build) and fuzz campaign (60 min)
- CI caching for Zig build artifacts
- Overnight fuzz infrastructure (
test/fuzz/fuzz_overnight.sh) - API boundary documentation (
docs/api-boundary.md) - CONTRIBUTING.md, CODE_OF_CONDUCT.md, issue templates
- Release automation: tag-triggered cross-platform builds
- Install script (
install.sh), Homebrew formula template - Host function and WASI examples (5 examples total)
- CHANGELOG.md
- SemVer versioning policy, deprecation guarantees
- BREAKING:
loadWasi()defaults tocli_defaultcapabilities (stdio, clock, random, proc_exit). UseloadWasiWithOptions(.{ .caps = .all })for full access. --env KEY=VALUEinjected variables now accessible without--allow-env- Error messages now use human-readable format (30 error variants)
- README: badges, install section, documentation links
--sandboxCLI flag: deny-all capabilities + fuel 1B + memory 256MBCapabilities.sandboxpreset for library API- Restrictive library API defaults for safe embedding
- GC proposal: 31 opcodes (struct, array, i31, cast operations)
- Threads: 79 atomic operations (load/store/RMW/cmpxchg), shared memory, wait/notify
- Exception handling: throw, throw_ref, try_table
- Function references: call_ref, br_on_null/non_null, ref.as_non_null
- Wide arithmetic: add128, sub128, mul_wide_s/u
- Custom page sizes proposal
- Multi-memory proposal
- JIT optimizations: inline self-call, smart spill, direct call, depth guard caching
- x86_64 JIT backend
- Fuel-based execution limits
- Max memory limits
- Resource limit enforcement (section counts, locals, nesting depth)
- Security audit (.dev/archive/audit-36.md, docs/security.md, SECURITY.md)
- Fuzz testing infrastructure with 25K+ corpus
- Spec test coverage: 62,158/62,158 (100%)
- E2E tests: 356/356 from wasmtime misc_testsuite
- Binary size: 1.28 MB (ReleaseSafe)
- Component Model: WIT parser, binary decoder, Canonical ABI, WASI P2 adapter
- WAT text format parser (
zwasm run file.wat) - WASI Preview 1: 46/46 syscalls (100%)
- Capability-based WASI security model
- Module linking (
--link name=file) - Host function imports
- Memory read/write API
- Batch mode (
--batch) - Inspect and validate commands
- ARM64 JIT backend
- Register IR with register allocation
- SIMD: 236 v128 opcodes + 20 relaxed SIMD
- Initial release
- WebAssembly MVP: 172 core opcodes
- Stack-based interpreter
- Basic CLI (
zwasm run) - Zig library API (
WasmModule.load,invoke)