Technique to allow a function call only within a macro

I’m playing around with creating a rather unidiomatic testing library and I’d like to constrain a macro call of a macro that my library exposes to only be available within the do block of one of the macros that my library defines. Is there a way to do that with some meta programming?

Here’s an example of what I mean:

import MyLib, only: [:lib_setup]
# Should succeed
lib_setup do
  MyLib.other_macro(42)
end

# Should fail (at compiletime or runtime)
MyLib.other_macro(42)

And the reason that I want to constrain it like so is because of how unidiomatic other_macro is.

In your example, having only imported :lib_setup and not run require MyLib, the second call would fail and the only way for the first call to succeed would be if the lib_setup call had traversed the do block and transformed the inner call into something that could succeed.

If that particular behavior wasn’t intended in the example, one option to “communicate” between macros is for lib_setup to set a module attribute at the beginning of the returned quote and unset it at the end, and then the inner macro can look for the presence of that attribute to ensure it’s being called in a proper context.

1 Like

Yeah you’re probably right on the import call. In my actual code I’m doing a full import but I was trying to simplify it for the example.

Storing a module attribute should work! I’ll try it out and report back.

I think it should be possible. In this case other_macro doesn’t even have to be itself a macro: lib_setup being a macro, it could well transform the whole AST and give other_macro - or rather not_a_macro - its meaning.
A bit like Nx.Defn redefines what + or Nx.exp would mean in its context.

You should be able to do something like this with Macro.prewalk/2:

defmacro lib_test(do: block_ast) do
  Macro.prewalk_ast(block_ast, fn
    {:not_a_macro, _, args} -> handle_not_a_macro(args)
    other -> other
  end)
end

Hope this helps / makes sense :slight_smile: