Why are variables seen as nil during macro expansion?

Hey guys, if you haven’t read my other posts, I’ve been experimenting around with metaprogramming and macros. Recently I found out that Elixir doesn’t seem able to “see” a given variables value, even if it is explicitly provided in the source code:

a = 10
some_macro(a) # The variable will be used here

# => Inside the macro the variable will be seen as something like `{:a, [line: x], nil}`

Essentially all variables are all seen as nil. I was hoping that Elixir could somehow see the value when the variable is explicitly bound in the code (seeing the value from dynamically passed variables — like in function parameters — would be a different story).

I looked at Macro.var/2but it doesn’t seem to do what I want.

var(var, context)

var(var, context) :: {var, [], context} when var: atom, context: atom
Generates an AST node representing the variable given by the atoms var and context.

Examples

In order to build a variable, a context is expected. Most of the times, in order to preserve hygiene, the context must be MODULE:

iex> Macro.var(:foo, MODULE)
{:foo, [], MODULE}
However, if there is a need to access the user variable, nil can be given:

iex> Macro.var(:foo, nil)
{:foo, [], nil}

Is there an actual way to achieve this? If not, why so? Is this a limitation of Elixir or is it a side effect from a certain feature/design decision?

EDIT:

I assume receiving a list as a parameter in a macro my_macro(args), do: IO.puts length(args) and having it output the length of args during compilation is not possible?

It is because macro expansion time takes place during compilation, the variable a is just that, ast of a variable, it has no other useful information yet because that variable has not been bound to anything yet. If you need to access the variable then you can return AST from the macro to be run at ‘run-time’ that can then access whatever binding that the variable will have at that time. :slight_smile:

3 Likes

Thank you for your reply @OvermindDL1. That was my reasoning at first, but something told me that due to the fact that the variable is explicitly bound to a certain literal, then the compiler would be able to “see” that somehow.

So the only point where I’ll be able to know a's value will be at runtime, exclusively?

a = 2

This may seem simple, but imagine %{foo: foo} = %{foo: 2}. This could clearly never work, because %{foo: 2} isn’t even a map yet, it’s just map AST. The same as true for your a = 2 example, but just less obviously.

Correct, the only way to know a's value is at runtime.

3 Likes

@benwilson512 nice comparison! I understand now :slight_smile: Thank you both

@benwilson512 and @OvermindDL1 but why doesn’t the compiler see the AST of the expression where the variable’s value is passed? If we have %{foo: foo} = %{foo: 2} then the %{foo: 2} part must be parsed and read by the compiler, no?

Sorry for being so insistent on this topic :stuck_out_tongue:

It’s read by the parser sure, but when you get access to it’s in the MIDDLE of the compilation phase, not after. Macros are expanded as basically a pre-compilation phase that operates on the AST. For all intents and purposes no, the compiler has not actually run.

1 Like