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:
- Core → parser (side repo): the core depends on the published
{wa_parser, "~> 0.1.3"}hex package;rebar3fetches it from hex. To develop against a sibling../wa_parsercheckout instead, opt in manually with a rebar3 checkout (mkdir -p _checkouts && ln -sfn ../../wa_parser _checkouts/wa_parser); a checkout takes precedence over the hex dep. Nothing wires it for you (remove it before packaging). - Wrapper → core (in-repo): the wrapper depends on the published
{:wa_embedder, "~> 0.3"}hex package by default. To build against the Erlang core at the repo root instead, opt in manually by exportingWA_EMBEDDER_PATH=..beforemix(e.g.WA_EMBEDDER_PATH=.. mix test).
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.