It seems that a macro returning a :macrocall expression with escaped macro name does not work whereas a :call expression with escaped function name is no problem:
julia> macro a(ex::Expr) Expr(ex.head, esc.(ex.args)...) end
@a (macro with 1 method)
julia> @a one(0)
1
julia> @a @inbounds 0
ERROR: LoadError: syntax: "esc(...)" used outside of macro expansion
Stacktrace:
[1] top-level scope
@ REPL[3]:1
in expression starting at <macrocall>:0
Same example with dumped expressions
julia> macro a(ex::Expr) escex = Expr(ex.head, esc.(ex.args)...); dump(escex); escex end
@a (macro with 1 method)
julia> @a one(0)
Expr
head: Symbol call
args: Array{Any}((2,))
1: Expr
head: Symbol escape
args: Array{Any}((1,))
1: Symbol one
2: Expr
head: Symbol escape
args: Array{Any}((1,))
1: Int64 0
1
julia> @a @inbounds 0
Expr
head: Symbol macrocall
args: Array{Any}((3,))
1: Expr
head: Symbol escape
args: Array{Any}((1,))
1: Symbol @inbounds
2: Expr
head: Symbol escape
args: Array{Any}((1,))
1: LineNumberNode
line: Int64 1
file: Symbol REPL[3]
3: Expr
head: Symbol escape
args: Array{Any}((1,))
1: Int64 0
ERROR: LoadError: syntax: "esc(...)" used outside of macro expansion
Stacktrace:
[1] top-level scope
@ REPL[3]:1
in expression starting at <macrocall>:0
julia> @macroexpand1 @a @inbounds 0
Expr
head: Symbol macrocall
args: Array{Any}((3,))
1: Expr
head: Symbol escape
args: Array{Any}((1,))
1: Symbol @inbounds
2: Expr
head: Symbol escape
args: Array{Any}((1,))
1: LineNumberNode
line: Int64 1
file: Symbol REPL[4]
3: Expr
head: Symbol escape
args: Array{Any}((1,))
1: Int64 0
:($(Expr(:escape, Symbol("@inbounds"))) $(Expr(:escape, 0)))
It works if the macro symbol isn’t escaped, so I assume Expr(:escape, Symbol("@inbounds")) ends up in the wrong eval call somewhere. No idea why this shouldn’t work though.
julia> function b(ex::Expr) Expr(ex.head, ex.args[1], esc.(ex.args[2:end])...) end
julia> macro b(ex::Expr) b(ex) end
@b (macro with 1 method)
julia> @b @inbounds one(0)
1
Good question. There are two reasons for this that I could think of:
It’s a rare thing to want. The first argument mac_expr in an Expr(:macrocall, mac_expr, args...) is evaluated at top level (locals require lowering), so the only thing Expr(:escape) could change here is the module mac_expr is evaluated into.
The way it’s currently implemented, macro expansion runs recursively and evaluates every mac_expr before the Expr(:escape)s are handled in a second pass.
Still, I think this could be made to work. In your example, the escape would cause macro expansion to resolve @inbounds in the module of the code calling @a, instead of the module where @a is defined.
Yes - the internals would require changing to make this work in a sane way. There’s a somewhat-functional workaround using the hygienic-scope head instead of Expr(:escape) as follows:
julia> module A
macro inbounds(ex)
"lol surprise! Not Base.@inbounds"
end
macro a(ex::Expr)
Expr(ex.head, Expr(:var"hygienic-scope", ex.args[1], __module__), esc.(ex.args[2:end])...)
end
macro b(ex::Expr)
Expr(ex.head, ex.args[1], esc.(ex.args[2:end])...)
end
end
Main.A
julia> A.@a @inbounds 1
1
julia> A.@b @inbounds 1
"lol surprise! Not Base.@inbounds"
Note this is a bit of a hack because it’s not composable when the macro name or function call contains esc() itself. (I’m also unsure how much the hygienic-scope head is considered a public API vs a detail of macro expansion. At this point it might be stable I guess!)
I am working on overhauling macros (@mlechu is working on this too!). In the new system esc() isn’t required here and it should already work as expected.
The old macro system isn’t going away though - it will just work compatibly alongside the new system which people can opt into.