Skip to content

Latest commit

 

History

History
255 lines (202 loc) · 6.18 KB

File metadata and controls

255 lines (202 loc) · 6.18 KB

Elixir to WebAssembly Compilation Guide

Firebird can compile a subset of Elixir directly to WebAssembly. This enables running Elixir-authored logic as native WASM modules — useful for edge computing, sandboxed execution, and high-performance numeric workloads.

Compilation Pipeline

Elixir Source (.ex)
  → Elixir AST         (Code.string_to_quoted)
  → Firebird IR         (normalized intermediate representation)
  → Optimization        (constant folding, dead code elimination)
  → Tail Call Opt       (loop-based TCO for tail-recursive functions)
  → Type Inference      (i64/f64 type assignment)
  → Validation          (check compilable subset)
  → WAT                 (WebAssembly Text Format)
  → WASM binary         (via wat2wasm)

Quick Start

1. Write an Elixir module with @wasm true

# lib/wasm_modules/math.ex
defmodule MyMath do
  @wasm true
  def add(a, b), do: a + b

  @wasm true
  def fibonacci(0), do: 0
  def fibonacci(1), do: 1
  def fibonacci(n), do: fibonacci(n - 1) + fibonacci(n - 2)
end

2. Compile to WASM

# Compile all @wasm annotated modules
mix firebird.target

# Or compile specific files
mix firebird.compile lib/wasm_modules/math.ex

# WAT only (no binary)
mix firebird.target --wat-only

3. Load and use the compiled WASM

{:ok, instance} = Firebird.load("_build/wasm/MyMath.wasm")
{:ok, [8]} = Firebird.call(instance, "add", [5, 3])
{:ok, [55]} = Firebird.call(instance, "fibonacci", [10])

Supported Elixir Subset

Arithmetic

  • +, -, * (standard operators)
  • div/2, rem/2 (integer division and remainder)
  • Unary - (negation)

Comparison

  • ==, !=, <, >, <=, >=

Boolean Logic

  • and, or, not

Control Flow

  • if/else expressions
  • unless/else expressions
  • cond expressions
  • case expressions with literal patterns

Functions

  • Multi-clause function definitions
  • Pattern matching on integer literals
  • Guard clauses (when n > 0, when n >= 0, etc.)
  • Recursive functions
  • Tail-recursive functions (with TCO optimization)
  • Local function calls between exported functions

Variables

  • Variable assignment (x = expr)
  • Block expressions (multiple statements, last one returned)

Pipe Operator

  • a |> f(b) compiles to f(a, b)

Compiler Options

Via mix firebird.target

Flag Description
--output DIR Output directory (default: _build/wasm)
--files FILE,... Compile specific files
--dirs DIR,... Scan specific directories
--wat-only Generate WAT only, skip binary
--verbose Show WAT output and sizes
--verify Load and verify compiled WASM
--stats Show compilation statistics
--clean Remove WASM build artifacts
--optimize Enable IR optimizations
--tco Enable tail call optimization
--inline Enable function inlining

Via mix.exs configuration

def project do
  [
    firebird: [
      wasm_output: "_build/wasm",
      wasm_sources: ["lib/wasm_modules"],
      wat_only: false
    ]
  ]
end

Programmatic API

# Compile source string
{:ok, result} = Firebird.Compiler.compile_source(source,
  optimize: true,   # Enable IR optimizations
  tco: true,        # Enable tail call optimization
  inline: true,     # Enable function inlining
  wat_only: false    # Generate WASM binary
)

# result.wat  - WAT text
# result.wasm - WASM binary
# result.module - Module name atom

Optimization Passes

Constant Folding

Evaluates constant expressions at compile time:

# Before: a + (2 * 3)
# After:  a + 6

Dead Code Elimination

Removes unreachable branches:

# Before: if true, do: x, else: y
# After:  x

Strength Reduction

Replaces expensive operations:

# x * 8  →  x << 3  (shift left)
# x * 4  →  x << 2

Identity Elimination

Removes no-op operations:

# x + 0  →  x
# x * 1  →  x
# x * 0  →  0

Function Inlining

Replaces calls to small helper functions with their body:

# Before: quad(n) calls double(double(n))
# After:  quad(n) = (n * 2) * 2  (no function call overhead)
def double(n), do: n * 2
def quad(n), do: double(double(n))

Tail Call Optimization

Converts tail-recursive functions to loops:

# This won't stack overflow for large n:
def sum_acc(0, acc), do: acc
def sum_acc(n, acc), do: sum_acc(n - 1, acc + n)

# Compiles to a WASM loop instead of recursive calls

Limitations

Type Restrictions

  • Only i64 (64-bit integers) and f64 (64-bit floats)
  • No lists, maps, tuples, strings, or atoms
  • No binary/bitstring operations

No BEAM Runtime Features

  • No processes, GenServers, or OTP
  • No message passing (send/receive)
  • No IO operations
  • No standard library functions (Enum, String, etc.)
  • No hot code reloading

Pattern Matching

  • Only integer literal patterns and variable/wildcard patterns
  • No tuple, list, or map patterns
  • No binary patterns
  • No pin operator

Other

  • No closures or anonymous functions
  • No module attributes at runtime
  • No macros (they expand before compilation)
  • Integer range limited to i64 (-2^63 to 2^63-1)

Performance Notes

Compiled WASM modules have different performance characteristics than BEAM:

  • WASM strengths: Tight numeric loops, recursive algorithms, pure computation
  • BEAM strengths: Concurrency, pattern matching on complex data, IO, hot reloading

The mix firebird.bench task can compare WASM vs BEAM performance:

mix firebird.bench
mix firebird.bench --functions fibonacci,factorial

Troubleshooting

"wat2wasm not found"

Install the WebAssembly Binary Toolkit:

# macOS
brew install wabt

# Ubuntu/Debian
apt-get install wabt

"No compilable sources found"

Add @wasm true annotations before functions you want to compile:

@wasm true
def my_function(a, b), do: a + b

"Unsupported expression"

The expression uses features outside the compilable subset. Check the limitations above.

"Validation errors"

The IR validator found constructs that can't be compiled to WASM. Common causes:

  • Using lists, maps, or tuples
  • Calling functions from other modules
  • Using string operations