"Illegal" macro names

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:

julia> @& Vector
Union{Vector, Borrowed{<:Vector}, LazyAccessor{<:Vector, P, S, <:Borrowed} where {P, S}}

julia> @& :mut Vector
Union{Vector, BorrowedMut{<:Vector}, LazyAccessor{<:Vector, P, S, <:BorrowedMut} where {P, S}}
[this looks nicer in the REPL]

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.

1 Like

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.:&.

4 Likes

Thanks!

P.S., I’m assuming it’s a different story for reserved keywords? I was thinking about defining @let which gives this error:

julia> macro let(ex) end
ERROR: ParseError:
# Error @ REPL[2]:1:7
macro let(ex) end
#     └─┘ ── invalid macro name

But similarly you can also get it working with an @eval:

julia> @eval macro $(:let)(ex) end
@let (macro with 1 method)

The idea here being you could have something more Rust-like for BorrowChecker.jl:

@let x = [1, 2, 3]
@let :mut y = [4, 5]
@lifetime lt begin
    @let rx = @& ~lt x
    @let ry = @& ~lt :mut y
end

which is about as close as you can reasonably get to

let x = vec![1, 2, 3];
let mut y = vec![4, 5];
let rx = &x;
let ry = &mut y;

other than the need for the explicit @lifetime scope in Julia.

1 Like

Slightly more straightforwardly, you can avoid the eval with the var"" string macro, which is how you can use anything as an identifier:

julia> macro var"let"(); @info "let"; end
@let (macro with 1 method)

julia> macro var"&"(); @info "&"; end
@& (macro with 1 method)

julia> @&
[ Info: &

julia> @let
[ Info: let

The nightly REPL wants uses of @let to tab-complete to @var"let", but both do work.

7 Likes

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.

2 Likes

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.

Good point! Ok I will avoid it.

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!