defmodule Firebird do @moduledoc """ Run WebAssembly from Elixir — load WASM modules written in Rust, Go, C, or any language and call them like native functions. ## Quick Start (30 seconds) # Verify setup Firebird.check!() # => :ok # One-liner: load → call → stop {:ok, 8} = Firebird.run_one("math.wasm", :add, [5, 3]) # Load once, call many wasm = Firebird.load!("math.wasm") 8 = Firebird.call_one!(wasm, :add, [5, 3]) 55 = Firebird.call_one!(wasm, :fibonacci, [10]) Firebird.stop(wasm) ## `use Firebird` — Declarative Wrappers defmodule MyApp.Math do use Firebird, wasm: "priv/wasm/math.wasm" wasm_fn :add, args: 2 wasm_fn :fibonacci, args: 1 end # Add to supervision tree: children = [MyApp.Math] # Then call: {:ok, [8]} = MyApp.Math.add(5, 3) Or auto-discover all exports: defmodule MyApp.Math do use Firebird, wasm: "priv/wasm/math.wasm", auto: true end ## API Overview | Function | Returns | |----------|---------| | `run_one(path, fn, args)` | `{:ok, value}` — one-shot, single value | | `run_one!(path, fn, args)` | `value` — one-shot, raises | | `call_one(wasm, fn, args)` | `{:ok, value}` — single value | | `call_one!(wasm, fn, args)` | `value` — raises | | `call(wasm, fn, args)` | `{:ok, [results]}` — list | | `call!(wasm, fn, args)` | `[results]` — raises | | `with_instance(path, fun)` | `{:ok, result}` — auto-cleanup | | `functions(wasm)` | `[%{name, params, results, arity}]` | | `validate(source)` | `:ok` or `{:error, reason}` | | `sample()` | `pid` — bundled sample for quick testing | See `Firebird.Pool` for concurrent execution, `Firebird.Lazy` for on-demand loading, `Firebird.Memory` for structured memory management (strings, typed arrays, arena allocation), and `Firebird.Module` for declarative wrappers. """ @doc """ Use Firebird in a module to create a WASM wrapper. This is a convenient shortcut for `use Firebird.Module`. ## Examples defmodule MyApp.Math do use Firebird, wasm: "priv/wasm/math.wasm" wasm_fn :add, args: 2 wasm_fn :multiply, args: 2 end All options from `Firebird.Module` are supported: - `:wasm` - Path to WASM file (required) - `:name` - Process name (default: module name) - `:auto` - Auto-discover exports (default: false) - `:opts` - Options passed to `Firebird.load/2` """ defmacro __using__(opts) do quote do use Firebird.Module, unquote(opts) end end @doc """ Load a WASM module from a file path or binary. ## Arguments - `wasm_source` - Path to a .wasm file (string) or raw WASM bytes (binary) - `opts` - Optional keyword list: - `:memory_limit` - Maximum memory in bytes (default: 64MB) - `:timeout` - Execution timeout in ms (default: 5000) - `:wasi` - Enable WASI support (default: false) ## Examples {:ok, instance} = Firebird.load("priv/wasm/math.wasm") {:ok, instance} = Firebird.load(wasm_bytes) {:ok, instance} = Firebird.load("app.wasm", wasi: true) """ @spec load(binary() | String.t(), keyword()) :: {:ok, pid()} | {:error, term()} def load(wasm_source, opts \\ []) do Firebird.Runtime.load(wasm_source, opts) end @doc """ Load a WASM module from the application's `priv/` directory. This is the idiomatic way to load WASM files that ship with your app. ## Examples # Loads from priv/wasm/math.wasm of your app {:ok, instance} = Firebird.load_priv(:my_app, "wasm/math.wasm") # Loads from priv/math.wasm of your app {:ok, instance} = Firebird.load_priv(:my_app, "math.wasm") """ @spec load_priv(atom(), String.t(), keyword()) :: {:ok, pid()} | {:error, term()} def load_priv(app, relative_path, opts \\ []) do path = Path.join(:code.priv_dir(app) |> to_string(), relative_path) load(path, opts) end @doc """ Load a WASM module from a file path explicitly. ## Examples {:ok, instance} = Firebird.load_file("fixtures/math.wasm") """ @spec load_file(String.t(), keyword()) :: {:ok, pid()} | {:error, term()} def load_file(path, opts \\ []) do Firebird.Runtime.load_file(path, opts) end @doc """ Load a WASM module using the SyncNif fast path (~2μs per-call overhead). Returns a reference (not a PID). Works with all `Firebird.Phoenix.*` modules. ~20x lower per-call overhead than `Firebird.load/2` which uses Wasmex (~40μs). ## Examples {:ok, instance} = Firebird.load_sync("fixtures/phoenix_router.wasm") {:ok, match} = Firebird.Phoenix.Router.match_compiled(instance, "GET", "/users/42") """ @spec load_sync(String.t()) :: {:ok, reference()} | {:error, term()} def load_sync(path) when is_binary(path) do resolved = Firebird.Runtime.resolve_wasm_path(path) Firebird.SyncNif.load(resolved) end @doc """ Call an exported function on a WASM instance. ## Examples {:ok, [8]} = Firebird.call(instance, :add, [5, 3]) {:ok, [55]} = Firebird.call(instance, "fibonacci", [10]) """ @spec call(pid(), atom() | String.t(), list()) :: {:ok, list()} | {:error, term()} def call(instance, function, args) when is_list(args) do Firebird.Runtime.call(instance, function, args) end def call(_instance, _function, args) when not is_list(args) do raise ArgumentError, """ Firebird.call/3 expects args as a list, got: #{inspect(args)} Example: Firebird.call(wasm, :add, [5, 3]) Firebird.call(wasm, :fibonacci, [10]) """ end @doc """ Call an exported function, raising on error. ## Examples [8] = Firebird.call!(instance, :add, [5, 3]) """ @spec call!(pid(), atom() | String.t(), list()) :: list() def call!(instance, function, args) when is_list(args) do Firebird.Runtime.call!(instance, function, args) end def call!(_instance, _function, args) when not is_list(args) do raise ArgumentError, """ Firebird.call!/3 expects args as a list, got: #{inspect(args)} Example: Firebird.call!(wasm, :add, [5, 3]) """ end @doc """ Call multiple functions in sequence. ## Examples {:ok, [[3], [12]]} = Firebird.call_many(instance, [ {:add, [1, 2]}, {:multiply, [3, 4]} ]) """ @spec call_many(pid(), [{atom() | String.t(), list()}]) :: {:ok, [list()]} | {:error, term()} def call_many(instance, calls) when is_list(calls) do Firebird.Runtime.call_many(instance, calls) end @doc """ Check if a function exists in the WASM module. ## Examples true = Firebird.function_exists?(instance, :add) false = Firebird.function_exists?(instance, :nonexistent) """ @spec function_exists?(pid(), atom() | String.t()) :: boolean() def function_exists?(instance, function) do Firebird.Runtime.function_exists?(instance, function) end @doc """ List all exported functions from a WASM instance. ## Examples exports = Firebird.exports(instance) # => [:add, :multiply, :fibonacci] """ @spec exports(pid()) :: [atom()] def exports(instance) do Firebird.Runtime.exports(instance) end @doc """ List all exports with their types. ## Examples all = Firebird.all_exports(instance) # => [{"add", :function}, {"memory", :memory}] """ @spec all_exports(pid()) :: [{String.t(), atom()}] def all_exports(instance) do Firebird.Runtime.all_exports(instance) end @doc """ List all exported functions with their type signatures. Returns a list of maps with `:name`, `:params`, `:results`, and `:arity` keys. This is a richer alternative to `exports/1` for understanding a module. ## Examples functions = Firebird.functions(instance) # => [ # %{name: :add, params: [:i32, :i32], results: [:i32], arity: 2}, # %{name: :fibonacci, params: [:i32], results: [:i32], arity: 1}, # ... # ] """ @spec functions(pid()) :: [map()] def functions(instance) do for func <- exports(instance) do case function_type(instance, func) do {:ok, {params, results}} -> %{name: func, params: params, results: results, arity: length(params)} _ -> %{name: func, params: [], results: [], arity: 0} end end end @doc """ Get the type signature of an exported function. ## Examples {:ok, {[:i32, :i32], [:i32]}} = Firebird.function_type(instance, :add) """ @spec function_type(pid(), atom() | String.t()) :: {:ok, {list(), list()}} | {:error, :not_found} def function_type(instance, function) do Firebird.Runtime.function_type(instance, function) end @doc """ Read bytes from WASM linear memory. ## Examples {:ok, bytes} = Firebird.read_memory(instance, 0, 10) """ @spec read_memory(pid(), non_neg_integer(), non_neg_integer()) :: {:ok, binary()} | {:error, term()} def read_memory(instance, offset, length) do Firebird.Runtime.read_memory(instance, offset, length) end @doc """ Write bytes to WASM linear memory. ## Examples :ok = Firebird.write_memory(instance, 0, <<1, 2, 3>>) """ @spec write_memory(pid(), non_neg_integer(), binary()) :: :ok | {:error, term()} def write_memory(instance, offset, data) do Firebird.Runtime.write_memory(instance, offset, data) end @doc """ Get the size of WASM linear memory in bytes. ## Examples {:ok, size} = Firebird.memory_size(instance) """ @spec memory_size(pid()) :: {:ok, non_neg_integer()} | {:error, term()} def memory_size(instance) do Firebird.Runtime.memory_size(instance) end @doc """ Grow WASM linear memory by pages (each page = 64KB). ## Examples {:ok, old_pages} = Firebird.grow_memory(instance, 1) """ @spec grow_memory(pid(), non_neg_integer()) :: {:ok, non_neg_integer()} | {:error, term()} def grow_memory(instance, pages) do Firebird.Runtime.grow_memory(instance, pages) end @doc """ Stop a WASM instance and free resources. ## Examples :ok = Firebird.stop(instance) """ @spec stop(pid() | reference()) :: :ok def stop(instance) when is_reference(instance) do # SyncNif references are garbage collected, no explicit stop needed :ok end def stop(instance) do Firebird.Runtime.stop(instance) catch :exit, _ -> :ok end @doc """ Check if a WASM instance is alive. ## Examples true = Firebird.alive?(instance) """ @spec alive?(pid()) :: boolean() def alive?(instance) do Firebird.Runtime.alive?(instance) end @doc """ Serialize a compiled module for caching. ## Examples {:ok, bytes} = Firebird.serialize(instance) """ @spec serialize(pid()) :: {:ok, binary()} | {:error, term()} def serialize(instance) do Firebird.Runtime.serialize(instance) end @doc """ Get a summary of a WASM instance: exports, memory size, alive status. ## Examples info = Firebird.info(instance) # => %{alive: true, exports: [:add, :multiply], memory_bytes: 65536} """ @spec info(pid()) :: map() def info(instance) do is_alive = alive?(instance) if is_alive do %{ alive: true, exports: exports(instance), memory_bytes: case memory_size(instance) do {:ok, size} -> size _ -> nil end } else %{alive: false, exports: [], memory_bytes: nil} end end # --------------------------------------------------------------------------- # Convenience APIs # --------------------------------------------------------------------------- @doc """ Load a WASM module, raising on error. ## Examples instance = Firebird.load!("priv/wasm/math.wasm") """ @spec load!(binary() | String.t(), keyword()) :: pid() def load!(wasm_source, opts \\ []) do case load(wasm_source, opts) do {:ok, pid} -> pid error -> raise_wasm_error!(nil, [], error) end end @doc """ Load a WASM module, call a function, and stop the instance — all in one step. Perfect for one-off computations where you don't need to keep the instance alive. ## Examples iex> Firebird.run("fixtures/math.wasm", :add, [5, 3]) {:ok, [8]} iex> Firebird.run("fixtures/math.wasm", :fibonacci, [10]) {:ok, [55]} """ @spec run(binary() | String.t(), atom() | String.t(), list(), keyword()) :: {:ok, list()} | {:error, term()} def run(wasm_source, function, args, opts \\ []) do with {:ok, pid} <- load(wasm_source, opts) do try do call(pid, function, args) after stop(pid) end end end @doc """ Load, call, and stop — raising on error. ## Examples iex> Firebird.run!("fixtures/math.wasm", :add, [5, 3]) [8] iex> Firebird.run!("fixtures/math.wasm", :fibonacci, [10]) [55] """ @spec run!(binary() | String.t(), atom() | String.t(), list(), keyword()) :: list() def run!(wasm_source, function, args, opts \\ []) do case run(wasm_source, function, args, opts) do {:ok, results} -> results error -> raise_wasm_error!(function, args, error) end end @doc """ Execute a block with a WASM instance, automatically cleaning up afterward. ## Examples iex> Firebird.with_instance("fixtures/math.wasm", fn instance -> ...> {:ok, [a]} = Firebird.call(instance, :add, [5, 3]) ...> {:ok, [b]} = Firebird.call(instance, :multiply, [a, 2]) ...> b ...> end) {:ok, 16} """ @spec with_instance(binary() | String.t(), (pid() -> term()), keyword()) :: {:ok, term()} | {:error, term()} def with_instance(wasm_source, fun, opts \\ []) when is_function(fun, 1) do case load(wasm_source, opts) do {:ok, pid} -> try do {:ok, fun.(pid)} after stop(pid) end {:error, _} = error -> error end end @doc """ Returns the Firebird library version. ## Examples iex> Firebird.version() "1.0.0" """ @spec version() :: String.t() def version do "1.0.0" end @doc """ Load a WASM module from bytes (binary data). Explicit API for loading from in-memory WASM bytes rather than file paths. Useful when you generate or fetch WASM dynamically. ## Examples bytes = File.read!("math.wasm") {:ok, instance} = Firebird.load_bytes(bytes) """ @spec load_bytes(binary(), keyword()) :: {:ok, pid()} | {:error, term()} def load_bytes(bytes, opts \\ []) when is_binary(bytes) do load(bytes, opts) end # --------------------------------------------------------------------------- # Pipe-friendly API # --------------------------------------------------------------------------- @doc """ Pipe-friendly call that threads the instance through. Returns `{:ok, results, instance}` so you can pipe further calls. ## Examples {:ok, instance} = Firebird.load("math.wasm") {:ok, [sum], instance} = Firebird.pipe(instance, :add, [5, 3]) {:ok, [product], instance} = Firebird.pipe(instance, :multiply, [sum, 2]) Firebird.stop(instance) # Or use the pipeline operator: {:ok, wasm} = Firebird.load("math.wasm") {:ok, [8], wasm} = Firebird.pipe(wasm, :add, [5, 3]) """ @spec pipe(pid(), atom() | String.t(), list()) :: {:ok, list(), pid()} | {:error, term()} def pipe(instance, function, args) when is_list(args) do case call(instance, function, args) do {:ok, results} -> {:ok, results, instance} error -> error end end @doc """ Pipe-friendly call that raises on error and returns `{results, instance}`. ## Examples wasm = Firebird.load!("math.wasm") {[sum], wasm} = Firebird.pipe!(wasm, :add, [5, 3]) {[product], wasm} = Firebird.pipe!(wasm, :multiply, [sum, 2]) Firebird.stop(wasm) """ @spec pipe!(pid(), atom() | String.t(), list()) :: {list(), pid()} def pipe!(instance, function, args) when is_list(args) do {call!(instance, function, args), instance} end # --------------------------------------------------------------------------- # Inspection convenience # --------------------------------------------------------------------------- @doc """ Inspect a WASM file and return information about its exports. Convenience wrapper around `Firebird.Inspector.inspect_file/2`. ## Examples {:ok, info} = Firebird.inspect_file("math.wasm") info.functions # => [%{name: "add", params: [:i32, :i32], results: [:i32], arity: 2}, ...] """ @spec inspect_file(String.t(), keyword()) :: {:ok, map()} | {:error, term()} defdelegate inspect_file(path, opts \\ []), to: Firebird.Inspector # --------------------------------------------------------------------------- # Pool convenience wrappers # --------------------------------------------------------------------------- @doc """ Start a pool of WASM instances for concurrent execution. ## Options - `:wasm` or `:wasm_path` - Path to WASM file - `:wasm_bytes` - Raw WASM bytes - `:size` or `:pool_size` - Number of instances (default: schedulers_online) - `:name` - Optional GenServer name ## Examples {:ok, pool} = Firebird.start_pool(wasm: "math.wasm", size: 4) {:ok, [8]} = Firebird.pool_call(pool, :add, [5, 3]) Firebird.stop_pool(pool) """ @spec start_pool(keyword()) :: {:ok, pid()} | {:error, term()} def start_pool(opts) do Firebird.Pool.start_link(opts) end @doc """ Call a WASM function using a pooled instance. ## Examples {:ok, [8]} = Firebird.pool_call(pool, :add, [5, 3]) """ @spec pool_call(GenServer.server(), atom() | String.t(), list()) :: {:ok, list()} | {:error, term()} def pool_call(pool, function, args) do Firebird.Pool.call(pool, function, args) end @doc """ Call a WASM function using a pooled instance, raising on error. ## Examples [8] = Firebird.pool_call!(pool, :add, [5, 3]) """ @spec pool_call!(GenServer.server(), atom() | String.t(), list()) :: list() def pool_call!(pool, function, args) do Firebird.Pool.call!(pool, function, args) end @doc """ Call a WASM function using a pooled instance, returning unwrapped single value. ## Examples {:ok, 8} = Firebird.pool_call_one(pool, :add, [5, 3]) """ @spec pool_call_one(GenServer.server(), atom() | String.t(), list()) :: {:ok, term()} | {:error, term()} def pool_call_one(pool, function, args) do Firebird.Pool.call_one(pool, function, args) end @doc """ Call a WASM function using a pooled instance, returning unwrapped single value, raising on error. ## Examples 8 = Firebird.pool_call_one!(pool, :add, [5, 3]) """ @spec pool_call_one!(GenServer.server(), atom() | String.t(), list()) :: term() def pool_call_one!(pool, function, args) do Firebird.Pool.call_one!(pool, function, args) end @doc """ Stop a pool and all its instances. """ @spec stop_pool(GenServer.server()) :: :ok def stop_pool(pool) do Firebird.Pool.stop(pool) catch :exit, _ -> :ok end # --------------------------------------------------------------------------- # WAT (WebAssembly Text) support # --------------------------------------------------------------------------- @doc """ Load a WASM module from WAT (WebAssembly Text Format) source code. This is great for prototyping and learning — write WASM in human-readable text format and load it directly. Requires `wat2wasm` to be installed. ## Examples {:ok, wasm} = Firebird.from_wat(\""" (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add) ) \""") {:ok, [8]} = Firebird.call(wasm, :add, [5, 3]) Firebird.stop(wasm) """ @spec from_wat(String.t(), keyword()) :: {:ok, pid()} | {:error, term()} def from_wat(wat_source, opts \\ []) do with {:ok, wasm_bytes} <- compile_wat(wat_source) do load(wasm_bytes, opts) end end @doc """ Load a WASM module from WAT source, raising on error. ## Examples wasm = Firebird.from_wat!(\""" (module (func (export "double") (param i32) (result i32) local.get 0 i32.const 2 i32.mul) ) \""") [10] = Firebird.call!(wasm, :double, [5]) """ @spec from_wat!(String.t(), keyword()) :: pid() def from_wat!(wat_source, opts \\ []) do case from_wat(wat_source, opts) do {:ok, pid} -> pid {:error, {:wat_compile_error, msg}} -> raise Firebird.WasmError.load_failed({:wat_compile_error, msg}) {:error, {:wat2wasm_not_found, _}} -> raise %Firebird.WasmError{ message: """ wat2wasm not found. Install it: • macOS: brew install wabt • Ubuntu: apt install wabt • Or download from: https://github.com/WebAssembly/wabt/releases """, reason: :wat2wasm_not_found } {:error, reason} -> raise Firebird.WasmError.load_failed(reason) end end @doc """ Compile WAT source to WASM binary bytes. ## Examples {:ok, bytes} = Firebird.compile_wat("(module)") """ @spec compile_wat(String.t()) :: {:ok, binary()} | {:error, term()} def compile_wat(wat_source) do case System.find_executable("wat2wasm") do nil -> {:error, {:wat2wasm_not_found, "Install wabt: brew install wabt (macOS) or apt install wabt (Ubuntu)"}} wat2wasm -> tmp_wat = Path.join(System.tmp_dir!(), "firebird_#{:erlang.unique_integer([:positive])}.wat") tmp_wasm = Path.rootname(tmp_wat) <> ".wasm" try do File.write!(tmp_wat, wat_source) case System.cmd(wat2wasm, [tmp_wat, "-o", tmp_wasm], stderr_to_stdout: true) do {_, 0} -> wasm_bytes = File.read!(tmp_wasm) {:ok, wasm_bytes} {error_output, _} -> {:error, {:wat_compile_error, error_output}} end after File.rm(tmp_wat) File.rm(tmp_wasm) end end end # --------------------------------------------------------------------------- # Quick describe # --------------------------------------------------------------------------- @doc """ Print a human-readable summary of a WASM file or instance. Works with file paths or running instances. ## Examples Firebird.describe("path/to/math.wasm") Firebird.describe(instance) """ @spec describe(pid() | String.t()) :: :ok def describe(instance) when is_pid(instance) do info = info(instance) IO.puts("🔥 WASM Instance (#{if info.alive, do: "alive", else: "stopped"})") IO.puts(" Memory: #{format_bytes(info.memory_bytes || 0)}") IO.puts(" Exports (#{length(info.exports)}):") for func <- info.exports do case function_type(instance, func) do {:ok, {params, results}} -> IO.puts( " #{func}(#{Enum.join(Enum.map(params, &inspect/1), ", ")}) → #{inspect(results)}" ) _ -> IO.puts(" #{func}") end end :ok end def describe(path) when is_binary(path) do # Use smart path resolution for bare filenames resolved = Firebird.Runtime.resolve_wasm_path(path) case inspect_file(resolved) do {:ok, info} -> IO.puts("🔥 #{Path.basename(resolved)} (#{format_bytes(info.size_bytes)})") IO.puts(" Functions (#{length(info.functions)}):") for f <- info.functions do IO.puts( " #{f.name}(#{Enum.join(Enum.map(f.params, &inspect/1), ", ")}) → #{inspect(f.results)}" ) end :ok {:error, reason} -> IO.puts("❌ Cannot inspect #{path}: #{inspect(reason)}") :ok end end defp format_bytes(0), do: "0 B" defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B" defp format_bytes(bytes) when bytes < 1_048_576, do: "#{Float.round(bytes / 1024, 1)} KB" defp format_bytes(bytes), do: "#{Float.round(bytes / 1_048_576, 1)} MB" # --------------------------------------------------------------------------- # Setup verification & bundled sample # --------------------------------------------------------------------------- @doc """ Returns the path to the bundled sample math.wasm file. This WASM module exports `add/2`, `multiply/2`, and `fibonacci/1` functions and is included with Firebird for testing and getting started quickly. ## Examples iex> path = Firebird.sample_wasm_path() iex> File.exists?(path) true iex> String.ends_with?(path, "sample_math.wasm") true """ @spec sample_wasm_path() :: String.t() def sample_wasm_path do Path.join(:code.priv_dir(:firebird), "wasm/sample_math.wasm") end @doc """ Load a pre-loaded sample WASM instance for quick experimentation. Shortcut for `Firebird.load!(Firebird.sample_wasm_path())`. Perfect for `iex` sessions: iex> wasm = Firebird.sample() iex> is_pid(wasm) and Process.alive?(wasm) true iex> Firebird.call_one!(wasm, :add, [5, 3]) 8 iex> Firebird.stop(wasm) :ok """ @spec sample() :: pid() def sample do load!(sample_wasm_path()) end @doc """ Validate that a WASM file or binary can be loaded without keeping the instance. Returns `:ok` if the WASM is valid and loadable, or `{:error, reason}` otherwise. Useful for build-time validation or CI checks. ## Examples iex> Firebird.validate("fixtures/math.wasm") :ok iex> match?({:error, _}, Firebird.validate("nonexistent_file.wasm")) true """ @spec validate(binary() | String.t()) :: :ok | {:error, term()} def validate(wasm_source) do case load(wasm_source) do {:ok, pid} -> stop(pid) :ok {:error, _} = error -> error end end @doc """ Verify that Firebird is correctly installed and working. Loads the bundled sample WASM module, calls `add(2, 3)`, and verifies the result is `[5]`. Returns `:ok` on success, raises on failure. Use this to quickly confirm your setup is working: iex> Firebird.check!() :ok ## What it checks 1. Wasmex NIF is loaded and working 2. WASM module loading works 3. Function calls work correctly 4. Results are returned properly """ @spec check!() :: :ok def check! do path = sample_wasm_path() unless File.exists?(path) do raise """ Firebird sample WASM file not found at: #{path} This usually means the :firebird dependency wasn't compiled properly. Try: mix deps.compile firebird --force """ end case run(path, :add, [2, 3]) do {:ok, [5]} -> :ok {:ok, other} -> raise "Firebird check failed: expected [5], got #{inspect(other)}" {:error, reason} -> raise """ Firebird check failed: #{inspect(reason)} Common fixes: • mix deps.get • mix deps.compile wasmex --force • Ensure your system has a compatible Rust toolchain for wasmex """ end end @doc """ Start a playground with a pre-loaded sample WASM instance. Returns a WASM instance loaded with the bundled sample math module. Perfect for exploring in `iex`: iex> wasm = Firebird.playground() iex> Firebird.call!(wasm, :add, [5, 3]) [8] iex> Firebird.call!(wasm, :fibonacci, [10]) [55] iex> Firebird.stop(wasm) :ok """ @spec playground() :: pid() def playground do wasm = load!(sample_wasm_path()) IO.puts("🔥 Firebird playground ready! Instance: #{inspect(wasm)}") IO.puts(" Try: Firebird.call!(wasm, :add, [5, 3])") IO.puts(" Try: Firebird.call!(wasm, :fibonacci, [10])") IO.puts(" Try: Firebird.describe(wasm)") IO.puts(" Done: Firebird.stop(wasm)") wasm end @doc """ Non-raising version of `check!/0`. Returns `:ok` or `{:error, reason}`. ## Examples iex> Firebird.check() :ok """ @spec check() :: :ok | {:error, term()} def check do check!() rescue e -> {:error, Exception.message(e)} end @doc """ Run a quick demo showing Firebird in action. Loads the bundled sample WASM, runs a few operations, and prints the results. Great for verifying setup in `iex`. Firebird.demo() # 🔥 Firebird Demo # Loading sample_math.wasm... # add(5, 3) = 8 # ... """ @spec demo() :: :ok def demo do path = sample_wasm_path() IO.puts("🔥 Firebird Demo") IO.puts(" Loading #{Path.basename(path)}...") {:ok, wasm} = load(path) exports = exports(wasm) IO.puts(" Exports: #{inspect(exports)}") {:ok, [sum]} = call(wasm, :add, [5, 3]) IO.puts(" add(5, 3) = #{sum}") {:ok, [product]} = call(wasm, :multiply, [4, 7]) IO.puts(" multiply(4, 7) = #{product}") {:ok, [fib]} = call(wasm, :fibonacci, [10]) IO.puts(" fibonacci(10) = #{fib}") stop(wasm) IO.puts(" ✅ Everything works!") :ok end # --------------------------------------------------------------------------- # Map convenience # --------------------------------------------------------------------------- @doc """ Map a WASM function over a list of argument lists. Returns a list of unwrapped single values. Perfect for batch processing. ## Examples wasm = Firebird.load!("math.wasm") results = Firebird.map(wasm, :fibonacci, [[1], [2], [3], [4], [5]]) # => [1, 1, 2, 3, 5] results = Firebird.map(wasm, :add, [[1, 2], [3, 4], [5, 6]]) # => [3, 7, 11] Firebird.stop(wasm) """ @spec map(pid(), atom() | String.t(), [list()]) :: [term()] def map(instance, function, args_list) when is_list(args_list) do Enum.map(args_list, fn args -> call_one!(instance, function, args) end) end # --------------------------------------------------------------------------- # Single-value convenience APIs # --------------------------------------------------------------------------- @doc """ Call a WASM function and return a single unwrapped value. Most WASM functions return exactly one value. This avoids the `[value]` list wrapping that `call/3` uses. For multi-value returns, use `call/3`. ## Examples {:ok, 8} = Firebird.call_one(instance, :add, [5, 3]) {:ok, 55} = Firebird.call_one(instance, :fibonacci, [10]) """ @spec call_one(pid(), atom() | String.t(), list()) :: {:ok, term()} | {:error, term()} def call_one(instance, function, args) do case call(instance, function, args) do {:ok, [single]} -> {:ok, single} {:ok, multi} -> {:ok, multi} error -> error end end @doc """ Call a WASM function and return a single unwrapped value, raising on error. ## Examples 8 = Firebird.call_one!(instance, :add, [5, 3]) 55 = Firebird.call_one!(instance, :fibonacci, [10]) """ @spec call_one!(pid(), atom() | String.t(), list()) :: term() def call_one!(instance, function, args) do case call_one(instance, function, args) do {:ok, result} -> result {:error, {:call_error, func_name, :instance_stopped}} -> raise Firebird.WasmError.instance_stopped(func_name) {:error, {:call_error, func_name, reason}} -> if Process.alive?(instance) do available = exports(instance) if to_string(func_name) not in Enum.map(available, &to_string/1) do raise Firebird.WasmError.function_not_found(func_name, available) else raise Firebird.WasmError.call_failed(func_name, reason, args) end else raise Firebird.WasmError.instance_stopped(func_name) end {:error, reason} -> raise Firebird.WasmError.call_failed(function, reason, args) end end @doc """ One-shot: load, call (single-value), and stop — all in one step. Like `run/4` but unwraps single-value results automatically. ## Examples iex> Firebird.run_one("fixtures/math.wasm", :add, [5, 3]) {:ok, 8} iex> Firebird.run_one("fixtures/math.wasm", :fibonacci, [10]) {:ok, 55} """ @spec run_one(binary() | String.t(), atom() | String.t(), list(), keyword()) :: {:ok, term()} | {:error, term()} def run_one(wasm_source, function, args, opts \\ []) do case run(wasm_source, function, args, opts) do {:ok, [single]} -> {:ok, single} {:ok, multi} -> {:ok, multi} error -> error end end @doc """ One-shot: load, call (single-value), and stop — raising on error. ## Examples iex> Firebird.run_one!("fixtures/math.wasm", :add, [5, 3]) 8 iex> Firebird.run_one!("fixtures/math.wasm", :fibonacci, [10]) 55 """ @spec run_one!(binary() | String.t(), atom() | String.t(), list(), keyword()) :: term() def run_one!(wasm_source, function, args, opts \\ []) do case run_one(wasm_source, function, args, opts) do {:ok, result} -> result error -> raise_wasm_error!(function, args, error) end end # --------------------------------------------------------------------------- # Result unwrapping # --------------------------------------------------------------------------- @doc """ Unwrap a single-value WASM result. WASM functions return results as lists. This pipes nicely to extract single values: {:ok, 8} = Firebird.call(wasm, :add, [5, 3]) |> Firebird.unwrap() {:ok, 8} = Firebird.run("math.wasm", :add, [5, 3]) |> Firebird.unwrap() """ @spec unwrap({:ok, list()} | {:error, term()}) :: {:ok, term()} | {:error, term()} defdelegate unwrap(result), to: Firebird.Unwrap @doc """ Unwrap a single-value WASM result from a bang call. 8 = Firebird.call!(wasm, :add, [5, 3]) |> Firebird.unwrap!() 8 = Firebird.run!("math.wasm", :add, [5, 3]) |> Firebird.unwrap!() """ @spec unwrap!(list()) :: term() defdelegate unwrap!(result), to: Firebird.Unwrap # --------------------------------------------------------------------------- # Builder API # --------------------------------------------------------------------------- @doc """ Build a WASM module from a source directory (Go or Rust). Auto-detects the project type and compiles using the appropriate toolchain. ## Examples {:ok, wasm_path} = Firebird.build("src/go_math") {:ok, wasm_path} = Firebird.build("src/rust_math", output: "priv/wasm/math.wasm") """ @spec build(String.t(), keyword()) :: {:ok, String.t()} | {:error, term()} defdelegate build(source_dir, opts \\ []), to: Firebird.Builder @doc """ Build a WASM module and immediately load it. Auto-detects WASI requirements for Go projects. ## Examples {:ok, instance} = Firebird.build_and_load("fixtures/go_math") {:ok, [8]} = Firebird.call(instance, :add, [5, 3]) """ @spec build_and_load(String.t(), keyword()) :: {:ok, pid()} | {:error, term()} defdelegate build_and_load(source_dir, opts \\ []), to: Firebird.Builder # --------------------------------------------------------------------------- # Cache API # --------------------------------------------------------------------------- @doc """ Load a WASM module with caching for fast subsequent loads. On first load, compiles the module and caches the serialized form. On subsequent loads, deserializes from cache (significantly faster). ## Examples {:ok, instance} = Firebird.load_cached("math.wasm") {:ok, instance} = Firebird.load_cached("math.wasm", cache_dir: "/tmp/cache") """ @spec load_cached(String.t(), keyword()) :: {:ok, pid()} | {:error, term()} defdelegate load_cached(wasm_path, opts \\ []), to: Firebird.Cache # --------------------------------------------------------------------------- # Preloader API # --------------------------------------------------------------------------- @doc """ Precompile a WASM module for fast multi-instance creation. Compiles the WASM bytes once, then allows creating instances without recompilation. Essential for pools and concurrent execution. ## Examples {:ok, compiled} = Firebird.precompile("math.wasm") {:ok, inst1} = Firebird.instantiate(compiled) {:ok, inst2} = Firebird.instantiate(compiled) """ @spec precompile(binary() | String.t(), keyword()) :: {:ok, Firebird.Preloader.compiled()} | {:error, term()} defdelegate precompile(wasm_source, opts \\ []), to: Firebird.Preloader, as: :compile @doc """ Create a new instance from a precompiled WASM module. Much faster than `load/2` because it skips compilation. ## Examples {:ok, compiled} = Firebird.precompile("math.wasm") {:ok, instance} = Firebird.instantiate(compiled) """ @spec instantiate(Firebird.Preloader.compiled()) :: {:ok, pid()} | {:error, term()} defdelegate instantiate(compiled), to: Firebird.Preloader # --------------------------------------------------------------------------- # Memory Management API # --------------------------------------------------------------------------- @doc """ Create a structured memory allocator for a WASM instance. Returns a `Firebird.Memory` struct with typed read/write helpers, bump allocation, and arena-style deallocation. Essential for passing strings, arrays, and complex data to/from Go/Rust WASM modules. ## Options - `:alignment` — byte alignment for allocations (default: 8) - `:base_offset` — starting offset in linear memory (default: 1024) ## Examples wasm = Firebird.load!("string_utils.wasm") alloc = Firebird.memory_allocator(wasm) # Write string, call WASM, read result {alloc, ptr, len} = Firebird.Memory.write_string(alloc, "hello") {:ok, [hash]} = Firebird.call(wasm, :hash, [ptr, len]) # Arena-style cleanup alloc = Firebird.Memory.free_all(alloc) See `Firebird.Memory` for the full API including typed arrays, scalar helpers, secure free, and scoped arenas. """ @spec memory_allocator(pid(), keyword()) :: Firebird.Memory.t() def memory_allocator(instance, opts \\ []) do Firebird.Memory.new(instance, opts) end # --------------------------------------------------------------------------- # Pipeline API # --------------------------------------------------------------------------- @doc """ Create a new WASM function pipeline for chaining calls. ## Examples {:ok, [result]} = Firebird.pipeline(instance) |> Firebird.Pipeline.call(:add, [5, 3]) |> Firebird.Pipeline.call(:multiply, [:_, 2]) |> Firebird.Pipeline.run() """ @spec pipeline(pid()) :: Firebird.Pipeline.t() def pipeline(instance), do: Firebird.Pipeline.new(instance) # --------------------------------------------------------------------------- # Quick / Inline Execution # --------------------------------------------------------------------------- @doc """ Execute a WASM function with the absolute minimum ceremony. Accepts a `.wasm` file path (auto-resolved) or inline WAT source. Returns the single unwrapped value. ## Examples # With a file — auto-resolves from priv/wasm, wasm/, fixtures/ 8 = Firebird.quick("math.wasm", :add, [5, 3]) # With inline WAT 42 = Firebird.quick(~s|(module (func (export "answer") (result i32) i32.const 42))|, :answer, []) # With the bundled sample 55 = Firebird.quick(:sample, :fibonacci, [10]) """ @spec quick(String.t() | :sample, atom() | String.t(), list()) :: term() def quick(:sample, function, args) do run_one!(sample_wasm_path(), function, args) end def quick(source, function, args) when is_binary(source) do # Check if it looks like WAT (starts with parenthesis) trimmed = String.trim(source) if String.starts_with?(trimmed, "(") do {:ok, instance} = from_wat(source) try do call_one!(instance, function, args) after stop(instance) end else run_one!(source, function, args) end end # --------------------------------------------------------------------------- # Introspection helpers # --------------------------------------------------------------------------- @doc """ List the function signatures of all exports in a WASM file. Loads the file, inspects it, and stops the instance. Useful for exploring unfamiliar WASM modules. ## Examples Firebird.list_functions("math.wasm") # => [ # %{name: :add, params: [:i32, :i32], results: [:i32], arity: 2}, # %{name: :multiply, params: [:i32, :i32], results: [:i32], arity: 2}, # %{name: :fibonacci, params: [:i32], results: [:i32], arity: 1} # ] """ @spec list_functions(String.t(), keyword()) :: [map()] | {:error, term()} def list_functions(path, opts \\ []) do case load(path, opts) do {:ok, instance} -> fns = functions(instance) stop(instance) fns {:error, _} = error -> error end end @doc """ Print a status overview of Firebird. Shows version, runtime status, and available WASM files. Useful for debugging and verifying your setup. Firebird.status() # 🔥 Firebird 1.0.0 # ✅ Runtime: operational # ... """ @spec status() :: :ok def status do IO.puts("🔥 Firebird #{version()}") IO.puts("") # Check runtime case check() do :ok -> IO.puts(" ✅ Runtime: working") {:error, reason} -> IO.puts(" ❌ Runtime: #{reason}") end # Check tools tools = Firebird.Builder.check_tools() IO.puts(" 🔧 Tools:") for {name, version} <- tools do status = if version, do: "✅ #{version}", else: "❌ not found" IO.puts(" #{name}: #{status}") end # Check WASM directories IO.puts(" 📁 WASM files:") for dir <- ["priv/wasm", "wasm", "fixtures"] do if File.dir?(dir) do wasm_files = Path.wildcard(Path.join(dir, "*.wasm")) if length(wasm_files) > 0 do IO.puts(" #{dir}/: #{length(wasm_files)} file(s)") end end end # Sample IO.puts("") IO.puts(" Try: Firebird.quick(:sample, :add, [5, 3])") :ok end # --------------------------------------------------------------------------- # Shared error raising for bang functions # --------------------------------------------------------------------------- # Raises the appropriate WasmError for a given error tuple. # Centralizes error dispatch so all bang functions (run!, run_one!, etc.) # produce the same helpful, structured error messages as call!/3. @doc false defp raise_wasm_error!(function, args, error) do case error do {:error, {:file_error, :enoent, path}} -> raise Firebird.WasmError.file_not_found(path) {:error, {:file_error, reason, path}} -> raise Firebird.WasmError.load_failed(reason, path) {:error, {:wasm_error, reason}} -> raise Firebird.WasmError.load_failed(reason) {:error, {:arity_mismatch, func_name, expected: expected, got: got}} -> raise Firebird.WasmError.arity_mismatch(func_name, expected, got) {:error, {:type_mismatch, func_name, arg_index, expected: expected_type, got: actual_value}} -> raise Firebird.WasmError.type_mismatch(func_name, arg_index, expected_type, actual_value) {:error, {:call_error, func_name, :instance_stopped}} -> raise Firebird.WasmError.instance_stopped(func_name) {:error, {:call_error, func_name, reason}} -> raise Firebird.WasmError.call_failed(func_name, reason, args) {:error, reason} -> raise Firebird.WasmError.call_failed(function, reason, args) end end end