How to expand AST fully?

Hey!
I’m playing around with macro and I’m thinking - is it possible to completely expand quoted expression (including all private and public macro)?
Simplest example which I actually can’t fully expand:

  defmodule Hello do
    defmacrop foo, do: {:y, [], nil}
    def bar(x, foo()), do: x + y
  end

for example let’s try to use Macro.expand/2

quote do
  defmodule Hello do
    defmacrop foo, do: {:y, [], nil}
    def bar(x, foo()), do: x + y
  end
end
|> Macro.expand(__ENV__)
|> Macro.to_string 
|> Code.format_string! 
|> IO.puts

result is

:elixir_module.compile(
  Hello,
  {:__block__, [],
   [
     {:=, [],
      [
        {:result, [], Kernel},
        {:__block__, [],
         [
           {:defmacrop, [context: Elixir, import: Kernel],
            [{:foo, [context: Elixir], Elixir}, [do: {:{}, [], [:y, [], nil]}]]},
           {:def, [context: Elixir, import: Kernel],
            [
              {:bar, [context: Elixir], [{:x, [], Elixir}, {:foo, [], []}]},
              [do: {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, {:y, [], Elixir}]}]
            ]}
         ]}
      ]},
     {{:., [], [:elixir_utils, :noop]}, [], []},
     {:result, [], Kernel}
   ]},
  [{:q, nil, :_@0, q}],
  __ENV__
)

interesting line is

{:bar, [context: Elixir], [{:x, [], Elixir}, {:foo, [], []}]},

where foo is AST of macro call, but not result of this call (should be {:y, [], Elixir})

Is there any way to expand it and get resulting quoted expression somehow?

Macro.expand only expands the specific AST node passed in, not every node within it. I generally do this to fully expand something as much as Elixir’s AST goes (won’t touch special forms and so forth):

quote do
  defmodule Hello do
    defmacrop foo, do: {:y, [], nil}
    def bar(x, foo()), do: x + y
  end
end |> Macro.prewalk(&Macro.expand(&1, __ENV__)) |> Macro.to_string |> Code.format_string! |> IO.puts

Results in:

╰─➤  iex
Erlang/OTP 21 [erts-10.0.5] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe]

Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> quote do
...(1)>   defmodule Hello do
...(1)>     defmacrop foo, do: {:y, [], nil}
...(1)>     def bar(x, foo()), do: x + y
...(1)>   end
...(1)> end |> Macro.prewalk(&Macro.expand(&1, __ENV__)) |> Macro.to_string |> Code.format_string! |> IO.puts
alias(Hello, as: nil, warn: false)

:elixir_module.compile(
  Hello,
  {:__block__, [],
   [
     {:=, [],
      [
        {:result, [], Kernel},
        {:__block__, [],
         [
           {:defmacrop, [context: Elixir, import: Kernel],
            [{:foo, [context: Elixir], Elixir}, [do: {:{}, [], [:y, [], nil]}]]},
           {:def, [context: Elixir, import: Kernel],
            [
              {:bar, [context: Elixir], [{:x, [], Elixir}, {:foo, [], []}]},
              [do: {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, {:y, [], Elixir}]}]
            ]}
         ]}
      ]},
     {{:., [], [:elixir_utils, :noop]}, [], []},
     {:result, [], Kernel}
   ]},
  [{:x1, :elixir_fn, :_@0, x1}],
  %{
    __struct__: Macro.Env,
    aliases: [],
    context: nil,
    context_modules: [],
    contextual_vars: [],
    current_vars: %{{:x1, :elixir_fn} => {0, :term}},
    file: "iex",
    function: nil,
    functions: [
      {IEx.Helpers,
       [
         break!: 3,
         break!: 4,
         breaks: 0,
         c: 1,
         c: 2,
         cd: 1,
         clear: 0,
         continue: 0,
         exports: 0,
         exports: 1,
         flush: 0,
         h: 0,
         i: 0,
         i: 1,
         l: 1,
         ls: 0,
         ls: 1,
         nl: 1,
         nl: 2,
         open: 0,
         pid: 1,
         pid: 3,
         pwd: 0,
         r: 1,
         recompile: 0,
         recompile: 1,
         ref: 1,
         ref: 4,
         remove_breaks: 0,
         remove_breaks: 1,
         reset_break: 1,
         reset_break: 3,
         respawn: 0,
         runtime_info: 0,
         runtime_info: 1,
         v: 0,
         v: 1,
         whereami: 0,
         whereami: 1
       ]},
      {Kernel,
       [
         !=: 2,
         !==: 2,
         *: 2,
         +: 1,
         +: 2,
         ++: 2,
         -: 1,
         -: 2,
         --: 2,
         /: 2,
         <: 2,
         <=: 2,
         ==: 2,
         ===: 2,
         =~: 2,
         >: 2,
         >=: 2,
         abs: 1,
         apply: 2,
         apply: 3,
         binary_part: 3,
         bit_size: 1,
         byte_size: 1,
         div: 2, 
         elem: 2,
         exit: 1,
         function_exported?: 3,
         get_and_update_in: 3,
         get_in: 2,
         hd: 1,
         inspect: 1,
         inspect: 2,
         is_atom: 1,
         is_binary: 1,
         is_bitstring: 1,
         is_boolean: 1,
         is_float: 1,
         is_function: 1,
         is_function: 2,
         is_integer: 1,
         is_list: 1,
         is_map: 1,
         is_number: 1,
         is_pid: 1,
         is_port: 1,
         is_reference: 1,
         is_tuple: 1,
         length: 1,
         macro_exported?: 3,
         make_ref: 0,
         map_size: 1,
         max: 2,
         min: 2,
         node: 0,
         node: 1,
         not: 1,
         pop_in: 2,
         put_elem: 3,
         put_in: 3,
         rem: 2,
         round: 1,
         self: 0,
         send: 2,
         spawn: 1,
         spawn: 3,
         spawn_link: 1,
         spawn_link: 3,
         spawn_monitor: 1,
         spawn_monitor: 3,
         struct: 1,
         struct: 2,
         struct!: 1,
         struct!: 2,
         throw: 1,
         tl: 1,
         trunc: 1,
         tuple_size: 1,
         update_in: 3
       ]}
    ],
    lexical_tracker: nil,
    line: 6,
    macro_aliases: [], 
    macros: [
      {IEx.Helpers,
       [
         b: 1,
         break!: 1,
         break!: 2,
         h: 1,
         import_file: 1,
         import_file: 2,
         import_file_if_available: 1,
         import_if_available: 1,
         import_if_available: 2,
         open: 1,
         t: 1,
         use_if_available: 1,
         use_if_available: 2
       ]},
      {Kernel,
       [
         !: 1,
         &&: 2,
         ..: 2,
         <>: 2,
         @: 1,
         alias!: 1,
         and: 2,
         binding: 0,
         binding: 1,
         def: 1,
         def: 2,
         defdelegate: 2,
         defexception: 1,
         defguard: 1,
         defguardp: 1,
         defimpl: 2,
         defimpl: 3,
         defmacro: 1,
         defmacro: 2,
         defmacrop: 1,
         defmacrop: 2,
         defmodule: 2,
         defoverridable: 1,
         defp: 1,
         defp: 2,
         defprotocol: 2,
         defstruct: 1,
         destructure: 2,
         get_and_update_in: 2,
         if: 2,
         in: 2,
         is_nil: 1,
         match?: 2,
         or: 2,
         pop_in: 1,
         put_in: 2,
         raise: 1,
         raise: 2,
         reraise: 2,
         reraise: 3,
         sigil_C: 2,
         sigil_D: 2,
         sigil_N: 2,
         sigil_R: 2,
         sigil_S: 2,
         sigil_T: 2,
         sigil_W: 2,
         sigil_c: 2,
         sigil_r: 2,
         sigil_s: 2,
         sigil_w: 2,
         to_char_list: 1,
         to_charlist: 1,
         to_string: 1,
         unless: 2,
         update_in: 2,
         use: 1,
         use: 2,
         var!: 1,
         var!: 2,
         |>: 2,
         ||: 2
       ]}
    ],
    module: nil,
    prematch_vars: :apply,
    requires: [IEx.Helpers, Kernel, Kernel.Typespec],
    unused_vars: %{},
    vars: [x1: :elixir_fn]
  }
)
:ok

I.E. pipe like |> Macro.prewalk(&Macro.expand(&1, __ENV__)) instead. :slight_smile:

Do note, when compiling a module it is the actual :elixir_module.compile call that expands macros ‘within’ a module, so when you are quoting an entire module definition then macro’s defined within it aren’t expanded until that itself is run. It will expand things in proper scope though otherwise.

Thanks! But in your example there is still this foo macro, which is not expanded

As I understood the only way to get fully expanded AST is :elixir_module.compile?

That’s because it is not a macro, :foo at that point is a function invocation of something called foo that is not in scope at that point. A macro cannot be expanded unless it is in scope. :slight_smile:

Nah, expanding is fine, you just have to make sure it is all in scope first, and def blah... inside the ast itself does not bring anything into scope at that time as it needs to be in scope ‘prior’ to the expand call. :slight_smile:

1 Like