Skip to content

Releases: edadma/wasm

0.4.0 — compact-imports, imported memories/tables, shared mutable globals, CLI flags

17 May 00:37

Choose a tag to compare

Third Maven Central release. Substantial drop on the wasm-3.0 conformance side — five spec manifests fully unlocked, the spec sweep passing count climbs by ~650, and the public host-import surface gains memories, tables, and live-cell mutable globals. CLI gets three new flags. Same zero-runtime-dependency profile, same three-platform cross-build (JVM / Scala.js / Scala Native).

Install

libraryDependencies ++= Seq(
  "io.github.edadma" %%% "wasm"      % "0.4.0",
  "io.github.edadma" %%% "wasm-wasi" % "0.4.0",  // optional — only if you want the WASI shim
)
Coordinate What you get
io.github.edadma:wasm:0.4.0 The interpreter — Runtime.instantiate, ModuleInstance
io.github.edadma:wasm-wasi:0.4.0 The WASI Preview 1 host shim — depends on wasm

The wasm-cli runner is built from this repo but not published; it's a runnable example, not a library.

What's new since 0.3.0

Compact-imports wire format

The wasm-3.0 testsuite emits a compact import-section encoding where a regular-looking import with field_name == "" and a kind byte of 0x7E (shared-kind across the group) or 0x7F (per-import-kind, sub-imports can mix) signals that the just-read mod_name is shared across a group of sub-imports. The 0x7E form is kind sub_count (field_name desc)*; the 0x7F form is sub_count (field_name kind desc)*. The outer count is the TOTAL imports across all groups — so the parser advances i by sub_count per compact group. Before 0.4.0 modules using the compact form failed at parse time with unknown import kind 0x7F.

Imported memories + tables

Parser silently skipped import kinds 0x01 (table) and 0x02 (memory) before 0.4.0; modules importing them then failed downstream when an instruction referenced a memidx or tableidx with "no memory" / "no table". Now:

  • MemoryImport and TableImport surface in the module model alongside GlobalImport.
  • HostModule.memories: Map[String, Memory] and HostModule.tables: Map[String, RuntimeTable] are the new host-side maps. Both forward by reference — guest reads and writes hit the same backing array the host can inspect.
  • Limits checking at instantiation: host's current size ≥ module's declared min, host's max (if any) ≤ module's declared max (if any). Reftype match for tables; shared-vs-unshared match for memories.

Cross-module mutable-global sharing

Module storage for globals is now Array[GlobalCell] instead of Array[Value]. A GlobalCell is a tiny mutable holder; an imported mutable global installs the exporter's cell directly into the importing module's slot, so global.set from either side writes through the same storage — matching the wasm-3.0 spec's "imported mutable globals alias the exporter's storage" rule.

The host-import surface gains HostGlobal.live(vt, mut, cell) for sharing externally-owned cells; the existing HostGlobal(vt, mut, value) factory still works for the snapshot case (immutable globals + mutable globals the host doesn't need to observe).

New ModuleInstance accessors

Hosts forwarding one module's exports as another module's imports need richer introspection than globalValue / exportedMemory alone. Added:

  • exportedTable(name): Either[WasmError, RuntimeTable]
  • exportedFunctionType(name): Either[WasmError, FuncType]
  • exportedGlobalCell(name): Either[WasmError, GlobalCell]
  • exportedGlobalMutability(name): Either[WasmError, Boolean]
  • exportedMemoryNames / exportedTableNames / exportedGlobalNames (sorted enumeration helpers)

CLI flags

Three new flags on wasm-cli:

  • --trace — installs Tracer.Counting and prints [trace] ops=N calls=N hostCalls=N throws=N traps=N maxDepth=N to stderr after _start / main / --invoke returns.
  • --validate-only — parses + validates the module and exits without instantiating. 0 on ok, 1 with diagnostic. Useful as a CI lint step on generated wasm.
  • --stdin <path> — redirects the guest's fd 0 to a host file. The file is read once and streamed byte-for-byte through fd_read until exhausted.

WASI stdin reader

WasiContext gained a stdin: (Array[Byte], Int, Int) => Int field (POSIX-read shape). Default returns 0 bytes (EOF) so the historical "fd 0 returns EBADF" behaviour is gone — fd 0 now reads cleanly as empty input. WasiContext.stdinFromBytes(payload) builds a cursor-tracking reader from a byte array (used by the CLI's --stdin <path>).

Documentation

  • README trimmed from ~285 lines to ~55 lines; detail moved into the juicer-rendered docs site.
  • New Development/ section with architecture.md (sub-projects + source-tree layout) and testing.md (unit suites, W3C testsuite runner, fixture regeneration).

Numbers

  • 881 tests on the JVM (653 interpreter + 209 WASI + 19 CLI), all green.
  • 653 + 209 also green on Scala.js (Node 20+) and Scala Native (0.5.11).
  • 142 W3C manifests in the spec runner / ~53,000 assertions / 51,714 passing / 224 failing / 1,273 skipped.
  • 133 of 142 manifests fully green (up from 129 in 0.3.0). 5 manifests fully unlocked: names, exports, memory_grow, table_copy, table_grow.
  • 9 manifests pinned in KnownFailures (down from 13 in 0.3.0). Residual causes are wasm-3.0 typed-function-references / GC reftype short forms (0x63 / 0x64) — a separate proposal we don't implement.

Cross-build matrix

Target Version Runtime requirement
Scala 3.8.3
JVM Java 17 or newer
Scala.js 1.21.0 Node.js 20+
Scala Native 0.5.11 Clang

Try it

git clone https://github.com/edadma/wasm
cd wasm
sbt 'cliJVM/run examples/hello.wasm'
# Hello, world!

Or against a real WASI binary with a host preopen and the new --trace flag:

mkdir -p /tmp/sandbox
echo "Hello from the host filesystem" > /tmp/sandbox/hello.txt

sbt 'cliJVM/run --trace --preopen /tmp/sandbox:/sandbox \
                wasi/shared/src/test/resources/fixtures/real_rust_fileread.wasm'
# Hello from the host filesystem
# [trace] ops=... calls=... hostCalls=... throws=... traps=... maxDepth=...

Documentation

Full docs at https://edadma.github.io/wasm/:

  • Getting Started — install + run your first module
  • Concepts — the validator, host imports, traps and errors, the tracer
  • WASI — the 29 syscalls implemented and the three preopen flavours
  • CLI — every flag, dispatch rules, recipes
  • Reference — supported opcodes, binary sections, error variants
  • Spec compliance — W3C testsuite slice, what the runner caught, what's pinned
  • Development — sub-project layout, test invocation, W3C testsuite + fixture regeneration

License

ISC.

0.3.0 — five new proposals, W3C testsuite runner, 13 bug fixes

16 May 20:32

Choose a tag to compare

Second Maven Central release. Adds five new WebAssembly proposals on top of 0.1.1's MVP + first-generation post-MVP set, ships the W3C testsuite runner, fixes a dozen real interpreter bugs surfaced along the way, and brings full strict-validation conformance across the wasm-3.0 binary format. Same zero-runtime-dependency surface, same three-platform cross-build (JVM / Scala.js / Scala Native).

Install

libraryDependencies ++= Seq(
  "io.github.edadma" %%% "wasm"      % "0.3.0",
  "io.github.edadma" %%% "wasm-wasi" % "0.3.0",  // optional — only if you want the WASI shim
)
Coordinate What you get
io.github.edadma:wasm:0.3.0 The interpreter — Runtime.instantiate, ModuleInstance
io.github.edadma:wasm-wasi:0.3.0 The WASI Preview 1 host shim — depends on wasm

The wasm-cli runner is built from this repo but not published; it's a runnable example, not a library.

What's new since 0.1.1

Five new proposals

  • Exception handling — legacy form (Phase 7.E): try / catch tagidx / catch_all / throw / rethrow / delegate plus a new Section 13 (Tag) and import/export kind 0x04. Uncaught throws surface as WasmError.UncaughtException(tagIdx, args).
  • Exception handling — modern try_table form: opcode 0x1F with all four catch shapes, plus the exnref value type (wire byte 0x69) and throw_ref. Both EH forms coexist; the validator and runtime share one tag-dispatch path.
  • Tail-call proposal: return_call and return_call_indirect, with proper stack-frame replacement (not just a "do a regular call and return" shim) so deeply tail-recursive programs run in constant stack.
  • Relaxed SIMD (Phase 8.F): all 20 sub-opcodes (0x100..0x113) — i32x4.relaxed_dot_i8x16_i7x16_add_s and friends. Implementation matches the deterministic-where-possible side of the spec.
  • Threads / atomics proposal: 66 atomic ops covering load / store / rmw (add, sub, and, or, xor, xchg, cmpxchg) at every width (8 / 16 / 32 / 64) for both i32 and i64, plus memory.atomic.wait32 / wait64 / notify / fence and shared memory (limits flag bit 0x02). Single-thread semantics: wait on a matching value traps as "would block".

Spec proposal landings on the imports/globals side

  • Imported globals (kind 0x03): GlobalImport in the module model, resolved at instantiation from a new HostGlobal value on HostModule.globals: Map[String, HostGlobal].
  • Extended-const proposal: i32.add / i32.sub / i32.mul / i64.add / i64.sub / i64.mul are now legal inside a constant expression. The parser flattens the wire-format stack sequence into an expression tree; validator and runtime walk it recursively.
  • Relaxed const-expr (wasm-3.0 GC-proposal relaxation): global.get N in a const-expr can reference any earlier-defined immutable global, not just imports. Active data and element segment offsets accept the same forms.

W3C testsuite runner

A new JVM-only integrated runner consumes wast2json JSON manifests + .wasm blobs and dispatches assert_return / assert_trap / assert_invalid / assert_malformed against Runtime.instantiate + inst.invoke. The curated slice covers 142 manifests / ~53,000 assertions — the full SIMD proposal, bulk memory + tables + element segments, EH and tail-call proposals, binary-format and UTF-8 edge cases. 129 manifests fully green; 13 pinned in KnownFailures with documented feature gaps. See Spec compliance for the full table.

Thirteen interpreter bugs fixed (each surfaced by the runner)

  1. i32.trunc_f64_s over-rejected values strictly between -2^31 and -2^31 - 1 (off-by-one range check).
  2. MemArg.offset was Int, so a wasm u32 offset of 0xFFFFFFFF was stored as Java -1 and the OOB trap silently wrapped to low memory. Widened to Long.
  3. align immediate validation was missing for plain load / store (only the atomic path enforced it).
  4. if-without-else accepted mismatched params/results (the implicit empty else-branch needs startTypes == endTypes).
  5. v128.const wasn't accepted in const expressions ((global v128 (v128.const ...)) failed to parse).
  6. Untyped select (0x1B) rejected v128 operands — SIMD treats v128 as a numtype for select.
  7. i8x16.popcnt was completely unimplemented.
  8. i16x8.q15mulr_sat_s was completely unimplemented (only the relaxed-SIMD variant was present).
  9. try_table catch labels counted with the try_table on the label stack — off-by-one in both the validator and the runtime's throw-dispatch.
  10. Export-section validation was missing entirely — duplicate names silently shadowed each other, and out-of-range idx for any kind instantiated.
  11. Name-field UTF-8 validation was missing — new String(bytes, "UTF-8") silently replaced bad bytes with U+FFFD. Now strict RFC 3629.
  12. Seven small binary-format strictness rules — LEB128 minimal-encoding range checks (u32 / s32 / s64), section ID range, section order + uniqueness (Tag and DataCount have non-monotonic numeric IDs), section size mismatch, custom-section name overruns size, too-many-locals u32 overflow.
  13. Imported globals + extended-const + relaxed const-expr were not surfaced — three spec features that travel together, now landing as one drop with full validator + runtime support.

Other additions

  • Tracer hooks for opcode dispatch, function-frame transitions, throws, and traps. No-op default keeps zero overhead for the common case.

Numbers

  • 870 tests on the JVM (648 interpreter + 208 WASI + 14 CLI), all green.
  • 648 + 208 also green on Scala.js (Node 20+) and Scala Native (0.5.11).
  • 142 W3C manifests in the spec runner / ~53,000 assertions / 51,084 passing.
  • Deterministic across all three platforms: every i32 / i64 / f32 / f64 opcode produces bit-identical results, including IEEE-754 edge cases.

Cross-build matrix

Target Version Runtime requirement
Scala 3.8.3
JVM Java 17 or newer
Scala.js 1.21.0 Node.js 20+
Scala Native 0.5.11 Clang

Try it

git clone https://github.com/edadma/wasm
cd wasm
sbt 'cliJVM/run examples/hello.wasm'
# Hello, world!

Or against a real WASI binary with a host preopen:

mkdir -p /tmp/sandbox
echo "Hello from the host filesystem" > /tmp/sandbox/hello.txt

sbt 'cliJVM/run --preopen /tmp/sandbox:/sandbox \
                wasi/shared/src/test/resources/fixtures/real_rust_fileread.wasm'
# Hello from the host filesystem

Documentation

Full docs at https://edadma.github.io/wasm/:

  • Getting Started — install + run your first module
  • Concepts — the validator, host imports, traps and errors
  • WASI — the 29 syscalls implemented and the three preopen flavours
  • CLI--preopen, --invoke, --args, dispatch rules
  • Reference — supported opcodes, binary sections, error variants
  • Spec compliance — W3C testsuite slice, what the runner caught, what's pinned

License

ISC.

0.1.1 — first Maven Central release

15 May 11:04

Choose a tag to compare

First Maven Central release. Cross-built on JVM, Scala.js, and Scala Native; zero external runtime dependencies on the interpreter, one (the interpreter) on the WASI shim.

Install

libraryDependencies ++= Seq(
  "io.github.edadma" %%% "wasm"      % "0.1.1",
  "io.github.edadma" %%% "wasm-wasi" % "0.1.1",  // optional — only if you want the WASI shim
)
Coordinate What you get
io.github.edadma:wasm:0.1.1 The interpreter — Runtime.instantiate, ModuleInstance
io.github.edadma:wasm-wasi:0.1.1 The WASI Preview 1 host shim — depends on wasm

The wasm-cli runner is built from this repo but not published; it's a runnable example, not a library.

What's in the box

WebAssembly support: the Core MVP plus five post-MVP proposals — ~250 opcodes total.

  • Core MVP — every numeric opcode (i32 / i64 / f32 / f64 arithmetic, comparisons, conversions), the full control-flow surface (block / loop / if / br / br_if / br_table / call / call_indirect / return), memory load / store at every width, linear-memory grow, globals (mutable + immutable), tables, element + data segments, function imports + exports, the start function, and multi-value blocks / functions.
  • Sign-extensioni32.extend8_s, i32.extend16_s, i64.extend8_s, i64.extend16_s, i64.extend32_s.
  • Non-trapping float-to-int (Phase 8.A) — *.trunc_sat_f*_* for every shape.
  • Bulk-memory (Phase 8.B) — memory.init, memory.copy, memory.fill, data.drop, table.init, table.copy, elem.drop.
  • Reference types (Phase 8.C) — funcref + externref value types, ref.null / ref.is_null / ref.func, table.get / table.set / table.grow / table.size / table.fill, typed select t* (0x1C).
  • Multi-memory (Phase 8.D) — the memarg memidx flag, a vector of memories per module, multi-memory imports/exports, plus a HostFuncMulti host surface for hosts that need to look at the called memidx.
  • Fixed-width SIMD (Phase 8.E) — the complete SIMD proposal: V128 value type, v128.const, every load + store (including v128.load{8,16,32,64}_splat, v128.load*_zero, v128.load*x*_s/u, v128.load*_lane, v128.store*_lane), full lane access (splat / extract_lane / replace_lane / shuffle / swizzle), integer + float arithmetic, shifts, min/max, bitwise + reductions, comparisons, narrow / extend / extadd_pairwise / extmul, float ↔ int conversions, demote / promote, and i32x4.dot_i16x8_s.

WASI Preview 1: 24 syscalls implemented end-to-end against either an in-memory FS, a name-only stub preopen, or a real on-disk directory via HostPreopen.fromDir. Real rustc-built wasm32-wasip1 binaries run unchanged — println!, std::fs::read_to_string, and std::fs::write are committed as test fixtures and pass in CI.

Validation up front. Every imported module runs through a separate validator before any code executes. Bad binaries fail at Runtime.instantiate with a function <N>: byte offset 0x<hex>: <details> error, not at run time.

Numbers

  • 687 tests on the JVM (521 interpreter + 157 WASI + 9 CLI), all green.
  • 521 + 157 also green on Scala.js (Node 20+) and Scala Native (0.5.11).
  • Deterministic across all three platforms: every i32 / i64 / f32 / f64 opcode produces bit-identical results, including IEEE-754 edge cases.

External validation: the sysl compiler ships seven backends; one of them (wasm32-WASI) targets this interpreter through the WASI shim. sysl's full standard-library test suite — 973 cases across a large mixed workload — runs end-to-end on wasm 0.1.1 with zero divergence from the reference run on wasmtime.

Cross-build matrix

Target Version Runtime requirement
Scala 3.8.3
JVM Java 17 or newer
Scala.js 1.21.0 Node.js 20+
Scala Native 0.5.11 Clang

Try it

git clone https://github.com/edadma/wasm
cd wasm
sbt 'cliJVM/run examples/hello.wasm'
# Hello, world!

Or against a real WASI binary with a host preopen:

mkdir -p /tmp/sandbox
echo "Hello from the host filesystem" > /tmp/sandbox/hello.txt

sbt 'cliJVM/run --preopen /tmp/sandbox:/sandbox \
                wasi/shared/src/test/resources/fixtures/real_rust_fileread.wasm'
# Hello from the host filesystem

Documentation

Full docs at https://edadma.github.io/wasm/:

  • Getting Started — install + run your first module
  • Concepts — the validator, host imports, traps and errors
  • WASI — the 24 syscalls implemented and the three preopen flavours
  • CLI--preopen, --invoke, --args, dispatch rules
  • Reference — supported opcodes, binary sections, error variants

License

ISC.