Should `esc` work for the macro name of a `macrocall` expression?

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

Is there a reason for this?

2 Likes

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:

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

1 Like

Do you mean by modifying Julia internals? I think it would be useful to be able to change the module where a macro is expanded.

(I believe that @c42f is working on a more general overhaul of the macro system. Changing the “old” macro code might therefore not be a priority.)

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.

1 Like