Current section
Files
Jump to
Current section
Files
lib/firebird/target/guard.ex
defmodule Firebird.Target.Guard do
@moduledoc """
Compile-time guard for validating WASM compilability.
Provides functions to check if Elixir code fits within the
WASM-compilable subset before attempting compilation.
## Usage
Check individual expressions:
Guard.compilable_expr?(quote do: a + b) # true
Guard.compilable_expr?(quote do: Enum.map(x)) # false
Check functions:
Guard.compilable_function?(quote do
def add(a, b), do: a + b
end)
Check full modules:
Guard.compilable_module?(source_code)
## What's Compilable
The WASM-compilable subset of Elixir includes:
- Integer arithmetic: `+`, `-`, `*`, `div`, `rem`
- Comparisons: `==`, `!=`, `<`, `>`, `<=`, `>=`
- Boolean operators: `and`, `or`, `not`
- Control flow: `if/else`, `case`, `cond`
- Pattern matching on integer literals
- Multi-clause functions with guards
- Recursive function calls
- Local function calls within the same module
NOT compilable:
- Lists, maps, tuples, strings, atoms (runtime)
- Process operations, IO, OTP behaviors
- Standard library calls (Enum, String, etc.)
- Imports, aliases, macros
"""
@supported_operators [:+, :-, :*, :div, :rem, :==, :!=, :<, :>, :<=, :>=, :and, :or, :not]
@supported_control [:if, :cond, :case]
@doc """
Check if a source code string is compilable to WASM.
This performs a quick static analysis without running the full
compilation pipeline.
"""
@spec compilable_source?(String.t()) :: boolean()
def compilable_source?(source) when is_binary(source) do
case Code.string_to_quoted(source, columns: true) do
{:ok, ast} -> compilable_ast?(ast)
{:error, _} -> false
end
end
@doc """
Check if an AST is compilable to WASM.
"""
@spec compilable_ast?(Macro.t()) :: boolean()
def compilable_ast?(ast) do
{_, issues} =
Macro.prewalk(ast, [], fn node, acc ->
case check_node(node) do
:ok -> {node, acc}
{:error, reason} -> {node, [reason | acc]}
end
end)
Enum.empty?(issues)
end
@doc """
Get a list of issues preventing WASM compilation.
Returns `[]` if the code is fully compilable.
"""
@spec check_issues(String.t()) :: [String.t()]
def check_issues(source) when is_binary(source) do
case Code.string_to_quoted(source, columns: true) do
{:ok, ast} ->
ast_issues(ast)
{:error, {meta, msg, token}} ->
line = if is_tuple(meta), do: elem(meta, 0), else: 0
["Parse error at line #{line}: #{msg}#{token}"]
end
end
@doc """
Check if a specific expression AST is compilable.
"""
@spec compilable_expr?(Macro.t()) :: boolean()
def compilable_expr?(expr) do
case check_node(expr) do
:ok -> true
{:error, _} -> false
end
end
@doc """
List supported operators for WASM compilation.
"""
@spec supported_operators() :: [atom()]
def supported_operators, do: @supported_operators
@doc """
List supported control flow constructs.
"""
@spec supported_control_flow() :: [atom()]
def supported_control_flow, do: @supported_control
# Private - check individual AST nodes
defp ast_issues(ast) do
{_, issues} =
Macro.prewalk(ast, [], fn node, acc ->
case check_node(node) do
:ok -> {node, acc}
{:error, reason} -> {node, [reason | acc]}
end
end)
Enum.reverse(issues) |> Enum.uniq()
end
# Module definitions are ok
defp check_node({:defmodule, _, _}), do: :ok
defp check_node({:def, _, _}), do: :ok
defp check_node({:defp, _, _}), do: :ok
# Module attributes (like @wasm true)
defp check_node({:@, _, _}), do: :ok
# Supported operators
defp check_node({op, _, _}) when op in @supported_operators, do: :ok
# Supported control flow
defp check_node({ctrl, _, _}) when ctrl in @supported_control, do: :ok
# do/end blocks
defp check_node(do: _), do: :ok
defp check_node(do: _, else: _), do: :ok
# Integer and float literals
defp check_node(n) when is_integer(n), do: :ok
defp check_node(n) when is_float(n), do: :ok
# Variables
defp check_node({name, _, context}) when is_atom(name) and is_atom(context), do: :ok
# Blocks
defp check_node({:__block__, _, _}), do: :ok
defp check_node({:__aliases__, _, _}), do: :ok
# when clauses (guards)
defp check_node({:when, _, _}), do: :ok
# fn/& captures (not supported but not worth flagging in module context)
defp check_node({:fn, _, _}), do: {:error, "Anonymous functions not supported in WASM"}
defp check_node({:&, _, _}), do: {:error, "Capture operator not supported in WASM"}
# Process operations
defp check_node({:spawn, _, _}), do: {:error, "Process spawning not supported in WASM"}
defp check_node({:send, _, _}), do: {:error, "Message passing not supported in WASM"}
defp check_node({:receive, _, _}), do: {:error, "Receive blocks not supported in WASM"}
# Unsupported constructs
defp check_node({:try, _, _}), do: {:error, "Try/rescue not supported in WASM"}
defp check_node({:raise, _, _}), do: {:error, "Exceptions not supported in WASM"}
defp check_node({:with, _, _}), do: {:error, "With expressions not supported in WASM"}
defp check_node({:for, _, _}), do: {:error, "Comprehensions not supported in WASM"}
defp check_node({:import, _, _}), do: {:error, "Imports not supported in WASM"}
defp check_node({:use, _, _}), do: {:error, "Use macros not supported in WASM"}
# Remote calls (Module.func)
defp check_node({{:., _, [{:__aliases__, _, parts}, _func]}, _, _args}) do
module = Enum.join(parts, ".")
{:error, "Remote calls not supported in WASM: #{module}"}
end
# Erlang module calls (:module.func)
defp check_node({{:., _, [mod, _func]}, _, _args}) when is_atom(mod) do
{:error, "Erlang calls not supported in WASM: :#{mod}"}
end
# Data structures
defp check_node({:%{}, _, _}), do: {:error, "Maps not supported in WASM"}
defp check_node({:{}, _, _}), do: {:error, "Tuples not supported in WASM"}
# Lists
defp check_node(list) when is_list(list) do
# Lists can appear as AST metadata, so only flag non-empty runtime lists
if Enum.any?(list, fn
# keyword list (metadata)
{key, _} when is_atom(key) -> false
_ -> true
end) do
# Could be AST metadata
:ok
else
:ok
end
end
# Strings/atoms (in non-metadata context)
# Strings in AST are often module names
defp check_node(s) when is_binary(s), do: :ok
# Atoms appear everywhere in AST
defp check_node(a) when is_atom(a), do: :ok
# Catch-all: allow unknown nodes (they may be structural AST elements)
defp check_node(_), do: :ok
end