I am wondering if there is any reason to avoid macro names which the parser would normally reject? For example, in BorrowChecker.jl, I want to define the @& macro:
This is essentially a shorthand to the OrBorrowed{T} alias which behaves like &T in Rust (and &mut T respectively). When integrating BorrowChecker.jl with a library, you tend to need to write OrBorrowed{...} a LOT, as a way of extending your functions to take immutable references to objects when running the borrow checker. The shorthand makes this easier and more readable.
However, when you try to define this normally, you get:
julia> macro &(expr)
esc(expr)
end
ERROR: ParseError:
# Error @ REPL[3]:1:7
macro &(expr)
# └─────┘ ── Invalid signature in macro definition
julia> macro Base.:&(expr)
esc(expr)
end
ERROR: syntax: invalid macro definition around REPL[1]:1
Stacktrace:
[1] top-level scope
@ REPL[1]:1
But you can get around this with an eval:
julia> @eval macro $(:&)(expr)
esc(expr)
end
@& (macro with 1 method)
I am just wondering if there’s any issues with this, or if I am completely fine to define this otherwise invalid macro.
It’s not an illegal macro name; :(@&) is a valid Expr, though this alone isn’t a test of valid syntax e.g. :(macro &() end), and attempted evaluation throws an UndefVarError instead of a ParseError.
I think this is just the parser being unable to intuitively handle symbols for infix operators; specifically for &, there seems to be missing :call and :block sub-Exprs, which also causes definitions for functions to fail e.g. &() = 0. Parentheses works, e.g. macro (&)() end. I wish there were some sort of guideline for when : and () are needed, it always takes me by surprise.
The syntax error for macro Base.:& is a separate issue #54488 for module qualification, rooted in little historical demand or support for extending macros, let alone across modules. That exact code wouldn’t make sense either because there is no @& macro in Base, nor is it related to the function Base.:&.
That seems right. macro (let)() end throws a ParseError because the parser expected a let expression and won’t accept the ). @eval and var"" indeed are ways to put keywords into macro names. It can also do that for function names, but you’d need module qualification in calls to get around the parser insisting on keywords.
julia> @eval $(:for)() = 0
for (generic function with 1 method)
julia> Main.for()
0
julia> eval(:(for())) # or simply for() and press Enter twice
ERROR: ParseError:
# Error @ REPL[43]:1:13
eval(:(for()))
# └ ── invalid iteration spec: expected one of `=` `in` or `∈`
I hope you’re sticking to the macros because the keyword-resembling functions are cursed IMO.
Regardless of syntactic workarounds, I’d find it really unfortunate if our code was littered with two different kinds of let that mean different things. The & case is a lot more defensible and (IMO ) innocuous, but I’d say the errors are doing the right job in let’s case.
I think paying the cost of slight deviation from Rust syntax (calling it @letrs or @bclet or anything slightly different like that) is well worth it to not have a confusing @let that’s semantically quite different from Julia’s let.
At the moment, BorrowChecker.jl has @own for creating owned values and @ref for creating references.
Since I’d like to add a @& for declaring borrowed types I wondered if it would make sense to combine @own and @ref into a single macro that would change type based on whether you used @& or not. I’m not sure.
@& would be used like this:
function push!(x::@&(:mut, MyVector), i::@&(Int))
#= ... =#
end
(it renders nicer in vscode)
Explanation: It is basically a way to declare the mutability of input references, something which Julia doesn’t have syntax for at the moment. The ! is often used as an indication the first arg might be changed, but there’s no guarantee, and it doesn’t allow for multiple mutable arguments. Also doesn’t constrain anything in type space, whereas this does!