Documentation for how macros handle keyword arguments

Macros seem to have a pretty weird way of dealing with keyword arguments, especially keyword arguments that are passed with ;. It seems arguments after ; are moved to the front of the macro call, except if the macro is combined with the do syntax (then they’re moved to the second place). I came across this problem while trying to write my own version of DrWatson.@produce_or_load (which includes code to handle this sort of situation).

To illustrate, consider this macro:

macro pr(exs...)
    for (i, ex) in enumerate(exs)
        print("$i: type $(typeof(ex))")
        if ex isa Expr
            print("   Head: $(repr(ex.head)); Args: $(ex.args)")
        end
        print("\n")
    end
end

This produces the following:

julia> @pr(projectdir("name.jld2"), a=5; b=b, c) do
       return nothing
       end
1: type Expr   Head: :->; Args: Any[:(()), quote
    #= REPL[2]:2 =#
    return nothing
end]
2: type Expr   Head: :parameters; Args: Any[:($(Expr(:kw, :b, :b))), :c]
3: type Expr   Head: :call; Args: Any[:projectdir, "name.jld2"]
4: type Expr   Head: :kw; Args: Any[:a, 5]

julia> @pr(_ -> nothing, projectdir("name.jld2"), a=5; b=b, c)
1: type Expr   Head: :parameters; Args: Any[:($(Expr(:kw, :b, :b))), :c]
2: type Expr   Head: :->; Args: Any[:_, quote
    #= REPL[3]:1 =#
    nothing
end]
3: type Expr   Head: :call; Args: Any[:projectdir, "name.jld2"]
4: type Expr   Head: :(=); Args: Any[:a, 5]

I can sort of work around all of that behavior, but my actual question is: where is any of this documented? That would include moving around keyword arguments, and the meaning of the “special” heads :parameters, :kw, and :->. I’m not finding any of these in Metaprogramming · The Julia Language. Is there some other resource that can serve as a reference for macro programming? Is any of this guaranteed to be stable between different Julia versions?

1 Like

For what it’s worth, I wrote this utility function to fix the exs:

@doc raw"""
Given a list of macro arguments, push all keyword parameters to the end.

A macro will receive keyword arguments after ";" as either the first or second
argument (depending on whether the macro is invoked together with `do`). The
`reorder_macro_kw_params` function reorders the arguments to put the keyword
arguments at the end or the argument list, as if they had been separated from
the positional arguments by a comma instead of a semicolon.

# Example

With

```
macro mymacro(exs...)
    @show exs
    exs = reorder_macro_kw_params(exs)
    @show exs
end
```

the `exs` in e.g. `@mymacro(1, 2; a=3, b)` will end up as

```
(1, 2, :($(Expr(:kw, :a, 3))), :($(Expr(:kw, :b, :b))))
```

instead of the original

```
(:($(Expr(:parameters, :($(Expr(:kw, :a, 3))), :b))), 1, 2)
```
"""
function reorder_macro_kw_params(exs)
    exs = Any[exs...]
    i = findfirst([(ex isa Expr && ex.head == :parameters) for ex in exs])
    if !isnothing(i)
        extra_kw_def = exs[i].args
        for ex in extra_kw_def
            push!(exs, ex isa Symbol ? Expr(:kw, ex, ex) : ex)
        end
        deleteat!(exs, i)
    end
    return Tuple(exs)
end

A lot of it seems to be documented at Julia ASTs · The Julia Language, although maybe how exactly this translates for the arguments of a macro call may not always be entirely clear.