ExPanda (ExPanda v0.2.0)

View Source

Full macro expansion for Elixir AST introspection.

ExPanda takes Elixir source code (or a pre-parsed AST) and produces an AST where all macros have been expanded to their underlying forms, while preserving structural constructs (defmodule, def/defp) as-is.

It uses the Elixir compiler's internal :elixir_expand.expand/3 as the primary expansion engine, with a fallback to Macro.expand/2 for environments where the internal API is unavailable.

Quick Start

# Expand a source code string
{:ok, expanded} = ExPanda.expand_string("unless true, do: :never")
# => {:case, _, [true, [do: [...]]]}

# Expand a file
{:ok, expanded} = ExPanda.expand_file("lib/my_module.ex")

# Expand a pre-parsed AST
{:ok, ast} = Code.string_to_quoted("1 |> to_string()")
{:ok, expanded} = ExPanda.expand(ast)
# => {{:., _, [String.Chars, :to_string]}, _, [1]}

Structural Preservation

defmodule and def/defp forms are kept intact in the output. Only their bodies are expanded:

{:ok, expanded} = ExPanda.expand_string("""
  defmodule Foo do
    def bar(x), do: unless(x, do: :fallback)
  end
  """)
# defmodule is preserved, but `unless` inside bar's body is expanded to `case`

Unexpandable Macros

When a macro cannot be expanded (e.g., the target module is not loaded), the original node is kept with an @unexpanded error marker prepended:

{:__block__, [], [
  {:@, [], [{:unexpanded, [], ["use/1: ..."]}]},
  {:use, [], [{:__aliases__, [], [:SomeUnloadedLib]}]}
]}

Summary

Functions

Expand all macros in a pre-parsed Elixir AST.

Expand all macros in a pre-parsed AST with an explicit environment.

Expand all macros in an Elixir source file.

Expand all macros in a source code string.

Expand all macros and return formatted Elixir code.

Functions

expand(ast, opts \\ [])

@spec expand(
  Macro.t(),
  keyword()
) :: {:ok, Macro.t()} | {:error, term()}

Expand all macros in a pre-parsed Elixir AST.

Options

  • :env - Custom Macro.Env to use as the base environment.
  • :file - File path to set in the environment.

Examples

iex> {:ok, ast} = Code.string_to_quoted("1 |> to_string()")
iex> {:ok, expanded} = ExPanda.expand(ast)
iex> match?({{:., _, [String.Chars, :to_string]}, _, [1]}, expanded)
true

expand(ast, env, opts)

@spec expand(Macro.t(), Macro.Env.t(), keyword()) ::
  {:ok, Macro.t(), Macro.Env.t()} | {:error, term()}

Expand all macros in a pre-parsed AST with an explicit environment.

Returns both the expanded AST and the final environment state.

Examples

iex> env = ExPanda.EnvManager.new_env()
iex> {:ok, ast} = Code.string_to_quoted("unless true, do: :never")
iex> {:ok, expanded, _final_env} = ExPanda.expand(ast, env, [])
iex> match?({:case, _, _}, expanded)
true

expand_file(path, opts \\ [])

@spec expand_file(
  String.t(),
  keyword()
) :: {:ok, Macro.t()} | {:error, term()}

Expand all macros in an Elixir source file.

Options

Same as expand_string/2. The :file option defaults to the given path.

Examples

{:ok, expanded} = ExPanda.expand_file("lib/my_module.ex")

expand_string(source, opts \\ [])

@spec expand_string(
  String.t(),
  keyword()
) :: {:ok, Macro.t()} | {:error, term()}

Expand all macros in a source code string.

Options

  • :env - Custom Macro.Env to use as the base environment. Defaults to a fresh environment from :elixir_env.new().
  • :file - File path to set in the environment (for error messages). Defaults to "nofile".
  • :preserve_lines - Whether to preserve line/column metadata in parsed AST. Defaults to true.

Examples

iex> {:ok, expanded} = ExPanda.expand_string("unless true, do: :never")
iex> match?({:case, _, _}, expanded)
true

expand_to_string(input, opts \\ [])

@spec expand_to_string(
  String.t() | Macro.t(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}

Expand all macros and return formatted Elixir code.

Accepts either a source code string or a pre-parsed AST. The result is formatted with Code.format_string!/2, preserving whitespace in docstrings.

Options

When given a string, accepts the same options as expand_string/2. When given an AST, accepts the same options as expand/2. Additionally:

Examples

iex> {:ok, code} = ExPanda.expand_to_string("unless true, do: :never")
iex> code =~ "case"
true

iex> {:ok, ast} = Code.string_to_quoted("1 |> to_string()")
iex> {:ok, code} = ExPanda.expand_to_string(ast)
iex> code =~ "String.Chars.to_string"
true