Julia: How do macros know when their arguments stop?

Viewed 192

This is a beginner question but I had some trouble finding an answer in the documentation (or on StackOverflow) and I think it will be helpful to others as well. The question is simple:

How does a Julia macro call know when the arguments to the macro are done?

When brackets are used to mark the arguments, it should be straightforward (I think...). However, in the other case it seems more subtle. I suspect it has something to do with "number of expressions" (this potentially being a slightly tricky concept in itself) but I'm not sure and I want to have the documented official rule.

Some examples of why I think this is not obvious:

julia> macro a(arg...)
print(arg)
end;
julia> @a gg=3 if true # the :(gg = 3) is the first argument of the macro
              print("val")
              a = 1;;; # the macro does not see the semicolons (which makes sense to me)
              end  # the if statement is the second argument of the macro
# note: replacing "true" by "@a true" makes the number of arguments of 
# the second macro depend on the following newline being there or not

# next: the first two assignments are two arguments. The last assignment is not an argument at all
julia> @a a = 4 f=3; aaa = "asdf" 

julia> @a af = 4 (f=3; aaa = "asdf") # two arguments; the second one is a quote block
julia> @a af = 4; (f=3; aaa = "asdf") # one argument.

# edit:
julia> @a @a aa # One argument, counterexample to last claim
(:(#= REPL[37]:1 =# @a aa),)

Is it possible, for example, to have two consecutive if blocks as separate arguments?

One rule I seem to notice is that (when not using parentheses to enclose arguments) any macro call with more than one argument becomes invalid syntax if the macro part (@a) is removed. Is this true in general?

edit: No, see counterexample in code snippet.

1 Answers

The rule for:

@name expr1 expr2 ...

macro call is a macro is fed all white space separated expressions until the end of statement is reached. Normally the statement ends when you reach a newline, a ; or some other character like ] or ) signaling end of statement is reached.

Here are some examples using your @a macro.

julia> @a @a aa # here @a aa produces one expression which is passed to outer @a call
(:(#= REPL[37]:1 =# @a aa),)

julia> @a x = 1 y = 2 # two expressions are passed to a macro
(:(x = 1), :(y = 2))

julia> @a x = 1 y = 2; # the same, but we terminated the statement using ;
(:(x = 1), :(y = 2))

julia> @a x = 1 y = 2; 1; # 1 does not go into the macro as ; terminated the statement
(:(x = 1), :(y = 2))

julia> @a (@a x) y # outer @a gets two expressions as we delimited the @a x with parentheses
(:(#= REPL[40]:1 =# @a x), :y)

julia> @a begin
       @a x
       end y # similar but with begin-end block
(quote
    #= REPL[44]:2 =#
    #= REPL[44]:2 =# @a x
end, :y)

julia> @a [@a x] y # this time we used square brackets
(:([#= REPL[39]:1 =# @a(x)]), :y)

I think in the above examples e.g. it is crucial to see that for @a x = 1 y = 2 the Julia parser sees x = 1 as a whole although there are spaces between = as it sees it as a single expression.

Also note that you can feed to a macro only things that are valid expressions, so e.g.:

julia> @a =
ERROR: syntax: unexpected "="

fails as:

julia> =
ERROR: syntax: unexpected "="

(this is actually an important consideration when designing DSL with Julia macros as you have to make sure that the syntax you design consists of valid expressions)

Also note that macro is fed all white space separated expressions so this is an error:

julia> macro b(x)
       print(x)
       end
@b (macro with 1 method)

julia> @b 1 2
ERROR: LoadError: MethodError: no method matching @b(::LineNumberNode, ::Module, ::Int64, ::Int64)

Finally note:

julia> @a [@a x] y
(:([#= REPL[64]:1 =# @a(x)]), :y)
julia> @a[@a x] y
ERROR: syntax: extra token "y" after end of expression

as Julia parses a macro call with a [ directly following the @a in a special way passing only one argument to the macro in this case.

Related