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.