A compact WebAssembly 3.0 runtime — an optimizing JIT and an interpreter, from desktop to microcontroller.
Benchmarks | Binary size | Wasm compatibility | Validation | Compiler pipeline
Silverfir-nano is a no_std WebAssembly runtime built to be strong on
every axis a Wasm runtime is judged on, not just one. It ships an
optimizing JIT and an interpreter behind one API
(compatibility).
- Fast, JIT and interpreter — register-allocated, region-optimized native code puts the JIT at parity with Wasmtime's fully-optimizing Cranelift and V8 TurboFan on Apple M4. The interpreter beats the best-in-class Wasm interpreters by 1.07–1.73× (full results).
- Small — pick an engine and pay for what you use. Measured on real
RP2350 firmware, flash is 337 KiB with the interpreter and 1,042 KiB with
the JIT (details); both run inside the board's 512 KB of
SRAM. Zero runtime dependencies,
alloconly,no_stdthroughout. - Portable — six ISAs, from x86_64 and ARM64 down to RV32 and Thumb-2, and both engines cover all six. The compiler that competes with Cranelift on M4 emits Thumb-2 on a Cortex-M33 — codegen quality doesn't degrade as you step down.
- Full Wasm 3.0 — GC, exception handling, SIMD and relaxed SIMD, tail calls, memory64, multi-memory, typeful references, and extended constant expressions. The JIT passes 100% of the official Wasm spec testsuite; the interpreter passes 100% of it less SIMD and GC.
- On-device JIT — verification and code generation both happen on the
target itself. You ship a
.wasmartifact, not a relocatable machine-code blob; the runtime verifies and JITs it on the chip, even on a Cortex-M. Where W^X or a tighter flash budget rules that out, the interpreter runs the same modules with no executable memory at all.
Full benchmark results — every chart, the method, and the caveats →
Against 21 other engines in wasmi-benchmarks →
Measured on real firmware, not a synthetic link: the Pico 2 demo host built
for the RP2350's two cores, release, with the engine swapped. Flash is the
whole loadable image — engine, ST7735 display driver, DMA, embedded-graphics,
defmt, the RP2350 HAL, and the embedded .wasm guest. SRAM is a separate
axis and is not what this table measures; the demo statically reserves a
448 KiB heap regardless of engine.
| Firmware | Flash |
|---|---|
| Cortex-M33, JIT | 1,066,744 B (1041.7 KiB) |
| Cortex-M33, interpreter | 345,304 B (337.2 KiB) |
| Hazard3 RV32, JIT | 1,032,080 B (1007.9 KiB) |
| Hazard3 RV32, interpreter | 381,696 B (372.8 KiB) |
Choosing the interpreter drops the whole compiler pipeline and its executable-memory substrate, for 2.7–3.1× smaller firmware. Of the 337 KiB Cortex-M33 image, 87 KiB is the generated dispatch engine itself. The same source builds either one — the engine is not visible to the embedder.
cd devices/pico2
cargo build --release --bin demo_host --target thumbv8m.main-none-eabihf
cargo build --release --bin demo_host --target thumbv8m.main-none-eabihf \
--no-default-features --features engine-interp,demo-mandelbrotThe Mandelbrot below is a Wasm guest that Silverfir-nano verifies and
JIT-compiles to native code on the Pico 2 itself — no interpreter in the
loop, no ahead-of-time toolchain in the deployment path. The .wasm is the
artifact that ships to the device.
sf-nano-mandelbrot-readme.mp4
The RP2350 packages two independent CPU cores — an Arm Cortex-M33 and a Hazard3 RISC-V (RV32IMAC) — and Silverfir-nano targets both. See devices/pico2/README.md for bring-up, the Cube demo, and per-core numbers.
The same RV32 backend also runs on the Waveshare ESP32-C6-LCD-1.47 board
(Espressif riscv32imac core), JITing a Wasm Mandelbrot guest on-chip. See
devices/Waveshare_ESP32_C6/README.md
for bring-up.
Silverfir-nano has six native backends:
- x86_64
- ARM64 (A64)
- RISC-V 64 (RV64GC)
- RISC-V 32 (RV32GC)
- ARMv7-A (A32)
- ARMv7-M and above (Thumb-2)
They all share the same frontend, middle-end, and register allocator. Codegen quality doesn't degrade as you move across ISAs or step down to smaller targets: the same compiler that produces Cranelift-competitive output on Apple M4 also runs on a Raspberry Pi Pico 2, targeting both its Arm Cortex-M33 (Thumb-2) and its Hazard3 RISC-V (RV32IMAC) cores.
Most WebAssembly runtimes aimed at microcontrollers stop at an interpreter, often with instruction fusion or a threaded dispatcher on top. Silverfir-nano ships one of those too — but it does not stop there. The JIT emits native machine code on the device itself, even on a Cortex-M.
What makes that credible is not any one trick but the shape of the compiler:
- The pipeline is streamable end-to-end. Each transform stage consumes its input and produces its output incrementally, per function. A fully materialized IR for the whole module is never held in memory — that is what makes JIT-on-MCU possible at all.
- The middle-end allocator is designed for JIT budget and good
codegen.
ALGORITHM4is a region-based cost-optimal cache residency allocator that runs per-function at JIT scale, with output competitive with much heavier optimizing compilers.
Validated against the official WebAssembly spec testsuite. Both engines are Wasm 3.0; they differ in two feature groups:
| JIT | interpreter | |
|---|---|---|
| Wasm 1.0 / 2.0 core | yes | yes |
Bulk memory, reference types, table.* |
yes | yes |
| Multiple memories | yes | yes |
| Imported memories, tables, globals | yes | yes |
| Extended const expressions | yes | yes |
| Typeful references | yes | yes |
| Tail calls | yes | yes |
| Exception handling | yes | yes |
memory64 / table64 |
yes | yes |
| SIMD / relaxed SIMD | yes | no |
| Garbage collection | yes | no |
The JIT is the full Wasm 3.0 engine. The interpreter is Wasm 3.0 less
SIMD and GC — and those two are out of scope by design, not pending: the
folded stack machine works in 8-byte slots, so a v128 lane and a GC object
are representation changes rather than more handlers. Anything the
interpreter cannot run is refused at instantiation or predecode with a named
error rather than mis-executed, so a module outside its surface fails loudly
rather than subtly.
Pick the JIT for speed, or for SIMD and GC. Pick the interpreter for size, or where runtime code generation is forbidden or impossible.
Both engines run the same harness, which has no per-directive skipping — every directive in every file it opens is executed and asserted. The JIT passes all 257 files. The interpreter passes all 174, which is every file the JIT runs except the 66 SIMD and 17 GC ones listed out of scope above.
cargo run --release -p sf-nano-spectest # JIT: 257/257
cargo run --release -p sf-nano-spectest -- --interp # interpreter: 174/174The Wasm 3.0 feature groups in detail — both engines except where noted:
- Extended constant expressions — arithmetic in const expressions and
global.getof previously declared immutable globals. - Tail calls —
return_call,return_call_indirect, andreturn_call_ref. - Multiple memories — multi-memory definitions, imports, exports, and indexed memory operations.
- 64-bit address space —
memory64,table64, and the correspondingi64-typed memory/table instruction paths. - Typeful references — typed
ref null,ref.func,call_ref,br_on_null,br_on_non_null, refined local initialization rules, and typed table initializers. - Garbage collection (JIT only) — recursive types, subtyping,
struct.*,array.*,ref.test,ref.cast,br_on_cast,br_on_cast_fail,ref.i31,any.convert_extern, andextern.convert_any. - Baseline SIMD (JIT only) —
v128values, loads/stores, lane ops, bitwise ops, arithmetic, comparisons, conversions, and the standard SIMD testsuite surface currently enabled in-tree. - Relaxed SIMD (JIT only) — relaxed swizzle, relaxed truncation, relaxed min/max, relaxed lane-select, relaxed q15mulr, relaxed dot-product, and relaxed madd/nmadd.
- Exception handling — tags,
throw,throw_ref, andtry_table.
# Default build
cargo build --release
# Run a WASI program
cargo run --release --bin sf-nano-cli -- program.wasm [args...]
# Run benchmarks (each one self-times to ~2s, on any engine)
python3 benchmarks/wasi/run_tests.py
python3 benchmarks/wasi/run_tests.py --time 10 # longer, for formal runs
python3 benchmarks/wasi/run_tests.py --interp # interpreter instead of JIT
# Rebuild the benchmark wasm binaries from source (needs wasi-sdk clang)
make -C benchmarks/wasi
# Run benchmarks under V8 (Node.js) for comparison
node benchmarks/wasi/run_v8.mjs
# Interpreter-only build (no JIT, no executable memory)
cargo run --release -p sf-nano-cli --no-default-features --features interp -- program.wasmLocal iteration uses ordinary Cargo commands:
cargo build --workspace
cargo test --workspaceGitHub Actions owns the exhaustive host, feature, QEMU cross-runtime,
bare-metal, spec, WASI, warning, and performance matrices. Their
implementation lives in ci/; see ci/README.md.
MIT / Apache-2.0