Description
I tried this code:
macro_rules! m {
($p:path) => {
($p {})
}
}
struct T {}
fn main() {
m!(T);
}
I expected to see this happen:
The code should expand to a main()
function that constructs a T {}
in a parenthesized expression and then ignores the value.
The code should be valid, and result in a program that does absolutely nothing.
Instead, this happened:
I get two compiler errors:
error: expected one of `)`, `,`, `.`, `?`, or an operator, found `{`
--> src/main.rs:3:13
|
3 | ($p {})
| -^ expected one of `)`, `,`, `.`, `?`, or an operator
| |
| help: missing `,`
...
10 | m!(T);
| ----- in this macro invocation
|
= note: this error originates in the macro `m` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0423]: expected value, found struct `T`
--> src/main.rs:10:8
|
7 | struct T {}
| ----------- `T` defined here
...
10 | m!(T);
| ^ help: use struct literal syntax instead: `T {}`
$p!()
macro call expressions
The exact same bug affects macro call expressions. ($p!())
complains about what it thinks is an infix !
operator.
Internals
The problem is that in the expression parser, we special case "path" metavar expansions, and just interpret them as path expressions. This is wrong, because an expression that starts with a path may also be a macro invocation or a struct literal, so we should not special case it.
The "obvious fix" is to simply delete these lines of code:
rust/compiler/rustc_parse/src/parser/expr.rs
Lines 1430 to 1434 in 35f6036
It will fall through to the branch that normally handles expressions that start with a path, where parsing a path will correctly uninterpolate a path fragment and it may be turned into a macro call or struct expression.
rust/compiler/rustc_parse/src/parser/expr.rs
Lines 1465 to 1466 in 35f6036
Unfortunately, fixing this bug is a breaking change, because you can detect it in macro_rules!
.
macro_rules! check_for_the_bug {
// setup: ensure that we capture `$p` as a path fragment.
($p:path) => {{
check_for_the_bug!($p {})
}};
// the following branches will *all*:
//
// - match a struct expression
// - expand to an expression equal to that struct expression
//
// but the expanded expressions have different side effects.
// these branches are to prevent `check_for_bug!(T {})` from printing "bug was fixed"
// (you can bypass this with generics but whatever who cares)
($($p:ident)::+ {}) => {{
println!("doofus, you called it wrong");
$($p)::+ {}
}};
($(::$p:ident)+ {}) => {{
println!("doofus, you called it wrong");
$(::$p)+
}};
// then, the actual disambiguation branches.
($e:expr) => {{
println!("bug was fixed!");
$e
}};
($p:path {}) => {{
println!("bug is still present :(");
$p {}
}};
}
struct T {}
fn main() {
let x: T = check_for_the_bug!(T);
}
So, uh, $:expr_2024
? Probably not, since surely new fragments are not required for "any change to the expression parser"; i'm not sure what the norms for breaking/additive changes are here.
Diagnostics
Let's come back to the diagnostics that the compiler emits.
The first diagnostic is "technically correct", because it explains the state that the parser is in:
error: expected one of `)`, `,`, `.`, `?`, or an operator, found `{`
--> src/main.rs:3:13
|
3 | ($p {})
| -^ expected one of `)`, `,`, `.`, `?`, or an operator
| |
| help: missing `,`
...
10 | m!(T);
| ----- in this macro invocation
|
= note: this error originates in the macro `m` (in Nightly builds, run with -Z macro-backtrace for more info)
But the second diagnostic is kinda wrong:
error[E0423]: expected value, found struct `T`
--> src/main.rs:10:8
|
7 | struct T {}
| ----------- `T` defined here
...
10 | m!(T);
| ^ help: use struct literal syntax instead: `T {}`
The diagnostic is "technically relevant" based on what was parsed; it detects that $p
(the path expression) resolves to a struct, and suggests using a struct expression instead.
But the suggested fix is not correct here; most notably because it points at the macro callsite where an expression is not even allowed (only accepts a path fragment). See #143220 which tracks that diagnostic pointing at the wrong location.
Assuming this parsing bug remains, the compiler should emit a more helpful diagnostic explaining that you can work around the bug by wrapping it in a block.
error: cannot make struct expression beginning with a path fragment here
--> src/main.rs:3:10
|
3 | ($p {})
| ^^^^^ this looks like a struct expression because `$p` is a path fragment
...
3 | ($p {})
| -^^ ^^-
| | |
| \--------- path fragments are eagerly parsed as path expressions
| |
| \--- and the braces are never allowed afterwards
|
= note: this is a bug in the expression parser
= help: if you intended to construct a `$p`, you can wrap it in a block:
3 | ({$p {}})
| +-----+
| help: add braces here to make it an expression statement
Wrapping it in a block works, because it never special-cases a path expansion group. It just falls through to the normal "statement starts with a path" branch:
rust/compiler/rustc_parse/src/parser/stmt.rs
Lines 195 to 213 in 35f6036
Meta
rustc --version --verbose
:
rustc 1.90.0-nightly (11ad40bb8 2025-06-28)
binary: rustc
commit-hash: 11ad40bb839ca16f74784b4ab72596ad85587298
commit-date: 2025-06-28
host: x86_64-unknown-linux-gnu
release: 1.90.0-nightly
LLVM version: 20.1.7