How to properly import a macro inside a begin-end block

Hi there, I am very surprised that the following fails in Julia 1.9

julia> begin
           import Crayons: Crayon, @crayon_str
           f() = crayon"dark_gray"
       end
ERROR: UndefVarError: `@crayon_str` not defined

I want to run this on a remote and want to have a general begin/end around to enable the use of local variables.

How can I twist julia to properly import the str macro?

You need two blocks – macros are expanded before any code is run. See JuliaLang/julia#46478 for some related discussion.

2 Likes

thank you. not a great solution, but at least it is clear what works and what not.

I found an alternative workaround which does not need two code blocks: using @macroexpand explicitly

julia> begin
           import Crayons: Crayon, @crayon_str
           f() = @macroexpand crayon"dark_gray"
       end
f (generic function with 1 method)

julia> f()
\e[90m

I guess it is a special case here, as string macros usually return values and no expressions.

You effectively delayed the expansion of @crayon_str because @macroexpand expands to a call on an input expression:

julia> @macroexpand begin
         import Crayons: Crayon, @crayon_str
         f() = @macroexpand crayon"dark_gray"
       end
quote
    #= REPL[13]:2 =#
    import Crayons: Crayon, @crayon_str
    #= REPL[13]:3 =#
    f() = begin
            #= REPL[13]:3 =#
            Base.macroexpand(Main, $(QuoteNode(:(crayon"dark_gray"))), recursive = true)
        end
end

However, the expand call is inside the function block, and it’s not constant folded so it got delayed to the method calls:

julia> @code_warntype f()
MethodInstance for f()
  from f() in Main at REPL[1]:3
Arguments
  #self#::Core.Const(f)
Body::Any
1 ─ %1 = (:recursive,)::Core.Const((:recursive,))
β”‚   %2 = Core.apply_type(Core.NamedTuple, %1)::Core.Const(NamedTuple{(:recursive,)})
β”‚   %3 = Core.tuple(true)::Core.Const((true,))
β”‚   %4 = (%2)(%3)::Core.Const((recursive = true,))
β”‚   %5 = Core.kwfunc(Base.macroexpand)::Core.Const(Base.var"#macroexpand##kw"())
β”‚   %6 = (%5)(%4, Base.macroexpand, Main, $(QuoteNode(:(crayon"dark_gray"))))::Any
└──      return %6

Another workaround is to run the import statement before the begin block is parsed, nesting it by interpolating an @eval call:

julia> @eval begin # fresh REPL session
         $(import Crayons: Crayon, @crayon_str) # $(nothing)
         f() = crayon"dark_gray"
       end
f (generic function with 1 method)

julia> @code_warntype f()
MethodInstance for f()
  from f() in Main at REPL[1]:3
Arguments
  #self#::Core.Const(f)
Body::Crayon
1 ─     return \e[90m

This is also how I like to compute constants without declaring const globals before nesting them in an expression, e.g. @eval log5(x) = log2(x)/$(log2(5)) (that is not a good example because it’s simple enough for the compiler to constant fold). Still, I personally prefer running the import statements separately, inserting text ruins the point of copy and pasting into begin blocks in the REPL.

1 Like

nice workaround. I am curious whether this also works for Distributed, i.e. whether the Module and/or macro can be easily serialized. (At least it is probably not the recommended way to send full modules over the wire).

(My concrete case is about Pluto, i.e. I am using Malt.jl actually, currently in Distributed-fallback-mode)