bartblast

bartblast

Creator of Hologram

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

Suppose we’ve got the following module:

defmodule TestModule do
  defmacro test_macro(param1, param2, param3) do
    quote do
      param1 + param2 + unqoute(param3)
    end
  end
end

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

Let’s say that param1 = 1, param2 = 2, param3 = 3
Given those params I’d like to get the following AST:

{:+, [context: Elixir, import: Kernel],
 [
   {:+, [context: Elixir, import: Kernel],
    [{:param1, [], Elixir}, {:param2, [], Elixir}]},
   3
 ]}

I tried to solve this problem with Macro.expand/2 and Code.eval_quoted/3 but wasn’t able to.

Can anyone let me know how to do this or give some hints?

Marked As Solved

ityonemo

ityonemo

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.

Also Liked

ityonemo

ityonemo

If you are doing an elixir to js transpiler, I recommend using the BEAM bytecode. You can obtain bytecode by doing :beam_disasm on the module binary.

hauleth

hauleth

What you are looking for is Macro.expand_once/2 or Macro.expand/2 if you want to expand it fully. Just beware, that it is no safer than Code.eval_quoted/3, so do not use it on untrusted input.

ityonemo

ityonemo

the problem is that macros are HIGHLY contextual and the “middle” of the macro code (everything between the defmacro and the resulting quoted retval), may not be able to evaluated without having access to a bunch of internal information, if for example, functions like Module.get_attribute (see note “This function can only be used on modules that have not yet been compiled”) and then if you have a macro inside of a macro, you will need to track the rolling environment, and then there’s the magic __CONTEXT__ special form, which quite frankly I’m not entirely sure how it works under the hood. I guess that’s why you’re trying to obtain the AST information at the point of egress, but note that even the AST emitted by module ITSELF might have macros inside of it, so even if you obtain the AST in a late-interception fashion, you still need something that can “execute” macros.

I think this complexity might have been what killed ElixirScript – elixir_script v0.32.1, which I have a ton of respect for. If you decide to go with reading bytecode, which honestly is IMO way simpler than elixir AST, let me know, I am very much intereseted in resurrecting these elixir on the FE type projects. I am thinking about tinkering with a WASM-based bytecode interpreter, (not to be confused with lumen, which is a beam-to-wasm compiler).

kip

kip

ex_cldr Core Team

Macros are just functions that take AST as parameters and are expected to return AST. Generally AST is created using quote blocks. But you can quote any code whether in a macro of not. So you to get the AST of the code you show, it would be a matter of:

defmodule TestModule do
  defmacro test_macro(param1, param2, param3) do
    quote do
      param1 + param2 + unqoute(param3)
    end
  end
end

Note that:

  1. In this case param1 and param2 in the quote block have nothing to do with the parameters passed to the macro. They are variable references that would need to be referenceable in the calling site.
  2. unquote will dereference the parameter param3. If your intent was to get the AST for unquote(param3) then change the quote do block to be quote unquote: false do
benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Do note that due to macro hygene, simply having param1 as a bound variable at the call site will actually not work without opting out of hygene with var!

@bartblast you can see the AST by putting an IO.inspect after your quote do block

defmodule TestModule do
  defmacro test_macro(param1, param2, param3) do
    param1 + param2 + unqoute(param3)
  end
  |> IO.inspect
end

Where Next?

Popular in Questions Top

SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

We're in Beta

About us Mission Statement