Current section
Files
Jump to
Current section
Files
lib/firebird/errors.ex
defmodule Firebird.WasmError do
@moduledoc """
Exception raised when a WASM operation fails.
Provides structured error information with helpful messages.
## Examples
try do
Firebird.call!(wasm, :nonexistent, [1, 2])
rescue
e in Firebird.WasmError ->
IO.puts(e.message)
IO.inspect(e.reason)
end
"""
defexception [:message, :reason, :function, :wasm_path]
@type t :: %__MODULE__{
message: String.t(),
reason: term(),
function: String.t() | atom() | nil,
wasm_path: String.t() | nil
}
@impl true
def message(%{message: message}), do: message
@doc """
Create a WasmError for a file not found.
"""
def file_not_found(path) do
%__MODULE__{
message: """
WASM file not found: #{path}
Make sure the file exists. Common locations:
• priv/wasm/#{Path.basename(path)}
• wasm/#{Path.basename(path)}
To initialize a project: mix firebird.init
To scaffold a Rust module: mix firebird.init --rust
""",
reason: :enoent,
wasm_path: path
}
end
@doc """
Create a WasmError for a function not found.
"""
def function_not_found(function, available_exports) do
suggestion = suggest_function(to_string(function), available_exports)
%__MODULE__{
message: """
WASM function '#{function}' not found.
Available functions: #{Enum.join(Enum.map(available_exports, &to_string/1), ", ")}
#{if suggestion, do: "Did you mean: #{suggestion}?", else: ""}
""",
reason: :function_not_found,
function: function
}
end
@doc """
Create a WasmError for an argument count mismatch.
Provides a clear message showing expected vs actual arity,
helping developers quickly fix function call signatures.
## Examples
error = Firebird.WasmError.arity_mismatch(:add, 2, 1)
# => "WASM function 'add' expects 2 argument(s), but got 1.
# Signature: add/2
# Fix: Firebird.call(wasm, :add, [arg1, arg2])"
"""
@spec arity_mismatch(atom() | String.t(), non_neg_integer(), non_neg_integer()) :: t()
def arity_mismatch(function, expected, got) do
placeholders = Enum.map(1..max(expected, 1), fn i -> "arg#{i}" end) |> Enum.join(", ")
%__MODULE__{
message: """
WASM function '#{function}' expects #{expected} argument(s), but got #{got}.
Signature: #{function}/#{expected}
Fix: Firebird.call(wasm, :#{function}, [#{placeholders}])
""",
reason: {:arity_mismatch, expected: expected, got: got},
function: function
}
end
@doc """
Create a WasmError for a type mismatch on a specific argument.
Shows which argument position has the wrong type, what was expected,
and what was actually provided, with a concrete fix suggestion.
## Examples
error = Firebird.WasmError.type_mismatch(:add, 0, :i32, "hello")
# => "WASM function 'add' argument 0 has wrong type.
# Expected: i32 (integer)
# Got: \"hello\" (string)
# Fix: Pass an integer value instead."
"""
@spec type_mismatch(atom() | String.t(), non_neg_integer(), atom(), term()) :: t()
def type_mismatch(function, arg_index, expected_type, actual_value) do
actual_type = type_label(actual_value)
expected_label = wasm_type_hint(expected_type)
%__MODULE__{
message: """
WASM function '#{function}' argument #{arg_index} has wrong type.
Expected: #{expected_type} (#{expected_label})
Got: #{inspect(actual_value)} (#{actual_type})
Fix: Pass #{article(expected_label)} #{expected_label} value instead.
""",
reason: {:type_mismatch, arg_index, expected: expected_type, got: actual_type},
function: function
}
end
@doc """
Create a WasmError when calling a function on a stopped instance.
## Examples
error = Firebird.WasmError.instance_stopped(:add)
# => "WASM instance is no longer alive (cannot call 'add')..."
"""
@spec instance_stopped(atom() | String.t()) :: t()
def instance_stopped(function) do
%__MODULE__{
message: """
WASM instance is no longer alive (cannot call '#{function}').
The process may have been stopped or crashed.
Fix: Reload with Firebird.load/1 or use Firebird.with_instance/2 for auto-cleanup.
""",
reason: :instance_stopped,
function: function
}
end
@doc """
Create a WasmError when a function call fails at runtime.
Provides context about what went wrong and suggests debugging steps.
## Examples
error = Firebird.WasmError.call_failed(:add, :timeout, [5, 3])
"""
@spec call_failed(atom() | String.t(), term(), list()) :: t()
def call_failed(function, reason, args) do
reason_hint =
case reason do
{:exit, {:timeout, _}} ->
"The call timed out. Try increasing the timeout."
{:exit, _} ->
"The WASM instance process exited unexpectedly. Try reloading."
reason when is_binary(reason) ->
if String.contains?(reason, "number of params does not match") do
"Argument count mismatch — check the function signature with Firebird.function_type(wasm, :#{function})"
else
reason
end
other ->
inspect(other)
end
%__MODULE__{
message: """
WASM function '#{function}' call failed: #{reason_hint}
Called with args: #{inspect(args)}
Check the function signature: Firebird.function_type(wasm, :#{function})
""",
reason: {:call_failed, reason},
function: function
}
end
@doc """
Create a WasmError for a load failure.
"""
def load_failed(reason, path \\ nil) do
%__MODULE__{
message: """
Failed to load WASM module#{if path, do: " from #{path}", else: ""}: #{inspect(reason)}
Common causes:
• Invalid or corrupt .wasm file
• Module requires WASI — try: Firebird.load(path, wasi: true)
• Missing imports — check the module's requirements
""",
reason: reason,
wasm_path: path
}
end
defp type_label(value) when is_integer(value), do: "integer"
defp type_label(value) when is_float(value), do: "float"
defp type_label(value) when is_binary(value), do: "string"
defp type_label(value) when is_atom(value), do: "atom"
defp type_label(value) when is_list(value), do: "list"
defp type_label(value) when is_tuple(value), do: "tuple"
defp type_label(value) when is_map(value), do: "map"
defp type_label(_), do: "unknown"
defp wasm_type_hint(:i32), do: "integer"
defp wasm_type_hint(:i64), do: "integer"
defp wasm_type_hint(:f32), do: "float"
defp wasm_type_hint(:f64), do: "float"
defp wasm_type_hint(other), do: to_string(other)
defp article("integer"), do: "an"
defp article(_), do: "a"
defp suggest_function(name, available) do
available
|> Enum.map(fn a -> {to_string(a), String.jaro_distance(name, to_string(a))} end)
|> Enum.sort_by(fn {_, d} -> d end, :desc)
|> Enum.take(1)
|> Enum.filter(fn {_, d} -> d > 0.6 end)
|> case do
[{suggestion, _} | _] -> suggestion
[] -> nil
end
end
end