Foreach for macros?

I am trying to define a foreach macro that works with macros. That is:

@foreach @amacro expr1 expr2 ... 

Should give

@amacro(expr1) @amacro(expr2) ... 

I couldn’t make this work even with a single expr input. I defined a trial macro:

macro trial(expr) 
   quote 
        println($(QuoteNode(expr))) 
   end
end

@trial works as expected. Then i tried to implement foreach macro for a single expr input:

macro foreach1(amacro,expr)
   quote
       $(amacro)($(expr))
   end
end

This doesn’t work. I also tried to construct the expression using :macrocall directly but that also didn’t work:

macro foreach(amacro,expr)       
    Expr(:macrocall,amacro,LineNumberNode((0),esc(expr))
end

This also didn’t work.

This should do it.

julia> function foreach_helper(x)
           mname = x.args[1]
           t = x.args[3:end]
           out = quote end
           for ti in t
               out = quote
                   $out
                   $(Expr(:macrocall, mname, LineNumberNode(0), ti))
               end
           end
           return out
       end

julia> macro t(x)
       println(x)
       esc(x)
       end
@t (macro with 1 method)

julia> @foreach @t 1 2;
1
2

I have no clue how to do LineNumberNode responsibly though. This is all kind of a black box for me.

Thanks a lot!

Your solution wasn’t working if macro was called from another module. So I came up with the following solution. This version works with macros with multiple arguments. For example, if @a_macro takes two arguments:

@my_foreach @a_macro [a_1,a_2,...,a_n] [b_1,b_2,...,b_n]

turns into

@a_macro a_1 b_1 ; @a_macro a_2 b_2; ...; @a_macro a_n b_n
function my_foreach_helper(x,calling_module)
    macro_name = x.args[1]
    input_lists = map(expr->expr.args,x.args[3:end])
    ex = quote end
    while !isempty(input_lists[1])
          inputs = map(pop!,input_lists)
          macro_expr = Expr(:macrocall,Expr(:.,calling_module,QuoteNode(macro_name)),LineNumberNode(0),inputs...)
          ex = quote
                  $(ex);
                  $(macro_expr);
               end
    end 
    ex
end 

macro my_foreach(x)
    my_foreach_helper(x,__module__)
end

I wanted to ask whether you have any suggestions as I am not very comfortable with escaping manually. On the other hand, I don’t want to escape all of the expressions as there may have situations where it actually changes something.

Your problem is probably solved by doing

macro my_foreach(x)
   esc( my_foreach_helper(x))
end

you missed the esc which might be affecting things.

1 Like

Thanks, my solution was working, I just wanted to ask whether you had any suggestions to improve it.

yes, you definitely need the esc. It ensures proper macro hygiene and will get rid of your issue about the modules.

1 Like

Actually, my solution is working without esc when calling from another module. module is the module of calling scope and I pass this to the helper function. It is used while constructing the macro expression:

Expr(:macrocall,Expr(:.,calling_module,QuoteNode(macro_name)),LineNumberNode(0),inputs...)

I have never seen

Expr(:.,calling_module,QuoteNode(macro_name))

anywhere. I found by examining dump(: (@m.amacro x)). It seems to be enough to escape hygiene.

Will this work inside functions though? With only local variables?

You are right, it wasn’t working. The following version seems to work.

module m 

function foreach(x,calling_scope)
      macro_name = x.args[1]
      input_lists = map(expr->expr.args,x.args[3:end])
      ex = Expr(:block)
      while !isempty(input_lists[1])
            inputs = map(pop!,input_lists)
            inputs = esc.(inputs)
            macro_expr = Expr(:macrocall,Expr(:.,calling_scope,QuoteNode(macro_name)),LineNumberNode(0),inputs...)
            pushfirst!(ex.args,macro_expr)
      end
      push!(ex.args,:(return nothing))
      ex
end 
       
macro foreach(x)
      foreach(x,__module__)
end

end

The difference is, I also escaped the inputs (note that esc(inputs) doesn’t work, you need to broadcase it). To test it, I defined the nif macro:

macro nif(expr,pos,zero,negative)
           quote 
               if $(esc(expr)) > 0 
                   $(esc(pos))
               elseif $(esc(expr)) == 0
                   $(esc(zero))
               else 
                   $(esc(negative))
               end
           end
       end

Then:

let 
     x = 1 
     y = -1
     @m.foreach @nif [x,y] [println("x-positive"),println("y-positive")] [println("x-zero"),println("y-zero")] [println("x-negative"),println("y-negative")] 
end

prints

x-positive
y-negative

There is something tricky going on I believe. It seems that the input macro (here it is the @nif), thinks that calling scope is the module m. Therefore, you need to escape the inputs in @foreach, before supplying them to @nif.

This wasn’t working if the input macro was called from a third module. Now I fixed that also. Escaping everything is much simpler but this was really instructive. One needs to obtain the module the input macro is defined in correctly and this is not straightforward because @a_macro, @a_module.amacro and @another_module.a_module.a_macro are parsed differently. The inner function make_input_module_and_macro_name returns the module input macro is defined and the name of the input macro.

#= escape manually 
 The function make_input_module_and_macro_name return (input_module,macro_name) if is_macro_name_found is false and returns input_module if is_macro_name_found is true.
=#
function foreach_helper(x,calling_scope)
           function make_input_module_and_macro_name(tree::Expr,is_macro_name_found)
               if is_macro_name_found
                   make_input_module_and_macro_name(tree.args[2],true)
               else 
                   (make_input_module_and_macro_name(tree.args[1],true), eval(tree.args[2]))
               end
           end
           make_input_module_and_macro_name(tree::Symbol,is_macro_name_found) = is_macro_name_found ? tree : (calling_scope,tree)
           make_input_module_and_macro_name(tree::QuoteNode,is_macro_name_found) = eval(tree)    
           input_module, macro_name = make_input_module_and_macro_name(x.args[1],false)
           input_lists = map(expr->expr.args,x.args[3:end])
           ex = Expr(:block)
           while !isempty(input_lists[1])
                 inputs = map(pop!,input_lists)
                 inputs = esc.(inputs)
                 macro_expr = Expr(:macrocall,Expr(:.,calling_scope,QuoteNode(macro_name)),LineNumberNode(0),inputs...)
                 pushfirst!(ex.args,macro_expr)
           end
           ex
       end
macro foreach(x)
    foreach_helper(x,__module__)
end

Now one can define @foreach and the input macro in different modules and call them in a local scope (say let block) from a third module.