Evaluate symbol passed to macro

I have a macro that builds some Expr from a string passed to it, a simplified MWE is

module MyExpr
export  @my_expr

macro my_expr(s::String)
    Symbol.(eachsplit(s))
end

end

So I can call it like this

julia> @my_expr "e1\ne2\ne3"

but not like this:

julia> str="e1\ne2\ne3";
julia> @my_expr str

Defining another method within the module MyExpr like this:

macro my_expr(s::Symbol)
  str = eval(s)
  Symbol.(eachsplit(s))
end

won’t work, because str is not defined within the MyExpr module.
Can I somehow interpolate the macro’s argument at the
scope where I call it, without resorting to Meta.parse()?

Macros get extra arguments for where they’re called, including what module:

julia> macro pass()
         println(__module__)
       end
@pass (macro with 1 method)

julia> @pass
Main

julia> module Blah
         using Main: @pass
         @pass
       end;
Main.Blah

You can use that module for Core.eval.

However, that will only work for global variables, there’s no way for any version of eval to run code in a local scope. Macros typically generate an expression instead of executing code within its call; expressions can be inserted into a local scope. It looks like you’re deliberately not doing that; is there a reason why my_expr isn’t just a normal function?

@my_expr is a simplified illustration, the actual macro generates functions. It is @problemset from the “ProblemSet” registered module.

I suppose I could construct and return to a local scope an Expr calling the macro with the string argument.

You would need a function, not a macro, to do that, and the returned Expr is not inserted into the local scope and can only be eval-uated in the global scope. If you only need to generate functions in the global scope, that’s fine, and not all expressions are transformed in macros e.g. @eval loops.

Practically speaking, that’s what I really need (maybe I’ll make a string macro for the purpose), but why such fundamental limitation exists? Why can’t expressions be evaluated in a local
scope? From earlier comments by @yuyichao I seem to recall that this is a matter of some principle.

I think it has to do with the world age mechanism. If you were allowed to eval inside functions into the current world age then the compiler could not optimize your code as much or at all, as any of the functions being called could be overwritten at any time. So going into a function call Julia considers the world fixed until it returns to top level. You can get around that using invokelatest which comes with a performance penalty. There’s also RuntimeGeneratedFunctions.jl

1 Like

World age is definitely one reason, but it’s also just for optimizable compilation in general. If you could generate and execute an expression during a method call and its variables are supposed to behave as if it belonged to the method’s scope, then it’s too late for the compiler to do anything about the expression and the compiler has to abandon many important optimizations for the rest of the method. So, expressions can only be incorporated into a local scope at parse-time (macros) or compile-time (@generated functions).

One such optimization is storing data for variables of known types in relative addresses of the stack frame; otherwise all variables would have to live in Dict{Symbol, Any} on the heap… Even languages with implementations that do put variables on the heap tend to make it unfeasible for users to dynamically change local scopes.