How to expand macros fully / recursively even it's within special forms?

It seems that Macro.expand/2 cannot expand macros within special forms, for example,

iex(1)> print_expand = fn expr -> expr |> Macro.expand(__ENV__) |> Macro.to_string() |> IO.puts() end
iex(2)> print_expand.(quote do if 1 do :ok end end)
case(1) do
  x when Kernel.in(x, [false, nil]) ->
    nil
  _ ->
    :ok
end
iex(3)> print_expand.(quote do fn -> if 1 do :ok end end end)
fn -> if(1) do
  :ok
end end

Above showed that Macro.expand/2 cannot expand if macro within fn special form.

Question: how to expand macros fully / recursively even it’s within special forms ?

One way I think maybe work is to walk through AST and execute Macro.expand/2 for all encountered special form nodes. But I would like to know if there is a better way ?

Kernel special forms are the most expanded. Elixir does a pass to expand macro’s to special forms. It then turns the special forms into BEAM code.

You can see part of that process if you wrap any of that code in a module. But at that point, it’s effectively just AST.

x = quote do
  defmodule X do
    def foo do
      1
    end
  end
end

x |> Macro.expand(__ENV__) |> Macro.to_string() |> IO.puts()
(
  alias(X, as: nil, warn: false)
  :elixir_module.compile(X, {:__block__, [], [{:=, [], [{:result, [], Kernel}, {:def, [context: Elixir, import: Kernel], [{:foo, [context: Elixir], Elixir}, [do: 1]]}]}, {{:., [], [:elixir_utils, :noop]}, [], []}, {:result, [], Kernel}]}, [{:x, nil, :_@0, x}], __ENV__)
)

If you want to look at anything beyond that, you’ll have to look at :elixir_module.

1 Like

Thanks, with your hint, I digged into :elixir_module and found that :elixir_expand.expand/2 can expand macros even it’s within special forms, for example,

iex(1)> x = quote do
...(1)>   fn ->
...(1)>     if 1 do
...(1)>       :ok
...(1)>     end
...(1)>   end
...(1)> end
iex(2)> x |> :elixir_expand.expand(__ENV__) |> elem(0) |> Macro.to_string() |> IO.puts()
fn -> case(1) do
  x when :erlang.orelse(:erlang."=:="(x, nil), :erlang."=:="(x, false)) ->
    nil
  _ ->
    :ok
end end

It can expand if macro within fn special form.

2 Likes

@gyson, just keep in mind this is private API at this point. So, preferably just use this for understanding and not something you use in real code. Or, if you do, don’t complain to Elixir core if it breaks. :wink:

3 Likes

Thanks for reminder, I understand that it’s private API and could change at any time without notice. I only use it for an experiment at the moment. If I do need to use it in serious code, I would consider to create my own expand based on :elixir_expand.expand/2.

2 Likes