How can Julia macro take an Expr variable as its arguments?

You can think of it this way. The macro definition you write takes in an expression and returns an expression, much like a method can. But only a method could take in a runtime Expr instance and return another. The macro definition is just a way for you to customize one part of a bigger process of parsing and evaluating source code. You can’t control the parser converting the source code to an Expr that the macro will work on, and you can’t control the macro’s output Expr being evaluated. (You can however use @macroexpand to wrap the output Expr in an extra Expr layer so when it is evaluated, you get the output Expr.)

This incidentally is why macros are said to only work on literals, symbols, and Expr of such; it’s because the parser can only produce such things from source code. However, there is actually an internal (not public API, not stable across versions) trick to get the macro definition’s underlying method, which can take in runtime Expr instances. Right after that trick’s comment in the thread, I also commented how to use the wholly public macroexpand function and $-interpolation to make a macro work on a runtime Expr.