Shared Variables In Nested Macros

I’d like to share variables between nested macros in a convenient manner, but this seems to be difficult. I will try to illustrate the situation with some code examples.

To create custom properties to Julia types one can do the following:

struct S 
    field
end

function Base.getproperty(this::S, sym::Symbol)
    if sym === Symbol("property")
        return this.field * 2
    end

    if sym === Symbol("fun")
        return function(n::Int)
            return this.field * n
        end
    end

    return getfield(this, sym)
end

s = S(10)
s.field
s.property
s.fun(3)

However, the branching Base.getproperty is not too pretty. What I’d like instead is the following (which should have the same behavior as above):

@defs S begin
    @def property this.field * 2
    
    @def fun function(n::Int)
        return this.field * n
    end
end

This looks significantly better, but this requires sharing of variables between macros:

module PropertyMacros
    export @defs, @def

    macro defs(type, body)
        type = esc(type)
        body = esc(body)

        return quote
            function Base.getproperty(this::$type, sym::Symbol)
                $body
                return getfield(this, sym)
            end
        end
    end

    macro def(symbol, body)
        symb = string(symb)
        body = esc(body)

        return quote

            # NOTE: "sym" has been defined in defs !
            if sym === Symbol($symb)
                return $body
            end
        end
    end
end

Great, this seems to be what I want, however, there is one problem. The def macro does not
know about sym and will always convert it to PropertyMacros.sym and thus breaking the
functionality.

Using @macroexpand on the “pretty” code gives the following (I cleaned the generated comments):

using PropertyMacros

@macroexpand @defs S begin
    @def property this.field * 2
    
    @def fun function(n::Int)
        return this.field * n
    end
end

=>

# NOTE: 
# The function signature defines: var"#8#sym"
# However, the variable used in the ifs is: Main.PropertyMacros.sym

quote
    function (Main.PropertyMacros.Base).getproperty(var"#7#this"::S, var"#8#sym"::Main.PropertyMacros.Symbol)
        begin
            begin
                if Main.PropertyMacros.sym === Main.PropertyMacros.Symbol("property")
                    return this.field * 2
                end
            end
            begin
                if Main.PropertyMacros.sym === Main.PropertyMacros.Symbol("fun")
                    return function (n::Int,)
                        return this.field * n
                    end
                end
            end
        end
        return Main.PropertyMacros.getfield(var"#7#this", var"#8#sym")
    end
end

The problem with the generated code is that var"#8#sym" != Main.PropertyMacros.sym (same
problem actually applies to the this identifier).

The easy solution to this problem would be to just tell the macro generator to not transform some identifiers or to just inject strings, like this fictional example: injectstring("sym"), but without the quotes.

Another solution would be to somehow specify macro dependencies, but this feature is unlikely implemented in Julia.

I did successfully implement this behavior using raw strings, Meta.parse, esc and @eval, but that code is quite ugly.

Is this problem solvable at the moment without using raw strings? Any help would be much appreciated!

You’ll need to esc some of the argument names, this, and sym, which should probably reference the right things then:

macro defs(type, body)
    type = esc(type)
    body = esc(body)
    this = esc(:this)
    sym = esc(:sym)
    return quote
        function Base.getproperty(this::$type, sym::Symbol)
            $body
            return getfield(this, sym)
        end
    end
end

macro def(symbol, body)
    symb = string(symbol)
    body = esc(body)
    sym = esc(:sym)
    return quote

        # NOTE: "sym" has been defined in defs !
        if sym === Symbol($symb)
            return $body
        end
    end
end

If @def is never used outside of @defs then maybe don’t even define a @def macro and just do all the processing inside of @defs instead. That would save you some of the headaches of dealing with macro hygiene.

The string to Symbol conversions in @def can be switched to use Meta.quot btw:

macro def(symbol, body)
    body = esc(body)
    sym = esc(:sym)
    return quote
        # NOTE: "sym" has been defined in defs !
        if $sym === $(Meta.quot(symbol))
            return $body
        end
    end
end
2 Likes

Thank you very much!

You missed some interpolating $ in you code, but otherwise it works perfectly!

This should be the final code, including the missing $:

module PropertyMacros
    export @defs, @def

    macro defs(type, body)
        type = esc(type)
        body = esc(body)
        this = esc(:this)
        sym = esc(:sym)
        return quote
            function Base.getproperty($this::$type, $sym::Symbol)
                $body
                return getfield(this, sym)
            end
        end
    end
    
    macro def(symbol, body)
        body = esc(body)
        sym = esc(:sym)
        return quote
            # NOTE: "sym" has been defined in defs !
            if $sym === $(Meta.quot(symbol))
                return $body
            end
        end
    end
end

And now this will work:

using PropertyMacros 

struct S 
    field
end

@defs S begin
    @def property this.field * 2
    
    @def fun function(n::Int)
        return this.field * n
    end
end

s = S(10)
s.field
s.property
s.fun(3)

=>

10
20
30