wa_embedder

A WebAssembly-to-BEAM compiler for the Erlang ecosystem, written in Erlang. It parses a .wasm binary (via wa_parser), translates each function to Erlang abstract format, and compiles and loads the result as a native BEAM module that you can call directly.

This is the Erlang package. Elixir projects can depend on it directly, or use the companion wa_embedder_ex package for an idiomatic Elixir API (WaEmbedder.compile/1..4 + WaEmbedder.ImportError).

Repository layout (monorepo)

Two hex packages live in this repository:

Path Package Tool Role
repo root wa_embedder rebar3 The compiler core (pure Erlang)
wa_embedder_ex/ wa_embedder_ex mix Thin Elixir wrapper + the test suite
test_data/ — cmake Shared WAT/C .wasm fixtures

The dependency direction is one-way: wa_embedder_ex (Elixir) → wa_embedder (Erlang) → wa_parser (Erlang). The core has no Elixir dependency.

Installation (rebar3)

%% rebar.config
{deps, [{wa_embedder, "~> 0.3"}]}.

Usage

{module, Mod} = wa_embedder:compile("module.wasm"),
Result = Mod:some_exported_fun(Arg).

compile/1,2,3,4 compiles and loads the module and returns the code:load_binary/3 result ({module, ModuleName} on success). Modules with imports take an imports map resolving each WASM import to an Erlang target (MFA tuple, external fun, or closure); unsatisfiable imports raise erlang:error({import_error, Map}).

Float values on the BEAM

The BEAM cannot represent an infinity or a NaN: no literal spells one, binary_to_term/1 rejects the bit patterns, and even 1.0e308 * 10.0 raises badarith. A wasm f32/f64 is therefore an ordinary Erlang float() for as long as it is finite, and one of

'+inf' | '-inf' | {nan, Payload} | {'-nan', Payload}

once it is not — the very shapes wa_parser 0.1.3 decodes a non-finite constant to. Payload is the raw fraction field with the quiet bit included (16#400000 for a bare f32 nan, 16#8000000000000 for a bare f64 one), so abs, neg, copysign, reinterpret, f32x4.extract_lane and a memory round trip preserve the bits exactly. An arithmetic operation that produces a NaN returns the canonical quiet NaN of its width, whose payload the spec leaves unspecified.

{module, Mod} = wa_embedder:compile("quickjs.wasm"),
Mod:some_f64_function()  %=> :"+inf" | {:nan, 16#8000000000000} | 1.5

Host functions bound to a wasm import see the same representation, so a host that passes an f64 argument back must be able to produce those terms.

Overflow of finite operands is a separate matter and unchanged: f64.add(1.7e308, 1.7e308) still raises badarith, because the BEAM cannot hold the result either.

Development

A nix develop shell provides erlang, elixir, rebar3, and the fixture toolchain (cmake, ninja, wabt, binaryen). Dependencies default to their published hex releases:

Build and test

# Erlang core (repo root) — uses the published wa_parser hex dep
rebar3 compile

# Elixir wrapper + full fixture suite (in wa_embedder_ex/).
# The core is not on hex yet, so build the wrapper against the in-repo core:
cd wa_embedder_ex && WA_EMBEDDER_PATH=.. mix test

The test suite lives in wa_embedder_ex/test/ and compiles the WAT/C fixtures under the repo-root test_data/ (built by CMake/Ninja before the suite runs). Once wa_embedder is published, mix test works without WA_EMBEDDER_PATH.

Building the fixtures manually

cd test_data
cmake -B build -G Ninja
ninja -C build            # WAT fixtures (default); `ninja -C build c_fixtures` for the C chain

Packaging

# Core: uses the published {wa_parser, "~> 0.1.3"} dep. (If you added a manual
# _checkouts override for parser dev, remove it first: rm -rf _checkouts.)
rebar3 hex build            # or: make package

# Wrapper: unset WA_EMBEDDER_PATH so the {:wa_embedder, "~> 0.3"} hex dep is used
cd wa_embedder_ex && env -u WA_EMBEDDER_PATH mix hex.build   # or: make package

Publish order: publish wa_embedder (core) first, then wa_embedder_ex (the wrapper's hex dep on the core cannot resolve until the core is on hex).

Bumping the version

The two packages ship as one release, so their versions move together — along with the changelog heading:

make version VERSION=0.3.1

That writes vsn into src/wa_embedder.app.src, @version into wa_embedder_ex/mix.exs, and moves the accumulated ## [Unreleased] entries in CHANGELOG.md under a ## [0.3.1] - <date> heading.

License

Released into the public domain under the Unlicense.