Module scoping and macro expansion

So I’ve been getting really confused with the interaction between nested macro expansion and module scoping…

In my module MyModel I have a set of macros with which I implement a simple DSL. Things work fine in this simpler case:

julia> using MyModel

julia> @macroexpand @habitat(MyModel.@landcover() == MyModel.water)
quote
    function (pos, model)
        if MyModel.landcover(pos, model) == MyModel.water
            return true
        else
            return false
        end
    end
end

But in this more complex case, the function that @habitat generates suddenly has (MyModel.pos, MyModel.model) as function arguments, instead of just (pos, model):

julia> @macroexpand MyModel.@initialise!(@habitat(MyModel.@landcover() == MyModel.water), pairs=true)
:(initialise! = MyModel.initpopulation(begin
              function (MyModel.pos, MyModel.model)
                  if MyModel.landcover(MyModel.pos, MyModel.model) == (MyModel.MyModel).water
                      return true
                  else
                      return false
                  end
              end
          end, $(Expr(:(=), Symbol("#527#pairs"), true))))

This throws an error if executed (LoadError: syntax: "MyModel.pos" is not a valid function argument name).

The definitions of the relevant macros are (note that MyModel exports @habitat, but not the other two):

macro initialise!(habitatdescriptor, kwargs...)
    :($(esc(:initialise!)) = initpopulation($habitatdescriptor, $(kwargs...)))
end

macro habitat(body)
    quote
        function($(esc(:pos)), $(esc(:model)))
            if $(esc(body))
                return true
            else
                return false
            end
        end
    end
end

macro landcover()
    :(landcover($(esc(:pos)), $(esc(:model))))
end

This is just one example of several similar problems I’m running into. So any help you can offer on this example would be great, but if you know of any good explanations on how macro expansion works in other module contexts, that would also be much appreciated!

Basically the question is: how I can I stop Julia sticking MyModel. in front of every symbol that it sees in a nested macro?

Usually it’s pretty rare to see macros that act on variables believed to exist in the current scope by the macro creator. In your case, you want @landcover to act on variables pos and model which have to exist already, otherwise the user will get a pretty obscure UndefVarError (they’d go looking where they’d written pos or model and not find anything).

But anyway, let’s assume you continue with that approach. In any code that is returned from a macro, symbols refer to things in the module containing the macro definition, unless they’re escaped. If they are escaped, they refer to things in the context where the macro is used.

macro habitat(body)
   quote
       function($(esc(:pos)), $(esc(:model)))
           if $(esc(body))
                return true
            else
                return false
            end
        end
    end
end

This doesn’t work because in an anonymous function expression, the variable names in the definition are declared for use in the function body. What you’re trying to do is to make the function arguments be the same thing as the variables pos and model outside. It’s a bit confusing for me what you actually want to do here, because you’re not using pos and model in the function at all. If you want to close over them (refer to them in the body of the function) use them inside and escape them there. If you want to call the function with the variables, leave alone the input arguments and then separately call the function with the escaped variables.

Maybe this helps you as well https://jkrumbiegel.com/pages/2021-06-07-macros-for-beginners/#whats-escaping

1 Like

Thank you for the quick response!

Usually it’s pretty rare to see macros that act on variables believed to exist in the current scope by the macro creator.

Yes, I am aware of the risk. The reason for doing so is that @landcover should only ever be used in the context of a call to @habitat. (I am very clear about this in the documentation, and also plan to put in some coded safeguards later on to make sure it’s not being called anywhere else.)

@habitat constructs a function that tests whether a given position in the model landscape is suitable habitat for a species. It is meant to be used in conjunction with @landcover and a set of similar wrapper macros, which allow the user to quickly construct a conditional statement to define the habitat test. The idea is to enable something like this:

habitats = @habitat((@landcover() == grass || 
                         (@landcover() == agriculture && @croptype() != maize &&
                          @cropheight() < 10)) &&
                        @distanceto(forest) > 20)

The variables pos and model are needed by the underlying helper functions (e.g. landcover()), and therefore need to be passed to the habitat testing function, which simply passes them on. That’s why they appear in @habitat, but are not directly used there. That is also why it’s important that the symbols generated by @landcover match those generated by @habitat. Does that make sense?

So my big question is: Why is the macroexpansion of @habitat different when it is called within another macro call (here: @initialise!), as compared to when it is called directly?`

Thank you for the link to the macro tutorial, it seems like a very well-written introduction :slight_smile: I’ll take a more detailed look at it after the weekend.

OK, I’ve solved it - I just needed to do some more escaping in @initialise!:

Before:

macro initialise!(habitatdescriptor, kwargs...)
    :($(esc(:initialise!)) = initpopulation($habitatdescriptor, $(kwargs...)))
end

After:

macro initialise!(habitatdescriptor, kwargs...)
    :($(esc(:initialise!)) = initpopulation($(esc(habitatdescriptor)); $(map(esc, kwargs)...)))
end