How to pass arguments to nested macros?

Hello!

I’m trying to create a macro that calls a function with 7 parameters. 2 of these is the file name and line number of the point where the macro is called (that’s why I can’t use functions), 2 is the same for all calls in a module and 3 is specific for each call. I’d like to pass the 2 “module-specific” parameters only once, so I tried to pass them to the __using__ macro, but couldn’t get it work. This is a minimal example of what I came up with so far:

defmodule Lib do
  defmacro __using__(opts) do
    object_name = Keyword.get(opts, :object_name)
    errors = Keyword.get(opts, :errors)

    quote bind_quoted: [object_name: object_name, errors: errors] do
      defmacro test_macro(term, op_code, args) do
        %{file: file, line: line} = __CALLER__

        quote do
          IO.puts([
            unquote(term),
            unquote(object_name),
            unquote(errors),
            unquote(op_code),
            unquote(args),
            unquote(file),
            unquote(line)
          ])
        end
      end
    end
  end
end

and I’m trying to use the macro this way:

defmodule MacroProblem do
  use Lib, object_name: :on, errors: [:x]

  def hello do
    test_macro("x", :a, ["b"])
  end
end

but I get this compilation error:

== Compilation error in file lib/macro_problem.ex ==
** (UndefinedFunctionError) function MacroProblem.object_name/0 is undefined (function not available)
    MacroProblem.object_name/0
    (stdlib) erl_eval.erl:567: :erl_eval.local_func/6
    (stdlib) erl_eval.erl:232: :erl_eval.expr/5
    (stdlib) erl_eval.erl:233: :erl_eval.expr/5
    (stdlib) erl_eval.erl:232: :erl_eval.expr/5
    (stdlib) erl_eval.erl:888: :erl_eval.expr_list/6
    (stdlib) erl_eval.erl:240: :erl_eval.expr/5
    expanding macro: MacroProblem.test_macro/3

I tried to quote/unquote various stuff in the code above, but only got different errors. How should this work?

It’s not your problem. Look that’s because object_name variable is not found in scope.

Simplified error example:

defmodule Example do
  data = 5

  def sample, do: data
end

Look that adding extra unquote/1 call solves this problem:

defmodule Example do
  data = 5

  def sample, do: unquote(data)
end

Following this please try something like this one:

defmodule Lib do
  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do
      defmacro test_macro(term, op_code, args) do
        data = [args: args, op_code: op_code, term: term]

        quote bind_quoted: [caller: Macro.escape(__CALLER__), data: data, opts: unquote(opts)] do
          IO.inspect([
            data[:term],
            opts[:object_name],
            opts[:errors],
            data[:op_code],
            data[:args],
            caller.file,
            caller.line
          ])
        end
      end
    end
  end
end

I tried, but now I get this compilation error:

== Compilation error in file lib/macro_problem.ex ==
** (CompileError) lib/macro_problem.ex:2: unquote called outside quote
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (stdlib) lists.erl:1355: :lists.mapfoldl/3

Just copied code into iex to make sure and it worked for me, so it’s really weird …

Which Erlang and Elixir version you have? I’m using Erlang in version 21.3 and Elixir in version: 1.8.1 - both are installed using asdf version manager.

I’m using Elixir 1.7.4 and Erlang/OTP 21. I’ll try with newer - I’m not sure I can actually use that in the “real” project, but let’s see if it works with the minimal example.

What about storing the module-level parameters in a module attribute (@object_name etc)?

I happen to be working on some ex_audit stuff right now, so here’s a relevant example:

1 Like