Argument confusion when calling macro with semicolon parameter syntax

Applying semicolon parameter syntax generates a surprising order of expressions;
parameters go in the first place, followed by the expected order of arguments

julia> macro m1(expressions...)
           @show expressions
           return nothing
       end
@m1 (macro with 1 method)

julia> @m1(a, b = "b"; c = "c")
expressions = (:($(Expr(:parameters, :($(Expr(:kw, :c, "c")))))), :a, :(b = "b"))

So far so good, one can adapt the macro accordingly. It gets confusing when one tries to require certain parameters

julia> macro m2(x, expressions...)
           @show x expressions
           return nothing
       end
@m2 (macro with 1 method)

julia> @m2(a, b = "b"; c = "c")
x = :($(Expr(:parameters, :($(Expr(:kw, :c, "c"))))))
expressions = (:a, :(b = "b"))

So the order of parameters is the same as before but it is wrongly attributed to the parameter names. Is this use of macros not recommended?
It is very handy when forwarding keyword arguments to functions.

I usually don’t use keyword arguments with macros using the semicolon syntax. I pass them as regular arguments, and with a little bit of magic in the body of the macro, convert every assignment argument into a keyword argument:

macro m(args...)
    kwargs = [Expr(:kw, arg.args...) for arg in args
              if Meta.isexpr(arg, :(=))]
    args = filter(arg -> !Meta.isexpr(arg, :(=)), args)
    # and then do whatever you like with kwargs and args
    @show kwargs args
    nothing
end

julia> @m 1 2 x=3 4 y=5
kwargs = Expr[:($(Expr(:kw, :x, 3))), :($(Expr(:kw, :y, 5)))]
args = (1, 2, 4)
1 Like

I didn’t know Meta.isexpr, which is very convenient instead of writing arg isa Expr && arg.head == (:=).
Thanks! :grinning:

Otherwise my way of argument handling is

for arg in args
    Meta.isexpr(arg, :(=)) && (arg.head = :kw)
end

which is an in-place version of yours.

1 Like

For whatever reason, the semicolon expression puts the keywords first. You can see it in the Expr structure:

julia> dump(:( a, b = "b"; c = "c" ))
Expr
  head: Symbol tuple
  args: Array{Any}((3,))
    1: Expr
      head: Symbol parameters
      args: Array{Any}((1,))
        1: Expr
          head: Symbol kw
          args: Array{Any}((2,))
            1: Symbol c
            2: String "c"
    2: Symbol a
    3: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol b
        2: String "b"

@blah(a, b; c=C) doesn’t work on 1 expression like @blah (a, b; c=C) and does separate into 3 inputs, but it does parse the expression (a, b; c=C) first, hence the keywords coming first. It’s better to avoid the semicolon in calls because the space-separated syntax @blah a b c=C is only equivalent to the call syntax @blah(a, b, c=C).

Ah, I haven’t realized Expr was mutable. Nonetheless, I tend to prefer functional style and no in-place modification, when there is no performance penalty, or it is irrelevant.

I understand that in-place modification in general is sometimes risky, but in this case it is just shorter to write and all args can be passed just as is to any function, you don’t even have to split args and kwargs.

Concerning inplace modification just be aware that your

will pass on the args as they are and you may still be modifying your args in-place, e.g.

macro m(args...)
    newargs = filter(arg -> !Meta.isexpr(arg, :(=)), args)
    for arg in newargs
        if Meta.isexpr(arg, :vect)
            arg.args = vcat(["test"], arg.args)
        end
    end
    :($args)
end

@m [1, 2, 3, 4] ["a", "b", "c"]
# (:(["test", 1, 2, 3, 4]), :(["test", "a", "b", "c"]))
1 Like

filtered elements are === equal to elements in the input iterable, not deep-copied, so this is always the case when the elements are mutated. I think HanD just meant “# and then do whatever you like with kwargs and args” does not involve mutation e.g. constructing Expr(:kw, arg.args...) instead of mutating arg.head = :kw. Personally I do mutation if it’s simpler to write and I didn’t need to keep the original Expression for something else, and if that ends up changing, I don’t have to replace the mutation code because I could just deepcopy the Expression.

1 Like

I totally agree. It’s quite the same practice that I have adopted over time …
So let’s close this, I think we agree that argument handling of macros has some pitfalls but they will probably not be changed due to performance and compatibility reasons.
Thanks everyone to their comments and inputs!