Can I "unescape" a variable name within an `esc(..)` node?

Hello, I’m curious whether it is possible to individually pick expressions within an esc()aped node to make them hygienic again. For instance:

macro f(...)
    xp = esc(:(...))
    unescape!(xp.args[2]) # or something
    xp
end

This would be useful for using temporary variables within generated code that otherwise needs to be escaped. For instance:

module M

# The decorated structs will subtype this union.
abstract type A{T} end

# Not all T's are valid, and this function is responsible to check that.
is_correct_type(T) = true # (dummy)

macro decorate_struct(type_expression, str)
    code = quote end

    # Generate code to evaluate invoker's expression once
    # and either check validity or display friendly error.
    append!(code.args,
        (
            quote
                T = try # <- This variable be hygienically gensym'ed.
                    $type_expression
                catch e
                    error("Could not evaluate type expression: $($(repr(type_expression))): $e")
                end
                $is_correct_type(T) || error("Invalid type: $T")
            end
        ).args)

    # Use T to generate the actual struct definition code.
    str_name = str.args[2]
    str.args[2] = :($str_name <: $A{T}) # <- How to "unescape" this `T` here so it does refer to the gensym'ed version?

    push!(code.args, esc(str)) # <- This needs be escaped.

    code
end
export @decorate_struct

end

using .M
@decorate_struct Int64 struct First end #  <- ERROR: `T` not defined.
@decorate_struct Int64 mutable struct Second
    a::First
    b::String
end

No, such a function does not exist.

Looking over your code, I can’t help but notice an eval. Since you’re writing a macro, you shouldn’t really need to do that; the result of the macro expansion will already be evaluated (and the result of any eval in a local scope will only be visible once the code returns to a global scope).

3 Likes

Wops, you’re right, I’m removing it. Not sure how it ended up there :slight_smile: Thank you for your answer. Is there any obvious reason why such a unescape! function does/should not exist?

It’s generally better to only escape the parts you definitely want to evaluate in the context where the macro was called; not everything the macro produces. Macro hygiene is not something you can just slap on an expression :slight_smile:

Well, I do totally agree with this. So maybe I can reformulate my problem better in these terms now :wink:

Escaping only the parts I need would have been easy in the following situation, because it’s a small part to escape within a big unescaped, generated node:

macro generate_struct(name)
    :(struct esc($name) <: SuperType # <- The only part I need to escape within this unescaped generated :struct.
        a::Int64
        b::Float64
    end)
end

@generate_struct A

But it is difficult when it happens to be the other way round: I need to ‘unescape’ a small part of a big, escaped, non-generated node:

macro decorate_struct(str)
    str_name = str.args[2]
    str.args[2] = :($str_name <: unescape(T)) # <- The only part I need *not* to escape..
    quote
        T = ...
        esc($str) # <- .. within this large escaped, *input* :struct.
    end
end

@generate_struct mutable struct A
    a::First
    b::String
end

How to best handle this situation then?

One thing you can do is using gensym yourself to generate an hygienic name for the temporary variable.

If I understand correctly what you want to do, a simplified version could look like this:

julia> module M
           abstract type A{T} end
       
           macro decorate_struct(str)
               str_name = str.args[2]
               Tsym = gensym(:T)  # Tsym will be escaped but it's OK because it can't clash with user-provided code
               str.args[2] = :($str_name <: $A{$Tsym})
               quote
                   $(esc(Tsym)) = Int
                   $(esc(str))
               end
           end
       end
Main.M

julia> @macroexpand M.@decorate_struct struct Foo end
quote
    #= REPL[1]:9 =#
    var"##T#292" = Main.M.Int
    #= REPL[1]:10 =#
    struct Foo <: (Main.M.A){var"##T#292"}
        #= REPL[2]:1 =#
    end
end

which seems to work as intended:

julia> M.@decorate_struct struct Foo end
julia> supertype(Foo)
Main.M.A{Int64}
2 Likes

Oh, I like this approach @ffevotte :slight_smile:

From the doc of gensym, I understand that the generated name is only hygienic “within the same module”. IIUC the gensym(:T) in your snippet is executed within the macro definition module M, so it is guaranteed to not clash with any variable name in M… but is it also guaranteed to not clash within the macro invocation module?

If not, should I enforce it with something akin to Tsym = Core.eval(__module__, :(gensym(:T))) instead? Or maybe I misunderstand how gensym works and this is not necessary?

1 Like