How to get the AST that would be generated by the given macro?

I think I misunderstood your original question. I do not know how to get what you want with binding/1 but I think I stumbled on a way to get the AST you were after. It requires some reconfiguring of the macro call itself, however. Sorry if this isn’t helpful.

defmodule TestModule do
  defmacro test_macro(ast) do
  binding() |> IO.inspect
    help(ast)
    |> Macro.escape
    |> IO.inspect
  end
  
  defp help([do: {:__block__, [], [a, b, c]}]) when is_integer(a) and is_integer(b) do 
    binding() |> IO.inspect
    quote do 
      a + b + unquote(c)     
    end
  end
end
defmodule Testing do
    require TestModule
    TestModule.test_macro do 
        1
        2
        3
    end
end

returns:

[ast: [do: {:__block__, [], [1, 2, 3]}]] # first binding
[a: 1, b: 2, c: 3] # second binding
# output of Macro.escape()
{:{}, [],
 [:+, [context: TestModule, import: Kernel],
  [{:{}, [],
    [:+, [context: TestModule, import: Kernel],
     [{:{}, [], [:a, [], TestModule]}, {:{}, [], [:b, [], TestModule]}]]}, 3]]}
1 Like

@stevensonmt
No worries, thanks for trying to help!

@ityonemo
That makes sense, thanks for the hints!
I’ve invested to much time already to parse Elixir AST and it works quite well, so working with bytecode would be plan B, in case I’m not able to achieve what I described.
Btw my goal isn’t to support such complex cases as macros inside macros, I’m OK with something that is usable in most common scenarios (e.g. use directives that allow to include imports and requires, simple function definitions, etc.)

I did a little bit of investigation… When you do a defmacro, elixir will turn your macro into a function using the following convention:

  defmacro foo(a) do...

becomes the function :"MACRO-foo"/2 under the hood. First argument is what becomes __CALLER__ which should be of type Macro.Env.t; second argument is a (or generally the macro arguments concatenated). This is how __CALLER__ gets into the macro system. Output is the ast (which is what you are looking for) So, in theory you could do a fair job of reproducing even nested macros, if you carefully keep track of ENV. But at that point you are basically rebuilding the elixir ast parser =P, and there are a lot of things that are deliberately kept as “private” aka reserved fields in ENV that have zero forward compatibility guarantees.

3 Likes

@ityonemo
That’s awesome! I tried the :“MACRO-” function trick you described, like this:
apply(TestModule, :"MACRO-test_macro", [__ENV__, 1, 2, 3])
and it seems to work so far :slight_smile: I’m experimenting with some more complex scenarios…

1 Like

One thing I’ve noticed already is that when you want to expand the using macro, you need to apply :“MACRO-__using” (and not :“MACRO-using”).
Actually __using__ works as the rest of macros.

1 Like

yes, but do keep in mind that any macro that uses Module.get_attribute (and that is a common use case, esp. for using macros that aren’t “abuse of using”) will fall flat.

1 Like