Using @which inside a macro on expr

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

after reading Is there a trick to input an `Expr` into a macro? - #4 by johnmyleswhite I tried the following:

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")
1 Like

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:

julia> @macroexpand @which println("hallo")
:(InteractiveUtils.which(println, (Base.typesof)("hallo")))

julia> @macroexpand @edit println("hallo")
:(InteractiveUtils.edit(println, (Base.typesof)("hallo")))

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"))).

2 Likes

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:

julia> @macroexpand @splitedit println("hallo")
:(Main.splitedit(println, Main.tuple(Main.typeof("hallo"))))
1 Like

Just a small add on why a macro should not try to evaluate the expression that it gets passed:

  1. 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
    
  2. 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.

1 Like