How to write a macro whose generated code depends on a runtime expression?

I want to define several structs having nearly identical bodies, and avoid repetitive coding using macros. Example: The As have identical bodies; B adds a single field.

struct A1
   a::Int
end 

struct A2
   a::Int 
end 

struct B
   a::Int
   b::Int
end 

I’d like to write a single macro that allows me to generate this code. What I’ve tried:

macro genstruct(name)
   expr= quote
      struct $name 
         a::Int
      end
   end
   return expr
end 

allows me to generate A1 and A2, but not B. Can I modify genstruct in such a way as to enable creation of B as well? Is there another approach I should be taking?

Sure, here’s a simple version that generates all of your examples:

julia> macro genstruct(name, fields...)
         quote
           struct $(esc(name))
             $((:($field::Int) for field in fields)...)
           end
         end
       end
@genstruct (macro with 1 method)

julia> @macroexpand @genstruct A1 a
quote
    #= REPL[10]:3 =#
    struct A1
        #= REPL[10]:4 =#
        a::Main.Int
    end
end

julia> @macroexpand @genstruct A2 a
quote
    #= REPL[10]:3 =#
    struct A2
        #= REPL[10]:4 =#
        a::Main.Int
    end
end

julia> @macroexpand @genstruct B a b
quote
    #= REPL[10]:3 =#
    struct B
        #= REPL[10]:4 =#
        a::Main.Int
        b::Main.Int
    end
end
3 Likes

Thanks. I’m struggling to understand the “splatting interpolation” you showed; I tried to break it out into a conventional for-loop, which I thought should have been equivalent to your syntax, but it fails:

macro makestruct(name, fields...) 
    quote 
        struct $(esc(name))
            for field in fields 
                :($(esc(field))::Int)
            end
        end
    end
end
julia> @makestruct A a
ERROR: syntax: field name "Array{Any, (0,)}[]" is not a symbol around /home/elliot/Documents/Programming/JuliaNotesVSCode/abstract_type_algebra.jl:31

Line 31 is for field in fields.

Evidently for-loop expressions are not allowed in macros? What about other control flow syntax like if...end?

No–the problem is that your macro is producing a for loop in the code returned by the macro. You’re producing a struct definition that looks something like:

struct A
  for field in fields
    x::Int
  end
end

That is, you’re mixing up the part of the macro that generates the code (which can have any control flow you want) with the actual code that the macro produces (which can also have any control flow as long as the result is valid Julia code).

This is easier to work with if you start with a simpler example. Given a list of args, let’s produce an array literal [arg[1], arg[2], ...] You don’t need a macro for this, but it’s a useful learning exercise.

Here’s a first attempt:

julia> macro foo(args...)
         quote
           [
             $(esc(args))
           ]
         end
       end
@foo (macro with 1 method)

Note that this doesn’t quite work:

julia> @macroexpand @foo a b c
quote
    #= REPL[14]:3 =#
    [(:a, :b, :c)]
end

We’ve created an array, but it only has one element-- the tuple (:a, :b, :c). We need to turn the tuple args into multiple separate arguments using ...:

julia> macro foo(args...)
         quote
           [
             $(esc.(args)...)
           ]
         end
       end
@foo (macro with 1 method)

This gives the desired result:

julia> @macroexpand @foo a b c
quote
    #= REPL[23]:3 =#
    [a, b, c]
end

If this is still confusing (it is for me, since it took me a few tries to get my example right), then you might want to try building the expressions directly, rather than messing around with splatting. For example, we can create an empty array literal with:

julia> Expr(:vect)
:([])

You can figure out the vect name by using dump to look at an existing expression:

julia> dump(:([a, b, c]))
Expr
  head: Symbol vect
  args: Array{Any}((3,))
    1: Symbol a
    2: Symbol b
    3: Symbol c

So to produce [a, b, c], we need a :vect Expr with arguments :a, :b, and :c. That’s easy to make:

julia> Expr(:vect, :a, :b, :c)
:([a, b, c])

and we can use it in our macro as well:

julia> macro foo2(args...)
         Expr(:vect, esc.(args)...)
       end
@foo2 (macro with 2 methods)

julia> @macroexpand @foo2 a b c
:([a, b, c])

If you’d rather do it with a loop, you can:

julia> macro foo3(args...)
         result = Expr(:vect)
         for arg in args
           push!(result.args, esc(arg))
         end
         return result
       end
@foo3 (macro with 1 method)

julia> @macroexpand @foo3 a b c
:([a, b, c])

You can do the same thing for your struct example:

julia> dump(:(
         struct A
           a::Int
         end
       ))
Expr
  head: Symbol struct
  args: Array{Any}((3,))
    1: Bool false
    2: Symbol A
    3: Expr
      head: Symbol block
      args: Array{Any}((2,))
        1: LineNumberNode
          line: Int64 3
          file: Symbol REPL[36]
        2: Expr
          head: Symbol ::
          args: Array{Any}((2,))
            1: Symbol a
            2: Symbol Int

It’s a bit more complicated, which is why it’s nice to use a shortcut like I wrote in my original post, but you can always create exactly the macro behavior you want by building up the Expr objects directly. Using dump is a great way to figure out what the final Expr tree should look like.

7 Likes

Hey thanks very much, especially for pointing out that esc can be broadcast. (For some reason I tend to forget things I already know about Julia when working in macro-land…) Thanks again.