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.1"}]}.

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"} 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.1"} 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).

License

Released into the public domain under the Unlicense.