Macro: is it possible to receive an evaluated argument?

Fundamentally, what is a macro? A macro transforms syntax into syntax, there’s no values involved here (in some sense, the values that will eventually be computed don’t exist during macro expansion at all - there’s only parsed syntax, none of the modified code has run yet). Let’s take a look at what we have (f(a,b) and :mystruct) and what we want (f(a, b*value(mystruct))). What do these two look like once they’re parsed, i.e. what does the julia compiler see? We can find out via Meta.@dump:

julia> Meta.@dump (f(a,b), mystruct)    
Expr                                    
  head: Symbol tuple                    
  args: Array{Any}((2,))                
    1: Expr                             
      head: Symbol call                 
      args: Array{Any}((3,))            
        1: Symbol f                     
        2: Symbol a                     
        3: Symbol b                     
    2: Symbol mystruct                  

This may look scary at first, so let’s break it down. This expression tells us what @dump saw is a tuple of 2 elements (hence head is :tuple and args is Array{Any}((2,))). The two arguments are another expression (which is the way function calls are represented) and a Symbol (in this case, :mystruct).

Now for the second expression:

julia> Meta.@dump f(a,b*value(mystruct))
Expr                                    
  head: Symbol call                     
  args: Array{Any}((3,))                
    1: Symbol f                         
    2: Symbol a                         
    3: Expr                             
      head: Symbol call                 
      args: Array{Any}((3,))            
        1: Symbol *                     
        2: Symbol b                     
        3: Expr                         
          head: Symbol call             
          args: Array{Any}((2,))        
            1: Symbol value             
            2: Symbol mystruct          

This is what we want to get out of the macro. Notice how this expression immediately starts with :call! It’s not wrapped in a :tuple expression anymore. We can also see that the third argument to the call (which used to be just :b above) is now another expression calling *, which itself has another :call embedded in it, this time to the function value. The whole inner call with :value and :mystruct corresponds to value(mystruct). Now, how do we get from the first form to the second form? We simply build the expression in our macro and return it:

macro myMacro(ex, mystruct)
  #do error handling here, e.g. if `ex` is not a function call!
  local tmp = ex.args[3] # get the third argument, which is `b`
  local valueExpr = :(value($mystruct))
  local multiplyExpr = Expr(:call, :*, tmp, valueExpr)
  ex.args[3] = multiplyExpr
  return ex
end

Using this, we get:

julia> struct MyStruct                                                
       end                                                            
                                                                      
julia> f(a, b) = a * b                                                
f (generic function with 1 method)                                    
                                                                      
julia> mystruct = MyStruct()                                          
MyStruct()                                                            
                                                                      
julia> value(mystruct::MyStruct) = 2                                  
value (generic function with 1 method)                                
                                                                      
julia> macro myMacro(ex, mystruct)                                    
         #do error handling here, e.g. if `ex` is not a function call!
         local tmp = ex.args[3] # get the third argument, which is `b`
         local valueExpr = :(value($mystruct))                        
         local multiplyExpr = Expr(:call, :*, tmp, valueExpr)         
         ex.args[3] = multiplyExpr                                    
         return ex                                                    
       end                                                            
@myMacro (macro with 1 method)                                        
                                                                      
julia> a = 3 # define our variables
3                                                                     
                                                                      
julia> b = 4 # define our variables
4                                                                     
                                                                      
julia> @macroexpand @myMacro(f(a,b), mystruct)                        
:(Main.f(Main.a, Main.b * Main.value(Main.mystruct)))  # sanity check, this looks like what we care about
                                                                      
julia> @myMacro(f(a,b), mystruct)                                     
24 # 3*4*2 = 24, seems to have worked!

I hope this helps!

On a side note, the way this macro is written is very crude and should use some sanity checking and escaping logic, to make sure the correct variables are referenced in all contexts :slight_smile: This is called “Macro Hygiene”, the documentation as a great chapter about it here.

If we don’t practice hygiene, the macro won’t work correctly in other scopes:

julia> function x()                                     
         a = 0                                          
         b = 0                                          
         mystruct = MyStruct()                          
         @myMacro(f(a,b), mystruct)                     
       end                                              
x (generic function with 1 method)                      
                                                        
julia> x()                                              
24 # we defined `a` and `b` to be `0` in `x()`, so we would expect to get `0` out as well! Why don't we?
                                                        
# Let's find out why by checking with `@macroexpand`
julia> @macroexpand function x()                        
         a = 0                                          
         b = 0                                          
         mystruct = MyStruct()                          
         @myMacro(f(a,b), mystruct)                     
       end                                              
:(function x()                                          
      #= REPL[25]:1 =#                                  
      #= REPL[25]:2 =#                                  
      a = 0                                             
      #= REPL[25]:3 =#                                  
      b = 0                                             
      #= REPL[25]:4 =#                                  
      mystruct = MyStruct()                             
      #= REPL[25]:5 =#                                  
      Main.f(Main.a, Main.b * Main.value(Main.mystruct)) # Here's the culprit! Everything is referencing `Main`, which it clearly shouldn't here...
  end)                                                  
3 Likes