What is the best way to call a macro with an Expr and then get the evaluated output of the macro? I want to write a macro that needs to call @which on its input and then use the returned method. Here is my current attempt
macro whichfile(ex...)
m = @which ex... # doesn't work, because `ex...` is not the original code, but an expr
file, line = functionloc(m)
println(file)
nothing
end
macro whichfile(ex...)
m = var"@which"(LineNumberNode(@__LINE__(), @__FILE__()), @__MODULE__, ex...)
# does not work, because m is now the code outpout of @which and not the method
file, line = functionloc(m)
println(file)
nothing
end
and also
macro whichfile(ex...)
m = var"@which"(LineNumberNode(@__LINE__(), @__FILE__()), @__MODULE__, ex...)
file, line = functionloc(eval(m)) # complains about invalid syntax (escape (outerref println))
println(file)
nothing
end
@whichfile println("hello world")
Okay, I found a solution with macroexpand myself. And because other people might like hit, here the complete macro:
julia> macro splitedit(ex...)
m = eval(macroexpand(Main, :(@which $(ex...))))
file, line = functionloc(m)
editor = ENV["EDITOR"]
editorcmd = "$editor +$line $file"
splitcmd = `tmux split-window "$editorcmd"`
run(splitcmd)
nothing
end
@splitedit (macro with 1 method)
julia> @splitedit println("hallo") # same as @edit, but in a new tmux pane
While this might work in an interactive context, but a macro should not evaluate code. Instead, it should rewrite the expression it gets passed.
Check for example, what @which or @edit are doing:
Thus, they just call a corresponding function with parts of the expression wrapped in typesof.
In your case, a similar setup would work:
function splitedit(fun, argtypes)
m = which(fun, argtypes)
file, line = functionloc(m)
editor = ENV["EDITOR"]
editorcmd = "$editor +$line $file"
splitcmd = `tmux split-window "$editorcmd"`
run(splitcmd)
nothing
end
and then have your macro expand into a call of that function, i.e., @macroexpand @splitedit println("hallo") should give the expression :(splitedit(println, Base.typesof("hallo"))).
Good point, while I couldn’t fully get around calling eval, the following version is hopefully a bit better, and also something very neat to have in the startup.jl file
"""
splitedit(file, line=0)
Open `file` at `line` with your favorite editor in a new tmux pane.
The editor can be changed by setting the `EDITOR` environment variable.
"""
function splitedit(file, line=0)
editor = ENV["EDITOR"]
editorcmd = "$editor +$line $file"
splitcmd = `tmux split-window "$editorcmd"`
run(splitcmd)
nothing
end
"""
@splitedit <functioncall>
Evaluates the arguments to the function call, determines their types,
and calls the `splitedit` function on the resulting expression.
"""
macro splitedit(ex)
fun = eval(ex.args[1])
argtypes = Base.typesof(ex.args[2:end])
m = which(fun, argtypes)
file, line = functionloc(m)
splitedit(file, line)
end
This is not what I meant, i.e., you have now just separated part of your code into a separate function. Your macro still evaluates the expression it gets passed and does all its processing at compile time. Keep in mind that a macro is just a function which happens to be executed on a parsed expression by the compiler. In turn, its return value – usually another expression – will then be compiled instead of the original expression and eventually evaluated just like regular code. Note that your macro returns nothing, i.e., all its work is done at compile time!
Let’s have a look at a slightly adapted example from the Julia documentation on Metaprogramming
julia> macro twostep(arg)
println("I execute at parse time. The argument is: ", arg, " with type ", typeof(arg))
return :(println("I execute at runtime. The argument is: ", $arg, " with type ", typeof($arg)))
end
@twostep (macro with 1 method)
# Note: Expanding the macro runs the first println and returns an expression
julia> ex = @macroexpand @twostep(1 + 2);
I execute at parse time. The argument is: 1 + 2 with type Expr
# The returned expression contains another call of println with the args expression inserted
julia> ex
:(Main.println("I execute at runtime. The argument is: ", 1 + 2, " with type ", Main.typeof(1 + 2)))
# Evaluating it actually executes it
julia> eval(ex)
I execute at runtime. The argument is: 3 with type Int64
# Calling the macro does both steps, i.e., first expanding it and then evaluating the expression it returned
julia> @twostep(1 + 2);
I execute at parse time. The argument is: 1 + 2 with type Expr
I execute at runtime. The argument is: 3 with type Int64
Now in your case, I was proposing the following:
# Function repeated from above
function splitedit(fun, argtypes)
m = which(fun, argtypes)
file, line = functionloc(m)
editor = ENV["EDITOR"]
editorcmd = "$editor +$line $file"
splitcmd = `tmux split-window "$editorcmd"`
run(splitcmd)
nothing
end
macro splitedit(expr)
@assert expr.head == :call # We only support simple call expressions
return :(splitedit($(esc(expr.args[1])), tuple($(map(arg -> :(typeof($(esc(arg)))), expr.args[2:end])...))))
end
Note that this macro does not evaluate expr, instead it constructs a new expression which will perform the desired computation/function call when evaluated:
Just a small add on why a macro should not try to evaluate the expression that it gets passed:
eval evaluates an expression in the global scope, i.e., cannot see local variables:
julia> let fun = println
@splitedit fun(3, 2) # Calling your macro here fails as fun cannot be resolved in the global environment
end
ERROR: LoadError: UndefVarError: fun not defined
An expression contains symbols for variables and not their values
julia> foo(x::Int64, y::Int64) = x + y
foo (generic function with 1 method)
julia> let z = 3
@splitedit foo(z, 2) # Your macro looks for a method with (Symbol, Int64) arguments which does not exist
end
ERROR: LoadError: no unique matching method found for the specified argument types
In contrast, my macro expands into a function call and is the same as if you had written
let fun = println, z = 3
splitedit(fun, tuple(typeof(z), typeof(2))) # expansion of @splitedit fun(z, 2)
end
and does not run into the above issues. In general, think of macros as a shorter notation for some longer boilerplate code that you could write yourself. The task of the macro is then to rewrite the desired shorter expression into this intended longer boilerplate expression, i.e., it takes an unevaluated expression as input and produces another unevaluated expression as output.