lakret
Getting variable by its dynamic atom name from the caller context in a macro
I’m struggling with overriding hygiene during manual manipulation of AST with Macro.postwalk. In short, I want to provide a syntax for interpolating variables with ^ in a DSL, similar to Ecto. So this:
some_col_name = "foo"
...
select [:x, ^some_col_name]
should be converted internally to
%Select{
columns: ["x", "foo"]
}
There is more logic around that, though, so I need to first replace these ^some_col_name in the AST with the values of corresponding variables, and then do some more manipulation with the AST.
My attempts so far were variations on a theme:
Macro.postwalk(quoted, fn
{:^, [], [{v, [], _}]} when is_atom(v) ->
quote do
var!(unquote(v))
end
x -> x
end)
I know that I need to override hygiene, but it seems var! only works with literal variable names, and there’s no example of using dynamic atom names with it that I could find. I also tried different combinations of Macro.var, unquote, and var!, to no avail.
Any suggestions?
Marked As Solved
al2o3cr
The REPL is obscuring things somewhat - interpolate runs during compilation and creates an AST which is then evaluated. bar has a value during the second part of that process.
Here’s a revised version that handles the example from your post:
defmodule Foo do
defmodule Column do
defstruct [:name]
end
defmacro select(columns) do
interpolate(columns)
end
def interpolate(q) do
IO.inspect(q, label: :q)
ast = Macro.postwalk(q, fn
{:^, _, [{name, _, _} = v]} when is_atom(name) ->
quote do
atom_to_column(var!(unquote(v)))
end
a when is_atom(a) ->
quote do
atom_to_column(unquote(a))
end
x -> x
end)
IO.inspect(ast, label: :ast)
ast
end
def atom_to_column(a) when is_atom(a) do
%Column{name: a}
end
end
Running this produces output like:
iex(32)> select([:foo, :bar])
q: [:foo, :bar]
ast: [
{:atom_to_column, [context: Foo, import: Foo], [:foo]},
{:atom_to_column, [context: Foo, import: Foo], [:bar]}
]
[%Foo.Column{name: :foo}, %Foo.Column{name: :bar}]
iex(33)> wat = :foobar
:foobar
iex(34)> select([:foo, ^wat])
q: [:foo, {:^, [line: 34], [{:wat, [line: 34], nil}]}]
ast: [
{:atom_to_column, [context: Foo, import: Foo], [:foo]},
{:atom_to_column, [context: Foo, import: Foo],
[{:var!, [context: Foo, import: Kernel], [{:wat, [line: 34], nil}]}]}
]
[%Foo.Column{name: :foo}, %Foo.Column{name: :foobar}]
Also Liked
al2o3cr
var! is a macro, so code like this:
var!(some_variable_name)
calls var! with {:some_variable_name, metadata, context}.
I found this example helpful:
defmodule MacroExample do
defmacro get_value(variable) do
IO.inspect(variable)
quote do
var!(unquote(variable))
end
end
end
iex(25)> require MacroExample
MacroExample
iex(26)> target_var = 42
42
iex(27)> MacroExample.get_value(target_var)
{:target_var, [line: 27], nil}
42
In your dynamic case, you likely want to capture the whole tuple and unquote it:
Macro.postwalk(quoted, fn
{:^, [], [{v, _, _} = v_tuple]} when is_atom(v) ->
quote do
var!(unquote(v_tuple))
end
x -> x
end)







